Compare commits

..

345 Commits
v3.12.2 ... 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
Alex
0d51b04d79 feat: adding icon for whoogle 2022-12-18 14:13:49 +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
407 changed files with 14487 additions and 24912 deletions

View File

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

View File

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

View File

@@ -1,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:

6
.gitignore vendored
View File

@@ -1,7 +1,8 @@
.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
@@ -11,9 +12,10 @@ dist
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

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
@@ -45,4 +45,4 @@ pnpm dev
You should be able to access `http://localhost:3000`. You should be able to access `http://localhost:3000`.
1. Click `Register` and setup your first user. 1. Click `Register` and setup your first user.

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 .
@@ -50,4 +50,4 @@ RUN pnpm install -p
EXPOSE 3000 EXPOSE 3000
ENV CHECKPOINT_DISABLE=1 ENV CHECKPOINT_DISABLE=1
CMD pnpm start CMD pnpm start

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}
@@ -28,4 +28,4 @@ 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
EXPOSE 3000 EXPOSE 3000
ENV CHECKPOINT_DISABLE=1 ENV CHECKPOINT_DISABLE=1

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)

4
apps/api/.gitignore vendored
View File

@@ -8,4 +8,6 @@ package
!.env.example !.env.example
dist dist
dev.db dev.db
client client
testTemplate.yaml
testTags.json

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -16,31 +16,29 @@
}, },
"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",
"axe": "11.0.0",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"bree": "9.1.2", "bree": "9.1.3",
"cabin": "11.0.1", "cabin": "11.1.1",
"compare-versions": "5.0.1", "compare-versions": "5.0.1",
"csv-parse": "5.3.2", "csv-parse": "5.3.3",
"csvtojson": "2.0.10", "csvtojson": "2.0.10",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.6", "dayjs": "1.11.7",
"dockerode": "3.3.4", "dockerode": "3.3.4",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"execa": "6.1.0", "execa": "6.1.0",
"fastify": "4.10.2", "fastify": "4.11.0",
"fastify-plugin": "4.3.0", "fastify-plugin": "4.3.0",
"fastify-socket.io": "4.0.0", "fastify-socket.io": "4.0.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
@@ -48,36 +46,36 @@
"is-ip": "5.0.0", "is-ip": "5.0.0",
"is-port-reachable": "4.0.0", "is-port-reachable": "4.0.0",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.0",
"minimist": "^1.2.7", "minimist": "^1.2.7",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-os-utils": "1.3.7", "node-os-utils": "1.3.7",
"p-all": "4.0.0", "p-all": "4.0.0",
"p-throttle": "5.0.0", "p-throttle": "5.0.0",
"prisma": "4.6.1", "prisma": "4.8.1",
"public-ip": "6.0.1", "public-ip": "6.0.1",
"pump": "3.0.0", "pump": "3.0.0",
"shell-quote": "^1.7.4", "shell-quote": "^1.7.4",
"socket.io": "4.5.3", "socket.io": "4.5.4",
"ssh-config": "4.1.6", "ssh-config": "4.2.0",
"strip-ansi": "7.0.1", "strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "18.11.9", "@types/node": "18.11.18",
"@types/node-os-utils": "1.3.0", "@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.44.0", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.44.0", "@typescript-eslint/parser": "5.48.1",
"esbuild": "0.15.15", "esbuild": "0.16.16",
"eslint": "8.28.0", "eslint": "8.31.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.6.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.20", "nodemon": "2.0.20",
"prettier": "2.7.1", "prettier": "2.8.2",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"tsconfig-paths": "4.1.0", "tsconfig-paths": "4.1.2",
"types-fastify-socket.io": "0.0.1", "types-fastify-socket.io": "0.0.1",
"typescript": "4.9.3" "typescript": "4.9.4"
}, },
"prisma": { "prisma": {
"seed": "node prisma/seed.js" "seed": "node prisma/seed.js"

View File

@@ -8,6 +8,7 @@ CREATE TABLE "new_GitSource" (
"apiUrl" TEXT, "apiUrl" TEXT,
"htmlUrl" TEXT, "htmlUrl" TEXT,
"customPort" INTEGER NOT NULL DEFAULT 22, "customPort" INTEGER NOT NULL DEFAULT 22,
"customUser" TEXT NOT NULL DEFAULT 'git',
"organization" TEXT, "organization" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL, "updatedAt" DATETIME NOT NULL,
@@ -17,7 +18,7 @@ CREATE TABLE "new_GitSource" (
CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE, 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 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", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "name", "organization", "type", "updatedAt" FROM "GitSource"; 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"; DROP TABLE "GitSource";
ALTER TABLE "new_GitSource" RENAME TO "GitSource"; ALTER TABLE "new_GitSource" RENAME TO "GitSource";
CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId"); CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId");

View File

@@ -10,11 +10,13 @@ CREATE TABLE "new_ApplicationSettings" (
"isBot" BOOLEAN NOT NULL DEFAULT false, "isBot" BOOLEAN NOT NULL DEFAULT false,
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false, "isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" 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, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL, "updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
); );
INSERT INTO "new_ApplicationSettings" ("applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings"; 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"; DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");

View File

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

View File

@@ -1,3 +1,7 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "basicAuthPw" TEXT;
ALTER TABLE "Application" ADD COLUMN "basicAuthUser" TEXT;
-- RedefineTables -- RedefineTables
PRAGMA foreign_keys=OFF; PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ApplicationSettings" ( CREATE TABLE "new_ApplicationSettings" (
@@ -11,11 +15,13 @@ CREATE TABLE "new_ApplicationSettings" (
"isPublicRepository" BOOLEAN NOT NULL DEFAULT false, "isPublicRepository" BOOLEAN NOT NULL DEFAULT false,
"isDBBranching" BOOLEAN NOT NULL DEFAULT false, "isDBBranching" BOOLEAN NOT NULL DEFAULT false,
"isCustomSSL" 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, "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL, "updatedAt" DATETIME NOT NULL,
CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 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", "isDBBranching", "isPublicRepository", "previews", "updatedAt") SELECT "applicationId", "autodeploy", "createdAt", "debug", "dualCerts", "id", "isBot", "isDBBranching", "isPublicRepository", "previews", "updatedAt" FROM "ApplicationSettings"; 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"; DROP TABLE "ApplicationSettings";
ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings";
CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId");

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

@@ -20,7 +20,8 @@ import {
decryptApplication, decryptApplication,
isDev, isDev,
pushToRegistry, pushToRegistry,
executeCommand executeCommand,
generateSecrets
} from '../lib/common'; } 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';
@@ -68,7 +69,15 @@ import * as buildpacks from '../lib/buildPacks';
teams: true teams: true
} }
}); });
if (!application) {
await prisma.build.update({
where: { id: queueBuild.id },
data: {
status: 'failed'
}
});
throw new Error('Application not found');
}
let { let {
id: buildId, id: buildId,
type, type,
@@ -109,6 +118,9 @@ import * as buildpacks from '../lib/buildPacks';
.replace(/\//gi, '-') .replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`; .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}`;
}) || []; }) || [];
@@ -140,33 +152,13 @@ import * as buildpacks from '../lib/buildPacks';
} catch (error) { } catch (error) {
// //
} }
const envs = [`PORT='${port}'`]; let envs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { envs = [
if (pullmergeRequestId) { ...envs,
const isSecretFound = secrets.filter( ...generateSecrets(secrets, pullmergeRequestId, false, port)
(s) => s.name === secret.name && s.isPRMRSecret ];
);
if (isSecretFound.length > 0) {
envs.push(`${secret.name}='${isSecretFound[0].value}'`);
} else {
envs.push(`${secret.name}='${secret.value}'`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}='${secret.value}'`);
}
}
});
} }
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile);
if (dockerRegistry) { if (dockerRegistry) {
const { url, username, password } = dockerRegistry; const { url, username, password } = dockerRegistry;
@@ -179,13 +171,24 @@ import * as buildpacks from '../lib/buildPacks';
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: {
@@ -197,7 +200,7 @@ import * as buildpacks from '../lib/buildPacks';
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}`] } : {}),
@@ -215,7 +218,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug: true, debug: true,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` 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) {
@@ -252,7 +255,7 @@ import * as buildpacks from '../lib/buildPacks';
applicationId: application.id applicationId: application.id
}); });
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
@@ -275,7 +278,7 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ await prisma.build.update({
where: { id: buildId }, where: { id: buildId },
data: { status: 'success' } data: { status: 'success' }
@@ -400,6 +403,9 @@ import * as buildpacks from '../lib/buildPacks';
.replace(/\//gi, '-') .replace(/\//gi, '-')
.replace('-app', '')}:${storage.path}`; .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}`;
}) || []; }) || [];
@@ -425,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;
@@ -438,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,
@@ -619,6 +626,7 @@ import * as buildpacks from '../lib/buildPacks';
} }
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,
@@ -648,7 +656,7 @@ import * as buildpacks from '../lib/buildPacks';
buildId, buildId,
applicationId, applicationId,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` 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({ await prisma.build.update({
@@ -698,45 +706,36 @@ import * as buildpacks from '../lib/buildPacks';
} catch (error) { } catch (error) {
// //
} }
const envs = [`PORT='${port}'`]; let envs = [];
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { envs = [
if (pullmergeRequestId) { ...envs,
const isSecretFound = secrets.filter( ...generateSecrets(secrets, pullmergeRequestId, false, port)
(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'));
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 { try {
envFound = !!(await fs.stat(`${workdir}/.env`)); const composeVolumes = volumes
} catch (error) { .filter((v) => {
// if (
} !v.startsWith('.') &&
try { !v.startsWith('..') &&
const composeVolumes = volumes.map((volume) => { !v.startsWith('/') &&
return { !v.startsWith('~')
[`${volume.split(':')[0]}`]: { ) {
name: volume.split(':')[0] return v;
} }
}; })
}); .map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = { const composeFile = {
version: '3.8', version: '3.8',
services: { services: {
@@ -744,7 +743,7 @@ 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],
@@ -763,7 +762,7 @@ import * as buildpacks from '../lib/buildPacks';
await executeCommand({ await executeCommand({
debug, debug,
dockerId: destinationDocker.id, dockerId: destinationDocker.id,
command: `docker compose --project-directory ${workdir} up -d` 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) {
@@ -807,7 +806,7 @@ import * as buildpacks from '../lib/buildPacks';
applicationId: application.id applicationId: application.id
}); });
} }
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
return; return;
} }
try { try {
@@ -828,7 +827,7 @@ import * as buildpacks from '../lib/buildPacks';
await saveBuildLog({ line: error.stderr, buildId, applicationId }); await saveBuildLog({ line: error.stderr, buildId, applicationId });
} }
} finally { } finally {
await fs.rm(workdir, { recursive: true, force: true }); if (!isDev) await fs.rm(workdir, { recursive: true, force: true });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
} }
}); });

View File

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

View File

@@ -3,6 +3,7 @@ import {
decrypt, decrypt,
encrypt, encrypt,
executeCommand, executeCommand,
generateSecrets,
generateTimestamp, generateTimestamp,
getDomain, getDomain,
isARM, isARM,
@@ -340,8 +341,8 @@ export function setDefaultBaseImage(
}; };
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) payload.baseImages = isARM()
? staticVersions.filter((version) => !version.value.includes('webdevops')) ? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions; : staticVersions;
payload.baseBuildImage = 'node:lts'; payload.baseBuildImage = 'node:lts';
@@ -354,8 +355,8 @@ export function setDefaultBaseImage(
} }
} }
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) payload.baseImages = isARM()
? staticVersions.filter((version) => !version.value.includes('webdevops')) ? staticVersions.filter((version) => !version.value.includes('webdevops'))
: staticVersions; : staticVersions;
payload.baseBuildImage = 'node:lts'; payload.baseBuildImage = 'node:lts';
@@ -375,18 +376,18 @@ export function setDefaultBaseImage(
payload.baseImage = 'denoland/deno:latest'; payload.baseImage = 'denoland/deno:latest';
} }
if (buildPack === 'php') { if (buildPack === 'php') {
payload.baseImage = isARM(process.arch) payload.baseImage = isARM()
? 'php:8.1-fpm-alpine' ? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine'; : 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch) payload.baseImages = isARM()
? phpVersions.filter((version) => !version.value.includes('webdevops')) ? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions; : phpVersions;
} }
if (buildPack === 'laravel') { if (buildPack === 'laravel') {
payload.baseImage = isARM(process.arch) payload.baseImage = isARM()
? 'php:8.1-fpm-alpine' ? 'php:8.1-fpm-alpine'
: 'webdevops/php-apache:8.2-alpine'; : 'webdevops/php-apache:8.2-alpine';
payload.baseImages = isARM(process.arch) payload.baseImages = isARM()
? phpVersions.filter((version) => !version.value.includes('webdevops')) ? phpVersions.filter((version) => !version.value.includes('webdevops'))
: phpVersions; : phpVersions;
payload.baseBuildImage = 'node:18'; payload.baseBuildImage = 'node:18';
@@ -428,7 +429,12 @@ 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 !== '/') if (baseDirectory.endsWith('/') && baseDirectory !== '/')
@@ -653,7 +659,7 @@ 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: {
@@ -701,9 +707,8 @@ export async function buildImage({
buildId, buildId,
applicationId, applicationId,
dockerId, dockerId,
command: `docker ${location ? `--config ${location}` : ''} build ${ command: `docker ${location ? `--config ${location}` : ''} build ${forceRebuild ? '--no-cache' : ''
forceRebuild ? '--no-cache' : '' } --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}`
} --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 } });
@@ -787,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) {
@@ -811,39 +803,26 @@ 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 });
@@ -865,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,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';
export default async function (data) { export default async function (data) {
@@ -13,19 +14,12 @@ export default async function (data) {
} }
}); });
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(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n'));

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.2'; 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

@@ -6,7 +6,7 @@ export async function getTemplates() {
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;

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

@@ -20,7 +20,7 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
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(process.arch) isARM: isARM()
}; };
} catch ({ status, message }) { } catch ({ status, message }) {
return errorHandler({ status, message }); return errorHandler({ status, message });

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
) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.application.updateMany({
where: { dockerRegistryId: id },
data: { dockerRegistryId: null }
});
await prisma.dockerRegistry.deleteMany({ where: { id, teamId } });
return reply.code(201).send();
} catch ({ status, message }) {
return errorHandler({ status, message });
}
} }
export async function deleteDockerRegistry(request: FastifyRequest<OnlyIdInBody>, reply: FastifyReply) {
try {
const teamId = request.user.teamId;
const { id } = request.body;
await prisma.application.updateMany({ where: { dockerRegistryId: id }, data: { dockerRegistryId: null } })
await prisma.dockerRegistry.deleteMany({ where: { id, teamId } })
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}

View File

@@ -1,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,19 +98,27 @@ 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
if (!isHttps && isWWW) { if (!isHttps && isWWW) {
https.middlewares.push('redirect-to-http'); https.middlewares.push('redirect-to-http');
httpsWWW.middlewares.push('redirect-to-http'); httpsWWW.middlewares.push('redirect-to-http');
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

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,20 +0,0 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};

View File

@@ -1,10 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -1,13 +0,0 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

View File

@@ -1,9 +0,0 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -1 +0,0 @@
# SvelteKit Static site

View File

@@ -1,50 +0,0 @@
{
"name": "client",
"description": "Coolify's SvelteKit UI",
"license": "Apache-2.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build && cp -Pr build/ ../../build/public",
"preview": "vite preview",
"test": "playwright test",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check . && eslint .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@playwright/test": "1.28.1",
"@sveltejs/adapter-static": "1.0.0-next.48",
"@sveltejs/kit": "1.0.0-next.572",
"@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.44.0",
"@typescript-eslint/parser": "5.44.0",
"autoprefixer": "10.4.13",
"eslint": "8.28.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0",
"postcss": "8.4.19",
"postcss-load-config": "4.0.1",
"prettier": "2.8.0",
"prettier-plugin-svelte": "2.8.1",
"svelte": "3.53.1",
"svelte-check": "2.9.2",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "3.2.4",
"tslib": "2.4.1",
"typescript": "4.9.3",
"vite": "3.2.4"
},
"type": "module",
"dependencies": {
"@trpc/client": "10.1.0",
"@trpc/server": "10.1.0",
"cuid": "2.1.8",
"daisyui": "2.41.0",
"flowbite-svelte": "0.28.0",
"js-cookie": "3.0.1",
"server": "workspace:*",
"superjson": "1.11.0"
}
}

View File

@@ -1,10 +0,0 @@
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run build && npm run preview',
port: 4173
}
};
export default config;

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +0,0 @@
const tailwindcss = require('tailwindcss');
const autoprefixer = require('autoprefixer');
const config = {
plugins: [
//Some plugins, like tailwindcss/nesting, need to run before Tailwind,
tailwindcss(),
//But others, like autoprefixer, need to run after,
autoprefixer
]
};
module.exports = config;

View File

@@ -1,9 +0,0 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
// interface Locals {}
// interface PageData {}
// interface Error {}
// interface Platform {}
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body>
<div class="h-screen">%sveltekit.body%</div>
</body>
</html>

View File

@@ -1,284 +0,0 @@
/* Write your global styles here, in PostCSS syntax */
@tailwind base;
@tailwind components;
@tailwind utilities;
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 400;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'),
url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff');
}
@font-face {
font-family: 'Poppins';
font-style: normal;
font-weight: 500;
src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'),
url('/poppins-v19-latin-ext_latin_devanagari-500.woff') format('woff');
}
button {
@apply text-sm !important;
}
html {
@apply h-full min-h-full overflow-y-scroll;
}
body {
@apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200;
}
input,
.input {
@apply h-12 w-96 rounded border border-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-200 disabled:bg-transparent disabled:bg-coolblack md:text-sm;
}
textarea {
@apply min-w-[14rem] rounded border border-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-200 disabled:bg-transparent md:text-sm;
}
#svelte .custom-select-wrapper .selectContainer.disabled input {
@apply placeholder:text-stone-600;
}
#svelte .custom-select-wrapper .selectContainer input {
@apply text-white;
}
#svelte .custom-select-wrapper .selectContainer {
@apply h-12 rounded bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm;
}
#svelte .listContainer {
@apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200;
}
#svelte .selectedItem {
@apply pl-2;
}
#svelte .item.hover {
@apply bg-coollabs text-white !important;
}
#svelte .item.active {
@apply bg-coolgray-100 text-white;
}
select {
@apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm;
}
.custom-select-wrapper {
--background: rgb(32 32 32);
--inputColor: white;
--multiItemPadding: 0;
--multiSelectPadding: 0 0.5rem 0 0.5rem;
--border: none;
--placeholderColor: rgb(87 83 78);
--listBackground: rgb(32 32 32);
--itemColor: white;
--itemHoverBG: rgb(107 22 237);
--multiItemBG: rgb(32 32 32);
--multiClearHoverBG: transparent;
--multiClearHoverFill: rgb(239 68 68);
--multiItemActiveBG: transparent;
--multiClearBG: transparent;
--clearSelectFocusColor: white;
--clearSelectHoverColor: rgb(239 68 68);
--multiItemBorderRadius: 0.25rem;
--listShadow: none;
}
label {
@apply inline-block;
}
.btn {
@apply text-white text-base min-w-fit no-animation;
}
a {
@apply underline hover:text-white;
}
.content {
@apply p-2 px-4;
}
.title {
@apply text-lg lg:text-2xl font-bold;
}
.subtitle {
@apply text-lg lg:text-xl font-bold text-indigo-300;
}
.label {
@apply text-sm leading-6 font-semibold text-sky-500 dark:text-sky-400;
}
.card {
@apply border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4 mb-2 items-center;
}
.icon-holder {
overflow: hidden;
height: 30px;
border-radius: 5px;
margin-right: 8px;
background: linear-gradient(0deg, #999, #ddd);
}
.instance-status-running {
box-shadow: 1px 4px 5px #3df721;
}
.instance-status-stopped {
box-shadow: 1px 4px 5px rgb(110, 191, 225);
}
.instance-status-error {
box-shadow: 1px 4px 5px #fb00ff;
}
.instance-status-degraded {
box-shadow: 1px 4px 5px #f7b121;
}
.badge-status-healthy,
.badge-status-running {
@apply text-green-500;
}
.badge-status-degraded {
@apply text-green-500;
}
.badge-status-stopped {
@apply text-sky-500;
}
.delete-button {
@apply bg-red-600;
}
.delete-button:hover {
@apply bg-red-500;
}
/* Interchange menu position */
.menu-left {
display: flex;
flex-direction: row;
}
.menu-left .menu-bar {
display: flex;
flex-direction: column;
}
.menu-left .menu-bar > * {
display: flex;
flex-direction: column;
}
.menu-top {
display: flex;
flex-direction: column;
}
.menu-top .menu-bar {
display: flex;
flex-direction: row;
}
.menu-top .menu-bar > * {
display: flex;
flex-direction: row;
}
.nav-main {
@apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-hidden border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible;
}
.nav-side {
@apply absolute right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white;
}
.add-icon {
@apply rounded p-1 transition duration-200;
}
.icons {
@apply rounded p-2 transition duration-200 hover:bg-coolgray-500 disabled:bg-coolblack disabled:text-coolgray-500 !important;
}
.arrow-right-applications {
@apply -ml-6 px-2 font-bold text-green-500;
}
.border-gradient {
border-bottom: 2px solid transparent;
-o-border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
.border-gradient-full {
border: 4px solid transparent;
-o-border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image: linear-gradient(
0.25turn,
rgba(255, 249, 34),
rgba(255, 0, 128),
rgba(56, 2, 155, 0)
);
border-image-slice: 1;
}
.box-selection {
@apply min-w-[16rem] justify-center rounded border-transparent bg-coolgray-200 p-6 hover:border-transparent hover:bg-coolgray-400;
}
.lds-heart {
animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
}
@keyframes lds-heart {
0% {
transform: scale(1);
}
5% {
transform: scale(1.2);
}
39% {
transform: scale(0.85);
}
45% {
transform: scale(1);
}
60% {
transform: scale(0.95);
}
100% {
transform: scale(0.9);
}
}
.sub-menu {
@apply w-48 text-base font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer;
}
.sub-menu-active {
@apply bg-coolgray-500 text-white;
}
.table tbody td,
.table tbody th,
.table thead th {
background-color: transparent;
}
.table * {
border: none;
}
.header {
@apply flex flex-row z-10 w-full py-5 px-5;
}
.burger {
@apply block m-[2px] h-[3px] w-5 rounded;
}
.bg-coollabs-gradient {
@apply bg-gradient-to-r from-purple-500 via-pink-500 to-red-500;
}

View File

@@ -1,20 +0,0 @@
import { addToast } from './store';
export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay));
export function errorNotification(error: any | { message: string }): void {
if (error instanceof Error) {
addToast({
message: error.message,
type: 'error'
});
} else {
addToast({
message: error,
type: 'error'
});
}
}
export function getRndInteger(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@@ -1,64 +0,0 @@
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-dark lg:bg-primary';
}
}
</script>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => dispatch('click')}
on:mouseover={() => dispatch('pause')}
on:focus={() => dispatch('pause')}
on:mouseout={() => dispatch('resume')}
on:blur={() => dispatch('resume')}
class={` flex flex-row justify-center alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
class:alert-error={type === 'error'}
class:alert-info={type === 'info'}
>
{#if type === 'success'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current flex-shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg
>
{:else if type === 'error'}
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current flex-shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg
>
{:else if type === 'info'}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current flex-shrink-0 w-6 h-6"
><path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/></svg
>
{/if}
<slot />
</div>

View File

@@ -1,25 +0,0 @@
<script lang="ts">
import Toast from './Toast.svelte';
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
</script>
{#if $toasts.length > 0}
<section>
<article class="toast toast-top toast-center rounded-none w-2/3 lg:w-[20rem]" role="alert">
{#each $toasts as toast (toast.id)}
<Toast
type={toast.type}
on:resume={() => resumeToast(toast.id)}
on:pause={() => pauseToast(toast.id)}
on:click={() => dismissToast(toast.id)}>{@html toast.message}</Toast
>
{/each}
</article>
</section>
{/if}
<style lang="postcss">
section {
@apply fixed top-0 left-0 right-0 w-full flex flex-col mt-4 justify-center z-[1000];
}
</style>

View File

@@ -1,10 +0,0 @@
<script lang="ts">
import { Tooltip } from 'flowbite-svelte';
export let placement = 'bottom';
export let color = 'bg-coollabs';
export let triggeredBy = '#tooltip-default';
</script>
<Tooltip {triggeredBy} {placement} arrow={false} defaultClass={color + ' font-thin text-xs text-left border-none p-2'} style="custom"
><slot /></Tooltip
>

View File

@@ -1,206 +0,0 @@
<script lang="ts">
import { dev } from '$app/environment';
import {
addToast,
appSession,
features,
updateLoading,
isUpdateAvailable,
latestVersion
} from '$lib/store';
import { asyncSleep, errorNotification } from '$lib/common';
import { onMount } from 'svelte';
import Tooltip from './Tooltip.svelte';
let updateStatus: any = {
found: false,
loading: false,
success: null
};
async function update() {
updateStatus.loading = true;
try {
if (dev) {
localStorage.setItem('lastVersion', $appSession.version);
await asyncSleep(1000);
updateStatus.loading = false;
return window.location.reload();
} else {
localStorage.setItem('lastVersion', $appSession.version);
// await post(`/update`, { type: 'update', latestVersion: $latestVersion });
addToast({
message: 'Update completed.<br><br>Waiting for the new version to start...',
type: 'success'
});
let reachable = false;
let tries = 0;
do {
await asyncSleep(4000);
try {
// await get(`/undead`);
reachable = true;
} catch (error) {
reachable = false;
}
if (reachable) break;
tries++;
} while (!reachable || tries < 120);
addToast({
message: 'New version reachable. Reloading...',
type: 'success'
});
updateStatus.loading = false;
updateStatus.success = true;
await asyncSleep(3000);
return window.location.reload();
}
} catch (error) {
updateStatus.success = false;
updateStatus.loading = false;
return errorNotification(error);
}
}
onMount(async () => {
if ($appSession.userId) {
const overrideVersion = $features.latestVersion;
if ($appSession.teamId === '0') {
if ($updateLoading === true) return;
try {
$updateLoading = true;
// const data = await get(`/update`);
if (overrideVersion || data?.isUpdateAvailable) {
$latestVersion = overrideVersion || data.latestVersion;
if (overrideVersion) {
$isUpdateAvailable = true;
} else {
$isUpdateAvailable = data.isUpdateAvailable;
}
}
} catch (error) {
return errorNotification(error);
} finally {
$updateLoading = false;
}
}
}
});
</script>
<div class="py-0 lg:py-2">
{#if $appSession.teamId === '0'}
{#if $isUpdateAvailable}
<button
id="update"
disabled={updateStatus.success === false}
on:click={update}
class="icons bg-coollabs-gradient text-white duration-75 hover:scale-105 w-full"
>
{#if updateStatus.loading}
<svg
xmlns="http://www.w3.org/2000/svg"
class="lds-heart h-8 w-8 mx-auto"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M19.5 13.572l-7.5 7.428l-7.5 -7.428m0 0a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572"
/>
</svg>
{:else if updateStatus.success === null}
<div class="flex items-center justify-center space-x-2">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="12" y1="8" x2="8" y2="12" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="8" />
</svg>
<span class="flex lg:hidden">Update available</span>
</div>
{:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-8 w-8"
><path
fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
/><path
fill="#EA596E"
d="M13 12L.416 32.506l-.282.635.011.011c-.208.403.14 1.223.853 1.937.232.232.473.408.709.557L17 17l-4-5z"
/><path
fill="#A0041E"
d="M23.012 13.066c4.67 4.672 7.263 9.652 5.789 11.124-1.473 1.474-6.453-1.118-11.126-5.788-4.671-4.672-7.263-9.654-5.79-11.127 1.474-1.473 6.454 1.119 11.127 5.791z"
/><path
fill="#AA8DD8"
d="M18.59 13.609c-.199.161-.459.245-.734.215-.868-.094-1.598-.396-2.109-.873-.541-.505-.808-1.183-.735-1.862.128-1.192 1.324-2.286 3.363-2.066.793.085 1.147-.17 1.159-.292.014-.121-.277-.446-1.07-.532-.868-.094-1.598-.396-2.11-.873-.541-.505-.809-1.183-.735-1.862.13-1.192 1.325-2.286 3.362-2.065.578.062.883-.057 1.012-.134.103-.063.144-.123.148-.158.012-.121-.275-.446-1.07-.532-.549-.06-.947-.552-.886-1.102.059-.549.55-.946 1.101-.886 2.037.219 2.973 1.542 2.844 2.735-.13 1.194-1.325 2.286-3.364 2.067-.578-.063-.88.057-1.01.134-.103.062-.145.123-.149.157-.013.122.276.446 1.071.532 2.037.22 2.973 1.542 2.844 2.735-.129 1.192-1.324 2.286-3.362 2.065-.578-.062-.882.058-1.012.134-.104.064-.144.124-.148.158-.013.121.276.446 1.07.532.548.06.947.553.886 1.102-.028.274-.167.511-.366.671z"
/><path
fill="#77B255"
d="M30.661 22.857c1.973-.557 3.334.323 3.658 1.478.324 1.154-.378 2.615-2.35 3.17-.77.216-1.001.584-.97.701.034.118.425.312 1.193.095 1.972-.555 3.333.325 3.657 1.479.326 1.155-.378 2.614-2.351 3.17-.769.216-1.001.585-.967.702.033.117.423.311 1.192.095.53-.149 1.084.16 1.233.691.148.532-.161 1.084-.693 1.234-1.971.555-3.333-.323-3.659-1.479-.324-1.154.379-2.613 2.353-3.169.77-.217 1.001-.584.967-.702-.032-.117-.422-.312-1.19-.096-1.974.556-3.334-.322-3.659-1.479-.325-1.154.378-2.613 2.351-3.17.768-.215.999-.585.967-.701-.034-.118-.423-.312-1.192-.096-.532.15-1.083-.16-1.233-.691-.149-.53.161-1.082.693-1.232z"
/><path
fill="#AA8DD8"
d="M23.001 20.16c-.294 0-.584-.129-.782-.375-.345-.432-.274-1.061.156-1.406.218-.175 5.418-4.259 12.767-3.208.547.078.927.584.849 1.131-.078.546-.58.93-1.132.848-6.493-.922-11.187 2.754-11.233 2.791-.186.148-.406.219-.625.219z"
/><path
fill="#77B255"
d="M5.754 16c-.095 0-.192-.014-.288-.042-.529-.159-.829-.716-.67-1.245 1.133-3.773 2.16-9.794.898-11.364-.141-.178-.354-.353-.842-.316-.938.072-.849 2.051-.848 2.071.042.551-.372 1.031-.922 1.072-.559.034-1.031-.372-1.072-.923-.103-1.379.326-4.035 2.692-4.214 1.056-.08 1.933.287 2.552 1.057 2.371 2.951-.036 11.506-.542 13.192-.13.433-.528.712-.958.712z"
/><circle fill="#5C913B" cx="25.5" cy="9.5" r="1.5" /><circle
fill="#9266CC"
cx="2"
cy="18"
r="2"
/><circle fill="#5C913B" cx="32.5" cy="19.5" r="1.5" /><circle
fill="#5C913B"
cx="23.5"
cy="31.5"
r="1.5"
/><circle fill="#FFCC4D" cx="28" cy="4" r="2" /><circle
fill="#FFCC4D"
cx="32.5"
cy="8.5"
r="1.5"
/><circle fill="#FFCC4D" cx="29.5" cy="12.5" r="1.5" /><circle
fill="#FFCC4D"
cx="7.5"
cy="23.5"
r="1.5"
/></svg
>
{:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path
fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
/><path
fill="#664500"
d="M22 27c0 2.763-1.791 3-4 3-2.21 0-4-.237-4-3 0-2.761 1.79-6 4-6 2.209 0 4 3.239 4 6zm8-12c-.124 0-.25-.023-.371-.072-5.229-2.091-7.372-5.241-7.461-5.374-.307-.46-.183-1.081.277-1.387.459-.306 1.077-.184 1.385.274.019.027 1.93 2.785 6.541 4.629.513.206.763.787.558 1.3-.157.392-.533.63-.929.63zM6 15c-.397 0-.772-.238-.929-.629-.205-.513.044-1.095.557-1.3 4.612-1.844 6.523-4.602 6.542-4.629.308-.456.929-.577 1.387-.27.457.308.581.925.275 1.383-.089.133-2.232 3.283-7.46 5.374C6.25 14.977 6.124 15 6 15z"
/><path fill="#5DADEC" d="M24 16h4v19l-4-.046V16zM8 35l4-.046V16H8v19z" /><path
fill="#664500"
d="M14.999 18c-.15 0-.303-.034-.446-.105-3.512-1.756-7.07-.018-7.105 0-.495.249-1.095.046-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.498-2.197 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552zm14 0c-.15 0-.303-.034-.446-.105-3.513-1.756-7.07-.018-7.105 0-.494.248-1.094.047-1.342-.447-.247-.494-.047-1.095.447-1.342.182-.09 4.501-2.196 8.895 0 .494.247.694.848.447 1.342-.176.35-.529.552-.896.552z"
/><ellipse fill="#5DADEC" cx="18" cy="34" rx="18" ry="2" /><ellipse
fill="#E75A70"
cx="18"
cy="27"
rx="3"
ry="2"
/></svg
>
{/if}
</button>
<Tooltip triggeredBy="#update" placement="right" color="bg-coolgray-200 text-white"
>New Version Available!</Tooltip
>
{/if}
{/if}
</div>

View File

@@ -1,17 +0,0 @@
<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" />
<line x1="4" y1="7" x2="20" y2="7" />
<line x1="10" y1="11" x2="10" y2="17" />
<line x1="14" y1="11" x2="14" y2="17" />
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12" />
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3" />
</svg>

Before

Width:  |  Height:  |  Size: 486 B

View File

@@ -1,10 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
stroke-width="3"
stroke="currentColor"
class="w-3 h-3 text-white"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25" />
</svg>

Before

Width:  |  Height:  |  Size: 262 B

View File

@@ -1,47 +0,0 @@
<script lang="ts">
import * as Icons from '$lib/components/icons/applications';
export let application: any;
export let isAbsolute = true;
</script>
{#if application.buildPack?.toLowerCase() === 'rust'}
<Icons.Rust {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'node'}
<Icons.Nodejs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'react'}
<Icons.React {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'svelte'}
<Icons.Svelte {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'vuejs'}
<Icons.Vuejs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'php'}
<Icons.Php {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'python'}
<Icons.Python {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'static'}
<Icons.Static {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nestjs'}
<Icons.Nestjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nuxtjs'}
<Icons.Nuxtjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'nextjs'}
<Icons.Nextjs {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'gatsby'}
<Icons.Gatsby {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'docker'}
<Icons.Docker {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'astro'}
<Icons.Astro {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'eleventy'}
<Icons.Eleventy {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'deno'}
<Icons.Deno {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'laravel'}
<Icons.Laravel {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'heroku'}
<Icons.Heroku {isAbsolute} />
{:else if application.buildPack?.toLowerCase() === 'compose'}
<Icons.Compose {isAbsolute} />
{:else if application.simpleDockerfile}
<Icons.Docker {isAbsolute} />
{/if}

View File

@@ -1,25 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-6 h-14 w-14' : 'mx-auto w-8 h-8'}
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="a"
fill="#302649"
fill-rule="evenodd"
clip-rule="evenodd"
d="M163.008 18.929c1.944 2.413 2.935 5.67 4.917 12.181l43.309 142.27a180.277 180.277 0 00-51.778-17.53l-28.198-95.29a3.67 3.67 0 00-7.042.01l-27.857 95.232a180.225 180.225 0 00-52.01 17.557l43.52-142.281c1.99-6.502 2.983-9.752 4.927-12.16a15.999 15.999 0 016.484-4.798c2.872-1.154 6.271-1.154 13.07-1.154h31.085c6.807 0 10.211 0 13.086 1.157a16.004 16.004 0 016.487 4.806z"
/>
<path
id="flame"
fill="#EF661E"
fill-rule="evenodd"
clip-rule="evenodd"
d="M168.19 180.151c-7.139 6.105-21.39 10.268-37.804 10.268-20.147 0-37.033-6.272-41.513-14.707-1.602 4.835-1.961 10.367-1.961 13.902 0 0-1.056 17.355 11.015 29.426 0-6.268 5.081-11.349 11.349-11.349 10.743 0 10.731 9.373 10.721 16.977v.679c0 11.542 7.054 21.436 17.086 25.606a23.27 23.27 0 01-2.339-10.2c0-11.008 6.463-15.107 13.974-19.87 5.976-3.79 12.616-8.001 17.192-16.449a31.024 31.024 0 003.743-14.82c0-3.299-.513-6.479-1.463-9.463z"
/>
</svg>

View File

@@ -1,9 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="docker compose logo"
class={isAbsolute ? 'w-16 h-16 absolute top-0 left-0 -m-8' : 'w-8 h-8 mx-auto'}
src="/icons/compose.png"
/>

File diff suppressed because one or more lines are too long

View File

@@ -1,9 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg viewBox="0 0 128 128" class={isAbsolute ? 'absolute top-0 left-0 -m-5 h-12 w-12' : 'mx-auto w-10 h-10'}>
<path d="M124.8 52.1c-4.3-2.5-10-2.8-14.8-1.4-.6-5.2-4-9.7-8-12.9l-1.6-1.3-1.4 1.6c-2.7 3.1-3.5 8.3-3.1 12.3.3 2.9 1.2 5.9 3 8.3-1.4.8-2.9 1.9-4.3 2.4-2.8 1-5.9 2-8.9 2H79V49H66V24H51v12H26v13H13v14H1.8l-.2 1.5c-.5 6.4.3 12.6 3 18.5l1.1 2.2.1.2c7.9 13.4 21.7 19 36.8 19 29.2 0 53.3-13.1 64.3-40.6 7.4.4 15-1.8 18.6-8.9l.9-1.8-1.6-1zM28 39h10v11H28V39zm13.1 44.2c0 1.7-1.4 3.1-3.1 3.1-1.7 0-3.1-1.4-3.1-3.1 0-1.7 1.4-3.1 3.1-3.1 1.7.1 3.1 1.4 3.1 3.1zM28 52h10v11H28V52zm-13 0h11v11H15V52zm27.7 50.2c-15.8-.1-24.3-5.4-31.3-12.4 2.1.1 4.1.2 5.9.2 1.6 0 3.2 0 4.7-.1 3.9-.2 7.3-.7 10.1-1.5 2.3 5.3 6.5 10.2 14 13.8h-3.4zM51 63H40V52h11v11zm0-13H40V39h11v11zm13 13H53V52h11v11zm0-13H53V39h11v11zm0-13H53V26h11v11zm13 26H66V52h11v11zM38.8 81.2c-.2-.1-.5-.2-.8-.2-1.2 0-2.2 1-2.2 2.2 0 1.2 1 2.2 2.2 2.2s2.2-1 2.2-2.2c0-.3-.1-.6-.2-.8-.2.3-.4.5-.8.5-.5 0-.9-.4-.9-.9.1-.4.3-.7.5-.8z" fill="#019BC6"></path>
</svg>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
viewBox="0 0 128 128"
class={isAbsolute ? 'absolute top-0 left-0 -m-8 h-16 w-16' : 'mx-auto w-8 h-8'}
>
<path fill="transparent" d="M18 0h92v128H18z" /><path
d="M55.3 36.3h.4c1.1 0 1.5.9 1.5 2.3v41.8c0 1.8-.4 3-1.6 3l-4.8-.1c-1.2 0-1.6-1-1.6-3V45.5l-2.1.5c-1 0-1.5-.8-1.5-2.2v-3c0-1.2.4-2 1.4-2.2l8.3-2.2zm16 36.1l.1 3 .6 1.3.6.6.8.1h2.2c1 0 1.7.8 1.7 2v1.9c0 1.2-.6 2-1.8 2h-3.2l-2.3-.1c-.7-.2-1.4-.5-2.2-1a5.7 5.7 0 01-2-1.9c-.4-.8-.8-1.9-1-3.2-.4-1.4-.5-3-.5-4.8v-16h-1.5c-1.1 0-1.6-1-1.6-2.4v-1.7c0-1.4.5-2.3 1.6-2.3h1.5v-.1l.6-12.3c0-1.5.5-2.5 1.6-2.5h3.1c1.2 0 1.6 1 1.6 2.5v12.3h3.6c1.1 0 1.6 1 1.6 2.4V54c0 1.4-.5 2.3-1.6 2.3h-3.6v16.2zm9.4 15.7c0-2 .3-3.2 1.5-3.2.2 0 .4 0 1.4.4l1.1.3.2-.1.4-.7c.3-.6.4-1.6.4-3l-.6-3.3L79 52.7v-.9c0-1.2.3-2 1.2-2H84c.5 0 1 .3 1.3.6.3.4.5.9.6 1.6l3.4 18 2.6-17.8c.1-.8.3-1.3.6-1.7.3-.4.8-.6 1.3-.6h2.6c1 0 1.4.8 1.4 2l-.1.8L92 82.2c-.5 2.5-1 4.4-1.5 5.7a6.6 6.6 0 01-2 3c-.9.6-1.9.9-3.1.9h-.3c-2 0-3.3-.6-4.1-1.7-.3-.4-.4-1-.4-2zM31.3 38.8l8.2-2.1h.5c1 0 1.4.8 1.4 2.2v41.9c0 1.8-.4 2.9-1.6 2.9h-4.7c-1.2 0-1.6-1.1-1.6-3v-35l-2 .6c-1.2 0-1.6-.9-1.6-2.2v-3c0-1.2.4-2 1.4-2.3z"
fill="#FFF"
/>
</svg>

View File

@@ -1,13 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
viewBox="0 0 128 128"
>
<path
fill="#64328B"
d="M64,0C28.7,0,0,28.7,0,64v0c0,35.3,28.7,64,64,64s64-28.7,64-64v0C128,28.7,99.3,0,64,0z M13.2,64L64,114.8 C35.9,114.8,13.2,92.1,13.2,64z M75.4,113.5l-60.9-61C19.7,30,39.9,13.2,64,13.2c16.6,0,31.3,7.9,40.5,20.2l-7.5,7.2 C89.7,30.2,77.7,23.5,64,23.5c-17.6,0-32.5,11.2-38.1,26.8C33.1,57,75.4,98.8,78.1,102c12.7-4.7,22.3-15.5,25.4-28.9H81.9v-9.4 l33,0.2C114.8,88.2,98,108.4,75.4,113.5z"
/>
</svg>

View File

@@ -1,15 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8 '}
viewBox="0 0 72 80"
>
<path
xmlns="http://www.w3.org/2000/svg"
fill="#430098"
d="M64.8,0 L7.2,0 C3.224,0 0,3.224 0,7.2 L0,72.8 C0,76.776 3.224,80 7.2,80 L64.8,80 C68.776,80 72,76.776 72,72.8 L72,7.2 C72,3.224 68.776,0 64.8,0 Z M18,68 L18,52 L27,60 L18,68 Z M46,68 L46,44.11 C45.961,42.243 45.062,40 41,40 C32.866,40 23.742,44.091 23.651,44.132 L18,46.692 L18,12 L26,12 L26,34.711 C29.994,33.411 35.577,32 41,32 C45.945,32 48.905,33.944 50.517,35.575 C53.958,39.055 54.005,43.488 54.0002258,44 L54.0002258,68 L46,68 Z M48,25 L40,25 C43.144,20.875 45.118,16.534 46,12 L54,12 C53.46,16.544 51.618,20.9 48,25 Z"
/>
</svg>

View File

@@ -1,14 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10' : 'mx-auto w-8 h-8'}
viewBox="0 0 50 52"
xmlns="http://www.w3.org/2000/svg"
><title>Logomark</title><path
d="M49.626 11.564a.809.809 0 0 1 .028.209v10.972a.8.8 0 0 1-.402.694l-9.209 5.302V39.25c0 .286-.152.55-.4.694L20.42 51.01c-.044.025-.092.041-.14.058-.018.006-.035.017-.054.022a.805.805 0 0 1-.41 0c-.022-.006-.042-.018-.063-.026-.044-.016-.09-.03-.132-.054L.402 39.944A.801.801 0 0 1 0 39.25V6.334c0-.072.01-.142.028-.21.006-.023.02-.044.028-.067.015-.042.029-.085.051-.124.015-.026.037-.047.055-.071.023-.032.044-.065.071-.093.023-.023.053-.04.079-.06.029-.024.055-.05.088-.069h.001l9.61-5.533a.802.802 0 0 1 .8 0l9.61 5.533h.002c.032.02.059.045.088.068.026.02.055.038.078.06.028.029.048.062.072.094.017.024.04.045.054.071.023.04.036.082.052.124.008.023.022.044.028.068a.809.809 0 0 1 .028.209v20.559l8.008-4.611v-10.51c0-.07.01-.141.028-.208.007-.024.02-.045.028-.068.016-.042.03-.085.052-.124.015-.026.037-.047.054-.071.024-.032.044-.065.072-.093.023-.023.052-.04.078-.06.03-.024.056-.05.088-.069h.001l9.611-5.533a.801.801 0 0 1 .8 0l9.61 5.533c.034.02.06.045.09.068.025.02.054.038.077.06.028.029.048.062.072.094.018.024.04.045.054.071.023.039.036.082.052.124.009.023.022.044.028.068zm-1.574 10.718v-9.124l-3.363 1.936-4.646 2.675v9.124l8.01-4.611zm-9.61 16.505v-9.13l-4.57 2.61-13.05 7.448v9.216l17.62-10.144zM1.602 7.719v31.068L19.22 48.93v-9.214l-9.204-5.209-.003-.002-.004-.002c-.031-.018-.057-.044-.086-.066-.025-.02-.054-.036-.076-.058l-.002-.003c-.026-.025-.044-.056-.066-.084-.02-.027-.044-.05-.06-.078l-.001-.003c-.018-.03-.029-.066-.042-.1-.013-.03-.03-.058-.038-.09v-.001c-.01-.038-.012-.078-.016-.117-.004-.03-.012-.06-.012-.09v-.002-21.481L4.965 9.654 1.602 7.72zm8.81-5.994L2.405 6.334l8.005 4.609 8.006-4.61-8.006-4.608zm4.164 28.764l4.645-2.674V7.719l-3.363 1.936-4.646 2.675v20.096l3.364-1.937zM39.243 7.164l-8.006 4.609 8.006 4.609 8.005-4.61-8.005-4.608zm-.801 10.605l-4.646-2.675-3.363-1.936v9.124l4.645 2.674 3.364 1.937v-9.124zM20.02 38.33l11.743-6.704 5.87-3.35-8-4.606-9.211 5.303-8.395 4.833 7.993 4.524z"
fill="#FF2D20"
fill-rule="evenodd"
/></svg
>

File diff suppressed because one or more lines are too long

View File

@@ -1,14 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute
? 'absolute top-0 left-0 -m-4 h-10 w-10 fill-current text-blue-500'
: 'mx-auto w-8 h-8 fill-current text-blue-500'}
viewBox="0 0 128 128"
>
<path
d="M64 0C28.7 0 0 28.7 0 64s28.7 64 64 64c11.2 0 21.7-2.9 30.8-7.9L48.4 55.3v36.6h-6.8V41.8h6.8l50.5 75.8C116.4 106.2 128 86.5 128 64c0-35.3-28.7-64-64-64zm22.1 84.6l-7.5-11.3V41.8h7.5v42.8z"
/>
</svg>

View File

@@ -1,18 +0,0 @@
<script lang="ts">
export let isAbsolute = true;
</script>
<svg
class={isAbsolute ? 'absolute top-0 left-0 -m-4 h-10 w-10 text-green-500' : 'mx-auto w-8 h-8 text-green-500'}
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
focusable="false"
data-prefix="fab"
data-icon="node-js"
role="img"
viewBox="0 0 448 512"
><path
fill="currentColor"
d="M224 508c-6.7 0-13.5-1.8-19.4-5.2l-61.7-36.5c-9.2-5.2-4.7-7-1.7-8 12.3-4.3 14.8-5.2 27.9-12.7 1.4-.8 3.2-.5 4.6.4l47.4 28.1c1.7 1 4.1 1 5.7 0l184.7-106.6c1.7-1 2.8-3 2.8-5V149.3c0-2.1-1.1-4-2.9-5.1L226.8 37.7c-1.7-1-4-1-5.7 0L36.6 144.3c-1.8 1-2.9 3-2.9 5.1v213.1c0 2 1.1 4 2.9 4.9l50.6 29.2c27.5 13.7 44.3-2.4 44.3-18.7V167.5c0-3 2.4-5.3 5.4-5.3h23.4c2.9 0 5.4 2.3 5.4 5.3V378c0 36.6-20 57.6-54.7 57.6-10.7 0-19.1 0-42.5-11.6l-48.4-27.9C8.1 389.2.7 376.3.7 362.4V149.3c0-13.8 7.4-26.8 19.4-33.7L204.6 9c11.7-6.6 27.2-6.6 38.8 0l184.7 106.7c12 6.9 19.4 19.8 19.4 33.7v213.1c0 13.8-7.4 26.7-19.4 33.7L243.4 502.8c-5.9 3.4-12.6 5.2-19.4 5.2zm149.1-210.1c0-39.9-27-50.5-83.7-58-57.4-7.6-63.2-11.5-63.2-24.9 0-11.1 4.9-25.9 47.4-25.9 37.9 0 51.9 8.2 57.7 33.8.5 2.4 2.7 4.2 5.2 4.2h24c1.5 0 2.9-.6 3.9-1.7s1.5-2.6 1.4-4.1c-3.7-44.1-33-64.6-92.2-64.6-52.7 0-84.1 22.2-84.1 59.5 0 40.4 31.3 51.6 81.8 56.6 60.5 5.9 65.2 14.8 65.2 26.7 0 20.6-16.6 29.4-55.5 29.4-48.9 0-59.6-12.3-63.2-36.6-.4-2.6-2.6-4.5-5.3-4.5h-23.9c-3 0-5.3 2.4-5.3 5.3 0 31.1 16.9 68.2 97.8 68.2 58.4-.1 92-23.2 92-63.4z"
/>
</svg>

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