Compare commits

..

378 Commits
v3.12.1 ... v3

Author SHA1 Message Date
Andras Bacsai
b80a519b80 updates 2023-09-05 11:31:55 +02:00
Andras Bacsai
2fa7ffc931 updates 2023-09-05 11:25:16 +02:00
Andras Bacsai
4abec14a21 updates 2023-09-05 11:24:42 +02:00
Andras Bacsai
18d0623011 force volume prune 2023-08-16 15:27:10 +02:00
Andras Bacsai
aa634c78d1 fix seed js 2023-08-16 15:26:36 +02:00
Andras Bacsai
a2d4373104 cleanup volumes as well 2023-08-16 15:26:33 +02:00
Andras Bacsai
702e16d643 rename rollback to upgrade 2023-08-14 09:23:33 +02:00
Andras Bacsai
3b25c8f96b fix: docker compose env file 2023-08-12 00:10:14 +02:00
Andras Bacsai
1c8c567791 fix: env variables in compose deplyoments 2023-07-27 12:40:58 +02:00
Andras Bacsai
807a3c9d66 fix: n8n double mount
version++
2023-07-26 10:38:36 +02:00
Andras Bacsai
2abd7bd7bb copy to persisten storage 2023-07-25 12:38:36 +02:00
Andras Bacsai
343957ab8b update backups 2023-07-25 12:33:05 +02:00
Andras Bacsai
49261308f7 update webhook 2023-07-25 12:00:31 +02:00
Andras Bacsai
d037409237 updates 2023-07-25 11:40:27 +02:00
Andras Bacsai
338cbf62a1 fix: encrypt decrypt 2023-07-25 10:48:31 +02:00
Andras Bacsai
4c51bffc7b save db backup on seed 2023-07-20 21:41:47 +02:00
Andras Bacsai
fd98ba8812 auto update every hour 2023-07-20 16:37:24 +02:00
Andras Bacsai
930251e9c8 autoupdate fixed 2023-07-20 16:19:54 +02:00
Andras Bacsai
7cd441266a remove console log 2023-07-20 14:52:32 +02:00
Andras Bacsai
990fb8ec15 fix 2023-07-20 14:52:16 +02:00
Andras Bacsai
3fe982b2f4 Merge pull request #1052 from f-kawamura/bugfix-http-git-source
[Bug] Added support for HTTP source URLs in Git source
2023-07-20 13:48:34 +02:00
Andras Bacsai
9dd874e959 Merge pull request #1147 from martijnmichel/v3
Update serviceFields.ts
2023-07-20 13:46:52 +02:00
Andras Bacsai
b91368223b update plausible docs 2023-07-20 13:39:29 +02:00
Andras Bacsai
139670372b updates for templates 2023-07-20 13:29:03 +02:00
Andras Bacsai
1c0769ad75 update tags + only download on service view 2023-07-20 13:12:54 +02:00
Andras Bacsai
e6cbcf98cb fix: cleanup plausible 2023-07-20 13:06:19 +02:00
martijnmichel
64b0481055 Update serviceFields.ts 2023-07-20 08:19:51 +02:00
Andras Bacsai
ce15161926 Merge pull request #1144 from coollabsio/feature/implement-basic-auth-handling
fix: traefik config + ui + api
2023-07-18 15:37:57 +02:00
Andras Bacsai
4003d4d894 Merge pull request #1071 from pascal-klesse/feature/implement-basic-auth-handling
feat: Implement basic auth for applications
2023-07-18 15:37:35 +02:00
Andras Bacsai
6e011025a7 fix: traefik config + ui + api 2023-07-18 15:34:05 +02:00
Andras Bacsai
6c0544adb2 Merge branch 'v2' into feature/implement-basic-auth-handling 2023-07-18 14:48:02 +02:00
Andras Bacsai
8e4f7c9065 remove console.log 2023-07-18 14:44:46 +02:00
Andras Bacsai
e71f890b54 Merge pull request #1084 from Geczy/main
add trycatch
2023-07-18 14:44:32 +02:00
Andras Bacsai
4dc35dea97 Merge pull request #1119 from jenishngl/patch-1
Fixing the help link - source.svelte line 263
2023-07-18 14:40:33 +02:00
Andras Bacsai
b63dfb4bcd feat: backup databases 2023-07-18 14:36:54 +02:00
Andras Bacsai
b2ffd9183b fix: set connection string on publicity change 2023-07-18 13:01:10 +02:00
Andras Bacsai
5cb0bcfd9b fix: increase query time for new services etc 2023-07-18 09:50:05 +02:00
Jenish J
1fbcfcaf74 Merge branch 'v3' into patch-1 2023-07-17 20:15:27 +05:30
Andras Bacsai
3ba44a1e23 backup db file 2023-07-17 15:17:18 +02:00
Andras Bacsai
de4efbb555 update GH actions 2023-07-17 14:36:33 +02:00
Andras Bacsai
6f443680f3 typo 2023-07-17 14:02:51 +02:00
Andras Bacsai
23b22e5ca8 wip: reencrypt everything 2023-07-17 13:50:26 +02:00
Andras Bacsai
1bba747ce5 update 2023-07-15 09:50:29 +02:00
Andras Bacsai
9ebfc6646e wip 2023-07-14 23:15:43 +02:00
Andras Bacsai
055ff6dbbd updates 2023-07-14 22:30:40 +02:00
Andras Bacsai
6430e7b288 fix seed 2023-07-14 22:19:52 +02:00
Andras Bacsai
87b0050161 update seed 2023-07-14 22:11:59 +02:00
Andras Bacsai
369d8b408d version ++ 2023-07-14 21:36:28 +02:00
Andras Bacsai
505abc592c fix: gh actions 2023-07-14 21:33:30 +02:00
Andras Bacsai
c9df812258 testing seeder 2023-07-14 21:30:08 +02:00
Andras Bacsai
0bfcf6b66f fix: gh actions 2023-07-14 21:06:26 +02:00
Andras Bacsai
67853acabd docs link update 2023-07-14 20:58:54 +02:00
Jenish J
79c98657b1 Update source.svelte line 263 - Correcting the help link
Updated to the correct help link, as the old link was not pointing to the correct section in the docs.coollabs.io page
2023-06-28 20:45:03 +05:30
Geczy
d1be7e44af add trycatch 2023-05-25 13:39:57 -04:00
Andras Bacsai
33b853b981 Merge pull request #1081 from coollabsio/next
v3.12.32
2023-05-25 09:14:17 +02:00
Andras Bacsai
e6063fb93a fix: force delete stucked destinations 2023-05-24 21:55:24 +02:00
Andras Bacsai
f30f23af59 fix: restart storage volumes 2023-05-24 20:51:33 +02:00
Andras Bacsai
d4798a3b22 fix: more aggressive cleanup 2023-05-24 20:34:40 +02:00
Pascal Klesse
eefc2a3d0e remove TODO 2023-05-15 09:33:03 +02:00
Pascal Klesse
d14ca724e9 feat: Implement basic auth for applications 2023-05-15 09:27:49 +02:00
f-kawamura
7b05aaffc3 fix: Added support for HTTP source URLs in Git source. Currently only support HTTPS 2023-04-24 16:12:47 +09:00
Andras Bacsai
f3beb5d8db Merge pull request #1045 from coollabsio/next
v3.12.31
2023-04-19 08:53:20 +02:00
Andras Bacsai
e86b916415 fix: application logs duplicate 2023-04-19 08:50:55 +02:00
Andras Bacsai
e14cc6f2f0 remove assignment for issues 2023-04-18 14:52:26 +02:00
Andras Bacsai
8c1eb94401 fix: remove git is not necessary for docker bp 2023-04-18 14:51:31 +02:00
Andras Bacsai
29fa421945 feat: add custom version/tag 2023-04-18 14:46:40 +02:00
Andras Bacsai
7cfe98d988 fix: fail build if no application found. 2023-04-18 14:32:29 +02:00
Andras Bacsai
e2314c350b update lock file 2023-04-18 14:27:54 +02:00
Andras Bacsai
3713b33578 Update templates
fix: non string inputs in templates
2023-04-18 14:22:46 +02:00
Andras Bacsai
e007a773fd Merge pull request #1030 from coollabsio/next
v3.12.30
2023-04-03 10:40:27 +02:00
Andras Bacsai
e2821118eb fix. removing git 2023-04-03 10:21:08 +02:00
Andras Bacsai
4c8e73ac86 fix: harder to remove destinations and sources 2023-04-03 09:55:13 +02:00
Andras Bacsai
cb980fb814 fix: docker compose generator 2023-04-03 08:59:48 +02:00
Andras Bacsai
41c84e3642 Merge pull request #1001 from coollabsio/next
v3.12.29
2023-03-20 13:57:01 +01:00
Andras Bacsai
2bad98424f switch back to aarch-runners 2023-03-20 13:49:41 +01:00
Andras Bacsai
bc6b1e2dea fix: remove .git dir from final image 2023-03-20 13:05:53 +01:00
Andras Bacsai
911c15d1be update versions 2023-03-20 12:44:45 +01:00
Andras Bacsai
f79d570870 fix: gitea 2023-03-20 12:28:23 +01:00
Andras Bacsai
7fffa9fba5 Merge branch 'main' into next 2023-03-20 12:05:21 +01:00
Andras Bacsai
cbd634fb99 Update README.md 2023-03-17 15:31:00 +01:00
Andras Bacsai
7ae7436d4f Update staging-release.yml 2023-03-17 15:27:16 +01:00
Andras Bacsai
641bada100 ignore dockerhub releases 2023-03-16 13:54:58 +01:00
Andras Bacsai
3416d8d88e only arm 2023-03-16 13:42:19 +01:00
Andras Bacsai
0bb503368b concurrency 2023-03-16 13:37:49 +01:00
Andras Bacsai
ac3a77c3c7 no qemu 2023-03-16 13:35:04 +01:00
Andras Bacsai
79b4178d76 vcpu increase 2023-03-16 13:32:48 +01:00
Andras Bacsai
42a61296d7 test buildjet 2023-03-16 13:29:13 +01:00
Andras Bacsai
e8088e2a70 Merge pull request #993 from coollabsio/next
fix: revert from dockerhub if ghcr.io does not exists
2023-03-16 13:10:58 +01:00
Andras Bacsai
c4d39aced2 fix: revert from dockerhub if ghcr.io does not exists 2023-03-16 13:10:34 +01:00
Andras Bacsai
b40a5adeb0 update GH actions 2023-03-16 12:28:40 +01:00
Andras Bacsai
558a900620 Merge pull request #992 from coollabsio/next
Move to ghcr.io
2023-03-16 12:18:57 +01:00
Andras Bacsai
6b5e5a504d updates 2023-03-16 12:09:48 +01:00
Andras Bacsai
e44dca2464 updates 2023-03-16 12:01:57 +01:00
Andras Bacsai
e1f84b277a updates 2023-03-16 11:57:04 +01:00
Andras Bacsai
2518f46b08 remove fluentbit + pocketbase builds 2023-03-16 11:56:19 +01:00
Andras Bacsai
01e18a9496 Merge pull request #991 from coollabsio/ghcr
Move to ghcr from dockerhub
2023-03-16 10:55:22 +01:00
Andras Bacsai
564ca709d3 updates 2023-03-16 10:53:54 +01:00
Andras Bacsai
a54a36ae18 updates 2023-03-16 10:50:26 +01:00
Andras Bacsai
43603b0961 update 2023-03-16 10:26:20 +01:00
Andras Bacsai
96cd99f904 fixes 2023-03-16 10:23:14 +01:00
Andras Bacsai
3438d10e25 test 2023-03-16 10:13:44 +01:00
Andras Bacsai
022ccb42a1 test 2023-03-16 10:04:53 +01:00
Andras Bacsai
e6d72e9f87 test 2023-03-16 09:56:39 +01:00
Andras Bacsai
06e8a6af23 test 2023-03-16 09:38:20 +01:00
Andras Bacsai
ac188d137a test 2023-03-16 09:32:20 +01:00
Andras Bacsai
cae466745a test 2023-03-16 09:10:11 +01:00
Andras Bacsai
d61f16dab0 test 2023-03-16 08:48:37 +01:00
Andras Bacsai
02ba277a86 fix: show ip address as host in public dbs 2023-03-07 13:25:08 +01:00
Andras Bacsai
470ff49a02 Merge pull request #981 from coollabsio/next
v3.12.26
2023-03-07 12:19:36 +01:00
Andras Bacsai
04d741581d Merge pull request #980 from hyenabyte/main
Fixing multiple remotes breaking the server overview
2023-03-07 12:12:59 +01:00
Andras Bacsai
038f210148 Merge branch 'main' into next 2023-03-07 11:47:29 +01:00
Andras Bacsai
2adad3a7bd fix: handle log format volumes 2023-03-07 11:46:23 +01:00
Andras Bacsai
05fb26a49b remove console logs 2023-03-07 11:15:43 +01:00
Andras Bacsai
1c237affb4 feat: add host path to any container 2023-03-07 11:15:05 +01:00
Andras Bacsai
3e81d7e9cb fix: replace . & .. & $PWD with ~ 2023-03-07 10:44:53 +01:00
David Koch Gregersen
edb66620c1 Adding a check when reading ssh config file
Also adds comments to the createRemoteEngineConfiguration function
2023-03-07 10:43:34 +01:00
Andras Bacsai
04f7e8e777 fix: host volumes 2023-03-07 10:31:10 +01:00
David Koch Gregersen
eee201013c Fixing multiple remotes breaking the server overview 2023-03-06 22:31:01 +01:00
Andras Bacsai
1190cb4ea1 Update deployApplication.ts 2023-03-04 18:49:34 +01:00
Andras Bacsai
507100ea0b Update package.json 2023-03-04 18:48:38 +01:00
Andras Bacsai
9b13912b6d Update common.ts 2023-03-04 18:48:28 +01:00
Andras Bacsai
ee65deebfd fix: nestjs buildpack 2023-03-04 18:05:01 +01:00
Andras Bacsai
ba9fa442d1 Merge pull request #973 from coollabsio/next
v3.12.23
2023-03-04 15:11:42 +01:00
Andras Bacsai
87da27f9bf fix: publishDirectory 2023-03-04 15:06:35 +01:00
Andras Bacsai
b5bc5fe2c6 Merge pull request #965 from coollabsio/next
v3.12.22
2023-03-03 09:13:56 +01:00
Andras Bacsai
d2329360d0 Merge pull request #950 from hemangjoshi37a/main
added `star-history`
2023-03-02 17:33:22 +01:00
Andras Bacsai
7ece0ae10a Merge pull request #955 from eltociear/patch-1
fix typo in _GitlabRepositories.svelte
2023-03-02 17:32:26 +01:00
Andras Bacsai
f931b47eb8 Merge pull request #957 from addianto/fix/pack
Fix PACK_VERSION build argument in Dockerfile
2023-03-02 17:31:55 +01:00
Andras Bacsai
7f7eb12ded fix: empty port in docker compose 2023-03-02 17:22:49 +01:00
Andras Bacsai
c0940f7a19 fix: cannot delete resource when you are not on root team 2023-03-02 17:12:29 +01:00
Andras Bacsai
9dfde11e35 possible fix: vaultwarden 2023-03-02 16:56:44 +01:00
Andras Bacsai
6f15cc2dbc fix: base directory not found 2023-03-02 16:52:55 +01:00
Daya Adianto
120308638f fix: set PACK_VERSION to 0.27.0
This commit removes the `v` prefix in the version identifier assigned to
PACK_VERSION build argument. The `pack` script actually available on
Coolify's CDN, but named without `v` prefix in the script's version identifier.

Related issue: #689, which reported that the `pack` script in the
container image is a HTML 404 file instead of the actual `pack`
executable.
2023-02-25 17:27:41 +07:00
Ikko Eltociear Ashimine
1d04ef99bb fix typo in _GitlabRepositories.svelte
occured -> occurred
2023-02-24 11:24:55 +09:00
Hemang Joshi
9b00d177ef added star-history
added `star-history`
2023-02-22 16:21:32 +05:30
Andras Bacsai
884524c448 Merge pull request #945 from coollabsio/next
v3.12.21
2023-02-21 13:55:29 +01:00
Andras Bacsai
3ae1e7e87d remove debug 2023-02-21 13:47:28 +01:00
Andras Bacsai
81f885311d debug 2023-02-21 13:24:23 +01:00
Andras Bacsai
d9362f09d8 debug 2023-02-21 13:23:34 +01:00
Andras Bacsai
906d181d1b debug 2023-02-21 13:15:17 +01:00
Andras Bacsai
44b8812a7b debug 2023-02-21 13:08:14 +01:00
Andras Bacsai
3308c45e88 Merge pull request #943 from coollabsio/next
v3.12.21
2023-02-21 13:02:39 +01:00
Andras Bacsai
e530ecf9f9 fix 2023-02-21 12:59:21 +01:00
Andras Bacsai
51b5edb04f hmm fix 2023-02-21 12:48:06 +01:00
Andras Bacsai
f0d89f850e fix 2023-02-21 12:45:22 +01:00
Andras Bacsai
b777e08542 fix: arm servics 2023-02-21 12:35:20 +01:00
Andras Bacsai
2e485df530 Merge pull request #935 from coollabsio/next
v3.12.20
2023-02-20 12:03:57 +01:00
Andras Bacsai
3c37d22a6e typo 2023-02-20 11:55:08 +01:00
Andras Bacsai
08ab7a504a fix: applications cannot be deleted 2023-02-20 11:54:43 +01:00
Andras Bacsai
06563ef921 add latest tag with prod release 2023-02-20 10:23:31 +01:00
Andras Bacsai
34f6210bc0 Merge pull request #931 from coollabsio/next
v3.12.19
2023-02-20 10:04:37 +01:00
Andras Bacsai
0bbde0c605 add ccareer logo 2023-02-20 09:40:54 +01:00
Andras Bacsai
a8f24fd1b7 update icon 2023-02-20 09:11:05 +01:00
Andras Bacsai
c3e0237696 Merge pull request #909 from scshiv29-dev/main
changed copypassword fields in databases
2023-02-20 09:06:35 +01:00
Andras Bacsai
bb6925920f fix: escape new line chars in wp custom configs 2023-02-17 15:00:29 +01:00
Andras Bacsai
63ec2a33ae fix: network in compose files 2023-02-17 14:45:13 +01:00
Andras Bacsai
c89a959fe8 remove debug 2023-02-17 14:35:42 +01:00
Andras Bacsai
150b50e0ba isarm simplification 2023-02-17 14:20:17 +01:00
Andras Bacsai
4ef824f665 typo fix 2023-02-17 14:16:12 +01:00
Andras Bacsai
5a56cca0aa debug a thing 2023-02-17 14:09:15 +01:00
Andras Bacsai
b9189d7647 readd compose icon 2023-02-17 13:34:21 +01:00
Andras Bacsai
20226c914b update templates 2023-02-17 13:29:14 +01:00
Andras Bacsai
434e7f8a09 fix versions 2023-02-17 13:11:50 +01:00
Andras Bacsai
a29d733a02 Merge pull request #924 from jloewe/main
fix: gitlab personal repo listing
2023-02-17 13:06:46 +01:00
Andras Bacsai
9abe4b967b package updates + remove local icons 2023-02-17 13:05:18 +01:00
Andras Bacsai
3b6a4ece0f reset production release 2023-02-17 13:00:31 +01:00
Andras Bacsai
28d2471b4d dump pocketbase version 2023-02-17 13:00:31 +01:00
Andras Bacsai
d122af9fed Merge pull request #891 from TetrisIQ/moveIconsInComunityRepo
Move icons in comunity repo
2023-02-17 13:00:02 +01:00
Andras Bacsai
77271f3856 Merge pull request #919 from rawalplawit/docfix
fix: typos in docs
2023-02-17 12:47:17 +01:00
Andras Bacsai
ededfb68a6 Merge pull request #926 from simonorzel26/main
Spelling correction README.md
2023-02-17 12:45:10 +01:00
Andras Bacsai
4a3affdd24 Merge pull request #930 from usr3/patch-1
fix: link 404 in contribution.md
2023-02-17 12:44:46 +01:00
Andras Bacsai
8f8ea120d3 test prod release 2023-02-17 12:15:17 +01:00
Andras Bacsai
0fa88009f8 remove rc gh action 2023-02-17 11:56:10 +01:00
Andras Bacsai
4375a807df remove trpc 2023-02-17 11:55:21 +01:00
usr3
b2d97c5908 Update link 2023-02-17 13:36:14 +05:30
usr3
ec89dd606d Fix link 404
Fix 404 for `Setup Docker Compose Plugin`
2023-02-17 13:09:16 +05:30
Simon O
198508a7c3 Update README.md 2023-02-15 17:58:14 +01:00
Jan Loewe
4845e986bb fix gitlab personal repo listing 2023-02-14 17:33:50 +00:00
Shivam Deepak Chaudhary
1da8a307fc fixded copyvolumefield css 2023-02-11 09:32:04 +00:00
Shivam Deepak Chaudhary
b4886e604e added copyvolumefield 2023-02-11 09:24:08 +00:00
rawalplawit
e84544136e fix: typos in docs 2023-02-10 14:54:16 +05:45
Shivam Deepak Chaudhary
ce70252a69 changed copypassword fields in databases 2023-02-09 07:51:33 +00:00
Andras Bacsai
5c56962ea1 Merge pull request #888 from Rados51/883
Fixes coollabsio/coolify#883
2023-02-08 15:00:57 +01:00
Andras Bacsai
d2ed53b946 Merge pull request #889 from Rados51/884
Fixes coollabsio/coolify#884
2023-02-08 14:59:24 +01:00
Andras Bacsai
a4da80b498 Merge pull request #894 from m0ddixx/main
feat: Add environment variables for proxy ports
2023-02-08 14:57:22 +01:00
Andras Bacsai
9bd01492b1 Merge pull request #896 from inluxc/PocketBase-v0.12.0
Update PocketBase to v0.12.2
2023-02-08 14:54:22 +01:00
Fulvio Carvalhido
da032941b4 Update version 0.12.2 2023-02-07 12:31:44 +00:00
Fulvio Carvalhido
c138fcc2e2 Update PocketBase to v0.12.0
Release:
https://github.com/pocketbase/pocketbase/releases/tag/v0.12.0
2023-01-30 13:18:03 +00:00
Nico Kranz
cbb69b0350 add env support for traefik ports 2023-01-30 11:04:22 +00:00
Alex
a8aed3354d fix: url 2023-01-28 21:02:16 +00:00
Alex
e8790a4d4c feat: remove svg support 2023-01-28 21:00:57 +00:00
Radoš
df6ef3aaa0 Fixes coollabsio/coolify#884 2023-01-27 18:52:23 +01:00
Radoš
2820d99f7b Fixes coollabsio/coolify#883 2023-01-27 18:35:34 +01:00
Alex
077aa4445a feat: github raw icon url 2023-01-24 20:04:43 +00:00
Andras Bacsai
23bfc119d9 Fix GH 2023-01-24 15:43:43 +01:00
Andras Bacsai
ab712ac637 version++ 2023-01-24 15:32:32 +01:00
Andras Bacsai
b056826e94 Fixing prod release 2023-01-24 15:31:26 +01:00
Andras Bacsai
6311627899 Merge pull request #882 from coollabsio/next
v3.12.18
2023-01-24 15:09:51 +01:00
Andras Bacsai
37cea5fb61 update mattermost + traefik 2023-01-24 14:48:03 +01:00
Andras Bacsai
655a8cd60d feat: able to use $$ in traefik config gen
fix: repman icon
2023-01-24 13:52:39 +01:00
Andras Bacsai
4c8babc96a version++ 2023-01-23 10:45:19 +01:00
Andras Bacsai
612bacebed fix: cleanupStuckedContainers 2023-01-23 10:37:28 +01:00
Andras Bacsai
ade7c8566d fix: cleanupStuckedContainers 2023-01-23 10:37:14 +01:00
Andras Bacsai
19553ce5c8 Merge pull request #874 from coollabsio/next
v3.12.17
2023-01-20 21:14:06 +01:00
Andras Bacsai
18ed2527e8 fix 2023-01-20 20:51:37 +01:00
Andras Bacsai
b0652bc884 Merge pull request #872 from coollabsio/next
Next
2023-01-20 14:07:46 +01:00
Andras Bacsai
15c9ad23fe fix: stucked containers 2023-01-20 14:06:55 +01:00
Andras Bacsai
578bb12562 test new release gh action 2023-01-20 14:05:07 +01:00
Andras Bacsai
f82cfda07f version++ 2023-01-20 13:55:05 +01:00
Andras Bacsai
9e52b2788d Pocketbase GH release updated 2023-01-20 13:49:39 +01:00
Andras Bacsai
2e56a113d9 Test GH release thing 2023-01-20 13:48:57 +01:00
Andras Bacsai
4722d777e6 Merge pull request #871 from coollabsio/next
v3.12.15
2023-01-20 13:22:11 +01:00
Andras Bacsai
2141d54ae0 fix 2023-01-20 13:15:52 +01:00
Andras Bacsai
e346225136 fix 2023-01-20 13:10:40 +01:00
Andras Bacsai
012d4dae56 testing 2023-01-20 11:15:38 +01:00
Andras Bacsai
b4d9fe70af fix 2023-01-20 10:43:21 +01:00
Andras Bacsai
85e83b5441 Test new gh actions 2023-01-20 10:42:13 +01:00
Andras Bacsai
6b2a453b8f fix: deletion + cleanupStuckedContainers 2023-01-20 10:10:36 +01:00
Andras Bacsai
27021538d8 fix: cleanup stucked containers 2023-01-20 09:40:29 +01:00
Andras Bacsai
8b57a2b055 fix: cleanup function 2023-01-20 09:26:48 +01:00
Andras Bacsai
75dd894685 Merge pull request #867 from coollabsio/next
v3.12.14
2023-01-19 14:36:05 +01:00
Andras Bacsai
9101ef8774 version++ 2023-01-19 14:33:33 +01:00
Andras Bacsai
5932540630 fix: www redirect 2023-01-19 14:33:20 +01:00
Andras Bacsai
ec376b2e47 Merge pull request #864 from coollabsio/next
v3.12.13
2023-01-18 19:00:03 +01:00
Andras Bacsai
a176562ad0 fix: secrets 2023-01-18 18:51:03 +01:00
Andras Bacsai
becf37b676 Merge pull request #858 from coollabsio/next
v3.12.12
2023-01-17 12:33:51 +01:00
Andras Bacsai
9b5efab8f8 fix: grpc 2023-01-17 11:51:53 +01:00
Andras Bacsai
e98a8ba599 traefik dashbord in dev 2023-01-17 11:12:52 +01:00
Andras Bacsai
7ddac50008 feat: http + h2c paralel 2023-01-17 11:12:42 +01:00
Andras Bacsai
9837ae359f feat: init h2c (http2/grpc) support 2023-01-17 10:35:04 +01:00
Andras Bacsai
710a829dcb version++ 2023-01-17 10:00:50 +01:00
Andras Bacsai
ccd84fa454 fix: build args docker compose 2023-01-17 10:00:27 +01:00
Andras Bacsai
335b36d3a9 Merge pull request #857 from zek/patch-2
Fix docker-compose build args
2023-01-17 09:26:12 +01:00
Talha Zekeriya Durmuş
2be30fae00 Handle string build parameter 2023-01-17 02:11:06 +01:00
Talha Zekeriya Durmuş
db5cd21884 Fix docker-compose build args 2023-01-17 02:04:01 +01:00
Andras Bacsai
bfd3020031 Merge pull request #853 from coollabsio/next
v3.12.11
2023-01-16 12:44:04 +01:00
Andras Bacsai
344c36997a fix: public gh repo reload compose 2023-01-16 12:36:59 +01:00
Andras Bacsai
dfd9272b70 version ++ 2023-01-16 12:17:48 +01:00
Andras Bacsai
359f4520f5 test template + tags during dev 2023-01-16 11:45:45 +01:00
Andras Bacsai
aecf014f4e Merge pull request #843 from zek/soketi-logo
Add Soketi Logo
2023-01-16 11:10:48 +01:00
Andras Bacsai
d2a89ddf84 Merge pull request #844 from zek/repman-logo
Add Repman logo
2023-01-16 11:10:35 +01:00
Andras Bacsai
c01fe153ae Merge pull request #847 from zek/mattermost
Add Matermost Logo
2023-01-16 11:10:14 +01:00
Andras Bacsai
4f4a838799 update templates+tags 2023-01-16 10:58:44 +01:00
Andras Bacsai
ac6f2567eb fix: build env variables with docker compose 2023-01-16 10:42:23 +01:00
Andras Bacsai
05a5816ac6 fix: do not cleanup compose applications as unconfigured 2023-01-16 10:22:14 +01:00
Andras Bacsai
9c8f6e9195 fix: delete apps with previews 2023-01-16 10:16:49 +01:00
Andras Bacsai
2fd001f6d2 fix: docker log sequence 2023-01-16 10:06:41 +01:00
Andras Bacsai
d641d32413 fix: compose file location 2023-01-16 09:48:15 +01:00
Andras Bacsai
18064ef6a2 fixes related to docker-compose 2023-01-16 09:44:08 +01:00
Andras Bacsai
5cb9216add wip: trpc 2023-01-13 15:50:20 +01:00
Andras Bacsai
91c36dc810 wip: trpc 2023-01-13 15:24:43 +01:00
Andras Bacsai
6efb02fa32 wip: trpc 2023-01-13 15:21:54 +01:00
Andras Bacsai
97313e4180 wip: trpc 2023-01-13 14:54:21 +01:00
Andras Bacsai
568ab24fd9 wip: trpc 2023-01-13 14:17:36 +01:00
Talha Zekeriya Durmuş
5a745efcd3 Add Matermost Logo 2023-01-13 02:38:33 +01:00
Andras Bacsai
c651570e62 wip: trpc 2023-01-12 16:50:17 +01:00
Andras Bacsai
8980598085 wip: trpc 2023-01-12 16:43:41 +01:00
Talha Zekeriya Durmuş
c07c742feb Add Repman logo 2023-01-12 01:45:13 +01:00
Talha Zekeriya Durmuş
1053abb9a9 Add Soketi Logo 2023-01-12 01:41:35 +01:00
Andras Bacsai
2c9e57cbb1 Merge pull request #841 from coollabsio/next
v3.12.10
2023-01-11 11:44:22 +01:00
Andras Bacsai
c6eaa2c8a6 update packages in api 2023-01-11 11:35:57 +01:00
Andras Bacsai
5ab5e913ee Merge pull request #840 from zek/patch-1
Fix: add missing variables
2023-01-11 11:33:17 +01:00
Talha Zekeriya Durmuş
cea53ca476 Fix: add missing variables 2023-01-11 11:12:44 +01:00
Andras Bacsai
58af09114b Merge pull request #834 from coollabsio/next
v3.12.9
2023-01-11 11:00:56 +01:00
Andras Bacsai
c4c0417e2d new pocketbase 2023-01-11 10:55:55 +01:00
Andras Bacsai
74f90e6947 Merge pull request #838 from zek/patch-1
Add Build Time Secrets for Laravel
2023-01-11 10:12:59 +01:00
Andras Bacsai
ad5c339780 fix 2023-01-11 10:11:32 +01:00
Andras Bacsai
305823db00 fix: secrets 2023-01-11 09:29:59 +01:00
Talha Zekeriya Durmuş
baf58b298f Add Build Time Secrets 2023-01-11 01:43:43 +01:00
Andras Bacsai
c37367d018 add directus 2023-01-10 15:30:10 +01:00
Andras Bacsai
1c98796e64 new templates + tags + dev mode updated 2023-01-10 13:24:04 +01:00
Andras Bacsai
e686d9a6ea add lock file 2023-01-10 13:01:37 +01:00
Andras Bacsai
a1936b9d59 update jsonwebtoken 2023-01-10 13:01:03 +01:00
Andras Bacsai
834f9c9337 template updates 2023-01-10 13:01:03 +01:00
Andras Bacsai
615f8cfd3b feat: handle invite_only plausible analytics 2023-01-10 13:01:03 +01:00
Andras Bacsai
8ed134105f remove console.log 2023-01-10 13:01:03 +01:00
Andras Bacsai
5d6169b270 Merge pull request #781 from kaname-png/libretranslate
feat(ui): add libretranslate service icon
2023-01-10 12:57:05 +01:00
Andras Bacsai
e83de8b938 fix: local images for reverting 2023-01-10 12:24:22 +01:00
Andras Bacsai
ee55e039b2 Merge pull request #798 from hyddeos/main
fix the console error on Documentation hover
2023-01-10 11:52:57 +01:00
Andras Bacsai
086dd89144 fix: temporary disable dns check with dns servers 2023-01-10 11:50:41 +01:00
Andras Bacsai
68e5d4dd2c fix: doc link 2023-01-10 11:35:10 +01:00
Andras Bacsai
55a35c6bec fix: remove prefetches 2023-01-10 11:31:44 +01:00
Andras Bacsai
d09b4885fe Merge pull request #784 from hyddeos/tool-tip
Change color for Tooltip on hover
2023-01-10 11:29:54 +01:00
Andras Bacsai
a46773e6d8 Merge branch 'next' into tool-tip 2023-01-10 11:29:36 +01:00
Andras Bacsai
a422d0220c fix: add documentation link again 2023-01-10 11:27:43 +01:00
Andras Bacsai
e5eba8430a Merge pull request #783 from hyddeos/doc-in-mob-menu
Add link to the Documentation in the mobile menu
2023-01-10 11:26:42 +01:00
Andras Bacsai
3d235dc316 Merge pull request #794 from TetrisIQ/main
feat: adding icon for whoogle
2023-01-10 11:19:42 +01:00
Andras Bacsai
80d3b4be8c Merge pull request #825 from MrSquaare/feature/openblocks-service
feat: add Openblocks icon
2023-01-10 11:19:13 +01:00
Andras Bacsai
fe8b7480df Merge pull request #836 from coollabsio/feat/git-source-custom-user
fix: custom gitlab git user
2023-01-10 11:17:42 +01:00
Andras Bacsai
cebfc3aaa0 Merge pull request #804 from titouanmathis/feat/git-source-custom-user
feat(git-source): Add support for custom SSH user for GitLab self-hosted
2023-01-10 11:17:01 +01:00
Andras Bacsai
f778b5a12d fix: custom gitlab git user 2023-01-10 11:15:21 +01:00
Andras Bacsai
2244050160 Merge pull request #816 from Yarmeli/main
[Bug] Fixed issue with docker-compose not loading for Gitlab instances
2023-01-10 10:56:03 +01:00
Andras Bacsai
9284e42b62 fix: $ sign in secrets 2023-01-10 10:52:40 +01:00
Andras Bacsai
ee40120496 fix: read-only iam 2023-01-10 10:26:11 +01:00
Andras Bacsai
30cd2149ea fix: read-only permission 2023-01-10 10:15:03 +01:00
Andras Bacsai
395df36d57 chore: version++ 2023-01-10 09:57:27 +01:00
Andras Bacsai
79597ea0e5 fix: parsing secrets 2023-01-10 09:57:01 +01:00
Guillaume Bonnet
283f39270a feat: add Openblocks icon 2023-01-05 12:26:50 +00:00
Andras Bacsai
7d892bb19d esbuild 2022-12-29 22:33:31 +01:00
Yarmeli
a025f124f3 Updated index.svelte with the same changes from +page.svelte 2022-12-29 19:00:11 +00:00
Yarmeli
84f7287bf8 Fixed issue unable to find the docker compose file 2022-12-29 18:54:54 +00:00
Andras Bacsai
a58544b502 Merge pull request #813 from coollabsio/next
v3.12.8
2022-12-27 21:10:41 +01:00
Andras Bacsai
4d26175ebe omg, what have I done.. 2022-12-27 21:02:04 +01:00
Andras Bacsai
78f0e6ff6b Update package.json 2022-12-27 14:22:49 +01:00
Andras Bacsai
3af97af634 Update common.ts 2022-12-27 14:22:29 +01:00
Andras Bacsai
2c2663c8a4 Update common.ts 2022-12-27 14:20:19 +01:00
Andras Bacsai
1122b8a2f7 Update index.ts 2022-12-27 13:48:19 +01:00
Andras Bacsai
5b9f38948b Update index.ts 2022-12-27 13:46:33 +01:00
Andras Bacsai
507eb3b424 Update common.ts 2022-12-27 13:40:00 +01:00
Andras Bacsai
56fbc0ed6c Update package.json 2022-12-27 13:39:38 +01:00
Andras Bacsai
7aaad314e3 Update common.ts 2022-12-27 13:39:03 +01:00
Andras Bacsai
356949dd54 Merge pull request #811 from coollabsio/next
v3.12.5
2022-12-26 21:51:09 +01:00
Andras Bacsai
9878baca53 Update index.ts 2022-12-26 21:29:54 +01:00
Andras Bacsai
9cbc7c2939 Merge pull request #809 from Tiagofv/main
Fix bug: value.environment is not iterable
2022-12-26 21:19:11 +01:00
Andras Bacsai
4680b63911 fix: cleanupstorage 2022-12-26 21:17:53 +01:00
Tiago Braga
ce4a2d95f2 fix: remove unused imports 2022-12-24 16:28:02 -03:00
Tiago Braga
b2e048de8d Fix: conditional on environment 2022-12-24 16:27:10 -03:00
Andras Bacsai
d25a9d7515 devtemplates update 2022-12-22 11:31:46 +01:00
Andras Bacsai
dc130d3705 new pocketbase version 2022-12-22 11:22:37 +01:00
Titouan Mathis
2391850218 Add support for custom SSH user for GitLab self-hosted 2022-12-21 15:10:51 +01:00
Andras Bacsai
c8f7ca920e wip: trpc 2022-12-21 15:06:33 +01:00
Andras Bacsai
e3e39af6fb remove console.log 2022-12-21 14:11:07 +01:00
Andras Bacsai
f38114f5a5 Merge pull request #802 from coollabsio/next
v3.12.4
2022-12-21 13:25:46 +01:00
Andras Bacsai
1ee9d041df fix: duplicate env variables 2022-12-21 13:24:30 +01:00
Andras Bacsai
9c6f412f04 wip: trpc 2022-12-21 13:06:44 +01:00
Andras Bacsai
4fa0f2d04a fix: gh actions 2022-12-21 12:47:42 +01:00
Andras Bacsai
e566a66ea4 test 2022-12-21 12:33:27 +01:00
Andras Bacsai
58a42abc67 test 2022-12-21 11:40:47 +01:00
Andras Bacsai
5676bd9d0d test 2022-12-21 11:24:19 +01:00
Andras Bacsai
9691010e7b test 2022-12-21 11:11:55 +01:00
Andras Bacsai
d19be3ad52 Merge pull request #801 from coollabsio/next
v3.12.3
2022-12-21 10:53:09 +01:00
Andras Bacsai
ec3cbf788b fix: secrets 2022-12-21 10:40:27 +01:00
Andras Bacsai
1282fd0b76 fix: secrets 2022-12-21 10:11:03 +01:00
Andras Bacsai
93430e5607 fix: add default node_env variable 2022-12-19 23:07:01 +01:00
Andras Bacsai
14201f4052 fix: add default node_env variable 2022-12-19 22:15:00 +01:00
Andras Bacsai
47979bf16d fix: secrets 2022-12-19 22:11:21 +01:00
Andras Bacsai
29530f3b17 fix: secrets with newline 2022-12-19 21:48:31 +01:00
Eric
af548e6ef8 Change of link "rel" to "external"
To prevent hover console-error like on the documentations-icon in the desktop mode
2022-12-19 20:33:22 +01:00
hyddeos
ed24a9c990 fix the consol error on documentation hover 2022-12-19 20:02:24 +01:00
Andras Bacsai
cb1d86d08b Merge pull request #796 from coollabsio/next
v3.12.2
2022-12-19 11:59:03 +01:00
Andras Bacsai
88f3f628ef fix: docker buildpack env 2022-12-19 11:51:44 +01:00
Andras Bacsai
295bea37bc fix: envs 2022-12-19 11:01:29 +01:00
Andras Bacsai
bd7d756254 fix: escape env vars 2022-12-19 10:22:11 +01:00
Andras Bacsai
4261147fe8 fix: escape secrets 2022-12-19 10:04:28 +01:00
Andras Bacsai
a70adc5eb3 fix: root user for dbs on arm 2022-12-19 09:52:50 +01:00
Alex
0d51b04d79 feat: adding icon for whoogle 2022-12-18 14:13:49 +01:00
Andras Bacsai
06d40b8a81 debug secret problem 2022-12-14 12:34:13 +01:00
Andras Bacsai
2358510cba Update --bug-report.yaml 2022-12-13 21:30:52 +01:00
Andras Bacsai
e6d13cb7d7 Update --bug-report.yaml 2022-12-13 21:29:42 +01:00
Andras Bacsai
39e21c3f36 chore: version++ 2022-12-13 13:32:50 +01:00
Andras Bacsai
8da900ee72 fix: do not replace secret 2022-12-13 13:32:11 +01:00
Andras Bacsai
9f4e81a1a3 wip: trpc 2022-12-13 13:22:45 +01:00
Andras Bacsai
0b918c2f51 wip: trpc 2022-12-13 13:17:32 +01:00
Andras Bacsai
085cd2a314 wip: trpc 2022-12-13 13:15:23 +01:00
Andras Bacsai
98d2399568 wip: trpc 2022-12-13 13:13:28 +01:00
Andras Bacsai
515d9a0008 wip: trpc 2022-12-13 13:11:49 +01:00
Andras Bacsai
aece1fa7d3 wip: trpc 2022-12-13 13:04:47 +01:00
Andras Bacsai
abc614ecfd wip: trpc 2022-12-13 12:54:57 +01:00
Andras Bacsai
1180d3fdde wip: trpc 2022-12-13 12:47:14 +01:00
Andras Bacsai
1639d1725a Merge branch 'main' into next 2022-12-13 09:35:33 +01:00
Andras Bacsai
5df1deecbc update templates and tags 2022-12-13 09:34:42 +01:00
Andras Bacsai
fe3c0cf76e fix: appwrite tmp volume 2022-12-13 09:17:33 +01:00
Andras Bacsai
cc0df0182c Merge pull request #785 from jugglingjsons/main
fix: adding missing appwrite volume
2022-12-13 09:15:11 +01:00
jugglingjsons
02c530dcbe fix: adding missing appwrite volume 2022-12-12 19:11:40 +01:00
hyddeos
379b1de64f change color for Tooltip on hover 2022-12-12 18:24:13 +01:00
hyddeos
f3ff324925 fixed size on icon 2022-12-12 17:50:35 +01:00
hyddeos
0f2160222f Add Documents link to Mobile-Menu 2022-12-12 17:45:04 +01:00
hyddeos
ce3750c51c Add link document to Mobile-menu 2022-12-12 17:38:45 +01:00
Kaname
72a7ea6e91 feat(ui): add libretranslate service icon 2022-12-12 15:48:23 +00:00
Andras Bacsai
4ad7e1f8e6 wip 2022-12-12 16:04:41 +01:00
Andras Bacsai
2007ba0c3b fix: build commands 2022-12-12 14:52:56 +01:00
Andras Bacsai
2009dc11db wip trpc 2022-12-12 14:48:56 +01:00
Andras Bacsai
62f2196a0c Merge branch 'next' into trpc 2022-12-12 09:38:38 +01:00
Andras Bacsai
e63c65da4f Merge pull request #775 from hyddeos/main
Add link to the documentation
2022-12-12 09:11:18 +01:00
Andras Bacsai
570a082227 Merge pull request #776 from rickaard/close-sidedrawer
Close the sidedrawer when clicking a link in mobile view
2022-12-12 09:10:31 +01:00
Andras Bacsai
c445fc0f8a wip 2022-12-12 08:44:23 +01:00
Rickard Jonsson
699493cf24 Make sure sidedrawer is closed on link click 2022-12-11 20:59:02 +01:00
hyddeos
6c89686f31 Add link to the documentation 2022-12-11 20:32:15 +01:00
168 changed files with 15062 additions and 11796 deletions

View File

@@ -8,7 +8,6 @@ package
.env.* .env.*
!.env.example !.env.example
dist dist
client
apps/api/db/*.db apps/api/db/*.db
local-serve local-serve
apps/api/db/migration.db-journal apps/api/db/migration.db-journal

View File

@@ -2,20 +2,25 @@ name: 🐞 Bug report
description: Create a bug report to help us improve coolify description: Create a bug report to help us improve coolify
title: "[Bug]: " title: "[Bug]: "
labels: [Bug] labels: [Bug]
assignees:
- andrasbacsai
- vasani-arpit
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thanks for taking the time to fill out this bug report! Please fill the form in English Thanks for taking the time to fill out this bug report! Please fill the form in English.
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
options: options:
- label: I have searched the existing issues - label: I have searched the existing issues
required: true required: true
- type: input
id: repository
attributes:
label: Example public repository
description: "An example public git repository to reproduce the issue easily (if applicable)."
placeholder: "ex: https://github.com/coollabsio/coolify"
validations:
required: false
- type: textarea - type: textarea
attributes: attributes:
label: Description label: Description

View File

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

View File

@@ -1,93 +0,0 @@
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

@@ -1,93 +0,0 @@
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

@@ -1,91 +1,81 @@
name: production-release name: Production Release to ghcr.io
on: on:
release: release:
types: [released] types: [released]
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs: jobs:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/arm64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-arm64,mode=max
amd64: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to ghcr.io
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.actor }}
- name: Get current package version password: ${{ secrets.GITHUB_TOKEN }}
uses: martinbeentjes/npm-get-version-action@v1.2.3 - name: Extract metadata
id: package-version id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64 labels: ${{ steps.meta.outputs.labels }}
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
aarch64: aarch64:
runs-on: [self-hosted, arm64] runs-on: [self-hosted, arm64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v1
- name: Login to DockerHub - name: Login to ghcr.io
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.actor }}
- name: Get current package version password: ${{ secrets.GITHUB_TOKEN }}
uses: martinbeentjes/npm-get-version-action@v1.2.3 - name: Extract metadata
id: package-version id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}-aarch64
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/aarch64 platforms: linux/aarch64
push: true push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 tags: ${{ steps.meta.outputs.tags }}-aarch64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64 labels: ${{ steps.meta.outputs.labels }}
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [amd64, arm64, aarch64] needs: [amd64, aarch64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -93,18 +83,22 @@ jobs:
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to ghcr.io
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.actor }}
- name: Get current package version password: ${{ secrets.GITHUB_TOKEN }}
uses: martinbeentjes/npm-get-version-action@v1.2.3 - name: Extract metadata
id: package-version id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 docker buildx imagetools create --append ${{ fromJSON(steps.meta.outputs.json).tags[0] }}-aarch64 --tag ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

View File

@@ -1,90 +0,0 @@
name: release-candidate
on:
release:
types: [prereleased]
jobs:
arm64-making-something-cool:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/arm64
push: true
tags: coollabsio/coolify:${{github.event.release.name}}-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc-arm64,mode=max
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
amd64-making-something-cool:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:${{github.event.release.name}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-rc-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-rc-amd64,mode=max
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}
merge-manifest-to-be-cool:
runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Create & publish manifest
run: |
docker manifest create coollabsio/coolify:${{github.event.release.name}} --amend coollabsio/coolify:${{github.event.release.name}}-amd64 --amend coollabsio/coolify:${{github.event.release.name}}-arm64
docker manifest push coollabsio/coolify:${{github.event.release.name}}

View File

@@ -1,76 +1,76 @@
name: staging-release name: Staging Release to ghcr.io
concurrency:
group: staging_environment
cancel-in-progress: true
on: on:
push: push:
paths:
- "**"
- "!others/fluentbit"
- "!others/pocketbase"
- "!.github/workflows/fluent-bit-release.yml"
- "!.github/workflows/pocketbase-release.yml"
branches: branches:
- next - "v3"
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs: jobs:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "next"
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/arm64
push: true
tags: coollabsio/coolify:next-arm64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-arm64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-arm64,mode=max
amd64: amd64:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
ref: "next" ref: "v3"
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to ghcr.io
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.actor }}
- name: Get current package version password: ${{ secrets.GITHUB_TOKEN }}
uses: martinbeentjes/npm-get-version-action@v1.2.3 - name: Extract metadata (tags, labels)
id: package-version id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
context: . context: .
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: coollabsio/coolify:next-amd64 tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64 labels: ${{ steps.meta.outputs.labels }}
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max aarch64:
runs-on:
group: aarch-runners
steps:
- name: Checkout
uses: actions/checkout@v3
with:
ref: "v3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to ghcr.io
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
platforms: linux/aarch64
push: true
tags: ${{ steps.meta.outputs.tags }}-aarch64
labels: ${{ steps.meta.outputs.labels }}
merge-manifest: merge-manifest:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [arm64, amd64] needs: [amd64, aarch64]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -78,15 +78,20 @@ jobs:
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
- name: Login to DockerHub - name: Login to ghcr.io
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKERHUB_TOKEN }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Create & publish manifest - name: Create & publish manifest
run: | run: |
docker manifest create coollabsio/coolify:next --amend coollabsio/coolify:next-amd64 --amend coollabsio/coolify:next-arm64 docker buildx imagetools create --append ${{ steps.meta.outputs.tags }}-aarch64 --tag ${{ steps.meta.outputs.tags }}
docker manifest push coollabsio/coolify:next
- uses: sarisia/actions-status-discord@v1 - uses: sarisia/actions-status-discord@v1
if: always() if: always()
with: with:

11
.gitignore vendored
View File

@@ -1,20 +1,25 @@
.DS_Store .DS_Store
node_modules node_modules
.pnpm-store .pnpm-store
build /apps/ui/build
/build
.svelte-kit .svelte-kit
package package
.env .env
.env.* .env.*
!.env.example !.env.example
dist dist
client
apps/api/db/*.db apps/api/db/*.db
apps/api/db/migration.db-journal apps/api/db/migration.db-journal
apps/api/core* apps/api/core*
apps/server/build
apps/backup/backups/* apps/backup/backups/*
!apps/backup/backups/.gitkeep !apps/backup/backups/.gitkeep
logs /logs
others/certificates others/certificates
backups/* backups/*
!backups/.gitkeep !backups/.gitkeep
# Trpc
apps/server/db/*.db
apps/server/db/*.db-journal

11
.vscode/settings.json vendored
View File

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

View File

@@ -10,7 +10,7 @@ You'll need a set of skills to [get started](docs/contribution/GettingStarted.md
## 1) Setup your development environment ## 1) Setup your development environment
- 🌟 [Container based](docs/dev_setup/Container.md) ← *Recomended* - 🌟 [Container based](docs/dev_setup/Container.md) ← *Recommended*
- 📦 [DockerContainer](docs/dev_setup/DockerContiner.md) *WIP - 📦 [DockerContainer](docs/dev_setup/DockerContiner.md) *WIP
- 🐙 [Github Codespaces](docs/dev_setup/GithubCodespaces.md) - 🐙 [Github Codespaces](docs/dev_setup/GithubCodespaces.md)
- ☁️ [GitPod](docs/dev_setup/GitPod.md) - ☁️ [GitPod](docs/dev_setup/GitPod.md)
@@ -20,7 +20,7 @@ You'll need a set of skills to [get started](docs/contribution/GettingStarted.md
- [Install Pnpm](https://pnpm.io/installation) - [Install Pnpm](https://pnpm.io/installation)
- [Install Docker Engine](https://docs.docker.com/engine/install/) - [Install Docker Engine](https://docs.docker.com/engine/install/)
- [Setup Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) - [Setup Docker Compose Plugin](https://docs.docker.com/compose/install/)
- [Setup GIT LFS Support](https://git-lfs.github.com/) - [Setup GIT LFS Support](https://git-lfs.github.com/)
## 3) Setup Coolify ## 3) Setup Coolify
@@ -28,12 +28,12 @@ You'll need a set of skills to [get started](docs/contribution/GettingStarted.md
- Copy `apps/api/.env.example` to `apps/api/.env` - Copy `apps/api/.env.example` to `apps/api/.env`
- Edit `apps/api/.env`, set the `COOLIFY_APP_ID` environment variable to something cool. - Edit `apps/api/.env`, set the `COOLIFY_APP_ID` environment variable to something cool.
- Run `pnpm install` to install dependencies. - Run `pnpm install` to install dependencies.
- Run `pnpm db:push` to o create a local SQlite database. This will apply all migrations at `db/dev.db`. - Run `pnpm db:push` to create a local SQlite database. This will apply all migrations at `db/dev.db`.
- Run `pnpm db:seed` seed the database. - Run `pnpm db:seed` seed the database.
- Run `pnpm dev` start coding. - Run `pnpm dev` start coding.
```sh ```sh
# Or... Copy and paste commands bellow: # Or... Copy and paste commands below:
cp apps/api/.env.example apps/api/.env cp apps/api/.env.example apps/api/.env
pnpm install pnpm install
pnpm db:push pnpm db:push

View File

@@ -22,9 +22,9 @@ ARG DOCKER_VERSION=20.10.18
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. # Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1 ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0 ARG PACK_VERSION=0.27.0
RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 RUN apt update && apt -y install --no-install-recommends ca-certificates git git-lfs openssh-client curl jq cmake sqlite3 openssl psmisc python3 vim
RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/ RUN apt-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION} RUN npm install -g npm@${PNPM_VERSION}
@@ -38,7 +38,7 @@ RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
COPY --from=build /app/apps/api/build/ . COPY --from=build /app/apps/api/build/ .
COPY --from=build /app/others/fluentbit/ ./fluentbit # COPY --from=build /app/others/fluentbit/ ./fluentbit
COPY --from=build /app/apps/ui/build/ ./public COPY --from=build /app/apps/ui/build/ ./public
COPY --from=build /app/apps/api/prisma/ ./prisma COPY --from=build /app/apps/api/prisma/ ./prisma
COPY --from=build /app/apps/api/package.json . COPY --from=build /app/apps/api/package.json .

View File

@@ -9,7 +9,7 @@ ARG DOCKER_VERSION=20.10.18
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug. # Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
ARG DOCKER_COMPOSE_VERSION=2.6.1 ARG DOCKER_COMPOSE_VERSION=2.6.1
# https://github.com/buildpacks/pack/releases # https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0 ARG PACK_VERSION=0.27.0
WORKDIR /app WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION} RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}

View File

@@ -16,7 +16,7 @@ If you have a new service / build pack you would like to add, raise an idea [her
## How to install ## How to install
For more details goto the [docs](https://docs.coollabs.io/coolify/installation). For more details goto the [docs](https://docs.coollabs.io/coolify-v3/installation).
Installation is automated with the following command: Installation is automated with the following command:
@@ -79,9 +79,9 @@ Deploy your resource to:
### Services ### Services
- [Appwrite](https://appwrite.io) - [Appwrite](https://appwrite.io)
- [WordPress](https://docs.coollabs.io/coolify/services/wordpress) - [WordPress](https://docs.coollabs.io/coolify-v3/services/wordpress)
- [Ghost](https://ghost.org) - [Ghost](https://ghost.org)
- [Plausible Analytics](https://docs.coollabs.io/coolify/services/plausible-analytics) - [Plausible Analytics](https://docs.coollabs.io/coolify-v3/services/plausible-analytics)
- [NocoDB](https://nocodb.com) - [NocoDB](https://nocodb.com)
- [VSCode Server](https://github.com/cdr/code-server) - [VSCode Server](https://github.com/cdr/code-server)
- [MinIO](https://min.io) - [MinIO](https://min.io)
@@ -100,7 +100,7 @@ Deploy your resource to:
- Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai) - Mastodon: [@andrasbacsai@fosstodon.org](https://fosstodon.org/@andrasbacsai)
- Telegram: [@andrasbacsai](https://t.me/andrasbacsai) - Telegram: [@andrasbacsai](https://t.me/andrasbacsai)
- Twitter: [@andrasbacsai](https://twitter.com/andrasbacsai) - Twitter: [@andrasbacsai](https://twitter.com/heyandras)
- Email: [andras@coollabs.io](mailto:andras@coollabs.io) - Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord) - Discord: [Invitation](https://coollabs.io/discord)
@@ -119,7 +119,7 @@ Learn how to contribute to Coolify as as ...
<!-- <!--
&rarr; 🧑🏽‍🎨 Designer &rarr; 🧑🏽‍🎨 Designer
&rarr; 🙋‍♀️ Community Managemer &rarr; 🙋‍♀️ Community Manager
&rarr; 🧙🏻‍♂️ Text Content Creator &rarr; 🧙🏻‍♂️ Text Content Creator
&rarr; 👨🏼‍🎤 Video Content Creator &rarr; 👨🏼‍🎤 Video Content Creator
--> -->
@@ -130,12 +130,12 @@ Learn how to contribute to Coolify as as ...
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)] Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
### Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
### Organizations ### Organizations
Special thanks to our biggest sponsor, [CCCareers](https://cccareers.org/)!
![CCCareers](./others/logo/ccc-logo.webp)
Support this project with your organization. Your logo will show up here with a link to your website. Support this project with your organization. Your logo will show up here with a link to your website.
<a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/0/website"><img src="https://opencollective.com/coollabsio/organization/0/avatar.svg"></a>
@@ -148,3 +148,11 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/7/website"><img src="https://opencollective.com/coollabsio/organization/7/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
### Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

2
apps/api/.gitignore vendored
View File

@@ -9,3 +9,5 @@ package
dist dist
dev.db dev.db
client client
testTemplate.yaml
testTags.json

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,85 +1,83 @@
{ {
"name": "api", "name": "api",
"description": "Coolify's Fastify API", "description": "Coolify's Fastify API",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"db:generate": "prisma generate", "db:generate": "prisma generate",
"db:push": "prisma db push && prisma generate", "db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"db:studio": "prisma studio", "db:studio": "prisma studio",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"dev": "nodemon", "dev": "nodemon",
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --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}'", "format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .", "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" "start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js"
}, },
"dependencies": { "dependencies": {
"@breejs/ts-worker": "2.0.0", "@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.5.0", "@fastify/autoload": "5.7.0",
"@fastify/cookie": "8.3.0", "@fastify/cookie": "8.3.0",
"@fastify/cors": "8.2.0", "@fastify/cors": "8.2.0",
"@fastify/env": "4.1.0", "@fastify/env": "4.2.0",
"@fastify/jwt": "6.3.3", "@fastify/jwt": "6.5.0",
"@fastify/multipart": "7.3.0", "@fastify/multipart": "7.4.1",
"@fastify/static": "6.5.1", "@fastify/static": "6.6.0",
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2", "@ladjs/graceful": "3.2.1",
"@prisma/client": "4.6.1", "@prisma/client": "4.8.1",
"@sentry/node": "7.21.1", "axe": "11.2.1",
"@sentry/tracing": "7.21.1", "bcryptjs": "2.4.3",
"axe": "11.0.0", "bree": "9.1.3",
"bcryptjs": "2.4.3", "cabin": "11.1.1",
"bree": "9.1.2", "compare-versions": "5.0.1",
"cabin": "11.0.1", "csv-parse": "5.3.3",
"compare-versions": "5.0.1", "csvtojson": "2.0.10",
"csv-parse": "5.3.2", "cuid": "2.1.8",
"csvtojson": "2.0.10", "dayjs": "1.11.7",
"cuid": "2.1.8", "dockerode": "3.3.4",
"dayjs": "1.11.6", "dotenv-extended": "2.9.0",
"dockerode": "3.3.4", "execa": "6.1.0",
"dotenv-extended": "2.9.0", "fastify": "4.11.0",
"execa": "6.1.0", "fastify-plugin": "4.3.0",
"fastify": "4.10.2", "fastify-socket.io": "4.0.0",
"fastify-plugin": "4.3.0", "generate-password": "1.7.0",
"fastify-socket.io": "4.0.0", "got": "12.5.3",
"generate-password": "1.7.0", "is-ip": "5.0.0",
"got": "12.5.3", "is-port-reachable": "4.0.0",
"is-ip": "5.0.0", "js-yaml": "4.1.0",
"is-port-reachable": "4.0.0", "jsonwebtoken": "9.0.0",
"js-yaml": "4.1.0", "minimist": "^1.2.7",
"jsonwebtoken": "8.5.1", "node-forge": "1.3.1",
"minimist": "^1.2.7", "node-os-utils": "1.3.7",
"node-forge": "1.3.1", "p-all": "4.0.0",
"node-os-utils": "1.3.7", "p-throttle": "5.0.0",
"p-all": "4.0.0", "prisma": "4.8.1",
"p-throttle": "5.0.0", "public-ip": "6.0.1",
"prisma": "4.6.1", "pump": "3.0.0",
"public-ip": "6.0.1", "shell-quote": "^1.7.4",
"pump": "3.0.0", "socket.io": "4.5.4",
"shell-quote": "^1.7.4", "ssh-config": "4.2.0",
"socket.io": "4.5.3", "strip-ansi": "7.0.1",
"ssh-config": "4.1.6", "unique-names-generator": "4.7.1"
"strip-ansi": "7.0.1", },
"unique-names-generator": "4.7.1" "devDependencies": {
}, "@types/node": "18.11.18",
"devDependencies": { "@types/node-os-utils": "1.3.0",
"@types/node": "18.11.9", "@typescript-eslint/eslint-plugin": "5.48.1",
"@types/node-os-utils": "1.3.0", "@typescript-eslint/parser": "5.48.1",
"@typescript-eslint/eslint-plugin": "5.44.0", "esbuild": "0.16.16",
"@typescript-eslint/parser": "5.44.0", "eslint": "8.31.0",
"esbuild": "0.15.15", "eslint-config-prettier": "8.6.0",
"eslint": "8.28.0", "eslint-plugin-prettier": "4.2.1",
"eslint-config-prettier": "8.5.0", "nodemon": "2.0.20",
"eslint-plugin-prettier": "4.2.1", "prettier": "2.8.2",
"nodemon": "2.0.20", "rimraf": "3.0.2",
"prettier": "2.7.1", "tsconfig-paths": "4.1.2",
"rimraf": "3.0.2", "types-fastify-socket.io": "0.0.1",
"tsconfig-paths": "4.1.0", "typescript": "4.9.4"
"types-fastify-socket.io": "0.0.1", },
"typescript": "4.9.3" "prisma": {
}, "seed": "node prisma/seed.js"
"prisma": { }
"seed": "node prisma/seed.js"
}
} }

View File

@@ -0,0 +1,27 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_GitSource" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"forPublic" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"apiUrl" TEXT,
"htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22,
"customUser" TEXT NOT NULL DEFAULT 'git',
"organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"githubAppId" TEXT,
"gitlabAppId" TEXT,
"isSystemWide" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt" FROM "GitSource";
DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");
CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ApplicationPersistentStorage" ADD COLUMN "hostPath" TEXT;

View File

@@ -0,0 +1,29 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT;
ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT;
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" (
"id" TEXT NOT NULL PRIMARY KEY,
"applicationId" TEXT NOT NULL,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"debug" BOOLEAN NOT NULL DEFAULT false,
"previews" BOOLEAN NOT NULL DEFAULT false,
"autodeploy" BOOLEAN NOT NULL DEFAULT true,
"isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" BOOLEAN NOT NULL DEFAULT false,
"isHttp2" BOOLEAN NOT NULL DEFAULT false,
"basicAuth" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isCustomSSL", "isDBBranching", "isHttp2", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings";
DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -135,6 +135,8 @@ model Application {
dockerRegistryId String? dockerRegistryId String?
dockerRegistryImageName String? dockerRegistryImageName String?
simpleDockerfile String? simpleDockerfile String?
basicAuthUser String?
basicAuthPw String?
persistentStorage ApplicationPersistentStorage[] persistentStorage ApplicationPersistentStorage[]
secrets Secret[] secrets Secret[]
@@ -186,6 +188,8 @@ model ApplicationSettings {
isPublicRepository Boolean @default(false) isPublicRepository Boolean @default(false)
isDBBranching Boolean @default(false) isDBBranching Boolean @default(false)
isCustomSSL Boolean @default(false) isCustomSSL Boolean @default(false)
isHttp2 Boolean @default(false)
basicAuth Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
@@ -194,6 +198,7 @@ model ApplicationSettings {
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
applicationId String applicationId String
hostPath String?
path String path String
oldPath Boolean @default(false) oldPath Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -325,6 +330,7 @@ model GitSource {
apiUrl String? apiUrl String?
htmlUrl String? htmlUrl String?
customPort Int @default(22) customPort Int @default(22)
customUser String @default("git")
organization String? organization String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt

View File

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

View File

@@ -1,35 +1,47 @@
import Fastify from 'fastify';
import cors from '@fastify/cors';
import serve from '@fastify/static';
import env from '@fastify/env';
import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload'; import autoLoad from '@fastify/autoload';
import socketIO from 'fastify-socket.io' import cookie from '@fastify/cookie';
import socketIOServer from './realtime' import cors from '@fastify/cors';
import env from '@fastify/env';
import multipart from '@fastify/multipart';
import serve from '@fastify/static';
import Fastify from 'fastify';
import socketIO from 'fastify-socket.io';
import path, { join } from 'path';
import socketIOServer from './realtime';
import { cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeCommand, generateDatabaseConfiguration, isDev, listSettings, prisma, sentryDSN, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; import Graceful from '@ladjs/graceful';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful'
import yaml from 'js-yaml'
import fs from 'fs/promises'; import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers'; import yaml from 'js-yaml';
import { checkContainer } from './lib/docker';
import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib'; import { migrateApplicationPersistentStorage, migrateServicesToNewTemplate } from './lib';
import {
cleanupDockerStorage,
createRemoteEngineConfiguration,
decrypt,
executeCommand,
generateDatabaseConfiguration,
isDev,
listSettings,
prisma,
startTraefikProxy,
startTraefikTCPProxy,
version
} from './lib/common';
import { checkContainer } from './lib/docker';
import { scheduler } from './lib/scheduler';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers'; import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
import * as Sentry from '@sentry/node';
declare module 'fastify' { declare module 'fastify' {
interface FastifyInstance { interface FastifyInstance {
config: { config: {
COOLIFY_APP_ID: string, COOLIFY_APP_ID: string;
COOLIFY_SECRET_KEY: string, COOLIFY_SECRET_KEY: string;
COOLIFY_DATABASE_URL: string, COOLIFY_SECRET_KEY_BETTER: string | null;
COOLIFY_IS_ON: string, COOLIFY_DATABASE_URL: string;
COOLIFY_WHITE_LABELED: string, COOLIFY_IS_ON: string;
COOLIFY_WHITE_LABELED_ICON: string | null, COOLIFY_WHITE_LABELED: string;
COOLIFY_AUTO_UPDATE: string, COOLIFY_WHITE_LABELED_ICON: string | null;
COOLIFY_AUTO_UPDATE: string;
}; };
} }
} }
@@ -38,7 +50,7 @@ const port = isDev ? 3001 : 3000;
const host = '0.0.0.0'; const host = '0.0.0.0';
(async () => { (async () => {
const settings = await prisma.setting.findFirst() const settings = await prisma.setting.findFirst();
const fastify = Fastify({ const fastify = Fastify({
logger: settings?.isAPIDebuggingEnabled || false, logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true trustProxy: true
@@ -49,10 +61,14 @@ const host = '0.0.0.0';
required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'], required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'],
properties: { properties: {
COOLIFY_APP_ID: { COOLIFY_APP_ID: {
type: 'string', type: 'string'
}, },
COOLIFY_SECRET_KEY: { COOLIFY_SECRET_KEY: {
type: 'string'
},
COOLIFY_SECRET_KEY_BETTER: {
type: 'string', type: 'string',
default: null
}, },
COOLIFY_DATABASE_URL: { COOLIFY_DATABASE_URL: {
type: 'string', type: 'string',
@@ -73,8 +89,7 @@ const host = '0.0.0.0';
COOLIFY_AUTO_UPDATE: { COOLIFY_AUTO_UPDATE: {
type: 'string', type: 'string',
default: 'false' default: 'false'
}, }
} }
}; };
const options = { const options = {
@@ -103,13 +118,13 @@ const host = '0.0.0.0';
fastify.register(autoLoad, { fastify.register(autoLoad, {
dir: join(__dirname, 'routes') dir: join(__dirname, 'routes')
}); });
fastify.register(cookie) fastify.register(cookie);
fastify.register(cors); fastify.register(cors);
fastify.register(socketIO, { fastify.register(socketIO, {
cors: { cors: {
origin: isDev ? "*" : '' origin: isDev ? '*' : ''
} }
}) });
// To detect allowed origins // To detect allowed origins
// fastify.addHook('onRequest', async (request, reply) => { // fastify.addHook('onRequest', async (request, reply) => {
// console.log(request.headers.host) // console.log(request.headers.host)
@@ -131,10 +146,9 @@ const host = '0.0.0.0';
// } // }
// }) // })
try { try {
await fastify.listen({ port, host }) await fastify.listen({ port, host });
await socketIOServer(fastify) await socketIOServer(fastify);
console.log(`Coolify's API is listening on ${host}:${port}`); console.log(`Coolify's API is listening on ${host}:${port}`);
migrateServicesToNewTemplate(); migrateServicesToNewTemplate();
@@ -148,106 +162,125 @@ const host = '0.0.0.0';
if (!scheduler.workers.has('deployApplication')) { if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication'); scheduler.run('deployApplication');
} }
}, 2000) }, 2000);
// autoUpdater // autoUpdater
setInterval(async () => { setInterval(async () => {
await autoUpdater() await autoUpdater();
}, 60000 * 15) }, 60000 * 60);
// cleanupStorage // cleanupStorage
setInterval(async () => { setInterval(async () => {
await cleanupStorage() await cleanupStorage();
}, 60000 * 10) }, 60000 * 15);
// Cleanup stucked containers (not defined in Coolify, but still running and managed by Coolify)
setInterval(async () => {
await cleanupStuckedContainers();
}, 60000);
// checkProxies, checkFluentBit & refresh templates // checkProxies, checkFluentBit & refresh templates
setInterval(async () => { setInterval(async () => {
await checkProxies(); await checkProxies();
await checkFluentBit(); await checkFluentBit();
}, 60000) }, 60000);
// Refresh and check templates // Refresh and check templates
setInterval(async () => { setInterval(async () => {
await refreshTemplates() await refreshTemplates();
}, 60000) }, 60000 * 10);
setInterval(async () => { setInterval(async () => {
await refreshTags() await refreshTags();
}, 60000) }, 60000 * 10);
setInterval(async () => { setInterval(
await migrateServicesToNewTemplate() async () => {
}, isDev ? 10000 : 60000) await migrateServicesToNewTemplate();
},
isDev ? 10000 : 60000 * 10
);
setInterval(async () => { setInterval(async () => {
await copySSLCertificates(); await copySSLCertificates();
}, 10000) }, 10000);
await Promise.all([ await Promise.all([
getTagsTemplates(), getTagsTemplates(),
getArch(), getArch(),
getIPAddress(), getIPAddress(),
configureRemoteDockers(), configureRemoteDockers(),
]) refreshTemplates(),
refreshTags()
// cleanupStuckedContainers()
]);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
process.exit(1); process.exit(1);
} }
})(); })();
async function getIPAddress() { async function getIPAddress() {
const { publicIpv4, publicIpv6 } = await import('public-ip') const { publicIpv4, publicIpv6 } = await import('public-ip');
try { try {
const settings = await listSettings(); const settings = await listSettings();
if (!settings.ipv4) { if (!settings.ipv4) {
const ipv4 = await publicIpv4({ timeout: 2000 }) const ipv4 = await publicIpv4({ timeout: 2000 });
console.log(`Getting public IPv4 address...`); console.log(`Getting public IPv4 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } }) await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } });
} }
if (!settings.ipv6) { if (!settings.ipv6) {
const ipv6 = await publicIpv6({ timeout: 2000 }) const ipv6 = await publicIpv6({ timeout: 2000 });
console.log(`Getting public IPv6 address...`); console.log(`Getting public IPv6 address...`);
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } }) await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } });
} }
} catch (error) { } } catch (error) { }
} }
async function getTagsTemplates() { async function getTagsTemplates() {
const { default: got } = await import('got') const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const templates = await fs.readFile('./devTemplates.yaml', 'utf8') let templates = await fs.readFile('./devTemplates.yaml', 'utf8');
const tags = await fs.readFile('./devTags.json', 'utf8') let tags = await fs.readFile('./devTags.json', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))) try {
await fs.writeFile('./tags.json', tags) if (await fs.stat('./testTemplate.yaml')) {
console.log('[004] Tags and templates loaded in dev mode...') templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
} else { }
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() } catch (error) { }
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() try {
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) if (await fs.stat('./testTags.json')) {
await fs.writeFile('/app/tags.json', tags) const testTags = await fs.readFile('./testTags.json', 'utf8');
console.log('[004] Tags and templates loaded...') if (testTags.length > 0) {
} tags = JSON.stringify(JSON.parse(tags).concat(JSON.parse(testTags)));
}
}
} catch (error) { }
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)));
await fs.writeFile('./tags.json', tags);
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('[004] Tags and templates loaded...');
}
} catch (error) { } catch (error) {
console.log("Couldn't get latest templates.") console.log("Couldn't get latest templates.");
console.log(error) console.log(error);
} }
} }
async function initServer() { async function initServer() {
const appId = process.env['COOLIFY_APP_ID']; const appId = process.env['COOLIFY_APP_ID'];
const settings = await prisma.setting.findUnique({ where: { id: '0' } }) const settings = await prisma.setting.findUnique({ where: { id: '0' } });
try { try {
if (settings.doNotTrack === true) { if (settings.doNotTrack === true) {
console.log('[000] Telemetry disabled...') console.log('[000] Telemetry disabled...');
} else { } else {
if (settings.sentryDSN !== sentryDSN) {
await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } })
}
// Initialize Sentry // Initialize Sentry
// Sentry.init({ // Sentry.init({
// dsn: sentryDSN, // dsn: sentryDSN,
@@ -257,7 +290,7 @@ async function initServer() {
// console.log('[000] Sentry initialized...') // console.log('[000] Sentry initialized...')
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error);
} }
try { try {
console.log(`[001] Initializing server...`); console.log(`[001] Initializing server...`);
@@ -267,27 +300,73 @@ async function initServer() {
console.log(`[002] Cleanup stucked builds...`); console.log(`[002] Cleanup stucked builds...`);
const isOlder = compareVersions('3.8.1', version); const isOlder = compareVersions('3.8.1', version);
if (isOlder === 1) { if (isOlder === 1) {
await prisma.build.updateMany({ where: { status: { in: ['running', 'queued'] } }, data: { status: 'failed' } }); await prisma.build.updateMany({
where: { status: { in: ['running', 'queued'] } },
data: { status: 'failed' }
});
} }
} catch (error) { } } catch (error) { }
try { try {
console.log('[003] Cleaning up old build sources under /tmp/build-sources/...'); console.log('[003] Cleaning up old build sources under /tmp/build-sources/...');
await fs.rm('/tmp/build-sources', { recursive: true, force: true }) if (!isDev) await fs.rm('/tmp/build-sources', { recursive: true, force: true });
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
async function getArch() { async function getArch() {
try { try {
const settings = await prisma.setting.findFirst({}) const settings = await prisma.setting.findFirst({});
if (settings && !settings.arch) { if (settings && !settings.arch) {
console.log(`Getting architecture...`); console.log(`Getting architecture...`);
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } }) await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } });
} }
} catch (error) { } } catch (error) { }
} }
async function cleanupStuckedContainers() {
try {
const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set();
for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress))
return;
if (destination.engine) {
enginesDone.add(destination.engine);
}
if (destination.remoteIpAddress) {
if (!destination.remoteVerified) continue;
enginesDone.add(destination.remoteIpAddress);
}
const { stdout: containers } = await executeCommand({
dockerId: destination.id,
command: `docker container ps -a --filter "label=coolify.managed=true" --format '{{ .Names}}'`
});
if (containers) {
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0) {
for (const container of containersArray) {
const containerId = container.split('-')[0];
const application = await prisma.application.findFirst({
where: { id: { startsWith: containerId } }
});
const service = await prisma.service.findFirst({
where: { id: { startsWith: containerId } }
});
const database = await prisma.database.findFirst({
where: { id: { startsWith: containerId } }
});
if (!application && !service && !database) {
await executeCommand({ command: `docker container rm -f ${container}` });
}
}
}
}
}
} catch (error) {
console.log(error);
}
}
async function configureRemoteDockers() { async function configureRemoteDockers() {
try { try {
const remoteDocker = await prisma.destinationDocker.findMany({ const remoteDocker = await prisma.destinationDocker.findMany({
@@ -296,37 +375,51 @@ async function configureRemoteDockers() {
if (remoteDocker.length > 0) { if (remoteDocker.length > 0) {
console.log(`Verifying Remote Docker Engines...`); console.log(`Verifying Remote Docker Engines...`);
for (const docker of remoteDocker) { for (const docker of remoteDocker) {
console.log('Verifying:', docker.remoteIpAddress) console.log('Verifying:', docker.remoteIpAddress);
await verifyRemoteDockerEngineFn(docker.id); await verifyRemoteDockerEngineFn(docker.id);
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
async function autoUpdater() { async function autoUpdater() {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
const currentVersion = version; const currentVersion = version;
const { coolify } = await got.get('https://get.coollabs.io/versions.json', { const { coolify } = await got
searchParams: { .get('https://get.coollabs.io/versions.json', {
appId: process.env['COOLIFY_APP_ID'] || undefined, searchParams: {
version: currentVersion appId: process.env['COOLIFY_APP_ID'] || undefined,
} version: currentVersion
}).json() }
})
.json();
const latestVersion = coolify.main.version; const latestVersion = coolify.main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion); const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) { if (isUpdateAvailable === 1) {
const activeCount = 0 const activeCount = 0;
if (activeCount === 0) { if (activeCount === 0) {
if (!isDev) { if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) { if (isAutoUpdateEnabled) {
await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }) let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }) try {
await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }) await executeCommand({ command: `docker pull ${image}` });
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"` }) } catch (error) {
image = `coollabsio/coolify:${latestVersion}`;
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
});
} }
} else { } else {
console.log('Updating (not really in dev mode).'); console.log('Updating (not really in dev mode).');
@@ -334,7 +427,7 @@ async function autoUpdater() {
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
@@ -345,14 +438,18 @@ async function checkFluentBit() {
const { id } = await prisma.destinationDocker.findFirst({ const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' } where: { engine, network: 'coolify' }
}); });
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true }); const { found } = await checkContainer({
dockerId: id,
container: 'coolify-fluentbit',
remove: true
});
if (!found) { if (!found) {
await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` });
await executeCommand({ command: `docker compose up -d fluent-bit` }); await executeCommand({ command: `docker compose up -d fluent-bit` });
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
async function checkProxies() { async function checkProxies() {
@@ -368,7 +465,7 @@ async function checkProxies() {
where: { engine, network: 'coolify', isCoolifyProxyUsed: true } where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
}); });
if (localDocker) { if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 }) portReachable = await isReachable(80, { host: ipv4 || ipv6 });
if (!portReachable) { if (!portReachable) {
await startTraefikProxy(localDocker.id); await startTraefikProxy(localDocker.id);
} }
@@ -380,13 +477,13 @@ async function checkProxies() {
if (remoteDocker.length > 0) { if (remoteDocker.length > 0) {
for (const docker of remoteDocker) { for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) { if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress }) portReachable = await isReachable(80, { host: docker.remoteIpAddress });
if (!portReachable) { if (!portReachable) {
await startTraefikProxy(docker.id); await startTraefikProxy(docker.id);
} }
} }
try { try {
await createRemoteEngineConfiguration(docker.id) await createRemoteEngineConfiguration(docker.id);
} catch (error) { } } catch (error) { }
} }
} }
@@ -426,112 +523,148 @@ async function checkProxies() {
// await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); // await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
// } // }
// } // }
} catch (error) { } catch (error) { }
}
} }
async function copySSLCertificates() { async function copySSLCertificates() {
try { try {
const pAll = await import('p-all'); const pAll = await import('p-all');
const actions = [] const actions = [];
const certificates = await prisma.certificate.findMany({ include: { team: true } }) const certificates = await prisma.certificate.findMany({ include: { team: true } });
const teamIds = certificates.map(c => c.teamId) const teamIds = certificates.map((c) => c.teamId);
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } }) const destinations = await prisma.destinationDocker.findMany({
where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } }
});
for (const certificate of certificates) { for (const certificate of certificates) {
const { id, key, cert } = certificate const { id, key, cert } = certificate;
const decryptedKey = decrypt(key) const decryptedKey = decrypt(key);
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey);
await fs.writeFile(`/tmp/${id}-cert.pem`, cert) await fs.writeFile(`/tmp/${id}-cert.pem`, cert);
for (const destination of destinations) { for (const destination of destinations) {
if (destination.remoteEngine) { if (destination.remoteEngine) {
if (destination.remoteVerified) { if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination const { id: dockerId, remoteIpAddress } = destination;
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress)) actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress));
} }
} else { } else {
actions.push(async () => copyLocalCertificates(id)) actions.push(async () => copyLocalCertificates(id));
} }
} }
} }
await pAll.default(actions, { concurrency: 1 }) await pAll.default(actions, { concurrency: 1 });
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} finally { } finally {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }) try {
await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` });
} catch (e) {
console.log(e);
}
} }
} }
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) { async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try { try {
await executeCommand({ command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` }) await executeCommand({
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/'` }) command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`
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/` }) 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) { } catch (error) {
console.log({ error }) console.log({ error });
} }
} }
async function copyLocalCertificates(id: string) { async function copyLocalCertificates(id: string) {
try { try {
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({
await executeCommand({ command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`,
await executeCommand({ command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/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) { } catch (error) {
console.log({ error }) console.log({ error });
} }
} }
async function cleanupStorage() { async function cleanupStorage() {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
let enginesDone = new Set() let enginesDone = new Set();
for (const destination of destinationDockers) { for (const destination of destinationDockers) {
if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return;
if (destination.engine) enginesDone.add(destination.engine) if (destination.engine) {
if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress) enginesDone.add(destination.engine);
}
if (destination.remoteIpAddress) {
if (!destination.remoteVerified) continue;
enginesDone.add(destination.remoteIpAddress);
}
await cleanupDockerStorage(destination.id);
// let lowDiskSpace = false;
// try {
// let stdout = null;
// if (!isDev) {
// const output = await executeCommand({
// dockerId: destination.id,
// command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`,
// shell: true
// });
// stdout = output.stdout;
// } else {
// const output = await executeCommand({
// command: `df -kPT /`
// });
// stdout = output.stdout;
// }
// let lines = stdout.trim().split('\n');
// let header = lines[0];
// let regex =
// /^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
// const boundaries = [];
// let match;
let lowDiskSpace = false; // while ((match = regex.exec(header))) {
try { // boundaries.push(match[0].length);
let stdout = null // }
if (!isDev) {
const output = await executeCommand({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, shell: true })
stdout = output.stdout;
} else {
const output = await executeCommand({
command:
`df -kPT /`
});
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) { // boundaries[boundaries.length - 1] = -1;
boundaries.push(match[0].length); // const data = lines.slice(1).map((line) => {
} // const cl = boundaries.map((boundary) => {
// const column = boundary > 0 ? line.slice(0, boundary) : line;
boundaries[boundaries.length - 1] = -1; // line = line.slice(boundary);
const data = lines.slice(1).map((line) => { // return column.trim();
const cl = boundaries.map((boundary) => { // });
const column = boundary > 0 ? line.slice(0, boundary) : line; // return {
line = line.slice(boundary); // capacity: Number.parseInt(cl[5], 10) / 100
return column.trim(); // };
}); // });
return { // if (data.length > 0) {
capacity: Number.parseInt(cl[5], 10) / 100 // const { capacity } = data[0];
}; // if (capacity > 0.8) {
}); // lowDiskSpace = true;
if (data.length > 0) { // }
const { capacity } = data[0]; // }
if (capacity > 0.8) { // } catch (error) {}
lowDiskSpace = true; // if (lowDiskSpace) {
} // await cleanupDockerStorage(destination.id);
} // }
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
} }
} }

View File

@@ -3,8 +3,26 @@ import crypto from 'crypto';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, saveDockerRegistryCredentials, setDefaultConfiguration } from '../lib/buildPacks/common'; import {
import { createDirectories, decrypt, defaultComposeConfiguration, getDomain, prisma, decryptApplication, isDev, pushToRegistry, executeCommand } from '../lib/common'; copyBaseConfigurationFiles,
makeLabelForSimpleDockerfile,
makeLabelForStandaloneApplication,
saveBuildLog,
saveDockerRegistryCredentials,
setDefaultConfiguration
} from '../lib/buildPacks/common';
import {
createDirectories,
decrypt,
defaultComposeConfiguration,
getDomain,
prisma,
decryptApplication,
isDev,
pushToRegistry,
executeCommand,
generateSecrets
} from '../lib/common';
import * as importers from '../lib/importers'; import * as importers from '../lib/importers';
import * as buildpacks from '../lib/buildPacks'; import * as buildpacks from '../lib/buildPacks';
@@ -14,33 +32,63 @@ import * as buildpacks from '../lib/buildPacks';
if (message === 'error') throw new Error('oops'); if (message === 'error') throw new Error('oops');
if (message === 'cancel') { if (message === 'cancel') {
parentPort.postMessage('cancelled'); parentPort.postMessage('cancelled');
await prisma.$disconnect() await prisma.$disconnect();
process.exit(0); process.exit(0);
} }
}); });
const pThrottle = await import('p-throttle') const pThrottle = await import('p-throttle');
const throttle = pThrottle.default({ const throttle = pThrottle.default({
limit: 1, limit: 1,
interval: 2000 interval: 2000
}); });
const th = throttle(async () => { const th = throttle(async () => {
try { try {
const queuedBuilds = await prisma.build.findMany({ where: { status: { in: ['queued', 'running'] } }, orderBy: { createdAt: 'asc' } }); const queuedBuilds = await prisma.build.findMany({
const { concurrentBuilds } = await prisma.setting.findFirst({}) where: { status: { in: ['queued', 'running'] } },
orderBy: { createdAt: 'asc' }
});
const { concurrentBuilds } = await prisma.setting.findFirst({});
if (queuedBuilds.length > 0) { if (queuedBuilds.length > 0) {
parentPort.postMessage({ deploying: true }); parentPort.postMessage({ deploying: true });
const concurrency = concurrentBuilds; const concurrency = concurrentBuilds;
const pAll = await import('p-all'); const pAll = await import('p-all');
const actions = [] const actions = [];
for (const queueBuild of queuedBuilds) { for (const queueBuild of queuedBuilds) {
actions.push(async () => { actions.push(async () => {
let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { dockerRegistry: true, destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) let application = await prisma.application.findUnique({
where: { id: queueBuild.applicationId },
let { id: buildId, type, gitSourceId, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild include: {
application = decryptApplication(application) dockerRegistry: true,
destinationDocker: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
persistentStorage: true,
secrets: true,
settings: true,
teams: true
}
});
if (!application) {
await prisma.build.update({
where: { id: queueBuild.id },
data: {
status: 'failed'
}
});
throw new Error('Application not found');
}
let {
id: buildId,
type,
gitSourceId,
sourceBranch = null,
pullmergeRequestId = null,
previewApplicationId = null,
forceRebuild,
sourceRepository = null
} = queueBuild;
application = decryptApplication(application);
if (!gitSourceId && application.simpleDockerfile) { if (!gitSourceId && application.simpleDockerfile) {
const { const {
@@ -53,102 +101,110 @@ import * as buildpacks from '../lib/buildPacks';
exposePort, exposePort,
simpleDockerfile, simpleDockerfile,
dockerRegistry dockerRegistry
} = application } = application;
const { workdir } = await createDirectories({ repository: applicationId, buildId }); const { workdir } = await createDirectories({ repository: applicationId, buildId });
try { try {
if (queueBuild.status === 'running') { if (queueBuild.status === 'running') {
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); await saveBuildLog({
line: 'Building halted, restarting...',
buildId,
applicationId: application.id
});
} }
const volumes = const volumes =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
if (storage.oldPath) { if (storage.oldPath) {
return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; return `${applicationId}${storage.path
.replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`;
}
if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}`;
} }
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
if (destinationDockerId) { if (destinationDockerId) {
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); await prisma.build.update({
where: { id: buildId },
data: { status: 'running' }
});
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}` command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}`
}) });
if (containers) { if (containers) {
const containerArray = containers.split('\n'); const containerArray = containers.split('\n');
if (containerArray.length > 0) { if (containerArray.length > 0) {
for (const container of containerArray) { for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) await executeCommand({
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
} }
} }
} }
} catch (error) { } catch (error) {
// //
} }
const envs = [ let envs = [];
`PORT=${port}`
];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { envs = [
if (pullmergeRequestId) { ...envs,
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) ...generateSecrets(secrets, pullmergeRequestId, false, port)
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); await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
if (dockerRegistry) { if (dockerRegistry) {
const { url, username, password } = dockerRegistry const { url, username, password } = dockerRegistry;
await saveDockerRegistryCredentials({ url, username, password, workdir }) await saveDockerRegistryCredentials({ url, username, password, workdir });
} }
const labels = makeLabelForSimpleDockerfile({ const labels = makeLabelForSimpleDockerfile({
applicationId, applicationId,
type, type,
port: exposePort ? `${exposePort}:${port}` : port, port: exposePort ? `${exposePort}:${port}` : port
}); });
try { try {
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes
return { .filter((v) => {
[`${volume.split(':')[0]}`]: { if (
name: volume.split(':')[0] !v.startsWith('.') &&
!v.startsWith('..') &&
!v.startsWith('/') &&
!v.startsWith('~')
) {
return v;
} }
}; })
}); .map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
[applicationId]: { [applicationId]: {
build: { build: {
context: workdir, context: workdir
}, },
image: `${applicationId}:${buildId}`, image: `${applicationId}:${buildId}`,
container_name: applicationId, container_name: applicationId,
volumes, volumes,
labels, labels,
env_file: envFound ? [`${workdir}/.env`] : [], environment: envs,
depends_on: [], depends_on: [],
expose: [port], expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network), ...defaultComposeConfiguration(destinationDocker.network)
} }
}, },
networks: { networks: {
@@ -159,11 +215,15 @@ import * as buildpacks from '../lib/buildPacks';
volumes: Object.assign({}, ...composeVolumes) volumes: Object.assign({}, ...composeVolumes)
}; };
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); 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 executeCommand({
debug: true,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) { if (foundBuild) {
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
@@ -174,10 +234,9 @@ import * as buildpacks from '../lib/buildPacks';
} }
throw new Error(error); throw new Error(error);
} }
} }
} catch (error) { } catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) { if (foundBuild) {
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
@@ -190,18 +249,26 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error, buildId, applicationId: application.id }); await saveBuildLog({ line: error, buildId, applicationId: application.id });
} }
if (error instanceof Error) { if (error instanceof Error) {
await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); await saveBuildLog({
line: error.message,
buildId,
applicationId: application.id
});
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
if (application.dockerRegistryImageName) { if (application.dockerRegistryImageName) {
const customTag = application.dockerRegistryImageName.split(':')[1] || buildId; const customTag = application.dockerRegistryImageName.split(':')[1] || buildId;
const imageName = application.dockerRegistryImageName.split(':')[0]; 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 saveBuildLog({
await pushToRegistry(application, workdir, buildId, imageName, customTag) line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); buildId,
applicationId: application.id
});
await pushToRegistry(application, workdir, buildId, imageName, customTag);
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
} }
} catch (error) { } catch (error) {
if (error.stdout) { if (error.stdout) {
@@ -211,13 +278,16 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.build.update({
where: { id: buildId },
data: { status: 'success' }
});
} }
return; return;
} }
const originalApplicationId = application.id const originalApplicationId = application.id;
const { const {
id: applicationId, id: applicationId,
name, name,
@@ -241,7 +311,7 @@ import * as buildpacks from '../lib/buildPacks';
deploymentType, deploymentType,
gitCommitHash, gitCommitHash,
dockerRegistry dockerRegistry
} = application } = application;
let { let {
branch, branch,
@@ -257,7 +327,7 @@ import * as buildpacks from '../lib/buildPacks';
dockerComposeFileLocation, dockerComposeFileLocation,
dockerComposeConfiguration, dockerComposeConfiguration,
denoMainFile denoMainFile
} = application } = application;
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
@@ -272,9 +342,11 @@ import * as buildpacks from '../lib/buildPacks';
let imageFoundRemotely = false; let imageFoundRemotely = false;
if (pullmergeRequestId) { if (pullmergeRequestId) {
const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } }) const previewApplications = await prisma.previewApplication.findMany({
where: { applicationId: originalApplicationId, pullmergeRequestId }
});
if (previewApplications.length > 0) { if (previewApplications.length > 0) {
previewApplicationId = previewApplications[0].id previewApplicationId = previewApplications[0].id;
} }
// Previews, we need to get the source branch and set subdomain // Previews, we need to get the source branch and set subdomain
branch = sourceBranch; branch = sourceBranch;
@@ -285,7 +357,11 @@ import * as buildpacks from '../lib/buildPacks';
const { workdir, repodir } = await createDirectories({ repository, buildId }); const { workdir, repodir } = await createDirectories({ repository, buildId });
try { try {
if (queueBuild.status === 'running') { if (queueBuild.status === 'running') {
await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); await saveBuildLog({
line: 'Building halted, restarting...',
buildId,
applicationId: application.id
});
} }
const currentHash = crypto const currentHash = crypto
@@ -323,15 +399,19 @@ import * as buildpacks from '../lib/buildPacks';
const volumes = const volumes =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
if (storage.oldPath) { if (storage.oldPath) {
return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; return `${applicationId}${storage.path
.replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`;
}
if (storage.hostPath) {
return `${storage.hostPath}:${storage.path}`;
} }
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || []; }) || [];
try { try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration) dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration);
} catch (error) { } } catch (error) {}
let deployNeeded = true; let deployNeeded = true;
let destinationType; let destinationType;
@@ -339,7 +419,10 @@ import * as buildpacks from '../lib/buildPacks';
destinationType = 'docker'; destinationType = 'docker';
} }
if (destinationType === 'docker') { if (destinationType === 'docker') {
await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); await prisma.build.update({
where: { id: buildId },
data: { status: 'running' }
});
const configuration = await setDefaultConfiguration(application); const configuration = await setDefaultConfiguration(application);
@@ -348,7 +431,7 @@ import * as buildpacks from '../lib/buildPacks';
installCommand = configuration.installCommand; installCommand = configuration.installCommand;
startCommand = configuration.startCommand; startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand; buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory || '';
baseDirectory = configuration.baseDirectory || ''; baseDirectory = configuration.baseDirectory || '';
dockerFileLocation = configuration.dockerFileLocation; dockerFileLocation = configuration.dockerFileLocation;
dockerComposeFileLocation = configuration.dockerComposeFileLocation; dockerComposeFileLocation = configuration.dockerComposeFileLocation;
@@ -361,6 +444,7 @@ import * as buildpacks from '../lib/buildPacks';
githubAppId: gitSource.githubApp?.id, githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id, gitlabAppId: gitSource.gitlabApp?.id,
customPort: gitSource.customPort, customPort: gitSource.customPort,
customUser: gitSource.customUser,
gitCommitHash, gitCommitHash,
configuration, configuration,
repository, repository,
@@ -381,10 +465,10 @@ import * as buildpacks from '../lib/buildPacks';
tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`; tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`;
} }
if (application.dockerRegistryImageName) { if (application.dockerRegistryImageName) {
imageName = application.dockerRegistryImageName.split(':')[0] imageName = application.dockerRegistryImageName.split(':')[0];
customTag = application.dockerRegistryImageName.split(':')[1] || tag customTag = application.dockerRegistryImageName.split(':')[1] || tag;
} else { } else {
customTag = tag customTag = tag;
imageName = applicationId; imageName = applicationId;
} }
@@ -394,13 +478,17 @@ import * as buildpacks from '../lib/buildPacks';
try { try {
await prisma.build.update({ where: { id: buildId }, data: { commit } }); await prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } } catch (err) {}
if (!pullmergeRequestId) { if (!pullmergeRequestId) {
if (configHash !== currentHash) { if (configHash !== currentHash) {
deployNeeded = true; deployNeeded = true;
if (configHash) { if (configHash) {
await saveBuildLog({ line: 'Configuration changed', buildId, applicationId }); await saveBuildLog({
line: 'Configuration changed',
buildId,
applicationId
});
} }
} else { } else {
deployNeeded = false; deployNeeded = false;
@@ -413,30 +501,43 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker image inspect ${applicationId}:${tag}` command: `docker image inspect ${applicationId}:${tag}`
}) });
imageFoundLocally = true; imageFoundLocally = true;
} catch (error) { } catch (error) {
// //
} }
if (dockerRegistry) { if (dockerRegistry) {
const { url, username, password } = dockerRegistry const { url, username, password } = dockerRegistry;
location = await saveDockerRegistryCredentials({ url, username, password, workdir }) location = await saveDockerRegistryCredentials({
url,
username,
password,
workdir
});
} }
try { try {
await executeCommand({ await executeCommand({
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker ${location ? `--config ${location}` : ''} pull ${imageName}:${customTag}` command: `docker ${
}) location ? `--config ${location}` : ''
} pull ${imageName}:${customTag}`
});
imageFoundRemotely = true; imageFoundRemotely = true;
} catch (error) { } catch (error) {
// //
} }
let imageFound = `${applicationId}:${tag}` let imageFound = `${applicationId}:${tag}`;
if (imageFoundRemotely) { if (imageFoundRemotely) {
imageFound = `${imageName}:${customTag}` imageFound = `${imageName}:${customTag}`;
} }
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); await copyBaseConfigurationFiles(
buildPack,
workdir,
buildId,
applicationId,
baseImage
);
const labels = makeLabelForStandaloneApplication({ const labels = makeLabelForStandaloneApplication({
applicationId, applicationId,
fqdn, fqdn,
@@ -455,7 +556,7 @@ import * as buildpacks from '../lib/buildPacks';
baseDirectory, baseDirectory,
publishDirectory publishDirectory
}); });
if (forceRebuild) deployNeeded = true if (forceRebuild) deployNeeded = true;
if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) { if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) {
if (buildpacks[buildPack]) if (buildpacks[buildPack])
await buildpacks[buildPack]({ await buildpacks[buildPack]({
@@ -496,33 +597,53 @@ import * as buildpacks from '../lib/buildPacks';
baseImage, baseImage,
baseBuildImage, baseBuildImage,
deploymentType, deploymentType,
forceRebuild
}); });
else { else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({
line: `Build pack ${buildPack} not found`,
buildId,
applicationId
});
throw new Error(`Build pack ${buildPack} not found.`); throw new Error(`Build pack ${buildPack} not found.`);
} }
} else { } else {
if (imageFoundRemotely || deployNeeded) { if (imageFoundRemotely || deployNeeded) {
await saveBuildLog({ line: `Container image ${imageFound} found in Docker Registry - reuising it`, buildId, applicationId }); await saveBuildLog({
line: `Container image ${imageFound} found in Docker Registry - reuising it`,
buildId,
applicationId
});
} else { } else {
if (imageFoundLocally || deployNeeded) { if (imageFoundLocally || deployNeeded) {
await saveBuildLog({ line: `Container image ${imageFound} found locally - reuising it`, buildId, applicationId }); await saveBuildLog({
line: `Container image ${imageFound} found locally - reuising it`,
buildId,
applicationId
});
} }
} }
} }
if (buildPack === 'compose') { if (buildPack === 'compose') {
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`;
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}` command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}`
}) });
if (containers) { if (containers) {
const containerArray = containers.split('\n'); const containerArray = containers.split('\n');
if (containerArray.length > 0) { if (containerArray.length > 0) {
for (const container of containerArray) { for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) await executeCommand({
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
} }
} }
} }
@@ -530,17 +651,25 @@ import * as buildpacks from '../lib/buildPacks';
// //
} }
try { try {
console.log({ debug }) await executeCommand({
await executeCommand({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) debug,
buildId,
applicationId,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} -f ${fileYaml} up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.build.update({
where: { id: buildId },
data: { status: 'success' }
});
await prisma.application.update({ await prisma.application.update({
where: { id: applicationId }, where: { id: applicationId },
data: { configHash: currentHash } data: { configHash: currentHash }
}); });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) { if (foundBuild) {
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
@@ -551,64 +680,62 @@ import * as buildpacks from '../lib/buildPacks';
} }
throw new Error(error); throw new Error(error);
} }
} else { } else {
try { try {
const { stdout: containers } = await executeCommand({ const { stdout: containers } = await executeCommand({
dockerId: destinationDockerId, dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}` command: `docker ps -a --filter 'label=com.docker.compose.service=${
}) pullmergeRequestId ? imageId : applicationId
}' --format {{.ID}}`
});
if (containers) { if (containers) {
const containerArray = containers.split('\n'); const containerArray = containers.split('\n');
if (containerArray.length > 0) { if (containerArray.length > 0) {
for (const container of containerArray) { for (const container of containerArray) {
await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) await executeCommand({
await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) dockerId: destinationDockerId,
command: `docker stop -t 0 ${container}`
});
await executeCommand({
dockerId: destinationDockerId,
command: `docker rm --force ${container}`
});
} }
} }
} }
} catch (error) { } catch (error) {
// //
} }
const envs = [ let envs = [];
`PORT=${port}`
];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { envs = [
if (pullmergeRequestId) { ...envs,
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) ...generateSecrets(secrets, pullmergeRequestId, false, port)
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'));
if (dockerRegistry) { if (dockerRegistry) {
const { url, username, password } = dockerRegistry const { url, username, password } = dockerRegistry;
await saveDockerRegistryCredentials({ url, username, password, workdir }) await saveDockerRegistryCredentials({ url, username, password, workdir });
}
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
} }
try { try {
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes
return { .filter((v) => {
[`${volume.split(':')[0]}`]: { if (
name: volume.split(':')[0] !v.startsWith('.') &&
!v.startsWith('..') &&
!v.startsWith('/') &&
!v.startsWith('~')
) {
return v;
} }
}; })
}); .map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -616,12 +743,12 @@ import * as buildpacks from '../lib/buildPacks';
image: imageFound, image: imageFound,
container_name: imageId, container_name: imageId,
volumes, volumes,
env_file: envFound ? [`${workdir}/.env`] : [], environment: envs,
labels, labels,
depends_on: [], depends_on: [],
expose: [port], expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network), ...defaultComposeConfiguration(destinationDocker.network)
} }
}, },
networks: { networks: {
@@ -632,11 +759,15 @@ import * as buildpacks from '../lib/buildPacks';
volumes: Object.assign({}, ...composeVolumes) volumes: Object.assign({}, ...composeVolumes)
}; };
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeCommand({ debug, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) await executeCommand({
debug,
dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml up -d`
});
await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId });
} catch (error) { } catch (error) {
await saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) { if (foundBuild) {
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
@@ -648,14 +779,15 @@ import * as buildpacks from '../lib/buildPacks';
throw new Error(error); throw new Error(error);
} }
if (!pullmergeRequestId) await prisma.application.update({ if (!pullmergeRequestId)
where: { id: applicationId }, await prisma.application.update({
data: { configHash: currentHash } where: { id: applicationId },
}); data: { configHash: currentHash }
});
} }
} }
} catch (error) { } catch (error) {
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) const foundBuild = await prisma.build.findUnique({ where: { id: buildId } });
if (foundBuild) { if (foundBuild) {
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
@@ -668,16 +800,24 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error, buildId, applicationId: application.id }); await saveBuildLog({ line: error, buildId, applicationId: application.id });
} }
if (error instanceof Error) { if (error instanceof Error) {
await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); await saveBuildLog({
line: error.message,
buildId,
applicationId: application.id
});
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) { if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) {
await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id }); await saveBuildLog({
await pushToRegistry(application, workdir, tag, imageName, customTag) line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`,
await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); buildId,
applicationId: application.id
});
await pushToRegistry(application, workdir, tag, imageName, customTag);
await saveBuildLog({ line: 'Success', buildId, applicationId: application.id });
} }
} catch (error) { } catch (error) {
if (error.stdout) { if (error.stdout) {
@@ -686,21 +826,20 @@ import * as buildpacks from '../lib/buildPacks';
if (error.stderr) { if (error.stderr) {
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
} }
}); });
} }
await pAll.default(actions, { concurrency }) await pAll.default(actions, { concurrency });
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
}) });
while (true) { while (true) {
await th() await th();
} }
} else process.exit(0); } else process.exit(0);
})(); })();

View File

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

View File

@@ -1,6 +1,18 @@
import { base64Encode, decrypt, encrypt, executeCommand, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common"; import {
base64Encode,
decrypt,
encrypt,
executeCommand,
generateSecrets,
generateTimestamp,
getDomain,
isARM,
isDev,
prisma,
version
} from '../common';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { day } from "../dayjs"; import { day } from '../dayjs';
const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy'];
const nodeBased = [ const nodeBased = [
@@ -17,7 +29,10 @@ const nodeBased = [
'nextjs' 'nextjs'
]; ];
export function setDefaultBaseImage(buildPack: string | null, deploymentType: string | null = null) { export function setDefaultBaseImage(
buildPack: string | null,
deploymentType: string | null = null
) {
const nodeVersions = [ const nodeVersions = [
{ {
value: 'node:lts', value: 'node:lts',
@@ -316,8 +331,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
{ {
value: 'heroku/builder-classic:22', value: 'heroku/builder-classic:22',
label: 'heroku/builder-classic:22' label: 'heroku/builder-classic:22'
}, }
] ];
let payload: any = { let payload: any = {
baseImage: null, baseImage: null,
baseBuildImage: null, baseBuildImage: null,
@@ -326,8 +341,10 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
}; };
if (nodeBased.includes(buildPack)) { if (nodeBased.includes(buildPack)) {
if (deploymentType === 'static') { if (deploymentType === 'static') {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; payload.baseImage = isARM() ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; payload.baseImages = isARM()
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts'; payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} else { } else {
@@ -338,8 +355,10 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
} }
} }
if (staticApps.includes(buildPack)) { if (staticApps.includes(buildPack)) {
payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; payload.baseImage = isARM() ? 'nginx:alpine' : 'webdevops/nginx:alpine';
payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; payload.baseImages = isARM()
? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions;
payload.baseBuildImage = 'node:lts'; payload.baseBuildImage = 'node:lts';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
@@ -357,12 +376,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st
payload.baseImage = 'denoland/deno:latest'; payload.baseImage = 'denoland/deno:latest';
} }
if (buildPack === 'php') { if (buildPack === 'php') {
payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; payload.baseImage = isARM()
payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions ? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM()
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; payload.baseImage = isARM()
payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions ? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM()
? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions;
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
payload.baseBuildImages = nodeVersions; payload.baseBuildImages = nodeVersions;
} }
@@ -402,10 +429,16 @@ export const setDefaultConfiguration = async (data: any) => {
startCommand = template?.startCommand || 'yarn start'; startCommand = template?.startCommand || 'yarn start';
if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel') if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel')
buildCommand = template?.buildCommand || null; buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (!publishDirectory) {
publishDirectory = template?.publishDirectory || null;
} else {
if (!publishDirectory.startsWith('/')) publishDirectory = `/${publishDirectory}`;
if (publishDirectory.endsWith('/')) publishDirectory = publishDirectory.slice(0, -1);
}
if (baseDirectory) { if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (baseDirectory.endsWith('/') && baseDirectory !== '/') baseDirectory = baseDirectory.slice(0, -1); if (baseDirectory.endsWith('/') && baseDirectory !== '/')
baseDirectory = baseDirectory.slice(0, -1);
} }
if (dockerFileLocation) { if (dockerFileLocation) {
if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`;
@@ -414,8 +447,10 @@ export const setDefaultConfiguration = async (data: any) => {
dockerFileLocation = '/Dockerfile'; dockerFileLocation = '/Dockerfile';
} }
if (dockerComposeFileLocation) { if (dockerComposeFileLocation) {
if (!dockerComposeFileLocation.startsWith('/')) dockerComposeFileLocation = `/${dockerComposeFileLocation}`; if (!dockerComposeFileLocation.startsWith('/'))
if (dockerComposeFileLocation.endsWith('/')) dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1); dockerComposeFileLocation = `/${dockerComposeFileLocation}`;
if (dockerComposeFileLocation.endsWith('/'))
dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1);
} else { } else {
dockerComposeFileLocation = '/Dockerfile'; dockerComposeFileLocation = '/Dockerfile';
} }
@@ -479,7 +514,6 @@ export const scanningTemplates = {
} }
}; };
export const saveBuildLog = async ({ export const saveBuildLog = async ({
line, line,
buildId, buildId,
@@ -491,7 +525,7 @@ export const saveBuildLog = async ({
}): Promise<any> => { }): Promise<any> => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return; if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return; if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got') const { default: got } = await import('got');
if (typeof line === 'object' && line) { if (typeof line === 'object' && line) {
if (line.shortMessage) { if (line.shortMessage) {
line = line.shortMessage + '\n' + line.stderr; line = line.shortMessage + '\n' + line.stderr;
@@ -504,7 +538,11 @@ export const saveBuildLog = async ({
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@'); line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
} }
const addTimestamp = `[${generateTimestamp()}] ${line}`; const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224'; const fluentBitUrl = isDev
? process.env.COOLIFY_CONTAINER_DEV === 'true'
? 'http://coolify-fluentbit:24224'
: 'http://localhost:24224'
: 'http://coolify-fluentbit:24224';
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) { if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
console.debug(`[${applicationId}] ${addTimestamp}`); console.debug(`[${applicationId}] ${addTimestamp}`);
@@ -514,15 +552,17 @@ export const saveBuildLog = async ({
json: { json: {
line: encrypt(line) line: encrypt(line)
} }
}) });
} catch (error) { } catch (error) {
return await prisma.buildLog.create({ return await prisma.buildLog.create({
data: { data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId line: addTimestamp,
buildId,
time: Number(day().valueOf()),
applicationId
} }
}); });
} }
}; };
export async function copyBaseConfigurationFiles( export async function copyBaseConfigurationFiles(
@@ -610,7 +650,7 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma
export async function saveDockerRegistryCredentials({ url, username, password, workdir }) { export async function saveDockerRegistryCredentials({ url, username, password, workdir }) {
if (!username || !password) { if (!username || !password) {
return null return null;
} }
let decryptedPassword = decrypt(password); let decryptedPassword = decrypt(password);
@@ -619,17 +659,17 @@ export async function saveDockerRegistryCredentials({ url, username, password, w
try { try {
await fs.mkdir(`${workdir}/.docker`); await fs.mkdir(`${workdir}/.docker`);
} catch (error) { } catch (error) {
console.log(error); // console.log(error);
} }
const payload = JSON.stringify({ const payload = JSON.stringify({
"auths": { auths: {
[url]: { [url]: {
"auth": Buffer.from(`${username}:${decryptedPassword}`).toString('base64') auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64')
} }
} }
}) });
await fs.writeFile(`${location}/config.json`, payload) await fs.writeFile(`${location}/config.json`, payload);
return location return location;
} }
export async function buildImage({ export async function buildImage({
applicationId, applicationId,
@@ -640,29 +680,40 @@ export async function buildImage({
isCache = false, isCache = false,
debug = false, debug = false,
dockerFileLocation = '/Dockerfile', dockerFileLocation = '/Dockerfile',
commit commit,
forceRebuild = false
}) { }) {
if (isCache) { if (isCache) {
await saveBuildLog({ line: `Building cache image...`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image...`, buildId, applicationId });
} else { } else {
await saveBuildLog({ line: `Building production image...`, buildId, applicationId }); await saveBuildLog({ line: `Building production image...`, buildId, applicationId });
} }
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}` const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`;
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}` const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`;
let location = null;
let location = null const { dockerRegistry } = await prisma.application.findUnique({
where: { id: applicationId },
const { dockerRegistry } = await prisma.application.findUnique({ where: { id: applicationId }, select: { dockerRegistry: true } }) select: { dockerRegistry: true }
});
if (dockerRegistry) { if (dockerRegistry) {
const { url, username, password } = dockerRegistry const { url, username, password } = dockerRegistry;
location = await saveDockerRegistryCredentials({ url, username, password, workdir }) 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}` }) await executeCommand({
stream: true,
debug,
buildId,
applicationId,
dockerId,
command: `docker ${location ? `--config ${location}` : ''} build ${forceRebuild ? '--no-cache' : ''
} --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
});
const { status } = await prisma.build.findUnique({ where: { id: buildId } }) const { status } = await prisma.build.findUnique({ where: { id: buildId } });
if (status === 'canceled') { if (status === 'canceled') {
throw new Error('Canceled.') throw new Error('Canceled.');
} }
} }
export function makeLabelForSimpleDockerfile({ applicationId, port, type }) { export function makeLabelForSimpleDockerfile({ applicationId, port, type }) {
@@ -741,21 +792,8 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (isPnpm) { if (isPnpm) {
@@ -765,50 +803,33 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
if (installCommand) { if (installCommand) {
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
} }
// Dockerfile.push(`ARG CACHEBUST=1`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }
export async function buildCacheImageForLaravel(data, imageForBuild) { export async function buildCacheImageForLaravel(data, imageForBuild) {
const { workdir, buildId, secrets, pullmergeRequestId } = data; const { workdir, buildId, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
Dockerfile.push(`COPY *.json *.mix.js /app/`); Dockerfile.push(`COPY *.json *.mix.js /app/`);
Dockerfile.push(`COPY resources /app/resources`); Dockerfile.push(`COPY resources /app/resources`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`RUN yarn install && yarn production`); Dockerfile.push(`RUN yarn install && yarn production`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }
export async function buildCacheImageWithCargo(data, imageForBuild) { export async function buildCacheImageWithCargo(data, imageForBuild) {
const { const { applicationId, workdir, buildId } = data;
applicationId,
workdir,
buildId,
} = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`);
@@ -823,6 +844,7 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo install cargo-chef'); Dockerfile.push('RUN cargo install cargo-chef');
Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`); Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`);
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true }); await buildImage({ ...data, isCache: true });
} }

View File

@@ -1,5 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { defaultComposeConfiguration, executeCommand } from '../common'; import { defaultComposeConfiguration, executeCommand, generateSecrets } from '../common';
import { saveBuildLog } from './common'; import { saveBuildLog } from './common';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
@@ -19,65 +19,128 @@ export default async function (data) {
dockerComposeConfiguration, dockerComposeConfiguration,
dockerComposeFileLocation dockerComposeFileLocation
} = data; } = data;
const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`; const baseDir = `${workdir}${baseDirectory}`;
const envFile = `${baseDir}/.env`;
const fileYaml = `${baseDir}${dockerComposeFileLocation}`;
const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8'); const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8');
const dockerComposeYaml = yaml.load(dockerComposeRaw); const dockerComposeYaml = yaml.load(dockerComposeRaw);
if (!dockerComposeYaml.services) { if (!dockerComposeYaml.services) {
throw 'No Services found in docker-compose file.'; throw 'No Services found in docker-compose file.';
} }
const envs = []; let envs = [];
let buildEnvs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)];
if (pullmergeRequestId) { buildEnvs = [...buildEnvs, ...generateSecrets(secrets, pullmergeRequestId, true, null, true)];
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(envFile, envs.join('\n'));
const composeVolumes = []; const composeVolumes = [];
if (volumes.length > 0) { if (volumes.length > 0) {
for (const volume of volumes) { for (const volume of volumes) {
let [v, path] = volume.split(':'); let [v, path] = volume.split(':');
composeVolumes[v] = { if (!v.startsWith('.') && !v.startsWith('..') && !v.startsWith('/') && !v.startsWith('~')) {
name: v composeVolumes[v] = {
}; name: v
};
}
} }
} }
let networks = {}; let networks = {};
for (let [key, value] of Object.entries(dockerComposeYaml.services)) { for (let [key, value] of Object.entries(dockerComposeYaml.services)) {
value['container_name'] = `${applicationId}-${key}`; value['container_name'] = `${applicationId}-${key}`;
value['env_file'] = envFound ? [`${workdir}/.env`] : [];
if (value['env_file']) {
delete value['env_file'];
}
value['env_file'] = [envFile];
// let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'];
// let finalEnvs = [...envs];
// if (Object.keys(environment).length > 0) {
// for (const arg of Object.keys(environment)) {
// const [key, _] = arg.split('=');
// if (finalEnvs.filter((env) => env.startsWith(key)).length === 0) {
// finalEnvs.push(arg);
// }
// }
// }
// value['environment'] = [...finalEnvs];
let build = typeof value['build'] === 'undefined' ? [] : value['build'];
if (typeof build === 'string') {
build = { context: build };
}
const buildArgs = typeof build['args'] === 'undefined' ? [] : build['args'];
let finalBuildArgs = [...buildEnvs];
if (Object.keys(buildArgs).length > 0) {
for (const arg of Object.keys(buildArgs)) {
const [key, _] = arg.split('=');
if (finalBuildArgs.filter((env) => env.startsWith(key)).length === 0) {
finalBuildArgs.push(arg);
}
}
}
if (build.length > 0 || buildArgs.length > 0) {
value['build'] = {
...build,
args: finalBuildArgs
};
}
value['labels'] = labels; value['labels'] = labels;
// TODO: If we support separated volume for each service, we need to add it here // TODO: If we support separated volume for each service, we need to add it here
if (value['volumes']?.length > 0) { if (value['volumes']?.length > 0) {
value['volumes'] = value['volumes'].map((volume) => { value['volumes'] = value['volumes'].map((volume) => {
let [v, path, permission] = volume.split(':'); if (typeof volume === 'string') {
if (!path) { let [v, path, permission] = volume.split(':');
path = v; if (
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; v.startsWith('.') ||
} else { v.startsWith('..') ||
v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; v.startsWith('/') ||
v.startsWith('~') ||
v.startsWith('$PWD')
) {
v = v
.replace(/^\./, `~`)
.replace(/^\.\./, '~')
.replace(/^\$PWD/, '~');
} else {
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 (typeof volume === 'object') {
let { source, target, mode } = volume;
if (
source.startsWith('.') ||
source.startsWith('..') ||
source.startsWith('/') ||
source.startsWith('~') ||
source.startsWith('$PWD')
) {
source = source
.replace(/^\./, `~`)
.replace(/^\.\./, '~')
.replace(/^\$PWD/, '~');
} else {
if (!target) {
target = source;
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
} else {
source = `${applicationId}${source.replace(/\//gi, '-').replace(/\./gi, '')}`;
}
}
return `${source}:${target}${mode ? ':' + mode : ''}`;
} }
composeVolumes[v] = {
name: v
};
return `${v}:${path}${permission ? ':' + permission : ''}`;
}); });
} }
if (volumes.length > 0) { if (volumes.length > 0) {
@@ -85,17 +148,24 @@ export default async function (data) {
value['volumes'].push(volume); value['volumes'].push(volume);
} }
} }
if (dockerComposeConfiguration[key].port) { if (dockerComposeConfiguration[key]?.port) {
value['expose'] = [dockerComposeConfiguration[key].port]; value['expose'] = [dockerComposeConfiguration[key].port];
} }
if (value['networks']?.length > 0) { value['networks'] = [network];
value['networks'].forEach((network) => { if (value['build']?.network) {
networks[network] = { delete value['build']['network'];
name: network
};
});
} }
value['networks'] = [...(value['networks'] || ''), network]; // if (value['networks']?.length > 0) {
// value['networks'].forEach((network) => {
// networks[network] = {
// name: network
// };
// });
// value['networks'] = [...(value['networks'] || ''), network];
// } else {
// value['networks'] = [network];
// }
dockerComposeYaml.services[key] = { dockerComposeYaml.services[key] = {
...dockerComposeYaml.services[key], ...dockerComposeYaml.services[key],
restart: defaultComposeConfiguration(network).restart, restart: defaultComposeConfiguration(network).restart,
@@ -106,13 +176,14 @@ export default async function (data) {
dockerComposeYaml['volumes'] = { ...composeVolumes }; dockerComposeYaml['volumes'] = { ...composeVolumes };
} }
dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } });
await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml));
await executeCommand({ await executeCommand({
debug, debug,
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} pull` command: `docker compose --project-directory ${workdir} -f ${fileYaml} pull`
}); });
await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId }); await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId });
await executeCommand({ await executeCommand({
@@ -120,7 +191,7 @@ export default async function (data) {
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker compose --project-directory ${workdir} build --progress plain` command: `docker compose --project-directory ${workdir} -f ${fileYaml} build --progress plain`
}); });
await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId }); await saveBuildLog({ line: 'Building images from Compose file...', buildId, applicationId });
} }

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common'; import { buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (depsFound) { if (depsFound) {
@@ -48,6 +36,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN deno cache ${denoMainFile}`); Dockerfile.push(`RUN deno cache ${denoMainFile}`);
Dockerfile.push(`ENV NO_COLOR true`); Dockerfile.push(`ENV NO_COLOR true`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`); Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -1,47 +1,27 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common'; import { buildImage } from './common';
export default async function (data) { export default async function (data) {
let { let { workdir, buildId, baseDirectory, secrets, pullmergeRequestId, dockerFileLocation } = data;
applicationId,
debug,
tag,
workdir,
buildId,
baseDirectory,
secrets,
pullmergeRequestId,
dockerFileLocation
} = data
const file = `${workdir}${baseDirectory}${dockerFileLocation}`; const file = `${workdir}${baseDirectory}${dockerFileLocation}`;
data.workdir = `${workdir}${baseDirectory}`; data.workdir = `${workdir}${baseDirectory}`;
const DockerfileRaw = await fs.readFile(`${file}`, 'utf8') const DockerfileRaw = await fs.readFile(`${file}`, 'utf8');
const Dockerfile: Array<string> = DockerfileRaw const Dockerfile: Array<string> = DockerfileRaw.toString().trim().split('\n');
.toString()
.trim()
.split('\n');
Dockerfile.forEach((line, index) => { Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) { if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`); Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`);
} }
}); });
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.forEach((line, index) => {
if ( if (line.startsWith('FROM')) {
(pullmergeRequestId && secret.isPRMRSecret) || Dockerfile.splice(index + 1, 0, env);
(!pullmergeRequestId && !secret.isPRMRSecret)
) {
Dockerfile.forEach((line, index) => {
if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}=${secret.value}`);
}
});
} }
} });
}); });
} }
await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
await fs.writeFile(`${workdir}${dockerFileLocation}`, Dockerfile.join('\n'));
await buildImage(data); await buildImage(data);
} }

View File

@@ -8,10 +8,11 @@ const createDockerfile = async (data, imageforBuild): Promise<void> => {
Dockerfile.push(`FROM ${imageforBuild}`); Dockerfile.push(`FROM ${imageforBuild}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -1,12 +1,18 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageForLaravel, buildImage } from './common'; import { buildCacheImageForLaravel, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { workdir, applicationId, tag, buildId, port } = data; const { workdir, applicationId, tag, buildId, port, secrets, pullmergeRequestId } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) {
generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
Dockerfile.push(env);
});
}
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`); Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`);
Dockerfile.push(`COPY --chown=application:application composer.* ./`); Dockerfile.push(`COPY --chown=application:application composer.* ./`);
@@ -24,6 +30,7 @@ const createDockerfile = async (data, image): Promise<void> => {
`COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json`
); );
Dockerfile.push(`COPY --chown=application:application . ./`); Dockerfile.push(`COPY --chown=application:application . ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import { buildCacheImageWithNode, buildImage } from './common'; import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data; const { buildId, applicationId, tag, port, startCommand, workdir, publishDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
const isPnpm = startCommand.includes('pnpm'); const isPnpm = startCommand.includes('pnpm');
@@ -12,8 +12,8 @@ const createDockerfile = async (data, image): Promise<void> => {
if (isPnpm) { if (isPnpm) {
Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7');
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (isPnpm) { if (isPnpm) {
@@ -48,13 +36,15 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') { } else if (deploymentType === 'static') {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage, checkPnpm } from './common'; import { buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -20,21 +21,8 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (isPnpm) { if (isPnpm) {
@@ -46,6 +34,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
} }
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (isPnpm) { if (isPnpm) {
@@ -48,13 +36,15 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`); Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`RUN ${buildCommand}`); Dockerfile.push(`RUN ${buildCommand}`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
Dockerfile.push(`CMD ${startCommand}`); Dockerfile.push(`CMD ${startCommand}`);
} else if (deploymentType === 'static') { } else if (deploymentType === 'static') {
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
} }

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common'; import { buildImage } from './common';
const createDockerfile = async (data, image, htaccessFound): Promise<void> => { const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
@@ -13,21 +14,8 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
@@ -40,6 +28,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
} }
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildImage } from './common'; import { buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -18,21 +19,8 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (pythonWSGI?.toLowerCase() === 'gunicorn') { if (pythonWSGI?.toLowerCase() === 'gunicorn') {
@@ -64,7 +52,7 @@ const createDockerfile = async (data, image): Promise<void> => {
} else { } else {
Dockerfile.push(`CMD python ${pythonModule}`); Dockerfile.push(`CMD python ${pythonModule}`);
} }
Dockerfile.push('RUN rm -fr .git');
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -8,10 +8,11 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

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

View File

@@ -1,4 +1,5 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import { generateSecrets } from '../common';
import { buildCacheImageWithNode, buildImage } from './common'; import { buildCacheImageWithNode, buildImage } from './common';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image): Promise<void> => {
@@ -25,31 +26,19 @@ const createDockerfile = async (data, image): Promise<void> => {
} }
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => {
if (secret.isBuildSecret) { Dockerfile.push(env);
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`);
} else {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
}); });
} }
if (buildCommand) { if (buildCommand) {
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
} else { } else {
Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
} }
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -8,10 +8,11 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -8,10 +8,11 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.buildId=${buildId}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`);
Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app${publishDirectory} ./`);
if (baseImage?.includes('nginx')) { if (baseImage?.includes('nginx')) {
Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`);
} }
Dockerfile.push('RUN rm -fr .git');
Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`EXPOSE ${port}`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };

View File

@@ -1,6 +1,5 @@
import { exec } from 'node:child_process';
import util from 'util';
import fs from 'fs/promises'; import fs from 'fs/promises';
import fsNormal from 'fs';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import forge from 'node-forge'; import forge from 'node-forge';
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
@@ -8,21 +7,22 @@ import type { Config } from 'unique-names-generator';
import generator from 'generate-password'; import generator from 'generate-password';
import crypto from 'crypto'; import crypto from 'crypto';
import { promises as dns } from 'dns'; import { promises as dns } from 'dns';
import * as Sentry from '@sentry/node';
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
import os from 'os'; import os from 'os';
import sshConfig from 'ssh-config'; import * as SSHConfig from 'ssh-config/src/ssh-config';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { checkContainer, removeContainer } from './docker'; import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs'; import { day } from './dayjs';
import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common'; import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler'; import { scheduler } from './scheduler';
import type { ExecaChildProcess } from 'execa'; import type { ExecaChildProcess } from 'execa';
import { FastifyReply } from 'fastify';
export const version = '3.12.1'; export const version = '3.12.39';
export const isDev = process.env.NODE_ENV === 'development'; export const isDev = process.env.NODE_ENV === 'development';
export const sentryDSN = export const proxyPort = process.env.COOLIFY_PROXY_PORT;
'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; export const proxySecurePort = process.env.COOLIFY_PROXY_SECURE_PORT;
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
const customConfig: Config = { const customConfig: Config = {
dictionaries: [adjectives, colors, animals], dictionaries: [adjectives, colors, animals],
@@ -170,13 +170,19 @@ export const base64Encode = (text: string): string => {
export const base64Decode = (text: string): string => { export const base64Decode = (text: string): string => {
return Buffer.from(text, 'base64').toString('ascii'); return Buffer.from(text, 'base64').toString('ascii');
}; };
export const getSecretKey = () => {
if (process.env['COOLIFY_SECRET_KEY_BETTER']) {
return process.env['COOLIFY_SECRET_KEY_BETTER'];
}
return process.env['COOLIFY_SECRET_KEY'];
};
export const decrypt = (hashString: string) => { export const decrypt = (hashString: string) => {
if (hashString) { if (hashString) {
try { try {
const hash = JSON.parse(hashString); const hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv( const decipher = crypto.createDecipheriv(
algorithm, algorithm,
process.env['COOLIFY_SECRET_KEY'], getSecretKey(),
Buffer.from(hash.iv, 'hex') Buffer.from(hash.iv, 'hex')
); );
const decrpyted = Buffer.concat([ const decrpyted = Buffer.concat([
@@ -193,7 +199,7 @@ export const decrypt = (hashString: string) => {
export const encrypt = (text: string) => { export const encrypt = (text: string) => {
if (text) { if (text) {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); const cipher = crypto.createCipheriv(algorithm, getSecretKey(), iv);
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]); const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
return JSON.stringify({ return JSON.stringify({
iv: iv.toString('hex'), iv: iv.toString('hex'),
@@ -400,8 +406,8 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MongoDB', fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb', baseImage: 'bitnami/mongodb',
baseImageARM: 'mongo', baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'], versions: ['6.0', '5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2'] versionsARM: ['6.0', '5.0', '4.4', '4.2']
}, },
{ {
name: 'mysql', name: 'mysql',
@@ -416,16 +422,16 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'MariaDB', fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb', baseImage: 'bitnami/mariadb',
baseImageARM: 'mariadb', baseImageARM: 'mariadb',
versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'], versions: ['10.11', '10.10', '10.9', '10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] versionsARM: ['10.11', '10.10', '10.9', '10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
}, },
{ {
name: 'postgresql', name: 'postgresql',
fancyName: 'PostgreSQL', fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql', baseImage: 'bitnami/postgresql',
baseImageARM: 'postgres', baseImageARM: 'postgres',
versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'], versions: ['15.2.0', '14.7.0', '14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22'] versionsARM: ['15.2', '14.7', '14.5', '13.8', '12.12', '11.17', '10.22']
}, },
{ {
name: 'redis', name: 'redis',
@@ -440,14 +446,14 @@ export const supportedDatabaseTypesAndVersions = [
fancyName: 'CouchDB', fancyName: 'CouchDB',
baseImage: 'bitnami/couchdb', baseImage: 'bitnami/couchdb',
baseImageARM: 'couchdb', baseImageARM: 'couchdb',
versions: ['3.2.2', '3.1.2', '2.3.1'], versions: ['3.3.1', '3.2.2', '3.1.2', '2.3.1'],
versionsARM: ['3.2.2', '3.1.2', '2.3.1'] versionsARM: ['3.3', '3.2.2', '3.1.2', '2.3.1']
}, },
{ {
name: 'edgedb', name: 'edgedb',
fancyName: 'EdgeDB', fancyName: 'EdgeDB',
baseImage: 'edgedb/edgedb', baseImage: 'edgedb/edgedb',
versions: ['latest', '2.1', '2.0', '1.4'] versions: ['latest', '2.9', '2.8', '2.7']
} }
]; ];
@@ -496,33 +502,56 @@ export async function getFreeSSHLocalPort(id: string): Promise<number | boolean>
return false; return false;
} }
/**
* Update the ssh config file with a host
*
* @param id Destination ID
* @returns
*/
export async function createRemoteEngineConfiguration(id: string) { export async function createRemoteEngineConfiguration(id: string) {
const homedir = os.homedir();
const sshKeyFile = `/tmp/id_rsa-${id}`; const sshKeyFile = `/tmp/id_rsa-${id}`;
const localPort = await getFreeSSHLocalPort(id); const localPort = await getFreeSSHLocalPort(id);
const { const {
sshKey: { privateKey }, sshKey: { privateKey },
network,
remoteIpAddress, remoteIpAddress,
remotePort, remotePort,
remoteUser remoteUser
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }); } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
// Write new keyfile
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }); await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
const config = sshConfig.parse('');
const Host = `${remoteIpAddress}-remote`; const Host = `${remoteIpAddress}-remote`;
// Removes previous ssh-keys
try { try {
await executeCommand({ command: `ssh-keygen -R ${Host}` }); await executeCommand({ command: `ssh-keygen -R ${Host}` });
await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` }); await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` });
await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` }); await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` });
} catch (error) {} } catch (error) {
//
}
const homedir = os.homedir();
let currentConfigFileContent = '';
try {
// Read the current config file
currentConfigFileContent = (await fs.readFile(`${homedir}/.ssh/config`)).toString();
} catch (error) {
// File doesn't exist, so we do nothing, a new one is going to be created
}
// Parse the config file
const config = SSHConfig.parse(currentConfigFileContent);
// Remove current config for the given host
const found = config.find({ Host }); const found = config.find({ Host });
const foundIp = config.find({ Host: remoteIpAddress }); const foundIp = config.find({ Host: remoteIpAddress });
if (found) config.remove({ Host }); if (found) config.remove({ Host });
if (foundIp) config.remove({ Host: remoteIpAddress }); if (foundIp) config.remove({ Host: remoteIpAddress });
// Create the new config
config.append({ config.append({
Host, Host,
Hostname: remoteIpAddress, Hostname: remoteIpAddress,
@@ -535,13 +564,17 @@ export async function createRemoteEngineConfiguration(id: string) {
ControlPersist: '10m' ControlPersist: '10m'
}); });
// Check if .ssh folder exists, and if not create one
try { try {
await fs.stat(`${homedir}/.ssh/`); await fs.stat(`${homedir}/.ssh/`);
} catch (error) { } catch (error) {
await fs.mkdir(`${homedir}/.ssh/`); await fs.mkdir(`${homedir}/.ssh/`);
} }
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
// Write the config
return await fs.writeFile(`${homedir}/.ssh/config`, SSHConfig.stringify(config));
} }
export async function executeCommand({ export async function executeCommand({
command, command,
dockerId = null, dockerId = null,
@@ -550,7 +583,8 @@ export async function executeCommand({
stream = false, stream = false,
buildId, buildId,
applicationId, applicationId,
debug debug,
timeout = 0
}: { }: {
command: string; command: string;
sshCommand?: boolean; sshCommand?: boolean;
@@ -560,6 +594,7 @@ export async function executeCommand({
buildId?: string; buildId?: string;
applicationId?: string; applicationId?: string;
debug?: boolean; debug?: boolean;
timeout?: number;
}): Promise<ExecaChildProcess<string>> { }): Promise<ExecaChildProcess<string>> {
const { execa, execaCommand } = await import('execa'); const { execa, execaCommand } = await import('execa');
const { parse } = await import('shell-quote'); const { parse } = await import('shell-quote');
@@ -584,20 +619,26 @@ export async function executeCommand({
} }
if (sshCommand) { if (sshCommand) {
if (shell) { if (shell) {
return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`); return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`, {
timeout
});
} }
return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs]); return await execa('ssh', [`${remoteIpAddress}-remote`, dockerCommand, ...dockerArgs], {
timeout
});
} }
if (stream) { if (stream) {
return await new Promise(async (resolve, reject) => { return await new Promise(async (resolve, reject) => {
let subprocess = null; let subprocess = null;
if (shell) { if (shell) {
subprocess = execaCommand(command, { subprocess = execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} else { } else {
subprocess = execa(dockerCommand, dockerArgs, { subprocess = execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} }
const logs = []; const logs = [];
@@ -651,19 +692,26 @@ export async function executeCommand({
} else { } else {
if (shell) { if (shell) {
return await execaCommand(command, { return await execaCommand(command, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} else { } else {
return await execa(dockerCommand, dockerArgs, { return await execa(dockerCommand, dockerArgs, {
env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine },
timeout
}); });
} }
} }
} else { } else {
if (shell) { if (shell) {
return execaCommand(command, { shell: true }); return execaCommand(command, {
shell: true,
timeout
});
} }
return await execa(dockerCommand, dockerArgs); return await execa(dockerCommand, dockerArgs, {
timeout
});
} }
} }
@@ -712,10 +760,12 @@ export async function startTraefikProxy(id: string): Promise<void> {
-v coolify-traefik-letsencrypt:/etc/traefik/acme \ -v coolify-traefik-letsencrypt:/etc/traefik/acme \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--network coolify-infra \ --network coolify-infra \
-p "80:80" \ -p ${proxyPort ? `${proxyPort}:80` : `80:80`} \
-p "443:443" \ -p ${proxySecurePort ? `${proxySecurePort}:443` : `443:443`} \
${isDev ? '-p "8080:8080"' : ''} \
--name coolify-proxy \ --name coolify-proxy \
-d ${defaultTraefikImage} \ -d ${defaultTraefikImage} \
${isDev ? '--api.insecure=true' : ''} \
--entrypoints.web.address=:80 \ --entrypoints.web.address=:80 \
--entrypoints.web.forwardedHeaders.insecure=true \ --entrypoints.web.forwardedHeaders.insecure=true \
--entrypoints.websecure.address=:443 \ --entrypoints.websecure.address=:443 \
@@ -795,7 +845,7 @@ export function generateToken() {
{ {
nbf: Math.floor(Date.now() / 1000) - 30 nbf: Math.floor(Date.now() / 1000) - 30
}, },
process.env['COOLIFY_SECRET_KEY'] getSecretKey()
); );
} }
export function generatePassword({ export function generatePassword({
@@ -818,101 +868,101 @@ export function generatePassword({
type DatabaseConfiguration = type DatabaseConfiguration =
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
MYSQL_DATABASE: string; MYSQL_DATABASE: string;
MYSQL_PASSWORD: string; MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string; MYSQL_ROOT_USER: string;
MYSQL_USER: string; MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string; MYSQL_ROOT_PASSWORD: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
MONGO_INITDB_ROOT_USERNAME?: string; MONGO_INITDB_ROOT_USERNAME?: string;
MONGO_INITDB_ROOT_PASSWORD?: string; MONGO_INITDB_ROOT_PASSWORD?: string;
MONGODB_ROOT_USER?: string; MONGODB_ROOT_USER?: string;
MONGODB_ROOT_PASSWORD?: string; MONGODB_ROOT_PASSWORD?: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
MARIADB_ROOT_USER: string; MARIADB_ROOT_USER: string;
MARIADB_ROOT_PASSWORD: string; MARIADB_ROOT_PASSWORD: string;
MARIADB_USER: string; MARIADB_USER: string;
MARIADB_PASSWORD: string; MARIADB_PASSWORD: string;
MARIADB_DATABASE: string; MARIADB_DATABASE: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
POSTGRES_PASSWORD?: string; POSTGRES_PASSWORD?: string;
POSTGRES_USER?: string; POSTGRES_USER?: string;
POSTGRES_DB?: string; POSTGRES_DB?: string;
POSTGRESQL_POSTGRES_PASSWORD?: string; POSTGRESQL_POSTGRES_PASSWORD?: string;
POSTGRESQL_USERNAME?: string; POSTGRESQL_USERNAME?: string;
POSTGRESQL_PASSWORD?: string; POSTGRESQL_PASSWORD?: string;
POSTGRESQL_DATABASE?: string; POSTGRESQL_DATABASE?: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
REDIS_AOF_ENABLED: string; REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string; REDIS_PASSWORD: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
COUCHDB_PASSWORD: string; COUCHDB_PASSWORD: string;
COUCHDB_USER: string; COUCHDB_USER: string;
}; };
} }
| { | {
volume: string; volume: string;
image: string; image: string;
command?: string; command?: string;
ulimits: Record<string, unknown>; ulimits: Record<string, unknown>;
privatePort: number; privatePort: number;
environmentVariables: { environmentVariables: {
EDGEDB_SERVER_PASSWORD: string; EDGEDB_SERVER_PASSWORD: string;
EDGEDB_SERVER_USER: string; EDGEDB_SERVER_USER: string;
EDGEDB_SERVER_DATABASE: string; EDGEDB_SERVER_DATABASE: string;
EDGEDB_SERVER_TLS_CERT_MODE: string; EDGEDB_SERVER_TLS_CERT_MODE: string;
}; };
}; };
export function generateDatabaseConfiguration(database: any, arch: string): DatabaseConfiguration { export function generateDatabaseConfiguration(database: any): DatabaseConfiguration {
const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } = const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } =
database; database;
const baseImage = getDatabaseImage(type, arch); const baseImage = getDatabaseImage(type);
if (type === 'mysql') { if (type === 'mysql') {
const configuration = { const configuration = {
privatePort: 3306, privatePort: 3306,
@@ -927,7 +977,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/mysql/data`, volume: `${id}-${type}-data:/bitnami/mysql/data`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`; configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
} }
return configuration; return configuration;
@@ -945,7 +995,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/mariadb`, volume: `${id}-${type}-data:/bitnami/mariadb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/var/lib/mysql`; configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
} }
return configuration; return configuration;
@@ -960,7 +1010,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/mongodb`, volume: `${id}-${type}-data:/bitnami/mongodb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.environmentVariables = { configuration.environmentVariables = {
MONGO_INITDB_ROOT_USERNAME: rootUser, MONGO_INITDB_ROOT_USERNAME: rootUser,
MONGO_INITDB_ROOT_PASSWORD: rootUserPassword MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
@@ -981,8 +1031,8 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/postgresql`, volume: `${id}-${type}-data:/bitnami/postgresql`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`; configuration.volume = `${id}-${type}-data:/var/lib/postgresql/data`;
configuration.environmentVariables = { configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword, POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser, POSTGRES_USER: dbUser,
@@ -1005,11 +1055,10 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/redis/data`, volume: `${id}-${type}-data:/bitnami/redis/data`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/data`; configuration.volume = `${id}-${type}-data:/data`;
configuration.command = `/usr/local/bin/redis-server --appendonly ${ configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no'
appendOnly ? 'yes' : 'no' } --requirepass ${dbUserPassword}`;
} --requirepass ${dbUserPassword}`;
} }
return configuration; return configuration;
} else if (type === 'couchdb') { } else if (type === 'couchdb') {
@@ -1023,7 +1072,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
volume: `${id}-${type}-data:/bitnami/couchdb`, volume: `${id}-${type}-data:/bitnami/couchdb`,
ulimits: {} ulimits: {}
}; };
if (isARM(arch)) { if (isARM()) {
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`; configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
} }
return configuration; return configuration;
@@ -1043,16 +1092,17 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data
return configuration; return configuration;
} }
} }
export function isARM(arch: string) { export function isARM() {
const arch = process.arch;
if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') { if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
return true; return true;
} }
return false; return false;
} }
export function getDatabaseImage(type: string, arch: string): string { export function getDatabaseImage(type: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
if (isARM(arch)) { if (isARM()) {
return found.baseImageARM || found.baseImage; return found.baseImageARM || found.baseImage;
} }
return found.baseImage; return found.baseImage;
@@ -1060,10 +1110,10 @@ export function getDatabaseImage(type: string, arch: string): string {
return ''; return '';
} }
export function getDatabaseVersions(type: string, arch: string): string[] { export function getDatabaseVersions(type: string): string[] {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
if (isARM(arch)) { if (isARM()) {
return found.versionsARM || found.versions; return found.versionsARM || found.versions;
} }
return found.versions; return found.versions;
@@ -1093,12 +1143,12 @@ export type ComposeFileService = {
command?: string; command?: string;
ports?: string[]; ports?: string[];
build?: build?:
| { | {
context: string; context: string;
dockerfile: string; dockerfile: string;
args?: Record<string, unknown>; args?: Record<string, unknown>;
} }
| string; | string;
deploy?: { deploy?: {
restart_policy?: { restart_policy?: {
condition?: string; condition?: string;
@@ -1169,7 +1219,7 @@ export const createDirectories = async ({
let workdirFound = false; let workdirFound = false;
try { try {
workdirFound = !!(await fs.stat(workdir)); workdirFound = !!(await fs.stat(workdir));
} catch (error) {} } catch (error) { }
if (workdirFound) { if (workdirFound) {
await executeCommand({ command: `rm -fr ${workdir}` }); await executeCommand({ command: `rm -fr ${workdir}` });
} }
@@ -1629,8 +1679,8 @@ export function errorHandler({
type?: string | null; type?: string | null;
}) { }) {
if (message.message) message = message.message; if (message.message) message = message.message;
if (type === 'normal') { if (message.includes('Unique constraint failed')) {
Sentry.captureException(message); message = 'This data is unique and already exists. Please try again with a different value.';
} }
throw { status, message }; throw { status, message };
} }
@@ -1693,7 +1743,7 @@ export async function stopBuild(buildId, applicationId) {
} }
} }
count++; count++;
} catch (error) {} } catch (error) { }
}, 100); }, 100);
}); });
} }
@@ -1712,69 +1762,28 @@ export function convertTolOldVolumeNames(type) {
} }
} }
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { export async function cleanupDockerStorage(dockerId, volumes = false) {
// Cleanup old coolify images // Cleanup images that are not used by any container
try { try {
let { stdout: images } = await executeCommand({ await executeCommand({ dockerId, command: `docker image prune -af` });
} catch (error) { }
// Prune coolify managed containers
try {
await executeCommand({
dockerId, dockerId,
command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`, command: `docker container prune -f --filter "label=coolify.managed=true"`
shell: true
}); });
} catch (error) { }
images = images.trim(); // Cleanup build caches
if (images) { try {
await executeCommand({ await executeCommand({ dockerId, command: `docker builder prune -af` });
dockerId, } catch (error) { }
command: `docker rmi -f ${images}" -q | xargs -r`, if (volumes) {
shell: true
});
}
} catch (error) {}
if (lowDiskSpace || force) {
// Cleanup images that are not used
try { try {
await executeCommand({ dockerId, command: `docker image prune -f` }); await executeCommand({ dockerId, command: `docker volume prune -af` });
} catch (error) {} } 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) {}
// Cleanup build caches
try {
await executeCommand({ dockerId, command: `docker builder prune -a -f` });
} catch (error) {}
} }
} }
@@ -1875,3 +1884,112 @@ export async function pushToRegistry(
command: pushCommand command: pushCommand
}); });
} }
function parseSecret(secret, isBuild) {
if (secret.value.includes('$')) {
secret.value = secret.value.replaceAll('$', '$$$$');
}
if (secret.value.includes('\\n')) {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
} else if (secret.value.includes(' ')) {
if (isBuild) {
return `ARG ${secret.name}='${secret.value}'`;
} else {
return `${secret.name}='${secret.value}'`;
}
} else {
if (isBuild) {
return `ARG ${secret.name}=${secret.value}`;
} else {
return `${secret.name}=${secret.value}`;
}
}
}
export function generateSecrets(
secrets: Array<any>,
pullmergeRequestId: string,
isBuild = false,
port = null,
compose = false
): Array<string> {
const envs = [];
const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret);
const normalSecrets = secrets.filter((s) => !s.isPRMRSecret);
if (pullmergeRequestId && isPRMRSecret.length > 0) {
isPRMRSecret.forEach((secret) => {
if (isBuild && !secret.isBuildSecret) {
return;
}
const build = isBuild && secret.isBuildSecret;
envs.push(parseSecret(secret, compose ? false : build));
});
}
if (!pullmergeRequestId && normalSecrets.length > 0) {
normalSecrets.forEach((secret) => {
if (isBuild && !secret.isBuildSecret) {
return;
}
const build = isBuild && secret.isBuildSecret;
envs.push(parseSecret(secret, compose ? false : build));
});
}
const portFound = envs.filter((env) => env.startsWith('PORT'));
if (portFound.length === 0 && port && !isBuild) {
envs.push(`PORT=${port}`);
}
const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV'));
if (nodeEnv.length === 0 && !isBuild) {
envs.push(`NODE_ENV=production`);
}
return envs;
}
export async function backupDatabaseNow(database, reply) {
const backupFolder = '/tmp'
const fileName = `${database.id}-${new Date().getTime()}.gz`
const backupFileName = `${backupFolder}/${fileName}`
const backupStorageFilename = `/app/backups/${fileName}`
let command = null
switch (database?.type) {
case 'postgresql':
command = `docker exec ${database.id} sh -c "PGPASSWORD=${database.rootUserPassword} pg_dumpall -U postgres | gzip > ${backupFileName}"`
break;
case 'mongodb':
command = `docker exec ${database.id} sh -c "mongodump --archive=${backupFileName} --gzip --username=${database.rootUser} --password=${database.rootUserPassword}"`
break;
case 'mysql':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'mariadb':
command = `docker exec ${database.id} sh -c "mysqldump --all-databases --single-transaction --quick --lock-tables=false --user=${database.rootUser} --password=${database.rootUserPassword} | gzip > ${backupFileName}"`
break;
case 'couchdb':
command = `docker exec ${database.id} sh -c "tar -czvf ${backupFileName} /bitnami/couchdb/data"`
break;
default:
return;
}
await executeCommand({
dockerId: database.destinationDockerId,
command,
});
const copyCommand = `docker cp ${database.id}:${backupFileName} ${backupFileName}`
await executeCommand({
dockerId: database.destinationDockerId,
command: copyCommand
});
await executeCommand({
dockerId: database.destinationDockerId,
command: `docker cp ${database.id}:${backupFileName} /app/backups/`
});
const stream = fsNormal.createReadStream(backupFileName);
reply.header('Content-Type', 'application/octet-stream');
reply.header('Content-Disposition', `attachment; filename=${fileName}`);
reply.header('Content-Length', fsNormal.statSync(backupFileName).size);
reply.header('Content-Transfer-Encoding', 'binary');
return reply.send(stream)
}

View File

@@ -12,7 +12,8 @@ export default async function ({
buildId, buildId,
privateSshKey, privateSshKey,
customPort, customPort,
forPublic forPublic,
customUser,
}: { }: {
applicationId: string; applicationId: string;
workdir: string; workdir: string;
@@ -25,6 +26,7 @@ export default async function ({
privateSshKey: string; privateSshKey: string;
customPort: number; customPort: number;
forPublic: boolean; forPublic: boolean;
customUser: string;
}): Promise<string> { }): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
if (!forPublic) { if (!forPublic) {
@@ -53,7 +55,7 @@ export default async function ({
} else { } else {
await executeCommand({ await executeCommand({
command: 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 `git clone -q -b ${branch} ${customUser}@${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
} }
); );
} }

View File

@@ -1,51 +1,47 @@
import { isARM, isDev } from "./common"; import { isARM, isDev } from './common';
import fs from 'fs/promises'; import fs from 'fs/promises';
export async function getTemplates() { export async function getTemplates() {
const templatePath = isDev ? './templates.json' : '/app/templates.json'; const templatePath = isDev ? './templates.json' : '/app/templates.json';
const open = await fs.open(templatePath, 'r'); const open = await fs.open(templatePath, 'r');
try { try {
let data = await open.readFile({ encoding: 'utf-8' }); let data = await open.readFile({ encoding: 'utf-8' });
let jsonData = JSON.parse(data) let jsonData = JSON.parse(data);
if (isARM(process.arch)) { if (isARM()) {
jsonData = jsonData.filter(d => d.arch !== 'amd64') jsonData = jsonData.filter((d) => d.arch !== 'amd64');
} }
return jsonData; return jsonData;
} catch (error) { } catch (error) {
return [] return [];
} finally { } finally {
await open?.close() await open?.close();
} }
} }
const compareSemanticVersions = (a: string, b: string) => { const compareSemanticVersions = (a: string, b: string) => {
const a1 = a.split('.'); const a1 = a.split('.');
const b1 = b.split('.'); const b1 = b.split('.');
const len = Math.min(a1.length, b1.length); const len = Math.min(a1.length, b1.length);
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
const a2 = +a1[i] || 0; const a2 = +a1[i] || 0;
const b2 = +b1[i] || 0; const b2 = +b1[i] || 0;
if (a2 !== b2) { if (a2 !== b2) {
return a2 > b2 ? 1 : -1; return a2 > b2 ? 1 : -1;
} }
} }
return b1.length - a1.length; return b1.length - a1.length;
}; };
export async function getTags(type: string) { export async function getTags(type: string) {
try {
try { if (type) {
if (type) { const tagsPath = isDev ? './tags.json' : '/app/tags.json';
const tagsPath = isDev ? './tags.json' : '/app/tags.json'; const data = await fs.readFile(tagsPath, 'utf8');
const data = await fs.readFile(tagsPath, 'utf8') let tags = JSON.parse(data);
let tags = JSON.parse(data) if (tags) {
if (tags) { tags = tags.find((tag: any) => tag.name.includes(type));
tags = tags.find((tag: any) => tag.name.includes(type)) tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
tags.tags = tags.tags.sort(compareSemanticVersions).reverse(); return tags;
return tags }
} }
} } catch (error) {
} catch (error) { return [];
return [] }
}
} }

View File

@@ -40,7 +40,7 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
const { id } = request.params; const { id } = request.params;
const teamId = request.user.teamId; const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId }); const service = await getServiceFromDB({ id, teamId });
const arm = isARM(service.arch); const arm = isARM();
const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } = const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } =
service; service;
@@ -50,24 +50,12 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
const config = {}; const config = {};
for (const s in template.services) { for (const s in template.services) {
let newEnvironments = [] let newEnvironments = []
if (arm) { if (template.services[s]?.environment?.length > 0) {
if (template.services[s]?.environmentArm?.length > 0) { for (const environment of template.services[s].environment) {
for (const environment of template.services[s].environmentArm) { let [env, ...value] = environment.split("=");
let [env, ...value] = environment.split("="); value = value.join("=")
value = value.join("=") if (!value.startsWith('$$secret') && value !== '') {
if (!value.startsWith('$$secret') && value !== '') { newEnvironments.push(`${env}=${value}`)
newEnvironments.push(`${env}=${value}`)
}
}
}
} else {
if (template.services[s]?.environment?.length > 0) {
for (const environment of template.services[s].environment) {
let [env, ...value] = environment.split("=");
value = value.join("=")
if (!value.startsWith('$$secret') && value !== '') {
newEnvironments.push(`${env}=${value}`)
}
} }
} }
} }
@@ -87,12 +75,13 @@ export async function startService(request: FastifyRequest<ServiceStartStop>, fa
} }
const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } }) const customVolumes = await prisma.servicePersistentStorage.findMany({ where: { serviceId: id } })
let volumes = new Set() let volumes = new Set()
if (arm) { if (arm && template.services[s]?.volumesArm?.length > 0) {
template.services[s]?.volumesArm && template.services[s].volumesArm.length > 0 && template.services[s].volumesArm.forEach(v => volumes.add(v)) template.services[s].volumesArm.forEach(v => volumes.add(v))
} else { } else {
template.services[s]?.volumes && template.services[s].volumes.length > 0 && template.services[s].volumes.forEach(v => volumes.add(v)) if (template.services[s]?.volumes?.length > 0) {
template.services[s].volumes.forEach(v => volumes.add(v))
}
} }
// Workaround: old plausible analytics service wrong volume id name // Workaround: old plausible analytics service wrong volume id name
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) { if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
let temp = Array.from(volumes) let temp = Array.from(volumes)

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,154 +1,174 @@
import type { OnlyId } from "../../../../types"; import type { OnlyId } from '../../../../types';
export interface SaveApplication extends OnlyId { export interface SaveApplication extends OnlyId {
Body: { Body: {
name: string, name: string;
buildPack: string, buildPack: string;
fqdn: string, fqdn: string;
port: number, port: number;
exposePort: number, exposePort: number;
installCommand: string, installCommand: string;
buildCommand: string, buildCommand: string;
startCommand: string, startCommand: string;
baseDirectory: string, baseDirectory: string;
publishDirectory: string, publishDirectory: string;
pythonWSGI: string, pythonWSGI: string;
pythonModule: string, pythonModule: string;
pythonVariable: string, pythonVariable: string;
dockerFileLocation: string, dockerFileLocation: string;
denoMainFile: string, denoMainFile: string;
denoOptions: string, denoOptions: string;
baseImage: string, baseImage: string;
gitCommitHash: string, gitCommitHash: string;
baseBuildImage: string, baseBuildImage: string;
deploymentType: string, deploymentType: string;
baseDatabaseBranch: string, baseDatabaseBranch: string;
dockerComposeFile: string, dockerComposeFile: string;
dockerComposeFileLocation: string, dockerComposeFileLocation: string;
dockerComposeConfiguration: string, dockerComposeConfiguration: string;
simpleDockerfile: string, simpleDockerfile: string;
dockerRegistryImageName: string dockerRegistryImageName: string;
} basicAuthPw: string;
basicAuthUser: string;
};
} }
export interface SaveApplicationSettings extends OnlyId { export interface SaveApplicationSettings extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { debug: boolean; previews: boolean; dualCerts: boolean; autodeploy: boolean; branch: string; projectId: number; isBot: boolean; isDBBranching: boolean, isCustomSSL: boolean }; Body: {
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
branch: string;
projectId: number;
isBot: boolean;
isDBBranching: boolean;
isCustomSSL: boolean;
isHttp2: boolean;
basicAuth: boolean;
};
} }
export interface DeleteApplication extends OnlyId { export interface DeleteApplication extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { force: boolean } Body: { force: boolean };
} }
export interface CheckDomain extends OnlyId { export interface CheckDomain extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
} }
export interface CheckDNS extends OnlyId { export interface CheckDNS extends OnlyId {
Querystring: { domain: string; }; Querystring: { domain: string };
Body: { Body: {
exposePort: number, exposePort: number;
fqdn: string, fqdn: string;
forceSave: boolean, forceSave: boolean;
dualCerts: boolean dualCerts: boolean;
} };
} }
export interface DeployApplication { export interface DeployApplication {
Querystring: { domain: string } Querystring: { domain: string };
Body: { pullmergeRequestId: string | null, branch: string, forceRebuild?: boolean } Body: { pullmergeRequestId: string | null; branch: string; forceRebuild?: boolean };
} }
export interface GetImages { export interface GetImages {
Body: { buildPack: string, deploymentType: string } Body: { buildPack: string; deploymentType: string };
} }
export interface SaveApplicationSource extends OnlyId { export interface SaveApplicationSource extends OnlyId {
Body: { gitSourceId?: string | null, forPublic?: boolean, type?: string, simpleDockerfile?: string } Body: {
gitSourceId?: string | null;
forPublic?: boolean;
type?: string;
simpleDockerfile?: string;
};
} }
export interface CheckRepository extends OnlyId { export interface CheckRepository extends OnlyId {
Querystring: { repository: string, branch: string } Querystring: { repository: string; branch: string };
} }
export interface SaveDestination extends OnlyId { export interface SaveDestination extends OnlyId {
Body: { destinationId: string } Body: { destinationId: string };
} }
export interface SaveSecret extends OnlyId { export interface SaveSecret extends OnlyId {
Body: { Body: {
name: string, name: string;
value: string, value: string;
isBuildSecret: boolean, isBuildSecret: boolean;
previewSecret: boolean, previewSecret: boolean;
isNew: boolean isNew: boolean;
} };
} }
export interface DeleteSecret extends OnlyId { export interface DeleteSecret extends OnlyId {
Body: { name: string } Body: { name: string };
} }
export interface SaveStorage extends OnlyId { export interface SaveStorage extends OnlyId {
Body: { Body: {
path: string, hostPath?: string;
newStorage: boolean, path: string;
storageId: string newStorage: boolean;
} storageId: string;
};
} }
export interface DeleteStorage extends OnlyId { export interface DeleteStorage extends OnlyId {
Body: { Body: {
path: string, path: string;
} };
} }
export interface GetApplicationLogs { export interface GetApplicationLogs {
Params: { Params: {
id: string, id: string;
containerId: string containerId: string;
} };
Querystring: { Querystring: {
since: number, since: number;
} };
} }
export interface GetBuilds extends OnlyId { export interface GetBuilds extends OnlyId {
Querystring: { Querystring: {
buildId: string buildId: string;
skip: number, skip: number;
} };
} }
export interface GetBuildIdLogs { export interface GetBuildIdLogs {
Params: { Params: {
id: string, id: string;
buildId: string buildId: string;
}, };
Querystring: { Querystring: {
sequence: number sequence: number;
} };
} }
export interface SaveDeployKey extends OnlyId { export interface SaveDeployKey extends OnlyId {
Body: { Body: {
deployKeyId: number deployKeyId: number;
} };
} }
export interface CancelDeployment { export interface CancelDeployment {
Body: { Body: {
buildId: string, buildId: string;
applicationId: string applicationId: string;
} };
} }
export interface DeployApplication extends OnlyId { export interface DeployApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
branch: string, branch: string;
forceRebuild?: boolean forceRebuild?: boolean;
} };
} }
export interface StopPreviewApplication extends OnlyId { export interface StopPreviewApplication extends OnlyId {
Body: { Body: {
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
} };
} }
export interface RestartPreviewApplication { export interface RestartPreviewApplication {
Params: { Params: {
id: string, id: string;
pullmergeRequestId: string | null, pullmergeRequestId: string | null;
} };
} }
export interface RestartApplication { export interface RestartApplication {
Params: { Params: {
id: string, id: string;
}, };
Body: { Body: {
imageId: string | null, imageId: string | null;
} };
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -4,7 +4,7 @@ export interface SaveDatabaseType extends OnlyId {
Body: { type: string } Body: { type: string }
} }
export interface DeleteDatabase extends OnlyId { export interface DeleteDatabase extends OnlyId {
Body: { force: string } Body: { }
} }
export interface SaveVersion extends OnlyId { export interface SaveVersion extends OnlyId {
Body: { Body: {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import { compareVersions } from "compare-versions"; import { compareVersions } from 'compare-versions';
import cuid from "cuid"; import cuid from 'cuid';
import bcrypt from "bcryptjs"; import bcrypt from 'bcryptjs';
import fs from 'fs/promises'; import fs from 'fs/promises';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { import {
@@ -12,16 +12,14 @@ import {
prisma, prisma,
uniqueName, uniqueName,
version, version,
sentryDSN, executeCommand
executeCommand, } from '../../../lib/common';
} from "../../../lib/common"; import { scheduler } from '../../../lib/scheduler';
import { scheduler } from "../../../lib/scheduler"; import type { FastifyReply, FastifyRequest } from 'fastify';
import type { FastifyReply, FastifyRequest } from "fastify"; import type { Login, Update } from '.';
import type { Login, Update } from "."; import type { GetCurrentUser } from './types';
import type { GetCurrentUser } from "./types";
export async function hashPassword(password: string): Promise<string> { export async function hashPassword(password: string, saltRounds = 15): Promise<string> {
const saltRounds = 15;
return bcrypt.hash(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
@@ -29,9 +27,9 @@ export async function backup(request: FastifyRequest) {
try { try {
const { backupData } = request.params; const { backupData } = request.params;
let std = null; let std = null;
const [id, backupType, type, zipped, storage] = backupData.split(':') const [id, backupType, type, zipped, storage] = backupData.split(':');
console.log(id, backupType, type, zipped, storage) console.log(id, backupType, type, zipped, storage);
const database = await prisma.database.findUnique({ where: { id } }) const database = await prisma.database.findUnique({ where: { id } });
if (database) { if (database) {
// await executeDockerCmd({ // await executeDockerCmd({
// dockerId: database.destinationDockerId, // dockerId: database.destinationDockerId,
@@ -40,8 +38,7 @@ export async function backup(request: FastifyRequest) {
std = await executeCommand({ std = await executeCommand({
dockerId: database.destinationDockerId, 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` 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) { if (std.stdout) {
return std.stdout; return std.stdout;
@@ -58,9 +55,9 @@ export async function cleanupManually(request: FastifyRequest) {
try { try {
const { serverId } = request.body; const { serverId } = request.body;
const destination = await prisma.destinationDocker.findUnique({ const destination = await prisma.destinationDocker.findUnique({
where: { id: serverId }, where: { id: serverId }
}); });
await cleanupDockerStorage(destination.id, true, true); await cleanupDockerStorage(destination.id, true);
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -68,17 +65,25 @@ export async function cleanupManually(request: FastifyRequest) {
} }
export async function refreshTags() { export async function refreshTags() {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const tags = await fs.readFile('./devTags.json', 'utf8') let tags = await fs.readFile('./devTags.json', 'utf8');
await fs.writeFile('./tags.json', tags) try {
if (await fs.stat('./testTags.json')) {
const testTags = await fs.readFile('./testTags.json', 'utf8');
if (testTags.length > 0) {
tags = JSON.parse(tags).concat(JSON.parse(testTags));
}
}
} catch (error) { }
await fs.writeFile('./tags.json', tags);
} else { } else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text();
await fs.writeFile('/app/tags.json', tags) await fs.writeFile('/app/tags.json', tags);
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
return {}; return {};
@@ -88,17 +93,25 @@ export async function refreshTags() {
} }
export async function refreshTemplates() { export async function refreshTemplates() {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
try { try {
if (isDev) { if (isDev) {
const response = await fs.readFile('./devTemplates.yaml', 'utf8') let templates = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response))) try {
if (await fs.stat('./testTemplate.yaml')) {
templates = templates + (await fs.readFile('./testTemplate.yaml', 'utf8'));
}
} catch (error) { }
const response = await fs.readFile('./devTemplates.yaml', 'utf8');
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)));
} else { } else {
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() const response = await got
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) .get('https://get.coollabs.io/coolify/service-templates.yaml')
.text();
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)));
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
return {}; return {};
} catch ({ status, message }) { } catch ({ status, message }) {
@@ -107,28 +120,29 @@ export async function refreshTemplates() {
} }
export async function checkUpdate(request: FastifyRequest) { export async function checkUpdate(request: FastifyRequest) {
try { try {
const { default: got } = await import('got') const { default: got } = await import('got');
const isStaging = const isStaging =
request.hostname === "staging.coolify.io" || request.hostname === 'staging.coolify.io' || request.hostname === 'arm.coolify.io';
request.hostname === "arm.coolify.io";
const currentVersion = version; const currentVersion = version;
const { coolify } = await got.get('https://get.coollabs.io/versions.json', { const { coolify } = await got
searchParams: { .get('https://get.coollabs.io/versions.json', {
appId: process.env['COOLIFY_APP_ID'] || undefined, searchParams: {
version: currentVersion appId: process.env['COOLIFY_APP_ID'] || undefined,
} version: currentVersion
}).json() }
})
.json();
const latestVersion = coolify.main.version; const latestVersion = coolify.main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion); const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isStaging) { if (isStaging) {
return { return {
isUpdateAvailable: true, isUpdateAvailable: true,
latestVersion: "next", latestVersion: 'next'
}; };
} }
return { return {
isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1, isUpdateAvailable: isStaging ? true : isUpdateAvailable === 1,
latestVersion, latestVersion
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -140,10 +154,22 @@ export async function update(request: FastifyRequest<Update>) {
try { try {
if (!isDev) { if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }); let image = `ghcr.io/coollabsio/coolify:${latestVersion}`;
await executeCommand({ shell: true, command: `env | grep COOLIFY > .env` }); try {
await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }); await executeCommand({ command: `docker pull ${image}` });
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"` }); } catch (error) {
image = `coollabsio/coolify:${latestVersion}`;
await executeCommand({ command: `docker pull ${image}` });
}
await executeCommand({ shell: true, command: `ls .env || env | grep "^COOLIFY" | sort > .env` });
await executeCommand({
command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
});
await executeCommand({
shell: true,
command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db ${image} /bin/sh -c "env | grep "^COOLIFY" | sort > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
});
return {}; return {};
} else { } else {
await asyncSleep(2000); await asyncSleep(2000);
@@ -156,12 +182,12 @@ export async function update(request: FastifyRequest<Update>) {
export async function resetQueue(request: FastifyRequest<any>) { export async function resetQueue(request: FastifyRequest<any>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === "0") { if (teamId === '0') {
await prisma.build.updateMany({ await prisma.build.updateMany({
where: { status: { in: ["queued", "running"] } }, where: { status: { in: ['queued', 'running'] } },
data: { status: "canceled" }, data: { status: 'canceled' }
}); });
scheduler.workers.get("deployApplication").postMessage("cancel"); scheduler.workers.get('deployApplication').postMessage('cancel');
} }
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -170,7 +196,7 @@ export async function resetQueue(request: FastifyRequest<any>) {
export async function restartCoolify(request: FastifyRequest<any>) { export async function restartCoolify(request: FastifyRequest<any>) {
try { try {
const teamId = request.user.teamId; const teamId = request.user.teamId;
if (teamId === "0") { if (teamId === '0') {
if (!isDev) { if (!isDev) {
await executeCommand({ command: `docker restart coolify` }); await executeCommand({ command: `docker restart coolify` });
return {}; return {};
@@ -180,7 +206,7 @@ export async function restartCoolify(request: FastifyRequest<any>) {
} }
throw { throw {
status: 500, status: 500,
message: "You are not authorized to restart Coolify.", message: 'You are not authorized to restart Coolify.'
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
@@ -192,43 +218,52 @@ export async function showDashboard(request: FastifyRequest) {
const userId = request.user.userId; const userId = request.user.userId;
const teamId = request.user.teamId; const teamId = request.user.teamId;
let applications = await prisma.application.findMany({ let applications = await prisma.application.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true }
}); });
const databases = await prisma.database.findMany({ const databases = await prisma.database.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { settings: true, destinationDocker: true, teams: true }, include: { settings: true, destinationDocker: true, teams: true }
}); });
const services = await prisma.service.findMany({ const services = await prisma.service.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { destinationDocker: true, teams: true }, include: { destinationDocker: true, teams: true }
}); });
const gitSources = await prisma.gitSource.findMany({ const gitSources = await prisma.gitSource.findMany({
where: { OR: [{ teams: { some: { id: teamId === "0" ? undefined : teamId } } }, { isSystemWide: true }] }, where: {
include: { teams: true }, OR: [
{ teams: { some: { id: teamId === '0' ? undefined : teamId } } },
{ isSystemWide: true }
]
},
include: { teams: true }
}); });
const destinations = await prisma.destinationDocker.findMany({ const destinations = await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: { teams: true }, include: { teams: true }
}); });
const settings = await listSettings(); const settings = await listSettings();
let foundUnconfiguredApplication = false; let foundUnconfiguredApplication = false;
for (const application of applications) { for (const application of applications) {
if (((!application.buildPack || !application.branch) && !application.simpleDockerfile) || !application.destinationDockerId || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") { if (
foundUnconfiguredApplication = true ((!application.buildPack || !application.branch) && !application.simpleDockerfile) ||
!application.destinationDockerId ||
(!application.settings?.isBot && !application?.fqdn && application.buildPack !== 'compose')
) {
foundUnconfiguredApplication = true;
} }
} }
let foundUnconfiguredService = false; let foundUnconfiguredService = false;
for (const service of services) { for (const service of services) {
if (!service.fqdn) { if (!service.fqdn) {
foundUnconfiguredService = true foundUnconfiguredService = true;
} }
} }
let foundUnconfiguredDatabase = false; let foundUnconfiguredDatabase = false;
for (const database of databases) { for (const database of databases) {
if (!database.version) { if (!database.version) {
foundUnconfiguredDatabase = true foundUnconfiguredDatabase = true;
} }
} }
return { return {
@@ -240,101 +275,94 @@ export async function showDashboard(request: FastifyRequest) {
services, services,
gitSources, gitSources,
destinations, destinations,
settings, settings
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });
} }
} }
export async function login( export async function login(request: FastifyRequest<Login>, reply: FastifyReply) {
request: FastifyRequest<Login>,
reply: FastifyReply
) {
if (request.user) { if (request.user) {
return reply.redirect("/dashboard"); return reply.redirect('/dashboard');
} else { } else {
const { email, password, isLogin } = request.body || {}; const { email, password, isLogin } = request.body || {};
if (!email || !password) { if (!email || !password) {
throw { status: 500, message: "Email and password are required." }; throw { status: 500, message: 'Email and password are required.' };
} }
const users = await prisma.user.count(); const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({ const userFound = await prisma.user.findUnique({
where: { email }, where: { email },
include: { teams: true, permission: true }, include: { teams: true, permission: true },
rejectOnNotFound: false, rejectOnNotFound: false
}); });
if (!userFound && isLogin) { if (!userFound && isLogin) {
throw { status: 500, message: "User not found." }; throw { status: 500, message: 'User not found.' };
} }
const { isRegistrationEnabled, id } = await prisma.setting.findFirst(); const { isRegistrationEnabled, id } = await prisma.setting.findFirst();
let uid = cuid(); let uid = cuid();
let permission = "read"; let permission = 'read';
let isAdmin = false; let isAdmin = false;
if (users === 0) { if (users === 0) {
await prisma.setting.update({ await prisma.setting.update({
where: { id }, where: { id },
data: { isRegistrationEnabled: false }, data: { isRegistrationEnabled: false }
}); });
uid = "0"; uid = '0';
} }
if (userFound) { if (userFound) {
if (userFound.type === "email") { if (userFound.type === 'email') {
if (userFound.password === "RESETME") { if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) { if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
if (userFound.id === "0") { if (userFound.id === '0') {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: "RESETME" }, data: { password: 'RESETME' }
}); });
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: "RESETTIMEOUT" }, data: { password: 'RESETTIMEOUT' }
}); });
} }
throw { throw {
status: 500, status: 500,
message: message: 'Password reset link has expired. Please request a new one.'
"Password reset link has expired. Please request a new one.",
}; };
} else { } else {
await prisma.user.update({ await prisma.user.update({
where: { email: userFound.email }, where: { email: userFound.email },
data: { password: hashedPassword }, data: { password: hashedPassword }
}); });
return { return {
userId: userFound.id, userId: userFound.id,
teamId: userFound.id, teamId: userFound.id,
permission: userFound.permission, permission: userFound.permission,
isAdmin: true, isAdmin: true
}; };
} }
} }
const passwordMatch = await bcrypt.compare( const passwordMatch = await bcrypt.compare(password, userFound.password);
password,
userFound.password
);
if (!passwordMatch) { if (!passwordMatch) {
throw { throw {
status: 500, status: 500,
message: "Wrong password or email address.", message: 'Wrong password or email address.'
}; };
} }
uid = userFound.id; uid = userFound.id;
isAdmin = true; isAdmin = true;
} }
} else { } else {
permission = "owner"; permission = 'owner';
isAdmin = true; isAdmin = true;
if (!isRegistrationEnabled) { if (!isRegistrationEnabled) {
throw { throw {
status: 404, status: 404,
message: "Registration disabled by administrator.", message: 'Registration disabled by administrator.'
}; };
} }
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
@@ -344,17 +372,17 @@ export async function login(
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: "email", type: 'email',
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName(), name: uniqueName(),
destinationDocker: { connect: { network: "coolify" } }, destinationDocker: { connect: { network: 'coolify' } }
}, }
}, },
permission: { create: { teamId: uid, permission: "owner" } }, permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true }, include: { teams: true }
}); });
} else { } else {
await prisma.user.create({ await prisma.user.create({
@@ -362,16 +390,16 @@ export async function login(
id: uid, id: uid,
email, email,
password: hashedPassword, password: hashedPassword,
type: "email", type: 'email',
teams: { teams: {
create: { create: {
id: uid, id: uid,
name: uniqueName(), name: uniqueName()
}, }
}, },
permission: { create: { teamId: uid, permission: "owner" } }, permission: { create: { teamId: uid, permission: 'owner' } }
}, },
include: { teams: true }, include: { teams: true }
}); });
} }
} }
@@ -379,23 +407,20 @@ export async function login(
userId: uid, userId: uid,
teamId: uid, teamId: uid,
permission, permission,
isAdmin, isAdmin
}; };
} }
} }
export async function getCurrentUser( export async function getCurrentUser(request: FastifyRequest<GetCurrentUser>, fastify) {
request: FastifyRequest<GetCurrentUser>,
fastify
) {
let token = null; let token = null;
const { teamId } = request.query; const { teamId } = request.query;
try { try {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: request.user.userId }, where: { id: request.user.userId }
}); });
if (!user) { if (!user) {
throw "User not found"; throw 'User not found';
} }
} catch (error) { } catch (error) {
throw { status: 401, message: error }; throw { status: 401, message: error };
@@ -404,17 +429,15 @@ export async function getCurrentUser(
try { try {
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
where: { id: request.user.userId, teams: { some: { id: teamId } } }, where: { id: request.user.userId, teams: { some: { id: teamId } } },
include: { teams: true, permission: true }, include: { teams: true, permission: true }
}); });
if (user) { if (user) {
const permission = user.permission.find( const permission = user.permission.find((p) => p.teamId === teamId).permission;
(p) => p.teamId === teamId
).permission;
const payload = { const payload = {
...request.user, ...request.user,
teamId, teamId,
permission: permission || null, permission: permission || null,
isAdmin: permission === "owner" || permission === "admin", isAdmin: permission === 'owner' || permission === 'admin'
}; };
token = fastify.jwt.sign(payload); token = fastify.jwt.sign(payload);
} }
@@ -422,12 +445,13 @@ export async function getCurrentUser(
// No new token -> not switching teams // No new token -> not switching teams
} }
} }
const pendingInvitations = await prisma.teamInvitation.findMany({ where: { uid: request.user.userId } }) const pendingInvitations = await prisma.teamInvitation.findMany({
where: { uid: request.user.userId }
});
return { return {
settings: await prisma.setting.findUnique({ where: { id: "0" } }), settings: await prisma.setting.findUnique({ where: { id: '0' } }),
sentryDSN,
pendingInvitations, pendingInvitations,
token, token,
...request.user, ...request.user
}; };
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ export interface SaveGitLabSource extends OnlyId {
appSecret: string, appSecret: string,
groupName: string, groupName: string,
customPort: number, customPort: number,
customUser: string,
} }
} }
export interface CheckGitLabOAuthId extends OnlyId { export interface CheckGitLabOAuthId extends OnlyId {

View File

@@ -1,9 +1,33 @@
import { FastifyRequest } from "fastify"; import { FastifyRequest } from 'fastify';
import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common"; import { errorHandler, executeCommand, getDomain, isDev, prisma } from '../../../lib/common';
import { getTemplates } from "../../../lib/services"; import { getTemplates } from '../../../lib/services';
import { OnlyId } from "../../../types"; import { OnlyId } from '../../../types';
import { parseAndFindServiceTemplates } from '../../api/v1/services/handlers';
import { hashPassword } from '../../api/v1/handlers';
function generateServices(serviceId, containerId, port) { function generateServices(serviceId, containerId, port, isHttp2 = false, isHttps = false) {
if (isHttp2) {
return {
[serviceId]: {
loadbalancer: {
servers: [
{
url: `${isHttps ? 'https' : 'http'}://${containerId}:${port}`
}
]
}
},
[`${serviceId}-http2`]: {
loadbalancer: {
servers: [
{
url: `h2c://${containerId}:${port}`
}
]
}
}
};
}
return { return {
[serviceId]: { [serviceId]: {
loadbalancer: { loadbalancer: {
@@ -14,43 +38,59 @@ function generateServices(serviceId, containerId, port) {
] ]
} }
} }
} };
} }
function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, isDualCerts, isCustomSSL) { async function generateRouters({
let http: any = { serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts,
isCustomSSL,
isHttp2 = false,
httpBasicAuth = null,
}) {
const rule = `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`;
const ruleWWW = `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''
}`;
const http: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
} };
let https: any = { const https: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
} };
let httpWWW: any = { const httpWWW: any = {
entrypoints: ['web'], entrypoints: ['web'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
middlewares: [] middlewares: []
} };
let httpsWWW: any = { const httpsWWW: any = {
entrypoints: ['websecure'], entrypoints: ['websecure'],
rule: `Host(\`www.${nakedDomain}\`)${pathPrefix ? ` && PathPrefix(\`${pathPrefix}\`)` : ''}`, rule: ruleWWW,
service: `${serviceId}`, service: `${serviceId}`,
priority: 2, priority: 2,
tls: { tls: {
certresolver: 'letsencrypt' certresolver: 'letsencrypt'
}, },
middlewares: [] middlewares: []
} };
// 2. http + non-www only // 2. http + non-www only
if (!isHttps && !isWWW) { if (!isHttps && !isWWW) {
https.middlewares.push('redirect-to-http'); https.middlewares.push('redirect-to-http');
@@ -58,8 +98,12 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpWWW.middlewares.push('redirect-to-non-www'); httpWWW.middlewares.push('redirect-to-non-www');
httpsWWW.middlewares.push('redirect-to-non-www'); httpsWWW.middlewares.push('redirect-to-non-www');
delete https.tls delete https.tls;
delete httpsWWW.tls delete httpsWWW.tls;
if (httpBasicAuth) {
http.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 3. http + www only // 3. http + www only
@@ -69,8 +113,12 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
http.middlewares.push('redirect-to-www'); http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
delete https.tls delete https.tls;
delete httpsWWW.tls delete httpsWWW.tls;
if (httpBasicAuth) {
httpWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 5. https + non-www only // 5. https + non-www only
if (isHttps && !isWWW) { if (isHttps && !isWWW) {
@@ -86,19 +134,23 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
httpsWWW.tls = true; httpsWWW.tls = true;
} else { } else {
https.tls = true; https.tls = true;
delete httpsWWW.tls.certresolver delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = { httpsWWW.tls.domains = {
main: domain main: domain
} };
} }
} else { } else {
if (!isDualCerts) { if (!isDualCerts) {
delete httpsWWW.tls.certresolver delete httpsWWW.tls.certresolver;
httpsWWW.tls.domains = { httpsWWW.tls.domains = {
main: domain main: domain
} };
} }
} }
if (httpBasicAuth) {
https.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
} }
// 6. https + www only // 6. https + www only
if (isHttps && isWWW) { if (isHttps && isWWW) {
@@ -108,34 +160,75 @@ function generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, is
http.middlewares.push('redirect-to-www'); http.middlewares.push('redirect-to-www');
https.middlewares.push('redirect-to-www'); https.middlewares.push('redirect-to-www');
} }
if (httpBasicAuth) {
httpsWWW.middlewares.push(`${serviceId}-${pathPrefix}-basic-auth`);
}
if (isCustomSSL) { if (isCustomSSL) {
if (isDualCerts) { if (isDualCerts) {
https.tls = true; https.tls = true;
httpsWWW.tls = true; httpsWWW.tls = true;
} else { } else {
httpsWWW.tls = true; httpsWWW.tls = true;
delete https.tls.certresolver delete https.tls.certresolver;
https.tls.domains = { https.tls.domains = {
main: domain main: domain
} };
} }
} else { } else {
if (!isDualCerts) { if (!isDualCerts) {
delete https.tls.certresolver delete https.tls.certresolver;
https.tls.domains = { https.tls.domains = {
main: domain main: domain
} };
} }
} }
} }
return { if (isHttp2) {
const http2 = {
...http,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
const http2WWW = {
...httpWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
const https2 = {
...https,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
const https2WWW = {
...httpsWWW,
service: `${serviceId}-http2`,
rule: `${rule} && HeadersRegexp(\`Content-Type\`, \`application/grpc*\`)`
};
return {
[`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-http2`]: { ...http2 },
[`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-secure-http2`]: { ...https2 },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-www-http2`]: { ...http2WWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW },
[`${serviceId}-${pathPrefix}-secure-www-http2`]: { ...https2WWW }
};
}
const result = {
[`${serviceId}-${pathPrefix}`]: { ...http }, [`${serviceId}-${pathPrefix}`]: { ...http },
[`${serviceId}-${pathPrefix}-secure`]: { ...https }, [`${serviceId}-${pathPrefix}-secure`]: { ...https },
[`${serviceId}-${pathPrefix}-www`]: { ...httpWWW }, [`${serviceId}-${pathPrefix}-www`]: { ...httpWWW },
[`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }, [`${serviceId}-${pathPrefix}-secure-www`]: { ...httpsWWW }
} };
return result;
} }
export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote: boolean = false) { export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote = false) {
const traefik = { const traefik = {
tls: { tls: {
certificates: [] certificates: []
@@ -174,26 +267,26 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const coolifySettings = await prisma.setting.findFirst(); const coolifySettings = await prisma.setting.findFirst();
if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) { if (coolifySettings.isTraefikUsed && coolifySettings.proxyDefaultRedirect) {
traefik.http.routers['catchall-http'] = { traefik.http.routers['catchall-http'] = {
entrypoints: ["web"], entrypoints: ['web'],
rule: "HostRegexp(`{catchall:.*}`)", rule: 'HostRegexp(`{catchall:.*}`)',
service: "noop", service: 'noop',
priority: 1, priority: 1,
middlewares: ["redirect-regexp"] middlewares: ['redirect-regexp']
} };
traefik.http.routers['catchall-https'] = { traefik.http.routers['catchall-https'] = {
entrypoints: ["websecure"], entrypoints: ['websecure'],
rule: "HostRegexp(`{catchall:.*}`)", rule: 'HostRegexp(`{catchall:.*}`)',
service: "noop", service: 'noop',
priority: 1, priority: 1,
middlewares: ["redirect-regexp"] middlewares: ['redirect-regexp']
} };
traefik.http.middlewares['redirect-regexp'] = { traefik.http.middlewares['redirect-regexp'] = {
redirectregex: { redirectregex: {
regex: '(.*)', regex: '(.*)',
replacement: coolifySettings.proxyDefaultRedirect, replacement: coolifySettings.proxyDefaultRedirect,
permanent: false permanent: false
} }
} };
traefik.http.services['noop'] = { traefik.http.services['noop'] = {
loadBalancer: { loadBalancer: {
servers: [ servers: [
@@ -202,25 +295,41 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
} }
] ]
} }
} };
} }
const sslpath = '/etc/traefik/acme/custom'; const sslpath = '/etc/traefik/acme/custom';
let certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } } } } }) let certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: { some: { remoteEngine: false, isCoolifyProxyUsed: true } }
}
}
});
if (remote) { if (remote) {
certificates = await prisma.certificate.findMany({ where: { team: { applications: { some: { settings: { isCustomSSL: true } } }, destinationDocker: { some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true } } } } }) certificates = await prisma.certificate.findMany({
where: {
team: {
applications: { some: { settings: { isCustomSSL: true } } },
destinationDocker: {
some: { id, remoteEngine: true, isCoolifyProxyUsed: true, remoteVerified: true }
}
}
}
});
} }
let parsedCertificates = [] const parsedCertificates = [];
for (const certificate of certificates) { for (const certificate of certificates) {
parsedCertificates.push({ parsedCertificates.push({
certFile: `${sslpath}/${certificate.id}-cert.pem`, certFile: `${sslpath}/${certificate.id}-cert.pem`,
keyFile: `${sslpath}/${certificate.id}-key.pem` keyFile: `${sslpath}/${certificate.id}-key.pem`
}) });
} }
if (parsedCertificates.length > 0) { if (parsedCertificates.length > 0) {
traefik.tls.certificates = parsedCertificates traefik.tls.certificates = parsedCertificates;
} }
let applications = []; let applications = [];
@@ -236,7 +345,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
serviceSecret: true, serviceSecret: true,
serviceSetting: true, serviceSetting: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
@@ -251,23 +360,25 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
destinationDocker: true, destinationDocker: true,
persistentStorage: true, persistentStorage: true,
serviceSecret: true, serviceSecret: true,
serviceSetting: true, serviceSetting: true
}, },
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: 'desc' }
}); });
} }
if (applications.length > 0) { if (applications.length > 0) {
const dockerIds = new Set() const dockerIds = new Set();
const runningContainers = {} const runningContainers = {};
applications.forEach((app) => dockerIds.add(app.destinationDocker.id)); applications.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) { for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) { if (container) {
const containersArray = container.trim().split('\n'); const containersArray = container.trim().split('\n');
if (containersArray.length > 0) { if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray runningContainers[dockerId] = containersArray;
} }
} }
} }
@@ -281,7 +392,10 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
dockerComposeConfiguration, dockerComposeConfiguration,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
settings settings,
basicAuthUser,
basicAuthPw,
settings: { basicAuth: isBasicAuthEnabled }
} = application; } = application;
if (!destinationDockerId) { if (!destinationDockerId) {
continue; continue;
@@ -289,38 +403,68 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
if ( if (
!runningContainers[destinationDockerId] || !runningContainers[destinationDockerId] ||
runningContainers[destinationDockerId].length === 0 || runningContainers[destinationDockerId].length === 0 ||
runningContainers[destinationDockerId].filter((container) => container.startsWith(id)).length === 0 runningContainers[destinationDockerId].filter((container) => container.startsWith(id))
.length === 0
) { ) {
continue continue;
}
let httpBasicAuth = null;
if (basicAuthUser && basicAuthPw && isBasicAuthEnabled) {
httpBasicAuth = {
basicAuth: {
users: [basicAuthUser + ':' + await hashPassword(basicAuthPw, 1)]
}
};
} }
if (buildPack === 'compose') { if (buildPack === 'compose') {
const services = Object.entries(JSON.parse(dockerComposeConfiguration)) const services = Object.entries(JSON.parse(dockerComposeConfiguration));
if (services.length > 0) { if (services.length > 0) {
for (const service of services) { for (const service of services) {
const [key, value] = service const [key, value] = service;
if (key && value) { if (key && value) {
if (!value.fqdn || !value.port) { if (!value.fqdn || !value.port) {
continue; continue;
} }
const { fqdn, port } = value const { fqdn, port } = value;
const containerId = `${id}-${key}` const containerId = `${id}-${key}`;
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false; const isCustomSSL = false;
const dualCerts = false; const dualCerts = false;
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, containerId, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, containerId, port)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
continue; continue;
} }
const { previews, dualCerts, isCustomSSL } = settings; const { previews, dualCerts, isCustomSSL, isHttp2, basicAuth } = settings;
const { network, id: dockerId } = destinationDocker; const { network, id: dockerId } = destinationDocker;
if (!fqdn) { if (!fqdn) {
continue; continue;
@@ -329,12 +473,37 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL,
isHttp2,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port, isHttp2, isHttps)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
if (previews) { if (previews) {
const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) const { stdout } = await executeCommand({
dockerId,
command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
});
if (stdout) { if (stdout) {
const containers = stdout const containers = stdout
.trim() .trim()
@@ -343,44 +512,63 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
.map((c) => c.replace(/"/g, '')); .map((c) => c.replace(/"/g, ''));
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator}${domain}`; const previewDomain = `${container.split('-')[1]}${coolifySettings.previewSeparator
}${domain}`;
const nakedDomain = previewDomain.replace(/^www\./, ''); const nakedDomain = previewDomain.replace(/^www\./, '');
const pathPrefix = '/' const pathPrefix = '/';
const serviceId = `${container}-${port || 'default'}` const serviceId = `${container}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, previewDomain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain: previewDomain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL,
isHttp2: false,
httpBasicAuth
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port, isHttp2)
};
if (httpBasicAuth) {
traefik.http.middlewares[`${serviceId}-${pathPrefix}-basic-auth`] = {
...httpBasicAuth
};
}
} }
} }
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
} }
if (services.length > 0) { if (services.length > 0) {
const dockerIds = new Set() const dockerIds = new Set();
const runningContainers = {} const runningContainers = {};
services.forEach((app) => dockerIds.add(app.destinationDocker.id)); services.forEach((app) => dockerIds.add(app.destinationDocker.id));
for (const dockerId of dockerIds) { for (const dockerId of dockerIds) {
const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) const { stdout: container } = await executeCommand({
dockerId,
command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'`
});
if (container) { if (container) {
const containersArray = container.trim().split('\n'); const containersArray = container.trim().split('\n');
if (containersArray.length > 0) { if (containersArray.length > 0) {
runningContainers[dockerId] = containersArray runningContainers[dockerId] = containersArray;
} }
} }
} }
for (const service of services) { for (const service of services) {
try { try {
let { let { fqdn, id, type, destinationDockerId, dualCerts, serviceSetting } = service;
fqdn,
id,
type,
destinationDockerId,
dualCerts,
serviceSetting
} = service;
if (!fqdn) { if (!fqdn) {
continue; continue;
} }
@@ -392,7 +580,7 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
runningContainers[destinationDockerId].length === 0 || runningContainers[destinationDockerId].length === 0 ||
!runningContainers[destinationDockerId].includes(id) !runningContainers[destinationDockerId].includes(id)
) { ) {
continue continue;
} }
const templates = await getTemplates(); const templates = await getTemplates();
let found = templates.find((a) => a.type === type); let found = templates.find((a) => a.type === type);
@@ -401,88 +589,147 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
} }
found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id)); found = JSON.parse(JSON.stringify(found).replaceAll('$$id', id));
for (const oneService of Object.keys(found.services)) { for (const oneService of Object.keys(found.services)) {
const isDomainConfiguration = found?.services[oneService]?.proxy?.filter(p => p.domain) ?? []; const isDomainAndProxyConfiguration =
if (isDomainConfiguration.length > 0) { found?.services[oneService]?.proxy?.filter((p) => p.port) ?? [];
const { proxy } = found.services[oneService]; if (isDomainAndProxyConfiguration.length > 0) {
for (let configuration of proxy) { const template: any = await parseAndFindServiceTemplates(service, null, true);
const { proxy } = template.services[oneService] || found.services[oneService];
for (const configuration of proxy) {
if (configuration.hostPort) {
continue;
}
if (configuration.domain) { if (configuration.domain) {
const setting = serviceSetting.find((a) => a.variableName === configuration.domain); const setting = serviceSetting.find(
(a) => a.variableName === configuration.domain
);
if (setting) { if (setting) {
configuration.domain = configuration.domain.replace(configuration.domain, setting.value); configuration.domain = configuration.domain.replace(
configuration.domain,
setting.value
);
} }
} }
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) { if (foundPortVariable) {
configuration.port = foundPortVariable.value configuration.port = foundPortVariable.value;
} }
let port, pathPrefix, customDomain; let port, pathPrefix, customDomain;
if (configuration) { if (configuration) {
port = configuration?.port; port = configuration?.port;
pathPrefix = configuration?.pathPrefix || '/'; pathPrefix = configuration?.pathPrefix || '/';
customDomain = configuration?.domain customDomain = configuration?.domain;
} }
if (customDomain) { if (customDomain) {
fqdn = customDomain fqdn = customDomain;
} else { } else {
fqdn = service.fqdn fqdn = service.fqdn;
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const isCustomSSL = false; const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}` const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, oneService, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL,
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, oneService, port)
};
} }
} else { } else {
if (found.services[oneService].ports && found.services[oneService].ports.length > 0) { if (found.services[oneService].ports && found.services[oneService].ports.length > 0) {
for (let [index, port] of found.services[oneService].ports.entries()) { for (let [index, port] of found.services[oneService].ports.entries()) {
if (port == 22) continue; if (port == 22) continue;
if (index === 0) { if (index === 0) {
const foundPortVariable = serviceSetting.find((a) => a.name.toLowerCase() === 'port') const foundPortVariable = serviceSetting.find(
(a) => a.name.toLowerCase() === 'port'
);
if (foundPortVariable) { if (foundPortVariable) {
port = foundPortVariable.value port = foundPortVariable.value;
} }
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false const isCustomSSL = false;
const serviceId = `${oneService}-${port || 'default'}` const serviceId = `${oneService}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, id, port)
};
} }
} }
} }
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} }
} }
} }
if (!remote) { if (!remote) {
const { fqdn, dualCerts } = await prisma.setting.findFirst(); const { fqdn, dualCerts } = await prisma.setting.findFirst();
if (!fqdn) { if (!fqdn) {
return return;
} }
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace(/^www\./, ''); const nakedDomain = domain.replace(/^www\./, '');
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.'); const isWWW = fqdn.includes('www.');
const id = isDev ? 'host.docker.internal' : 'coolify' const id = isDev ? 'host.docker.internal' : 'coolify';
const container = isDev ? 'host.docker.internal' : 'coolify' const container = isDev ? 'host.docker.internal' : 'coolify';
const port = 3000 const port = 3000;
const pathPrefix = '/' const pathPrefix = '/';
const isCustomSSL = false const isCustomSSL = false;
const serviceId = `${id}-${port || 'default'}` const serviceId = `${id}-${port || 'default'}`;
traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.routers = {
traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, container, port) } ...traefik.http.routers,
...await generateRouters({
serviceId,
domain,
nakedDomain,
pathPrefix,
isHttps,
isWWW,
isDualCerts: dualCerts,
isCustomSSL
})
};
traefik.http.services = {
...traefik.http.services,
...generateServices(serviceId, container, port)
};
} }
} catch (error) { } catch (error) {
console.log(error) console.log(error);
} finally { } finally {
if (Object.keys(traefik.http.routers).length === 0) { if (Object.keys(traefik.http.routers).length === 0) {
traefik.http.routers = null; traefik.http.routers = null;
@@ -496,9 +743,9 @@ export async function proxyConfiguration(request: FastifyRequest<OnlyId>, remote
export async function otherProxyConfiguration(request: FastifyRequest<TraefikOtherConfiguration>) { export async function otherProxyConfiguration(request: FastifyRequest<TraefikOtherConfiguration>) {
try { try {
const { id } = request.query const { id } = request.query;
if (id) { if (id) {
const { privatePort, publicPort, type, address = id } = request.query const { privatePort, publicPort, type, address = id } = request.query;
let traefik = {}; let traefik = {};
if (publicPort && type && privatePort) { if (publicPort && type && privatePort) {
if (type === 'tcp') { if (type === 'tcp') {
@@ -559,18 +806,18 @@ export async function otherProxyConfiguration(request: FastifyRequest<TraefikOth
} }
} }
} else { } else {
throw { status: 500 } throw { status: 500 };
} }
} }
} else { } else {
throw { status: 500 } throw { status: 500 };
} }
return { return {
...traefik ...traefik
}; };
} }
throw { status: 500 } throw { status: 500 };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }) return errorHandler({ status, message });
} }
} }

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<script>
import { addToast } from '$lib/store';
export let value = '';
let isHttps = window.location.protocol === 'https:';
function copyToClipboard() {
if (isHttps && navigator.clipboard) {
navigator.clipboard.writeText(value);
addToast({
message: 'Copied to clipboard.',
type: 'success'
});
}
}
</script>
<div class="w-full relative box">
<p class="text-white p-2">{value}</p>
<div
class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 mr-3"
>
{#if isHttps}
<div on:click={copyToClipboard}>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2" />
</svg>
</div>
{/if}
</div>
</div>
<style>
.box {
position: relative;
border: 1px dashed #202020;
border-radius: 5px;
padding: 5px;
}
</style>

View File

@@ -1,23 +1,14 @@
<script lang="ts"> <script lang="ts">
export let type: string; export let type: string;
export let isAbsolute = false; export let isAbsolute = false;
let githubRawIconUrl =
'https://raw.githubusercontent.com/coollabsio/coolify-community-templates/main/services/icons';
let fallback = '/icons/default.png'; let fallback = '/icons/default.png';
const handleError = (ev: { target: { src: string } }) => (ev.target.src = fallback);
let extension = 'png'; const handleError = (ev: { target: { src: string } }) => {
let svgs = [ ev.target.src = fallback;
'pocketbase', };
'gitea',
'languagetool',
'meilisearch',
'n8n',
'glitchtip',
'searxng',
'umami',
'uptimekuma',
'vaultwarden',
'weblate',
'wordpress'
];
const name: any = const name: any =
type && type &&
@@ -27,32 +18,15 @@
.split('-')[0] .split('-')[0]
.toLowerCase(); .toLowerCase();
if (svgs.includes(name)) {
extension = 'svg';
}
function generateClass() { function generateClass() {
switch (name) { return isAbsolute ? 'w-10 h-10 absolute -m-4 -mt-9 left-0' : 'w-10 h-10';
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';
}
} }
</script> </script>
{#if name} {#if name}
<img <img
class={generateClass()} class={generateClass()}
src={`/icons/${name}.${extension}`} src={`${githubRawIconUrl}/${name}.png`}
on:error={handleError} on:error={handleError}
alt={`Icon of ${name}`} alt={`Icon of ${name}`}
/> />

View File

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

View File

@@ -23,7 +23,8 @@ interface AppSession {
github: string | null, github: string | null,
gitlab: string | null, gitlab: string | null,
}, },
pendingInvitations: Array<any> pendingInvitations: Array<any>,
isARM: boolean
} }
interface AddToast { interface AddToast {
type?: "info" | "success" | "error", type?: "info" | "success" | "error",
@@ -52,15 +53,15 @@ export const appSession: Writable<AppSession> = writable({
github: null, github: null,
gitlab: null gitlab: null
}, },
pendingInvitations: [] pendingInvitations: [],
isARM: false
}); });
export const disabledButton: Writable<boolean> = writable(false); export const disabledButton: Writable<boolean> = writable(false);
export const isDeploymentEnabled: Writable<boolean> = writable(false); export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { export function checkIfDeploymentEnabledApplications(application: any) {
return !!( return !!(
isAdmin &&
(application.buildPack === 'compose') || (application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) && (application.fqdn || application.settings?.isBot) &&
((application.gitSource && ((application.gitSource &&
application.repository && application.repository &&
application.buildPack) || application.simpleDockerfile) && application.buildPack) || application.simpleDockerfile) &&
@@ -68,9 +69,8 @@ export function checkIfDeploymentEnabledApplications(isAdmin: boolean, applicati
); );
} }
export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any) { export function checkIfDeploymentEnabledServices( service: any) {
return ( return (
isAdmin &&
service.fqdn && service.fqdn &&
service.destinationDocker && service.destinationDocker &&
service.version && service.version &&

View File

@@ -65,7 +65,6 @@
<script lang="ts"> <script lang="ts">
export let settings: any; export let settings: any;
export let sentryDSN: any;
export let baseSettings: any; export let baseSettings: any;
export let pendingInvitations: any = 0; export let pendingInvitations: any = 0;
@@ -75,6 +74,7 @@
$appSession.version = baseSettings.version; $appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled; $appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon; $appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.isARM = baseSettings.isARM;
$appSession.pendingInvitations = pendingInvitations; $appSession.pendingInvitations = pendingInvitations;
@@ -97,10 +97,6 @@
import Toasts from '$lib/components/Toasts.svelte'; import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import LocalePicker from '$lib/components/LocalePicker.svelte';
import * as Sentry from '@sentry/svelte';
import { BrowserTracing } from '@sentry/tracing';
import { dev } from '$app/env';
if (userId) $appSession.userId = userId; if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId; if (teamId) $appSession.teamId = teamId;
@@ -135,6 +131,10 @@
}); });
} }
}); });
let sidedrawerToggler: HTMLInputElement;
const closeDrawer = () => (sidedrawerToggler.checked = false);
</script> </script>
<svelte:head> <svelte:head>
@@ -154,9 +154,11 @@
{/if} {/if}
<div class="drawer"> <div class="drawer">
<input id="main-drawer" type="checkbox" class="drawer-toggle" /> <input id="main-drawer" type="checkbox" class="drawer-toggle" bind:this={sidedrawerToggler} />
<div class="drawer-content"> <div class="drawer-content">
{#if $appSession.userId} {#if $appSession.userId}
<Tooltip triggeredBy="#dashboard" placement="right" color="bg-pink-500">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right" color="bg-sky-500">Servers</Tooltip>
<Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip> <Tooltip triggeredBy="#iam" placement="right" color="bg-iam">IAM</Tooltip>
<Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black" <Tooltip triggeredBy="#settings" placement="right" color="bg-settings text-black"
>Settings</Tooltip >Settings</Tooltip
@@ -176,7 +178,6 @@
<div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}> <div class="flex flex-col space-y-2 py-2" class:mt-2={$appSession.whiteLabeled}>
<a <a
id="dashboard" id="dashboard"
sveltekit:prefetch
href="/" href="/"
class="icons hover:text-pink-500" class="icons hover:text-pink-500"
class:text-pink-500={$page.url.pathname === '/'} class:text-pink-500={$page.url.pathname === '/'}
@@ -203,7 +204,6 @@
{#if $appSession.teamId === '0'} {#if $appSession.teamId === '0'}
<a <a
id="servers" id="servers"
sveltekit:prefetch
href="/servers" href="/servers"
class="icons hover:text-sky-500" class="icons hover:text-sky-500"
class:text-sky-500={$page.url.pathname === '/servers'} class:text-sky-500={$page.url.pathname === '/servers'}
@@ -229,8 +229,6 @@
</a> </a>
{/if} {/if}
</div> </div>
<Tooltip triggeredBy="#dashboard" placement="right">Dashboard</Tooltip>
<Tooltip triggeredBy="#servers" placement="right">Servers</Tooltip>
<div class="flex-1" /> <div class="flex-1" />
<div class="lg:block hidden"> <div class="lg:block hidden">
<UpdateAvailable /> <UpdateAvailable />
@@ -238,7 +236,6 @@
<div class="flex flex-col space-y-2 py-2"> <div class="flex flex-col space-y-2 py-2">
<a <a
id="iam" id="iam"
sveltekit:prefetch
href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'} href={$appSession.pendingInvitations.length > 0 ? '/iam/pending' : '/iam'}
class="icons hover:text-iam indicator" class="icons hover:text-iam indicator"
class:text-iam={$page.url.pathname.startsWith('/iam')} class:text-iam={$page.url.pathname.startsWith('/iam')}
@@ -267,7 +264,6 @@
</a> </a>
<a <a
id="settings" id="settings"
sveltekit:prefetch
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/docker'} href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/docker'}
class="icons hover:text-settings" class="icons hover:text-settings"
class:text-settings={$page.url.pathname.startsWith('/settings')} class:text-settings={$page.url.pathname.startsWith('/settings')}
@@ -290,6 +286,28 @@
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
</svg> </svg>
</a> </a>
<a
id="documentation"
href="https://docs.coollabs.io/coolify-v3/"
target="_blank"
rel="noreferrer external"
class="icons hover:text-info"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-9 h-9"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
</a>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<div <div
@@ -361,13 +379,13 @@
</div> </div>
<div class="drawer-side"> <div class="drawer-side">
<label for="main-drawer" class="drawer-overlay w-full" /> <label for="main-drawer" class="drawer-overlay w-full" />
<ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4 "> <ul class="menu bg-coolgray-200 w-60 p-2 space-y-3 pt-4">
<li> <li>
<a <a
class="no-underline icons hover:text-white hover:bg-pink-500" class="no-underline icons hover:text-white hover:bg-pink-500"
sveltekit:prefetch
href="/" href="/"
class:bg-pink-500={$page.url.pathname === '/'} class:bg-pink-500={$page.url.pathname === '/'}
on:click={closeDrawer}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -393,9 +411,9 @@
<a <a
id="servers" id="servers"
class="no-underline icons hover:text-white hover:bg-sky-500" class="no-underline icons hover:text-white hover:bg-sky-500"
sveltekit:prefetch
href="/servers" href="/servers"
class:bg-sky-500={$page.url.pathname.startsWith('/servers')} class:bg-sky-500={$page.url.pathname.startsWith('/servers')}
on:click={closeDrawer}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -421,6 +439,7 @@
class="no-underline icons hover:text-white hover:bg-iam" class="no-underline icons hover:text-white hover:bg-iam"
href="/iam" href="/iam"
class:bg-iam={$page.url.pathname.startsWith('/iam')} class:bg-iam={$page.url.pathname.startsWith('/iam')}
on:click={closeDrawer}
><svg ><svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -450,6 +469,7 @@
href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'} href={$appSession.teamId === '0' ? '/settings/coolify' : '/settings/ssh'}
class:bg-settings={$page.url.pathname.startsWith('/settings')} class:bg-settings={$page.url.pathname.startsWith('/settings')}
class:text-black={$page.url.pathname.startsWith('/settings')} class:text-black={$page.url.pathname.startsWith('/settings')}
on:click={closeDrawer}
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -470,6 +490,30 @@
Settings Settings
</a> </a>
</li> </li>
<li>
<a
class="no-underline icons hover:text-white hover:bg-info"
href="https://docs.coollabs.io/coolify-v3/"
target="_blank"
rel="noreferrer external"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-8 h-8"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
/>
</svg>
Documentation
</a>
</li>
<li class="flex-1 bg-transparent" /> <li class="flex-1 bg-transparent" />
<div class="block lg:hidden"> <div class="block lg:hidden">
<UpdateAvailable /> <UpdateAvailable />

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
export let application: any; export let application: any;
import { status } from '$lib/store'; import { appSession, status } from '$lib/store';
import { page } from '$app/stores'; import { page } from '$app/stores';
</script> </script>
@@ -220,7 +220,7 @@
<li class="menu-title"> <li class="menu-title">
<span>Advanced</span> <span>Advanced</span>
</li> </li>
{#if application.gitSourceId} {#if application.gitSourceId && $appSession.isAdmin}
<li <li
class="rounded" class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/revert`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/revert`}
@@ -295,6 +295,7 @@
> >
</li> </li>
{/if} {/if}
{#if $appSession.isAdmin}
<li <li
class="rounded" class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`} class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/danger`}
@@ -318,4 +319,5 @@
</svg>Danger Zone</a </svg>Danger Zone</a
> >
</li> </li>
{/if}
</ul> </ul>

View File

@@ -9,7 +9,7 @@
import { del, post, put } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@@ -120,6 +120,7 @@
<label for="name" class="pb-5 uppercase lg:block hidden font-bold" /> <label for="name" class="pb-5 uppercase lg:block hidden font-bold" />
{/if} {/if}
{#if $appSession.isAdmin}
<div class="flex justify-center h-full items-center pt-3"> <div class="flex justify-center h-full items-center pt-3">
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
@@ -127,5 +128,6 @@
</div> </div>
</div> </div>
</div> </div>
{/if}
</div> </div>
</div> </div>

View File

@@ -10,7 +10,7 @@
import { del, post, put } from '$lib/api'; import { del, post, put } from '$lib/api';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@@ -124,8 +124,9 @@
<div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0"> <div class="flex justify-center h-full items-center pt-0 lg:pt-0 pl-4 lg:pl-0">
<button <button
on:click={() => updateSecret({ changeIsBuildSecret: true })} on:click={() => updateSecret({ changeIsBuildSecret: true })}
disabled={!$appSession.isAdmin}
aria-pressed="false" aria-pressed="false"
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out " class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret} class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret} class:bg-stone-700={!isBuildSecret}
> >
@@ -177,7 +178,7 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button> <button class="btn btn-sm btn-primary" on:click={addNewSecret}>Add</button>
</div> </div>
{:else} {:else if $appSession.isAdmin}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button> <button class="btn btn-sm btn-primary" on:click={() => updateSecret()}>Set</button>

View File

@@ -7,12 +7,24 @@
import { del, post } from '$lib/api'; import { del, post } from '$lib/api';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { browser } from '$app/env';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification } from '$lib/common'; import { errorNotification } from '$lib/common';
import { addToast } from '$lib/store'; import { addToast } from '$lib/store';
import CopyVolumeField from '$lib/components/CopyVolumeField.svelte';
import SimpleExplainer from '$lib/components/SimpleExplainer.svelte';
const { id } = $page.params; const { id } = $page.params;
let isHttps = browser && window.location.protocol === 'https:';
export let value: string;
function copyToClipboard() {
if (isHttps && navigator.clipboard) {
navigator.clipboard.writeText(value);
addToast({
message: 'Copied to clipboard.',
type: 'success'
});
}
}
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
async function saveStorage(newStorage = false) { async function saveStorage(newStorage = false) {
try { try {
@@ -22,11 +34,13 @@
storage.path.replace(/\/\//g, '/'); storage.path.replace(/\/\//g, '/');
await post(`/applications/${id}/storages`, { await post(`/applications/${id}/storages`, {
path: storage.path, path: storage.path,
hostPath: storage.hostPath,
storageId: storage.id, storageId: storage.id,
newStorage newStorage
}); });
dispatch('refresh'); dispatch('refresh');
if (isNew) { if (isNew) {
storage.hostPath = null;
storage.path = null; storage.path = null;
storage.id = null; storage.id = null;
} }
@@ -69,31 +83,42 @@
<div class="flex gap-4 pb-2" class:pt-8={isNew}> <div class="flex gap-4 pb-2" class:pt-8={isNew}>
{#if storage.applicationId} {#if storage.applicationId}
{#if storage.oldPath} {#if storage.oldPath}
<input <CopyVolumeField
disabled
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}" value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
/> />
{:else} {:else if !storage.hostPath}
<input <CopyVolumeField
disabled value="{storage.applicationId}{storage.path.replace(/\//gi, '-').replace('-app', '')}"
readonly
class="w-full"
value="{storage.applicationId}{storage.path.replace(/\//gi, '-')}"
/> />
{/if} {/if}
{/if} {/if}
{#if isNew}
<div class="w-full">
<input
disabled={!isNew}
readonly={!isNew}
bind:value={storage.hostPath}
placeholder="Host path, example: ~/.directory"
/>
<SimpleExplainer
text="You can mount <span class='text-yellow-400 font-bold'>host paths</span> from the operating system.<br>Leave it empty to define a volume based volume."
/>
</div>
{:else if storage.hostPath}
<input disabled readonly value={storage.hostPath} />
{/if}
<input <input
disabled={!isNew} disabled={!isNew}
readonly={!isNew} readonly={!isNew}
class="w-full" class="w-full"
bind:value={storage.path} bind:value={storage.path}
required required
placeholder="eg: /data" placeholder="Mount point inside the container, example: /data"
/> />
<div class="flex items-center justify-center"> <div class="flex items-start justify-center">
{#if isNew} {#if isNew}
<div class="w-full lg:w-64"> <div class="w-full lg:w-64">
<button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)} <button class="btn btn-sm btn-primary w-full" on:click={() => saveStorage(true)}

View File

@@ -83,13 +83,13 @@
let forceDelete = false; let forceDelete = false;
let stopping = false; let stopping = false;
const { id } = $page.params; const { id } = $page.params;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
async function deleteApplication(name: string, force: boolean) { async function deleteApplication(name: string, force: boolean) {
const sure = confirm($t('application.confirm_to_delete', { name })); const sure = confirm($t('application.confirm_to_delete', { name }));
if (sure) { if (sure) {
try { try {
await del(`/applications/${id}`, { id, force }); await del(`/applications/${id}`, {});
return await goto('/'); return await goto('/');
} catch (error) { } catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) { if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {
@@ -292,7 +292,6 @@
<a <a
href={$isDeploymentEnabled ? `/applications/${id}/logs` : null} href={$isDeploymentEnabled ? `/applications/${id}/logs` : null}
class="btn btn-sm text-sm gap-2" class="btn btn-sm text-sm gap-2"
sveltekit:prefetch
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -363,7 +362,7 @@
<button <button
on:click={restartApplication} on:click={restartApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@@ -383,7 +382,7 @@
</button> </button>
{/if} {/if}
<button <button
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
on:click={() => handleDeploySubmit(true)} on:click={() => handleDeploySubmit(true)}
> >
@@ -409,7 +408,7 @@
<button <button
on:click={stopApplication} on:click={stopApplication}
type="submit" type="submit"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
> >
<svg <svg
@@ -428,32 +427,9 @@
</svg> Stop </svg> Stop
</button> </button>
{:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)} {:else if $isDeploymentEnabled && !$page.url.pathname.startsWith(`/applications/${id}/configuration/`)}
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{/if}
<button <button
class="btn btn-sm gap-2" class="btn btn-sm gap-2"
disabled={!$isDeploymentEnabled} disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
on:click={() => handleDeploySubmit(true)} on:click={() => handleDeploySubmit(true)}
> >
{#if $status.application.overallStatus !== 'degraded'} {#if $status.application.overallStatus !== 'degraded'}
@@ -494,6 +470,29 @@
: 'Redeploy Stack' : 'Redeploy Stack'
: 'Deploy'} : 'Deploy'}
</button> </button>
{#if $status.application.overallStatus === 'degraded'}
<button
on:click={stopApplication}
type="submit"
disabled={!$isDeploymentEnabled || !$appSession.isAdmin}
class="btn btn-sm gap-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 text-error"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<rect x="6" y="5" width="4" height="14" rx="1" />
<rect x="14" y="5" width="4" height="14" rx="1" />
</svg> Stop
</button>
{/if}
{/if} {/if}
{#if $location && $status.application.overallStatus === 'healthy'} {#if $location && $status.application.overallStatus === 'healthy'}
<a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary" <a href={$location} target="_blank noreferrer" class="btn btn-sm gap-2 text-sm bg-primary"

View File

@@ -28,12 +28,15 @@
delete tempBuildPack.fancyName; delete tempBuildPack.fancyName;
delete tempBuildPack.color; delete tempBuildPack.color;
delete tempBuildPack.hoverColor; delete tempBuildPack.hoverColor;
let composeConfiguration: any = {} let composeConfiguration: any = {};
if (!dockerComposeConfiguration && dockerComposeFile) { if (!dockerComposeConfiguration && dockerComposeFile && buildPack.name === 'compose') {
for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) { const parsed = JSON.parse(dockerComposeFile);
if (!parsed?.services) {
throw new Error('No services found in docker-compose file. <br>Choose a different buildpack.');
}
for (const [name, _] of Object.entries(parsed.services)) {
composeConfiguration[name] = {}; composeConfiguration[name] = {};
} }
} }
await post(`/applications/${id}`, { await post(`/applications/${id}`, {
...tempBuildPack, ...tempBuildPack,

View File

@@ -125,6 +125,7 @@
} }
} }
</script> </script>
{#if repositories.length === 0 && loading.repositories === false} {#if repositories.length === 0 && loading.repositories === false}
<div class="flex-col text-center"> <div class="flex-col text-center">
<div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div> <div class="pb-4">{$t('application.configuration.no_repositories_configured')}</div>
@@ -134,43 +135,45 @@
</div> </div>
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="px-10"> <form on:submit|preventDefault={handleSubmit} class="px-10">
<div class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center"> <div
<div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Repository</label> class="flex lg:flex-row flex-col lg:space-y-0 space-y-2 space-x-0 lg:space-x-2 items-center lg:justify-center"
<Select >
placeholder={loading.repositories <div class="custom-select-wrapper w-full">
? $t('application.configuration.loading_repositories') <label for="repository" class="pb-1">Repository</label>
: $t('application.configuration.select_a_repository')} <Select
id="repository" placeholder={loading.repositories
showIndicator={!loading.repositories} ? $t('application.configuration.loading_repositories')
isWaiting={loading.repositories} : $t('application.configuration.select_a_repository')}
on:select={loadBranches} id="repository"
items={reposSelectOptions} showIndicator={!loading.repositories}
isDisabled={loading.repositories} isWaiting={loading.repositories}
isClearable={false} on:select={loadBranches}
/> items={reposSelectOptions}
</div> isDisabled={loading.repositories}
<input class="hidden" bind:value={selected.projectId} name="projectId" /> isClearable={false}
<div class="custom-select-wrapper w-full"><label for="repository" class="pb-1">Branch</label> />
<Select </div>
placeholder={loading.branches <input class="hidden" bind:value={selected.projectId} name="projectId" />
? $t('application.configuration.loading_branches') <div class="custom-select-wrapper w-full">
: !selected.repository <label for="repository" class="pb-1">Branch</label>
? $t('application.configuration.select_a_repository_first') <Select
: $t('application.configuration.select_a_branch')} placeholder={loading.branches
isWaiting={loading.branches} ? $t('application.configuration.loading_branches')
showIndicator={selected.repository && !loading.branches} : !selected.repository
id="branches" ? $t('application.configuration.select_a_repository_first')
on:select={selectBranch} : $t('application.configuration.select_a_branch')}
items={branchSelectOptions} isWaiting={loading.branches}
isDisabled={loading.branches || !selected.repository} showIndicator={selected.repository && !loading.branches}
isClearable={false} id="branches"
/> on:select={selectBranch}
</div></div> items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository}
isClearable={false}
/>
</div>
</div>
<div class="pt-5 flex-col flex justify-center items-center space-y-4"> <div class="pt-5 flex-col flex justify-center items-center space-y-4">
<button <button class="btn btn-wide btn-primary" type="submit" disabled={!showSave}
class="btn btn-wide btn-primary"
type="submit"
disabled={!showSave}
>{$t('forms.save')}</button >{$t('forms.save')}</button
> >
</div> </div>

View File

@@ -56,6 +56,10 @@
Authorization: `Bearer ${$appSession.tokens.gitlab}` Authorization: `Bearer ${$appSession.tokens.gitlab}`
}); });
username = user.username; username = user.username;
groups.push({
full_name: username,
name: username
});
await loadGroups(); await loadGroups();
} catch (error) { } catch (error) {
loading.base = false; loading.base = false;
@@ -402,7 +406,7 @@
> >
{#if tryAgain} {#if tryAgain}
<div class="p-5"> <div class="p-5">
An error occured during authenticating with GitLab. Please check your GitLab Source An error occurred during authenticating with GitLab. Please check your GitLab Source
configuration <a href={`/sources/${application.gitSource.id}`}>here.</a> configuration <a href={`/sources/${application.gitSource.id}`}>here.</a>
</div> </div>
<button <button

View File

@@ -72,7 +72,6 @@
<div class="flex justify-center"> <div class="flex justify-center">
<a <a
href={`/destinations/new?from=/applications/${id}/configuration/destination`} href={`/destinations/new?from=/applications/${id}/configuration/destination`}
sveltekit:prefetch
class="add-icon bg-sky-600 hover:bg-sky-500" class="add-icon bg-sky-600 hover:bg-sky-500"
> >
<svg <svg

View File

@@ -105,7 +105,7 @@
{#if ownSources.length > 0 || otherSources.length > 0} {#if ownSources.length > 0 || otherSources.length > 0}
<div class="title pb-8">Integrated with Git App</div> <div class="title pb-8">Integrated with Git App</div>
{/if} {/if}
{#if ownSources.length > 0} {#if ownSources.length > 0 || otherSources.length > 0}
<div class="flex flex-wrap justify-center"> <div class="flex flex-wrap justify-center">
<div class="flex flex-col lg:flex-row lg:flex-wrap justify-center"> <div class="flex flex-col lg:flex-row lg:flex-wrap justify-center">
{#each ownSources as source} {#each ownSources as source}
@@ -255,12 +255,12 @@
{/if} {/if}
<div class="flex flex-row items-center"> <div class="flex flex-row items-center">
<div class="title py-4 pr-4">Public Repository from Git</div> <div class="title py-4 pr-4">Public Repository from Git</div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#public-repository" /> <DocLink url="https://docs.coollabs.io/coolify-v3/applications/#public-repository-from-git" />
</div> </div>
<PublicRepository /> <PublicRepository />
<div class="flex flex-row items-center pt-10"> <div class="flex flex-row items-center pt-10">
<div class="title py-4 pr-4">Simple Dockerfile <Beta /></div> <div class="title py-4 pr-4">Simple Dockerfile <Beta /></div>
<DocLink url="https://docs.coollabs.io/coolify/applications/#dockerfile" /> <DocLink url="https://docs.coollabs.io/coolify-v3/applications/#simple-dockerfile" />
</div> </div>
<div class="mx-auto max-w-screen-2xl"> <div class="mx-auto max-w-screen-2xl">
<form class="flex flex-col" on:submit|preventDefault={handleDockerImage}> <form class="flex flex-col" on:submit|preventDefault={handleDockerImage}>

View File

@@ -34,7 +34,7 @@
if (sure) { if (sure) {
$status.application.initialLoading = true; $status.application.initialLoading = true;
try { try {
await del(`/applications/${id}`, { id, force }); await del(`/applications/${id}`,{});
return await goto('/') return await goto('/')
} catch (error) { } catch (error) {
if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) { if (error.message.startsWith(`Command failed: SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid`)) {

View File

@@ -51,6 +51,7 @@
let isDBBranching = application.settings.isDBBranching; let isDBBranching = application.settings.isDBBranching;
async function changeSettings(name: any) { async function changeSettings(name: any) {
if (!$appSession.isAdmin) return
if (name === 'previews') { if (name === 'previews') {
previews = !previews; previews = !previews;
} }
@@ -102,7 +103,7 @@
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
} }
} }
</script> </script>
@@ -119,6 +120,7 @@
id="autodeploy" id="autodeploy"
isCenter={false} isCenter={false}
bind:setting={autodeploy} bind:setting={autodeploy}
disabled={!$appSession.isAdmin}
on:click={() => changeSettings('autodeploy')} on:click={() => changeSettings('autodeploy')}
title={$t('application.enable_automatic_deployment')} title={$t('application.enable_automatic_deployment')}
description={$t('application.enable_auto_deploy_webhooks')} description={$t('application.enable_auto_deploy_webhooks')}
@@ -130,6 +132,7 @@
id="previews" id="previews"
isCenter={false} isCenter={false}
bind:setting={previews} bind:setting={previews}
disabled={!$appSession.isAdmin}
on:click={() => changeSettings('previews')} on:click={() => changeSettings('previews')}
title={$t('application.enable_mr_pr_previews')} title={$t('application.enable_mr_pr_previews')}
description={$t('application.enable_preview_deploy_mr_pr_requests')} description={$t('application.enable_preview_deploy_mr_pr_requests')}

View File

@@ -29,27 +29,28 @@
export let application: any; export let application: any;
export let settings: any; export let settings: any;
import yaml from 'js-yaml'; import { goto } from '$app/navigation';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte';
import Select from 'svelte-select';
import { get, getAPIUrl, post } from '$lib/api'; import { get, getAPIUrl, post } from '$lib/api';
import cuid from 'cuid'; import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common';
import Beta from '$lib/components/Beta.svelte';
import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { import {
addToast, addToast,
appSession, appSession,
checkIfDeploymentEnabledApplications, checkIfDeploymentEnabledApplications,
setLocation, features,
status,
isDeploymentEnabled, isDeploymentEnabled,
features setLocation,
status
} from '$lib/store'; } from '$lib/store';
import { t } from '$lib/translations'; import { t } from '$lib/translations';
import { errorNotification, getDomain, notNodeDeployments, staticDeployments } from '$lib/common'; import cuid from 'cuid';
import Setting from '$lib/components/Setting.svelte'; import yaml from 'js-yaml';
import Explainer from '$lib/components/Explainer.svelte'; import { onMount } from 'svelte';
import { goto } from '$app/navigation'; import Select from 'svelte-select';
import Beta from '$lib/components/Beta.svelte';
import { saveForm } from './utils'; import { saveForm } from './utils';
const { id } = $page.params; const { id } = $page.params;
@@ -58,7 +59,7 @@
$status.application.overallStatus === 'degraded' || $status.application.overallStatus === 'degraded' ||
$status.application.overallStatus === 'healthy' || $status.application.overallStatus === 'healthy' ||
$status.application.initialLoading; $status.application.initialLoading;
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
let statues: any = {}; let statues: any = {};
let loading = { let loading = {
save: false, save: false,
@@ -77,8 +78,10 @@
let isCustomSSL = application.settings?.isCustomSSL; let isCustomSSL = application.settings?.isCustomSSL;
let autodeploy = application.settings?.autodeploy; let autodeploy = application.settings?.autodeploy;
let isBot = application.settings?.isBot; let isBot = application.settings?.isBot;
let basicAuth = application.settings?.basicAuth;
let isDBBranching = application.settings?.isDBBranching; let isDBBranching = application.settings?.isDBBranching;
let htmlUrl = application.gitSource?.htmlUrl; let htmlUrl = application.gitSource?.htmlUrl;
let isHttp2 = application.settings.isHttp2;
let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null; let dockerComposeFile = JSON.parse(application.dockerComposeFile) || null;
let dockerComposeServices: any[] = []; let dockerComposeServices: any[] = [];
@@ -185,6 +188,9 @@
if (name === 'isCustomSSL') { if (name === 'isCustomSSL') {
isCustomSSL = !isCustomSSL; isCustomSSL = !isCustomSSL;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isBot') { if (name === 'isBot') {
if ($status.application.overallStatus !== 'stopped') return; if ($status.application.overallStatus !== 'stopped') return;
isBot = !isBot; isBot = !isBot;
@@ -195,6 +201,9 @@
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
if (name === 'isHttp2') {
isHttp2 = !isHttp2;
}
try { try {
await post(`/applications/${id}/settings`, { await post(`/applications/${id}/settings`, {
previews, previews,
@@ -204,8 +213,10 @@
autodeploy, autodeploy,
isDBBranching, isDBBranching,
isCustomSSL, isCustomSSL,
isHttp2,
branch: application.branch, branch: application.branch,
projectId: application.projectId projectId: application.projectId,
basicAuth
}); });
return addToast({ return addToast({
message: $t('application.settings_saved'), message: $t('application.settings_saved'),
@@ -227,6 +238,9 @@
if (name === 'isBot') { if (name === 'isBot') {
isBot = !isBot; isBot = !isBot;
} }
if (name === 'basicAuth') {
basicAuth = !basicAuth;
}
if (name === 'isDBBranching') { if (name === 'isDBBranching') {
isDBBranching = !isDBBranching; isDBBranching = !isDBBranching;
} }
@@ -235,7 +249,7 @@
} }
return errorNotification(error); return errorNotification(error);
} finally { } finally {
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
} }
} }
async function handleSubmit(toast: boolean = true) { async function handleSubmit(toast: boolean = true) {
@@ -267,9 +281,10 @@
} }
} }
} }
console.log(application);
await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration); await saveForm(id, application, baseDatabaseBranch, dockerComposeConfiguration);
setLocation(application, settings); setLocation(application, settings);
$isDeploymentEnabled = checkIfDeploymentEnabledApplications($appSession.isAdmin, application); $isDeploymentEnabled = checkIfDeploymentEnabledApplications(application);
forceSave = false; forceSave = false;
if (toast) { if (toast) {
@@ -366,11 +381,16 @@
async function reloadCompose() { async function reloadCompose() {
if (loading.reloadCompose) return; if (loading.reloadCompose) return;
loading.reloadCompose = true; loading.reloadCompose = true;
const composeLocation = application.dockerComposeFileLocation.startsWith('/') if (!$appSession.tokens.github && !isPublicRepository) {
? application.dockerComposeFileLocation const { token } = await get(`/applications/${id}/configuration/githubToken`);
: `/${application.dockerComposeFileLocation}`; $appSession.tokens.github = token;
}
try { try {
if (application.gitSource.type === 'github') { if (application.gitSource.type === 'github') {
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation
: `/${application.dockerComposeFileLocation}`;
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@@ -397,6 +417,17 @@
if (!$appSession.tokens.gitlab) { if (!$appSession.tokens.gitlab) {
await getGitlabToken(); await getGitlabToken();
} }
const composeLocation = application.dockerComposeFileLocation.startsWith('/')
? application.dockerComposeFileLocation.substring(1) // Remove the '/' from the start
: application.dockerComposeFileLocation;
// If the file is in a subdirectory, lastIndex will be > 0
// Otherwise it will be -1 and path will be an empty string
const lastIndex = composeLocation.lastIndexOf('/') + 1;
const path = composeLocation.substring(0, lastIndex);
const fileName = composeLocation.substring(lastIndex);
const headers = isPublicRepository const headers = isPublicRepository
? {} ? {}
: { : {
@@ -404,13 +435,12 @@
}; };
const url = isPublicRepository const url = isPublicRepository
? `` ? ``
: `/v4/projects/${application.projectId}/repository/tree`; : `/v4/projects/${application.projectId}/repository/tree?path=${path}`;
const files = await get(`${apiUrl}${url}`, { const files = await get(`${apiUrl}${url}`, {
...headers ...headers
}); });
const dockerComposeFileYml = files.find( const dockerComposeFileYml = files.find(
(file: { name: string; type: string }) => (file: { name: string; type: string }) => file.name === fileName && file.type === 'blob'
file.name === composeLocation && file.type === 'blob'
); );
const id = dockerComposeFileYml.id; const id = dockerComposeFileYml.id;
@@ -478,7 +508,7 @@
<div class="title font-bold pb-3">General</div> <div class="title font-bold pb-3">General</div>
{#if $appSession.isAdmin} {#if $appSession.isAdmin}
<button <button
class="btn btn-sm btn-primary" class="btn btn-sm btn-primary"
type="submit" type="submit"
class:loading={loading.save} class:loading={loading.save}
class:bg-orange-600={forceSave} class:bg-orange-600={forceSave}
@@ -490,7 +520,14 @@
<div class="grid grid-flow-row gap-2 px-4"> <div class="grid grid-flow-row gap-2 px-4">
<div class="mt-2 grid grid-cols-2 items-center"> <div class="mt-2 grid grid-cols-2 items-center">
<label for="name">{$t('forms.name')}</label> <label for="name">{$t('forms.name')}</label>
<input name="name" id="name" class="w-full" bind:value={application.name} required /> <input
name="name"
id="name"
class="w-full"
bind:value={application.name}
disabled={!$appSession.isAdmin}
required
/>
</div> </div>
{#if !isSimpleDockerfile} {#if !isSimpleDockerfile}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
@@ -712,7 +749,7 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
id="dualCerts" id="dualCerts"
dataTooltip={$t('forms.must_be_stopped_to_modify')} dataTooltip={$t('forms.must_be_stopped_to_modify')}
@@ -724,6 +761,7 @@
on:click={() => !isDisabled && changeSettings('dualCerts')} on:click={() => !isDisabled && changeSettings('dualCerts')}
/> />
</div> </div>
{#if isHttps && application.buildPack !== 'compose'} {#if isHttps && application.buildPack !== 'compose'}
<div class="grid grid-cols-2 items-center pb-4"> <div class="grid grid-cols-2 items-center pb-4">
<Setting <Setting
@@ -736,6 +774,56 @@
/> />
</div> </div>
{/if} {/if}
<div class="grid grid-cols-2 items-center pb-4">
<Setting
id="isHttp2"
isCenter={false}
bind:setting={isHttp2}
title="Enable HTTP/2 protocol?"
description="Enable HTTP/2 protocol. <br><br>HTTP/2 is a major revision of the HTTP network protocol used by the World Wide Web that allows faster web page loading by reducing the number of requests needed to load a web page.<br><br>Useful for gRPC and other HTTP/2 based services."
on:click={() => changeSettings('isHttp2')}
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
id="basicAuth"
isCenter={false}
bind:setting={basicAuth}
title={$t('application.basic_auth')}
description="Activate basic authentication for your application. <br>Useful if you want to protect your application with a password. <br><br>Use the <span class='font-bold text-settings'>username</span> and <span class='font-bold text-settings'>password</span> fields to set the credentials."
on:click={() => changeSettings('basicAuth')}
/>
</div>
{#if basicAuth}
<div class="grid grid-cols-2 items-center">
<label for="basicAuthUser">{$t('application.basic_auth_user')}</label>
<input
bind:this={fqdnEl}
class="w-full"
required={!application.settings?.basicAuth}
name="basicAuthUser"
id="basicAuthUser"
class:border={!application.settings?.basicAuth && !application.basicAuthUser}
class:border-red-500={!application.settings?.basicAuth &&
!application.basicAuthUser}
bind:value={application.basicAuthUser}
placeholder="eg: admin"
/>
</div>
<div class="grid grid-cols-2 items-center">
<label for="basicAuthPw">{$t('application.basic_auth_pw')}</label>
<CopyPasswordField
bind:this={fqdnEl}
isPasswordField={true}
required={!application.settings?.basicAuth}
name="basicAuthPw"
id="basicAuthPw"
bind:value={application.basicAuthPw}
placeholder="**********"
/>
</div>
{/if}
{/if} {/if}
</div> </div>
{#if isSimpleDockerfile} {#if isSimpleDockerfile}
@@ -744,7 +832,7 @@
</div> </div>
<div class="grid grid-flow-row gap-2 px-4 pr-5"> <div class="grid grid-flow-row gap-2 px-4 pr-5">
<div class="grid grid-cols-2 items-center pt-4"> <div class="grid grid-cols-2 items-center pt-4">
<label for="simpleDockerfile">Dockerfile</label> <label for="simpleDockerfile">Dockerfile</label>
<div class="flex gap-2"> <div class="flex gap-2">
<textarea <textarea

View File

@@ -21,7 +21,7 @@
onMount(async () => { onMount(async () => {
const response = await get(`/applications/${id}`); const response = await get(`/applications/${id}`);
application = response.application; application = response.application;
if (response.application.dockerComposeFile) { if (response.application.dockerComposeFile && application.buildPack === 'compose') {
services = normalizeDockerServices( services = normalizeDockerServices(
JSON.parse(response.application.dockerComposeFile).services JSON.parse(response.application.dockerComposeFile).services
); );
@@ -51,16 +51,22 @@
async function loadLogs() { async function loadLogs() {
if (logsLoading) return; if (logsLoading) return;
try { try {
const newLogs: any = await get( const since = lastLog?.split(' ')[0] || 0;
`/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}` const newLogs: any = await get(`/applications/${id}/logs/${selectedService}?since=${since}`);
);
if (newLogs.noContainer) { if (newLogs.noContainer) {
noContainer = true; noContainer = true;
} else { } else {
noContainer = false; noContainer = false;
} }
if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) {
logs = logs.concat(newLogs.logs); if (since === 0) {
logs = logs.concat(newLogs.logs);
} else {
const newParsedLogs = newLogs.logs.filter((log: any) => {
return log !== logs[logs.length - 1];
});
logs = logs.concat(newParsedLogs);
}
lastLog = newLogs.logs[newLogs.logs.length - 1]; lastLog = newLogs.logs[newLogs.logs.length - 1];
} }
} catch (error) { } catch (error) {
@@ -111,7 +117,7 @@
<div class="title font-bold pb-3">Application Logs</div> <div class="title font-bold pb-3">Application Logs</div>
</div> </div>
</div> </div>
<div class="flex gap-2 lg:gap-8 pb-4"> <div class="grid grid-cols-3 gap-2 lg:gap-8 pb-4">
{#each services as service} {#each services as service}
<button <button
on:click={() => selectService(service, true)} on:click={() => selectService(service, true)}
@@ -135,7 +141,7 @@
{:else} {:else}
<div class="relative w-full"> <div class="relative w-full">
<div class="flex justify-start sticky space-x-2 pb-2"> <div class="flex justify-start sticky space-x-2 pb-2">
<button on:click={followBuild} class="btn btn-sm " class:bg-coollabs={followingLogs}> <button on:click={followBuild} class="btn btn-sm" class:bg-coollabs={followingLogs}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6 mr-2" class="w-6 h-6 mr-2"

View File

@@ -23,7 +23,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { asyncSleep, errorNotification, getRndInteger } from '$lib/common'; import { asyncSleep, errorNotification, getRndInteger } from '$lib/common';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { addToast } from '$lib/store'; import { addToast, appSession } from '$lib/store';
import Tooltip from '$lib/components/Tooltip.svelte'; import Tooltip from '$lib/components/Tooltip.svelte';
import DeleteIcon from '$lib/components/DeleteIcon.svelte'; import DeleteIcon from '$lib/components/DeleteIcon.svelte';
@@ -264,6 +264,7 @@
{:else} {:else}
<button <button
id="restart" id="restart"
disabled={!$appSession.isAdmin}
on:click={() => restartPreview(preview)} on:click={() => restartPreview(preview)}
type="submit" type="submit"
class="icons bg-transparent text-sm flex items-center space-x-2" class="icons bg-transparent text-sm flex items-center space-x-2"
@@ -286,7 +287,12 @@
{/if} {/if}
<Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip> <Tooltip triggeredBy="#restart">Restart (useful to change secrets)</Tooltip>
<button id="forceredeploypreview" class="icons" on:click={() => redeploy(preview)}> <button
id="forceredeploypreview"
class="icons"
disabled={!$appSession.isAdmin}
on:click={() => redeploy(preview)}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6" class="w-6 h-6"
@@ -310,7 +316,7 @@
id="deletepreview" id="deletepreview"
class="icons" class="icons"
class:hover:text-error={!loading.removing} class:hover:text-error={!loading.removing}
disabled={loading.removing} disabled={loading.removing || !$appSession.isAdmin}
on:click={() => removeApplication(preview)} on:click={() => removeApplication(preview)}
><DeleteIcon /> ><DeleteIcon />
</button> </button>

View File

@@ -158,7 +158,7 @@
id="dockerImage" id="dockerImage"
name="dockerImage" name="dockerImage"
required required
placeholder="coollabsio/coolify:0.0.1" placeholder="ghcr.io/coollabsio/coolify:0.0.1"
bind:value={remoteImage} bind:value={remoteImage}
/> />
<button class="btn btn-sm btn-primary" type="submit">Revert Now</button> <button class="btn btn-sm btn-primary" type="submit">Revert Now</button>

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