Compare commits

...

221 Commits

Author SHA1 Message Date
Andras Bacsai
8994dde8f0 Merge pull request #715 from coollabsio/next
v3.11.2
2022-11-08 14:55:51 +01:00
Andras Bacsai
b7303a0828 fix: remove contribution docs 2022-11-08 14:44:54 +01:00
Andras Bacsai
5bc330162a Merge pull request #709 from gabrielengel/g-contribute
Organizing Contribution.md
2022-11-08 14:43:57 +01:00
Andras Bacsai
0ebc0217f3 fix: umami + ghost issues 2022-11-08 14:42:04 +01:00
Andras Bacsai
95c810b80a Merge pull request #714 from coollabsio/next
v3.11.2
2022-11-08 12:09:09 +01:00
Andras Bacsai
82d7fb883d fix: more simplified webhooks 2022-11-08 11:54:22 +01:00
Andras Bacsai
b96e710543 fix: remove ghost-mariadb from the list 2022-11-08 11:30:41 +01:00
Andras Bacsai
24e5e85225 revert staging release 2022-11-08 11:22:53 +01:00
Andras Bacsai
7b8f81f1b2 Merge pull request #713 from coollabsio/next
fixes
2022-11-08 11:21:41 +01:00
Andras Bacsai
62e60fc7ab fix: simplify webhooks 2022-11-08 11:15:56 +01:00
Andras Bacsai
ccd3d4aded fix: preview webhooks 2022-11-08 10:40:11 +01:00
Andras Bacsai
f0cf155b5c Merge pull request #712 from coollabsio/next
fix: migrate template
2022-11-08 10:33:20 +01:00
Andras Bacsai
b66f67d889 fix: migrate template 2022-11-08 10:19:02 +01:00
Andras Bacsai
e5dc07bde1 Merge pull request #711 from coollabsio/next
Quick fixes
2022-11-08 10:09:16 +01:00
Andras Bacsai
a955eb0fec fix: coolify instance proxy 2022-11-08 10:08:47 +01:00
Andras Bacsai
c070af9681 Merge pull request #710 from coollabsio/next
v3.11.1
2022-11-08 09:55:03 +01:00
Andras Bacsai
c15e060ef2 fix: appwrite webhook 2022-11-08 09:54:25 +01:00
Gabriel Engel
6d6f2454a7 Link Contribution on Readme 2022-11-07 19:02:59 -03:00
Gabriel Engel
9ff44ed46b Fix Typo 2022-11-07 18:58:54 -03:00
Gabriel Engel
adf3ef61b8 Link GettingStarted.md 2022-11-07 18:58:06 -03:00
Gabriel Engel
832107e0b8 Requirements 2022-11-07 18:56:14 -03:00
Gabriel Engel
0ea1e71808 Organizing contributing 2022-11-07 18:44:47 -03:00
Andras Bacsai
3b9b3f8ffa Merge pull request #708 from coollabsio/next
Fixes for v3.11.0
2022-11-07 15:21:38 +01:00
Andras Bacsai
fda8823050 fix: plausible analytics things 2022-11-07 14:59:39 +01:00
Andras Bacsai
b5756cb14f save templates 2022-11-07 14:10:37 +01:00
Andras Bacsai
617d3dbe52 fix: templates 2022-11-07 13:48:57 +01:00
Andras Bacsai
c18beb1c7c fix: templates 2022-11-07 13:40:18 +01:00
Andras Bacsai
a6957b919c fix: template 2022-11-07 13:38:55 +01:00
Andras Bacsai
816a362534 fix: confirm restart service 2022-11-07 13:35:45 +01:00
Andras Bacsai
7ce3ebde4e fix: templates 2022-11-07 13:30:58 +01:00
Andras Bacsai
cc2f83c4d9 Merge pull request #705 from coollabsio/next
v3.11.0
2022-11-07 12:02:32 +01:00
Andras Bacsai
6ce492049e fix 2022-11-07 11:52:34 +01:00
Andras Bacsai
a7999de4b0 fix: compose icon 2022-11-07 11:27:17 +01:00
Andras Bacsai
d4bdfabf19 chore: version++ 2022-11-07 11:03:16 +01:00
Andras Bacsai
85030ab804 fix: template files 2022-11-07 10:44:50 +01:00
Andras Bacsai
2a9bd00a50 fixes 2022-11-07 09:36:51 +01:00
Andras Bacsai
1c2d76e651 UI updates 2022-11-07 08:59:06 +01:00
Andras Bacsai
a97f7d225a fix: remove old minio proxies 2022-11-04 21:34:24 +01:00
Andras Bacsai
2781848aac cleanup 2022-11-04 21:15:08 +01:00
Andras Bacsai
d179da2bee fix 2022-11-04 21:11:38 +01:00
Andras Bacsai
60e7922734 stop minio proxy on restart 2022-11-04 21:07:19 +01:00
Andras Bacsai
1ece37ec3c update template 2022-11-04 15:20:31 +01:00
Andras Bacsai
8dad865146 contribution guide 2022-11-04 14:52:21 +01:00
Andras Bacsai
80d15e782b fixes 2022-11-04 14:41:52 +01:00
Andras Bacsai
d24e4c6518 fix icons 2022-11-04 14:24:22 +01:00
Andras Bacsai
6def46544c fix: wh catchall for all 2022-11-04 12:11:04 +01:00
Andras Bacsai
d66bae32d3 fix: preview wbh 2022-11-04 12:08:26 +01:00
Andras Bacsai
1b753a4020 fix: Pr stopps main deployment 2022-11-04 12:08:20 +01:00
Andras Bacsai
25e6a74a0a fix: wb for previews 2022-11-04 11:45:47 +01:00
Andras Bacsai
afde00a4be fix: websecure redirect 2022-11-04 11:44:04 +01:00
Andras Bacsai
0022d380bb fix: webhooks 2022-11-04 11:26:43 +01:00
Andras Bacsai
d80d2ab934 fix: previews wbh 2022-11-04 10:52:08 +01:00
Andras Bacsai
13c1734753 feat: redirect catch-all rule 2022-11-04 10:50:16 +01:00
Andras Bacsai
bf33d6c34e fix: remote webhooks 2022-11-04 09:58:37 +01:00
Andras Bacsai
1b02f9bd5d fix: webhook simplified 2022-11-04 09:54:13 +01:00
Andras Bacsai
9f63c645ff fix: load public repos 2022-11-04 09:53:57 +01:00
Andras Bacsai
abf271fb68 fixes 2022-11-03 15:54:39 +01:00
Andras Bacsai
363755c3bf fix doclinks 2022-11-03 15:37:39 +01:00
Andras Bacsai
ba2db666aa updates 2022-11-03 15:17:31 +01:00
Andras Bacsai
9f3677b694 updates 2022-11-03 14:59:37 +01:00
Andras Bacsai
e6024c997f debug more 2022-11-03 14:41:58 +01:00
Andras Bacsai
3cb83e2286 debug 2022-11-03 14:28:53 +01:00
Andras Bacsai
780d03e5e1 fixes 2022-11-03 14:16:51 +01:00
Andras Bacsai
214114e6ce fixes 2022-11-03 14:09:06 +01:00
Andras Bacsai
274d3fe679 fix wh again 2022-11-03 13:50:04 +01:00
Andras Bacsai
0ecf86d8a3 remove debug 2022-11-03 13:47:49 +01:00
Andras Bacsai
c2d4390a72 fix wh 2022-11-03 13:46:51 +01:00
Andras Bacsai
7627d59d43 debug 2022-11-03 13:37:48 +01:00
Andras Bacsai
71ce9a6b37 fix: pathprefix 2022-11-03 13:28:58 +01:00
Andras Bacsai
232018c925 fix 2022-11-03 11:40:55 +01:00
Andras Bacsai
9dfbbe58ff fix: toast, rde, webhooks 2022-11-03 11:32:18 +01:00
Andras Bacsai
fa9738a2e0 fix: tooltip 2022-11-03 10:13:36 +01:00
Andras Bacsai
94ecbc5921 fix: app logs view 2022-11-03 09:43:41 +01:00
Andras Bacsai
3c68d317d7 fix: traefik proxy q 10s 2022-11-03 09:43:34 +01:00
Andras Bacsai
56d4edfb9d fix: toast 2022-11-03 09:43:23 +01:00
Andras Bacsai
c6c037ff17 fixes 2022-11-03 09:31:01 +01:00
Andras Bacsai
44feba4d89 fix branches 2022-11-03 08:14:57 +01:00
Andras Bacsai
962f2c7380 Merge pull request #682 from Huskehhh/main
fix: expose port is readonly on the wrong condition
2022-11-02 22:49:43 +01:00
Andras Bacsai
22007426aa fix migration 2022-11-02 22:44:46 +01:00
Andras Bacsai
008e9a92d3 fixes 2022-11-02 22:32:09 +01:00
Andras Bacsai
41139ee2ab fixes 2022-11-02 22:11:32 +01:00
Andras Bacsai
845c40d23c fixes 2022-11-02 21:31:16 +01:00
Andras Bacsai
a22f26c4c8 fixes 2022-11-02 16:03:27 +01:00
Andras Bacsai
99ff020f56 fix 2022-11-02 15:37:51 +01:00
Andras Bacsai
f863b42b71 fix: heroku bp 2022-11-02 15:36:23 +01:00
Andras Bacsai
2e713b459e fixes 2022-11-02 15:19:20 +01:00
Andras Bacsai
923241ce1e fixes 2022-11-02 10:08:22 +01:00
Andras Bacsai
3a8929b9d7 fix 2022-11-02 09:58:14 +01:00
Andras Bacsai
eb92d39d40 replace ws with socketio 2022-11-02 09:49:21 +01:00
Andras Bacsai
bdc62a007e fixes 2022-10-28 20:30:34 +00:00
Andras Bacsai
4b35db6291 wss 2022-10-28 14:51:22 +00:00
Andras Bacsai
c8282b215d use window location for ws in prod 2022-10-28 14:29:21 +00:00
Andras Bacsai
c123669828 test ws 2022-10-28 15:50:57 +02:00
Andras Bacsai
781fd0a1cd fixes 2022-10-28 12:03:07 +02:00
Andras Bacsai
9bd99605fb stop wp ftp on service stop 2022-10-28 12:02:22 +02:00
Andras Bacsai
dc626bd4f0 fixes 2022-10-28 11:54:03 +02:00
Andras Bacsai
aa27aeafa1 fix fqdn check 2022-10-28 09:15:03 +02:00
Andras Bacsai
cdb25cd0e9 remove console.log 2022-10-27 20:06:21 +00:00
Andras Bacsai
dc2d15fd9c fix 2022-10-27 20:05:02 +00:00
Andras Bacsai
55cb788380 fixes 2022-10-27 19:21:33 +00:00
Andras Bacsai
0f3b7fe643 fixes 2022-10-27 18:55:21 +00:00
Andras Bacsai
4b812350a8 fix migrations 2022-10-27 13:37:45 +02:00
Andras Bacsai
aec37164de debug 2022-10-27 13:18:24 +02:00
Andras Bacsai
dec02bd8db update pnpm lock 2022-10-27 12:06:22 +02:00
Andras Bacsai
1bd6a8ed9e update dockerfile 2022-10-27 12:06:12 +02:00
Andras Bacsai
2030f714fa cleanup stuffs 2022-10-27 09:55:32 +02:00
Andras Bacsai
4416646954 package updates + tags selector 2022-10-26 15:50:10 +02:00
Andras Bacsai
52ba9dc02a Update devTemplates.yaml 2022-10-26 15:44:29 +02:00
Andras Bacsai
dad3d42d14 fixes 2022-10-26 14:12:27 +02:00
Andras Bacsai
0d12f3043b fix + cleanup 2022-10-26 13:44:32 +02:00
Andras Bacsai
1225786fc0 cleanup + fixes 2022-10-26 11:21:55 +02:00
Andras Bacsai
71496d5229 cleanup + arm support 2022-10-26 10:49:30 +02:00
Andras Bacsai
eb0aa20fe1 fixes 2022-10-26 10:27:33 +02:00
Andras Bacsai
c34de3d0a3 fixes 2022-10-26 10:12:17 +02:00
Andras Bacsai
54e0a9fc28 fix 2022-10-26 09:35:56 +02:00
Andras Bacsai
4bcd034b3d fixes 2022-10-26 09:27:43 +02:00
Andras Bacsai
111bd29cc8 updates 2022-10-25 22:43:31 +02:00
Andras Bacsai
b0fcd23ca6 template update 2022-10-25 15:59:04 +02:00
Andras Bacsai
f80b1d31f5 fixes, dev templates, etc 2022-10-25 15:12:40 +02:00
Andras Bacsai
811ea5b92a fixes 2022-10-24 22:54:19 +02:00
Andras Bacsai
f9dfbd5800 fix 2022-10-24 21:54:33 +02:00
Andras Bacsai
88f1c36929 fixes and remote tempaltes 2022-10-24 14:23:34 +02:00
Andras Bacsai
8bbe771f5b fake service in dev 2022-10-24 13:12:31 +02:00
Andras Bacsai
c578fa63e5 updates 2022-10-24 13:10:05 +02:00
Andras Bacsai
17badf95dc fix 2022-10-21 22:34:27 +02:00
Andras Bacsai
a267ee40d2 asd 2022-10-21 22:15:50 +02:00
Andras Bacsai
8ef645b3c2 fix 2022-10-21 22:13:29 +02:00
Andras Bacsai
35625b22f5 hm 2022-10-21 22:05:05 +02:00
Andras Bacsai
221dcefd6c fix 2022-10-21 21:24:52 +02:00
Andras Bacsai
9c74a9c1db fixes 2022-10-21 21:19:30 +02:00
Andras Bacsai
55fc3920fc updates 2022-10-21 20:54:33 +02:00
Andras Bacsai
5d60b5eb8b saving things 2022-10-21 15:51:32 +02:00
Andras Bacsai
049d5166e8 add template.json 2022-10-21 11:30:16 +02:00
Andras Bacsai
f4019db3d1 fixes 2022-10-20 16:06:33 +02:00
Andras Bacsai
9f3732d35b fix: service logs 2022-10-20 10:42:47 +02:00
Andras Bacsai
b4f17ac3c6 add weblate 2022-10-20 10:03:23 +02:00
Andras Bacsai
978e35d335 add searxng 2022-10-20 09:44:08 +02:00
Andras Bacsai
22cbbec960 add searxng 2022-10-20 09:18:13 +02:00
Andras Bacsai
21f3a70788 add glitchtip 2022-10-19 14:59:38 +02:00
Andras Bacsai
b4c6f80e1c add hasura 2022-10-19 14:15:48 +02:00
Andras Bacsai
e1198c42eb add hasura 2022-10-19 13:14:39 +02:00
Andras Bacsai
e09fdbcef0 add umami + hashedpws 2022-10-19 12:00:43 +02:00
Andras Bacsai
b708e79929 add umami 2022-10-19 11:26:27 +02:00
Andras Bacsai
cbaecff3b7 meilisearch 2022-10-19 10:55:16 +02:00
Andras Bacsai
4f7d2630af meilisearch 2022-10-19 10:29:08 +02:00
Andras Bacsai
92d3860240 ghost 2022-10-19 10:24:53 +02:00
Andras Bacsai
3757d5da9f ghost 2022-10-19 10:07:04 +02:00
Andras Bacsai
1d38a885bb add wordpress 2022-10-18 15:17:59 +02:00
Andras Bacsai
dbd767e8f1 wordpress 2022-10-18 15:01:18 +02:00
Andras Bacsai
8b83c38127 vscode 2022-10-18 14:45:30 +02:00
Andras Bacsai
f1ea01e709 vscodeserver + minio 2022-10-18 14:34:10 +02:00
Andras Bacsai
12a1aeb0f8 add minio 2022-10-18 14:12:33 +02:00
Andras Bacsai
413150012f fix 2022-10-18 13:57:48 +02:00
Andras Bacsai
8ef5604ce8 updates 2022-10-18 13:52:47 +02:00
Andras Bacsai
42e50c800b fix 2022-10-18 12:51:53 +02:00
Andras Bacsai
8fbd08003c fixes 2022-10-18 12:43:35 +02:00
Andras Bacsai
877577efdb plausible migration done 2022-10-18 12:02:09 +02:00
Andras Bacsai
a6f457749b lots of changes 2022-10-18 11:32:38 +02:00
Andras Bacsai
9afb713df1 add length option to template 2022-10-17 14:55:28 +00:00
Andras Bacsai
8f660c0276 work-work 2022-10-17 15:43:57 +02:00
Andras Bacsai
a7e86d9afd fix 2022-10-14 15:54:19 +02:00
Andras Bacsai
462eea90c0 tons of updates 2022-10-14 15:48:37 +02:00
Andras Bacsai
79c30dfc91 remove nix file 2022-10-14 09:45:45 +02:00
Jordyn
410a78b366 fix: expose port is readonly on the wrong condition 2022-10-14 10:49:53 +11:00
Andras Bacsai
065807a0bc fix: secret errors 2022-10-13 15:43:57 +02:00
Andras Bacsai
1d93658e56 Merge pull request #680 from coollabsio/next
v3.10.16
2022-10-12 22:05:39 +02:00
Andras Bacsai
2b7865e6ea update packages 2022-10-12 19:57:32 +00:00
Andras Bacsai
0cdba8c329 fix: single container logs and usage with compose 2022-10-12 19:53:21 +00:00
Andras Bacsai
11b317b788 ui: new resource label 2022-10-12 15:10:00 +02:00
Andras Bacsai
fb955e15f4 Merge pull request #656 from coollabsio/next
v3.10.15
2022-10-12 14:51:56 +02:00
Andras Bacsai
ae2d141f0d fix: pull does not work remotely on huge compose file 2022-10-12 14:27:13 +02:00
Andras Bacsai
68c983923e fix: dockerfile 2022-10-12 14:08:00 +02:00
Andras Bacsai
bf252f7f20 fix: appwrite v1 missing containers 2022-10-12 13:50:28 +02:00
Andras Bacsai
324038486f fixes 2022-10-12 12:02:47 +02:00
Andras Bacsai
bef5da49cf remove text 2022-10-12 11:43:47 +02:00
Andras Bacsai
24e7f547fa feat: monitoring by container 2022-10-12 11:42:45 +02:00
Andras Bacsai
3ee3ab0ad1 fix: port required if fqdn is set 2022-10-12 11:27:13 +02:00
Andras Bacsai
f734154da8 fix: check compose domains in general 2022-10-12 11:17:02 +02:00
Andras Bacsai
7a053ce697 fix: gitlab auth and compose reload 2022-10-12 11:11:18 +02:00
Andras Bacsai
25f250310e fix: dev container 2022-10-12 10:18:28 +02:00
Andras Bacsai
4eca05bbba fix: gh release 2022-10-12 10:07:18 +02:00
Andras Bacsai
45b0f791bb ci: update staging release 2022-10-11 10:54:22 +02:00
Andras Bacsai
42415a81c1 fix: update docker binaries 2022-10-11 10:54:13 +02:00
Andras Bacsai
6882e83d1e fix: logs for not running containers 2022-10-10 15:28:46 +02:00
Andras Bacsai
d4b7318413 fix: smart search for new services 2022-10-10 15:28:36 +02:00
Andras Bacsai
a2b4d400af fix: add git sha to build args 2022-10-10 15:28:16 +02:00
Andras Bacsai
f07868d24e fix: do not show nope as ip address for dbs 2022-10-10 15:28:02 +02:00
Andras Bacsai
d46ee049f4 Merge pull request #668 from bstst/bugfix/fix-deno-run-options
Fix deno options string
2022-10-10 15:26:00 +02:00
Andras Bacsai
c62eda5627 Merge pull request #666 from cmer/cmer-inject-git-sha
Inject GIT SHA into build
2022-10-10 15:17:18 +02:00
Martin Saulis
7683164ed2 fix deno options string 2022-10-07 14:59:16 +03:00
Carl Mercier
7090c16575 Update common.ts 2022-10-06 15:42:57 -04:00
Carl Mercier
9e634fed13 Inject GIT SHA into build process
As discussed at https://github.com/docker/hub-feedback/issues/600
2022-10-06 15:38:15 -04:00
Andras Bacsai
9bb125cebd feat: docker compose 2022-10-06 15:51:08 +02:00
Andras Bacsai
0c4850b91d fixes 2022-10-06 14:24:28 +02:00
Andras Bacsai
0eb7688c4d fixes 2022-10-06 14:15:05 +02:00
Andras Bacsai
f47cdb68d9 fixes 2022-10-06 12:01:24 +02:00
Andras Bacsai
d3c3cded37 debug: one less worker thread 2022-10-06 11:44:36 +02:00
Andras Bacsai
e91ea4ecbe feat: docker compose 2022-10-06 11:37:47 +02:00
Andras Bacsai
680b20d199 update dev container flow 2022-10-06 11:37:42 +02:00
Andras Bacsai
ec97e04fd4 revert last debug 2022-10-06 11:37:28 +02:00
Andras Bacsai
b0b2657fe0 debug: remove worker jobs 2022-10-06 10:26:40 +02:00
Andras Bacsai
d27426fd8f feat: docker compose support 2022-10-06 10:25:41 +02:00
Andras Bacsai
d8206c0e3e wip: docker compose 2022-10-05 15:34:52 +02:00
Andras Bacsai
3f1841a188 init: docker-compose support 2022-10-05 10:27:12 +00:00
Andras Bacsai
cb478e0dc8 add contribution guide on container based development flow 2022-10-05 09:13:51 +00:00
Andras Bacsai
02c42a7e3a fix: pure docker based development 2022-10-05 09:01:17 +00:00
Andras Bacsai
ef40f7349e Merge pull request #653 from coollabsio/next
v3.10.14
2022-10-05 09:24:39 +02:00
Andras Bacsai
86eebb35cb fix: do not use npx 2022-10-05 08:58:14 +02:00
Andras Bacsai
a901388887 revert 2022-10-04 23:06:46 +02:00
Andras Bacsai
6cd1c5de38 Dockerfile update 2022-10-04 22:22:29 +02:00
Andras Bacsai
7489f172a1 test prestart 2022-10-04 22:14:16 +02:00
Andras Bacsai
702798c275 revert things 2022-10-04 22:00:50 +02:00
Andras Bacsai
430d51866c test: remove prisma 2022-10-04 21:46:23 +02:00
Andras Bacsai
9d08421f01 dev intervals 2022-10-04 21:46:01 +02:00
Andras Bacsai
f4051874b2 Merge pull request #654 from vvvctr/patch-1
Proper capitalization for WordPress service type.
2022-10-04 21:14:52 +02:00
vvvctr
bbe0690056 Proper capitalization for WordPress service type. 2022-10-04 16:27:56 +02:00
Andras Bacsai
772c0d1e41 fix: nope if you are not logged in 2022-10-04 15:14:52 +02:00
Andras Bacsai
8eb9ca0260 fix: add buildkit features 2022-10-04 15:06:09 +02:00
Andras Bacsai
bd27afe0da fix: verify and configure remote docker engines 2022-10-04 15:01:47 +02:00
Andras Bacsai
a3af21275a fix: meilisearch data dir 2022-10-04 14:05:11 +02:00
Andras Bacsai
61eb155d13 chore: version++ 2022-10-04 14:05:01 +02:00
194 changed files with 9854 additions and 9444 deletions

View File

@@ -1,5 +1,6 @@
.DS_Store
node_modules
.pnpm-store
build
.svelte-kit
package
@@ -9,4 +10,8 @@ package
dist
client
apps/api/db/*.db
local-serve
local-serve
apps/api/db/migration.db-journal
apps/api/core*
logs
others/certificates

View File

@@ -5,7 +5,7 @@ on:
types: [released]
jobs:
arm64-build:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
@@ -31,7 +31,7 @@ jobs:
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-build:
amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -57,9 +57,35 @@ jobs:
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-amd64,mode=max
aarch64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Get current package version
uses: martinbeentjes/npm-get-version-action@v1.2.3
id: package-version
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/aarch64
push: true
tags: coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-aarch64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-aarch64,mode=max
merge-manifest:
runs-on: ubuntu-latest
needs: [amd64-build, arm64-build]
needs: [amd64, arm64, aarch64]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -77,7 +103,7 @@ jobs:
id: package-version
- name: Create & publish manifest
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
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 manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}}
- uses: sarisia/actions-status-discord@v1
if: always()

View File

@@ -6,7 +6,7 @@ on:
- next
jobs:
arm64-making-something-cool:
arm64:
runs-on: [self-hosted, arm64]
steps:
- name: Checkout
@@ -34,7 +34,7 @@ jobs:
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-making-something-cool:
amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -59,12 +59,12 @@ jobs:
context: .
platforms: linux/amd64
push: true
tags: coollabsio/coolify:next-amd64,coollabsio/coolify:next-test
tags: coollabsio/coolify:next-amd64
cache-from: type=registry,ref=coollabsio/coolify:buildcache-next-amd64
cache-to: type=registry,ref=coollabsio/coolify:buildcache-next-amd64,mode=max
merge-manifest-to-be-cool:
merge-manifest:
runs-on: ubuntu-latest
needs: [arm64-making-something-cool, amd64-making-something-cool]
needs: [arm64, amd64]
steps:
- name: Checkout
uses: actions/checkout@v3

2
.gitignore vendored
View File

@@ -14,4 +14,4 @@ local-serve
apps/api/db/migration.db-journal
apps/api/core*
logs
others/certificates
others/certificates

View File

@@ -1,115 +1,48 @@
# Contribution
# Contributing
First, thanks for considering to contribute to my project. It really means a lot! :)
> "First, thanks for considering to contribute to my project.
It really means a lot! 😁" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our Discord server in the #contribution channel.
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## Setup your development environment
### Github codespaces
You'll need a set of skills to [get started](docs/contribution/GettingStarted.md).
If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already.
## 1) Setup your development environment
### Gitpod
- 🌟 [Container based](docs/dev_setup/Container.md) ← *Recomended*
- 📦 [DockerContainer](docs/dev_setup/DockerContiner.md) *WIP
- 🐙 [Github Codespaces](docs/dev_setup/GithubCodespaces.md)
- ☁️ [GitPod](docs/dev_setup/GitPod.md)
- 🍏 [Local Mac](docs/dev_setup/Mac.md)
If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. All the required dependencies and packages has been configured for you already.
## 2) Basic requirements
### Local Machine
> At the moment, Coolify `doesn't support Windows`. You must use `Linux` or `MacOS` or consider using Gitpod or Github Codespaces.
- [Install Pnpm](https://pnpm.io/installation)
- [Install Docker Engine](https://docs.docker.com/engine/install/)
- [Setup Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/)
- [Setup GIT LFS Support](https://git-lfs.github.com/)
- Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient!
## 3) Setup Coolify
- You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally.
- You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally.
- You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally.
- Copy `apps/api/.env.example` to `apps/api/.env`
- Edit `apps/api/.env`, set the `COOLIFY_APP_ID` environment variable to something cool.
- 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:seed` seed the database.
- Run `pnpm dev` start coding.
Optional:
- To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally.
```sh
# Or... Copy and paste commands bellow:
cp apps/api/.env.example apps/api.env
pnpm install
pnpm db:push
pnpm db:seed
pnpm dev
```
### Inside a Docker container
`WIP`
## 4) Start Coding
## Setup Coolify
- Copy `apps/api/.env.template` to `apps/api/.env.template` and set the `COOLIFY_APP_ID` environment variable to something cool.
- `pnpm install` to install dependencies.
- `pnpm db:push` to o create a local SQlite database.
You should be able to access `http://localhost:3000`.
This will apply all migrations at `db/dev.db`.
- `pnpm db:seed` seed the database.
- `pnpm dev` start coding.
## Technical skills required
- **Languages**: Node.js / Javascript / Typescript
- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/)
- **Database ORM**: [Prisma.io](https://www.prisma.io/)
- **Docker Engine API**
## Add a new service
### Which service is eligable to add to Coolify?
The following statements needs to be true:
- Self-hostable
- Open-source
- Maintained (I do not want to add software full of bugs)
### Create Prisma / Database schema for the new service.
All data that needs to be persist for a service should be saved to the database in `cleartext` or `encrypted`.
very password/api key/passphrase needs to be encrypted. If you are not sure, whether it should be encrypted or not, just encrypt it.
Update Prisma schema in [src/apps/api/prisma/schema.prisma](https://github.com/coollabsio/coolify/blob/main/apps/api/prisma/schema.prisma).
- Add new model with the new service name.
- Make a relationship with `Service` model.
- In the `Service` model, the name of the new field should be with low-capital.
- If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field.
Once done, create Prisma schema with `pnpm db:push`.
> You may also need to restart `Typescript Language Server` in your IDE to get the new types.
### Add available versions
Versions are hardcoded into Coolify at the moment and based on Docker image tags.
- Update `supportedServiceTypesAndVersions` function [here](apps/api/src/lib/services/supportedVersions.ts)
### Include the new service in queries
At [here](apps/api/src/lib/services/common.ts) in `includeServices` function add the new table name, so it will be included in all places in the database queries where it is required.
### Define auto-generated fields
At [here](apps/api/src/lib/services/common.ts) in `configureServiceType` function add the initial auto-generated details such as password, users etc, and the encryption process of secrets (if applicable).
### Define input field details
At [here](apps/api/src/lib/services/serviceFields.ts) add details about the input fields shown in the UI, so every component (API/UI) will know what to do with the values (decrypt/show it by default/readonly/etc).
### Define the start process
- At [here](apps/api/src/lib/services/handlers.ts), define how the service should start. It could be complex and based on `docker-compose` definitions.
> See `startUmamiService()` function as example.
- At [here](apps/api/src/routes/api/v1/services/handlers.ts), add the new start service process to `startService` function.
### Define the deletion process
[Here](apps/api/src/lib/services/common.ts) in `removeService` add the database deletion process.
### Custom logo
- At [here](apps/ui/src/lib/components/svg/services) add the service custom log as a Svelte component and export it [here](apps/ui/src/lib/components/svg/services/index.ts).
> SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning.
- At [here](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) include the new logo with `isAbsolute` property.
- At [here](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) add links to the documentation of the service.
### Custom fields on the UI
By default the URL and name are shown on the UI. Everything else needs to be added [here](apps/ui/src/routes/services/[id]/_Services/_Services.svelte)
> If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component [here](apps/ui/src/routes/services/[id]/_Services) with an underscore. For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte).
Good job! 👏
1. Click `Register` and setup your first user.

View File

@@ -1,5 +1,4 @@
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
FROM node:18-slim as build
WORKDIR /app
@@ -17,20 +16,26 @@ WORKDIR /app
ENV NODE_ENV production
ARG TARGETPLATFORM
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# 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
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.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-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/
# https://download.docker.com/linux/static/stable/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
# https://github.com/docker/compose/releases
# Reverted to 2.6.1 because of this https://github.com/docker/compose/issues/9704. 2.9.0 still has a bug.
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.6.1 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN (curl -sSL "https://github.com/buildpacks/pack/releases/download/v0.27.0/pack-v0.27.0-linux.tgz" | tar -C /usr/local/bin/ --no-same-owner -xzv pack)
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /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/others/fluentbit/ ./fluentbit
@@ -38,6 +43,8 @@ COPY --from=build /app/apps/ui/build/ ./public
COPY --from=build /app/apps/api/prisma/ ./prisma
COPY --from=build /app/apps/api/package.json .
COPY --from=build /app/docker-compose.yaml .
COPY --from=build /app/apps/api/tags.json .
COPY --from=build /app/apps/api/templates.json .
RUN pnpm install -p

31
Dockerfile-dev Normal file
View File

@@ -0,0 +1,31 @@
FROM node:18-slim
ENV NODE_ENV development
ARG TARGETPLATFORM
ARG PNPM_VERSION=7.11.0
ARG NPM_VERSION=8.19.1
# https://download.docker.com/linux/static/stable/
ARG DOCKER_VERSION=20.10.18
# https://github.com/docker/compose/releases
# 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
# https://github.com/buildpacks/pack/releases
ARG PACK_VERSION=v0.27.0
WORKDIR /app
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
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-get clean autoclean && apt-get autoremove --yes && rm -rf /var/lib/{apt,dpkg,cache,log}/
RUN npm --no-update-notifier --no-fund --global install pnpm@${PNPM_VERSION}
RUN npm install -g npm@${PNPM_VERSION}
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-$DOCKER_VERSION -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-$DOCKER_COMPOSE_VERSION -o ~/.docker/cli-plugins/docker-compose
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/pack-$PACK_VERSION -o /usr/local/bin/pack
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker /usr/local/bin/pack
EXPOSE 3000
ENV CHECKPOINT_DISABLE=1

View File

@@ -105,6 +105,10 @@ A fresh installation is necessary. v2 and v3 are not compatible with v1.
- Email: [andras@coollabs.io](mailto:andras@coollabs.io)
- Discord: [Invitation](https://coollabs.io/discord)
## Development Contributions
Coolify is developed under the Apache License and you can help to make it grow → [Start coding!](./CONTRIBUTION.md)
## Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]

1
apps/api/devTags.json Normal file

File diff suppressed because one or more lines are too long

2792
apps/api/devTemplates.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"description": "Coolify's Fastify API",
"license": "Apache-2.0",
"scripts": {
"db:generate": "prisma generate",
"db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed",
"db:studio": "prisma studio",
@@ -11,36 +12,36 @@
"build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --minify=true --platform=node --outdir=build --format=cjs",
"format": "prettier --write 'src/**/*.{js,ts,json,md}'",
"lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .",
"start": "NODE_ENV=production npx -y prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js"
"start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js"
},
"dependencies": {
"@breejs/ts-worker": "2.0.0",
"@fastify/autoload": "5.4.0",
"@fastify/autoload": "5.4.1",
"@fastify/cookie": "8.3.0",
"@fastify/cors": "8.1.0",
"@fastify/cors": "8.1.1",
"@fastify/env": "4.1.0",
"@fastify/jwt": "6.3.2",
"@fastify/multipart": "7.2.0",
"@fastify/multipart": "7.3.0",
"@fastify/static": "6.5.0",
"@iarna/toml": "2.2.5",
"@ladjs/graceful": "3.0.2",
"@prisma/client": "4.4.0",
"axios": "0.27.2",
"@prisma/client": "4.5.0",
"bcryptjs": "2.4.3",
"bree": "9.1.2",
"cabin": "9.1.2",
"compare-versions": "5.0.1",
"csv-parse": "^5.3.0",
"csvtojson": "^2.0.10",
"csv-parse": "5.3.1",
"csvtojson": "2.0.10",
"cuid": "2.1.8",
"dayjs": "1.11.5",
"dayjs": "1.11.6",
"dockerode": "3.3.4",
"dotenv-extended": "2.9.0",
"execa": "6.1.0",
"fastify": "4.7.0",
"fastify-plugin": "4.2.1",
"fastify": "4.9.2",
"fastify-plugin": "4.3.0",
"fastify-socket.io": "4.0.0",
"generate-password": "1.7.0",
"got": "12.5.1",
"got": "12.5.2",
"is-ip": "5.0.0",
"is-port-reachable": "4.0.0",
"js-yaml": "4.1.0",
@@ -49,26 +50,28 @@
"node-os-utils": "1.3.7",
"p-all": "4.0.0",
"p-throttle": "5.0.0",
"prisma": "4.5.0",
"public-ip": "6.0.1",
"pump": "^3.0.0",
"pump": "3.0.0",
"socket.io": "4.5.3",
"ssh-config": "4.1.6",
"strip-ansi": "7.0.1",
"unique-names-generator": "4.7.1"
},
"devDependencies": {
"@types/node": "18.7.23",
"@types/node": "18.11.6",
"@types/node-os-utils": "1.3.0",
"@typescript-eslint/eslint-plugin": "5.38.1",
"@typescript-eslint/parser": "5.38.1",
"esbuild": "0.15.10",
"eslint": "8.23.0",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"esbuild": "0.15.12",
"eslint": "8.26.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"nodemon": "2.0.20",
"prettier": "2.7.1",
"prisma": "4.4.0",
"rimraf": "3.0.2",
"tsconfig-paths": "4.1.0",
"types-fastify-socket.io": "0.0.1",
"typescript": "4.8.4"
},
"prisma": {

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "dockerComposeFile" TEXT;
ALTER TABLE "Application" ADD COLUMN "dockerComposeFileLocation" TEXT;

View File

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

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "ServiceSetting" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ServicePersistentStorage" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"path" TEXT NOT NULL,
"volumeName" TEXT,
"predefined" BOOLEAN NOT NULL DEFAULT false,
"containerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ServicePersistentStorage_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ServicePersistentStorage" ("createdAt", "id", "path", "serviceId", "updatedAt") SELECT "createdAt", "id", "path", "serviceId", "updatedAt" FROM "ServicePersistentStorage";
DROP TABLE "ServicePersistentStorage";
ALTER TABLE "new_ServicePersistentStorage" RENAME TO "ServicePersistentStorage";
CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_path_key" ON "ServicePersistentStorage"("serviceId", "path");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,24 @@
/*
Warnings:
- Added the required column `variableName` to the `ServiceSetting` table without a default value. This is not possible if the table is not empty.
*/
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_ServiceSetting" (
"id" TEXT NOT NULL PRIMARY KEY,
"serviceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"variableName" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "ServiceSetting_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_ServiceSetting" ("createdAt", "id", "name", "serviceId", "updatedAt", "value") SELECT "createdAt", "id", "name", "serviceId", "updatedAt", "value" FROM "ServiceSetting";
DROP TABLE "ServiceSetting";
ALTER TABLE "new_ServiceSetting" RENAME TO "ServiceSetting";
CREATE UNIQUE INDEX "ServiceSetting_serviceId_name_key" ON "ServiceSetting"("serviceId", "name");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Service" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"fqdn" TEXT,
"exposePort" INTEGER,
"dualCerts" BOOLEAN NOT NULL DEFAULT false,
"type" TEXT,
"version" TEXT,
"templateVersion" TEXT NOT NULL DEFAULT '0.0.0',
"destinationDockerId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Service_destinationDockerId_fkey" FOREIGN KEY ("destinationDockerId") REFERENCES "DestinationDocker" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
INSERT INTO "new_Service" ("createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version") SELECT "createdAt", "destinationDockerId", "dualCerts", "exposePort", "fqdn", "id", "name", "type", "updatedAt", "version" FROM "Service";
DROP TABLE "Service";
ALTER TABLE "new_Service" RENAME TO "Service";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[serviceId,containerId,path]` on the table `ServicePersistentStorage` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "ServicePersistentStorage_serviceId_path_key";
-- CreateIndex
CREATE UNIQUE INDEX "ServicePersistentStorage_serviceId_containerId_path_key" ON "ServicePersistentStorage"("serviceId", "containerId", "path");

View File

@@ -0,0 +1,32 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"ownMysql" BOOLEAN NOT NULL DEFAULT false,
"mysqlHost" TEXT,
"mysqlPort" INTEGER,
"mysqlUser" TEXT,
"mysqlPassword" TEXT,
"mysqlRootUser" TEXT,
"mysqlRootUserPassword" TEXT,
"mysqlDatabase" TEXT,
"mysqlPublicPort" INTEGER,
"ftpEnabled" BOOLEAN NOT NULL DEFAULT false,
"ftpUser" TEXT,
"ftpPassword" TEXT,
"ftpPublicPort" INTEGER,
"ftpHostKey" TEXT,
"ftpHostKeyPrivate" TEXT,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Wordpress_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
INSERT INTO "new_Wordpress" ("createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "ftpEnabled", "ftpHostKey", "ftpHostKeyPrivate", "ftpPassword", "ftpPublicPort", "ftpUser", "id", "mysqlDatabase", "mysqlHost", "mysqlPassword", "mysqlPort", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "ownMysql", "serviceId", "tablePrefix", "updatedAt" FROM "Wordpress";
DROP TABLE "Wordpress";
ALTER TABLE "new_Wordpress" RENAME TO "Wordpress";
CREATE UNIQUE INDEX "Wordpress_serviceId_key" ON "Wordpress"("serviceId");
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

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

View File

@@ -29,6 +29,7 @@ model Setting {
proxyPassword String
proxyUser String
proxyHash String?
proxyDefaultRedirect String?
isAutoUpdateEnabled Boolean @default(false)
isDNSCheckEnabled Boolean @default(true)
DNSServers String?
@@ -94,43 +95,46 @@ model TeamInvitation {
}
model Application {
id String @id @default(cuid())
name String
fqdn String?
repository String?
configHash String?
branch String?
buildPack String?
projectId Int?
port Int?
exposePort Int?
installCommand String?
buildCommand String?
startCommand String?
baseDirectory String?
publishDirectory String?
deploymentType String?
phpModules String?
pythonWSGI String?
pythonModule String?
pythonVariable String?
dockerFileLocation String?
denoMainFile String?
denoOptions String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDockerId String?
gitSourceId String?
baseImage String?
baseBuildImage String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ApplicationPersistentStorage[]
settings ApplicationSettings?
secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
id String @id @default(cuid())
name String
fqdn String?
repository String?
configHash String?
branch String?
buildPack String?
projectId Int?
port Int?
exposePort Int?
installCommand String?
buildCommand String?
startCommand String?
baseDirectory String?
publishDirectory String?
deploymentType String?
phpModules String?
pythonWSGI String?
pythonModule String?
pythonVariable String?
dockerFileLocation String?
denoMainFile String?
denoOptions String?
dockerComposeFile String?
dockerComposeFileLocation String?
dockerComposeConfiguration String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDockerId String?
gitSourceId String?
baseImage String?
baseBuildImage String?
gitSource GitSource? @relation(fields: [gitSourceId], references: [id])
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ApplicationPersistentStorage[]
settings ApplicationSettings?
secrets Secret[]
teams Team[]
connectedDatabase ApplicationConnectedDatabase?
previewApplication PreviewApplication[]
}
model PreviewApplication {
@@ -190,14 +194,17 @@ model ApplicationPersistentStorage {
}
model ServicePersistentStorage {
id String @id @default(cuid())
serviceId String
path String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
id String @id @default(cuid())
serviceId String
path String
volumeName String?
predefined Boolean @default(false)
containerId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@unique([serviceId, path])
@@unique([serviceId, containerId, path])
}
model Secret {
@@ -389,12 +396,14 @@ model Service {
dualCerts Boolean @default(false)
type String?
version String?
templateVersion String @default("0.0.0")
destinationDockerId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationDocker DestinationDocker? @relation(fields: [destinationDockerId], references: [id])
persistentStorage ServicePersistentStorage[]
serviceSecret ServiceSecret[]
serviceSetting ServiceSetting[]
teams Team[]
fider Fider?
@@ -414,6 +423,19 @@ model Service {
taiga Taiga?
}
model ServiceSetting {
id String @id @default(cuid())
serviceId String
name String
value String
variableName String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
service Service @relation(fields: [serviceId], references: [id])
@@unique([serviceId, name])
}
model PlausibleAnalytics {
id String @id @default(cuid())
email String?
@@ -459,10 +481,10 @@ model Wordpress {
ownMysql Boolean @default(false)
mysqlHost String?
mysqlPort Int?
mysqlUser String
mysqlPassword String
mysqlRootUser String
mysqlRootUserPassword String
mysqlUser String?
mysqlPassword String?
mysqlRootUser String?
mysqlRootUserPassword String?
mysqlDatabase String?
mysqlPublicPort Int?
ftpEnabled Boolean @default(false)

View File

@@ -0,0 +1,67 @@
import fs from 'fs/promises';
import yaml from 'js-yaml';
import got from 'got';
const repositories = [];
const templates = await fs.readFile('./apps/api/devTemplates.yaml', 'utf8');
const devTemplates = yaml.load(templates);
for (const template of devTemplates) {
let image = template.services['$$id'].image.replaceAll(':$$core_version', '');
if (!image.includes('/')) {
image = `library/${image}`;
}
repositories.push({ image, name: template.type });
}
const services = []
const numberOfTags = 30;
// const semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/g)
for (const repository of repositories) {
console.log('Querying', repository.name, 'at', repository.image);
let semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/g)
if (repository.name.startsWith('wordpress')) {
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-php(0|[1-9]\d*)$/g)
}
if (repository.name.startsWith('minio')) {
semverRegex = new RegExp(/^RELEASE.*$/g)
}
if (repository.name.startsWith('fider')) {
semverRegex = new RegExp(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
}
if (repository.name.startsWith('searxng')) {
semverRegex = new RegExp(/^\d{4}[\.\-](0?[1-9]|[12][0-9]|3[01])[\.\-](0?[1-9]|1[012]).*$/)
}
if (repository.name.startsWith('umami')) {
semverRegex = new RegExp(/^postgresql-v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-([0-9]+)$/g)
}
if (repository.image.includes('ghcr.io')) {
const { execaCommand } = await import('execa');
const { stdout } = await execaCommand(`docker run --rm quay.io/skopeo/stable list-tags docker://${repository.image}`);
if (stdout) {
const json = JSON.parse(stdout);
const semverTags = json.Tags.filter((tag) => semverRegex.test(tag))
let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : json.Tags.sort().reverse().slice(0, numberOfTags)
if (!tags.includes('latest')) {
tags.push('latest')
}
services.push({ name: repository.name, image: repository.image, tags })
}
} else {
const { token } = await got.get(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repository.image}:pull`).json()
let data = await got.get(`https://registry-1.docker.io/v2/${repository.image}/tags/list`, {
headers: {
Authorization: `Bearer ${token}`
}
}).json()
const semverTags = data.tags.filter((tag) => semverRegex.test(tag))
let tags = semverTags.length > 10 ? semverTags.sort().reverse().slice(0, numberOfTags) : data.tags.sort().reverse().slice(0, numberOfTags)
if (!tags.includes('latest')) {
tags.push('latest')
}
services.push({
name: repository.name,
image: repository.image,
tags
})
}
}
await fs.writeFile('./apps/api/devTags.json', JSON.stringify(services));

View File

@@ -6,10 +6,20 @@ import cookie from '@fastify/cookie';
import multipart from '@fastify/multipart';
import path, { join } from 'path';
import autoLoad from '@fastify/autoload';
import { asyncExecShell, createRemoteEngineConfiguration, getDomain, isDev, listSettings, prisma, version } from './lib/common';
import socketIO from 'fastify-socket.io'
import socketIOServer from './realtime'
import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, encrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common';
import { scheduler } from './lib/scheduler';
import { compareVersions } from 'compare-versions';
import Graceful from '@ladjs/graceful'
import yaml from 'js-yaml'
import fs from 'fs/promises';
import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers';
import { checkContainer } from './lib/docker';
import { migrateServicesToNewTemplate } from './lib';
import { refreshTags, refreshTemplates } from './routes/api/v1/handlers';
declare module 'fastify' {
interface FastifyInstance {
config: {
@@ -27,7 +37,8 @@ declare module 'fastify' {
const port = isDev ? 3001 : 3000;
const host = '0.0.0.0';
prisma.setting.findFirst().then(async (settings) => {
(async () => {
const settings = await prisma.setting.findFirst()
const fastify = Fastify({
logger: settings?.isAPIDebuggingEnabled || false,
trustProxy: true
@@ -70,7 +81,6 @@ prisma.setting.findFirst().then(async (settings) => {
}
};
const options = {
schema,
dotenv: true
@@ -99,30 +109,40 @@ prisma.setting.findFirst().then(async (settings) => {
});
fastify.register(cookie)
fastify.register(cors);
fastify.addHook('onRequest', async (request, reply) => {
let allowedList = ['coolify:3000'];
const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
ipv4 && allowedList.push(`${ipv4}:3000`);
ipv6 && allowedList.push(ipv6);
fqdn && allowedList.push(getDomain(fqdn));
isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
if (remotes.length > 0) {
remotes.forEach(remote => {
allowedList.push(`${remote.remoteIpAddress}:3000`);
})
}
if (!allowedList.includes(request.headers.host)) {
// console.log('not allowed', request.headers.host)
fastify.register(socketIO, {
cors: {
origin: isDev ? "*" : ''
}
})
fastify.listen({ port, host }, async (err: any, address: any) => {
if (err) {
console.error(err);
process.exit(1);
}
// To detect allowed origins
// fastify.addHook('onRequest', async (request, reply) => {
// console.log(request.headers.host)
// let allowedList = ['coolify:3000'];
// const { ipv4, ipv6, fqdn } = await prisma.setting.findFirst({})
// ipv4 && allowedList.push(`${ipv4}:3000`);
// ipv6 && allowedList.push(ipv6);
// fqdn && allowedList.push(getDomain(fqdn));
// isDev && allowedList.push('localhost:3000') && allowedList.push('localhost:3001') && allowedList.push('host.docker.internal:3001');
// const remotes = await prisma.destinationDocker.findMany({ where: { remoteEngine: true, remoteVerified: true } })
// if (remotes.length > 0) {
// remotes.forEach(remote => {
// allowedList.push(`${remote.remoteIpAddress}:3000`);
// })
// }
// if (!allowedList.includes(request.headers.host)) {
// // console.log('not allowed', request.headers.host)
// }
// })
try {
await fastify.listen({ port, host })
await socketIOServer(fastify)
console.log(`Coolify's API is listening on ${host}:${port}`);
migrateServicesToNewTemplate()
await initServer();
const graceful = new Graceful({ brees: [scheduler] });
@@ -132,57 +152,98 @@ prisma.setting.findFirst().then(async (settings) => {
if (!scheduler.workers.has('deployApplication')) {
scheduler.run('deployApplication');
}
if (!scheduler.workers.has('infrastructure')) {
scheduler.run('infrastructure');
}
}, 2000)
// autoUpdater
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:autoUpdater")
}, isDev ? 5000 : 60000 * 15)
await autoUpdater()
}, 60000 * 15)
// cleanupStorage
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:cleanupStorage")
}, isDev ? 6000 : 60000 * 10)
await cleanupStorage()
}, 60000 * 10)
// checkProxies and checkFluentBit
// checkProxies, checkFluentBit & refresh templates
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkProxies")
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:checkFluentBit")
await checkProxies();
await checkFluentBit();
}, 60000)
// Refresh and check templates
setInterval(async () => {
await refreshTemplates()
}, 60000)
setInterval(async () => {
await refreshTags()
}, 60000)
setInterval(async () => {
await migrateServicesToNewTemplate()
}, 60000)
setInterval(async () => {
await copySSLCertificates();
}, 10000)
setInterval(async () => {
scheduler.workers.has('infrastructure') && scheduler.workers.get('infrastructure').postMessage("action:copySSLCertificates")
}, 2000)
await Promise.all([
getTagsTemplates(),
getArch(),
getIPAddress(),
configureRemoteDockers(),
])
});
})
} catch (error) {
console.error(error);
process.exit(1);
}
})();
async function getIPAddress() {
const { publicIpv4, publicIpv6 } = await import('public-ip')
try {
const settings = await listSettings();
if (!settings.ipv4) {
console.log(`Getting public IPv4 address...`);
const ipv4 = await publicIpv4({ timeout: 2000 })
await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } })
}
if (!settings.ipv6) {
console.log(`Getting public IPv6 address...`);
const ipv6 = await publicIpv6({ timeout: 2000 })
await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } })
}
} catch (error) { }
}
async function getTagsTemplates() {
const { default: got } = await import('got')
try {
if (isDev) {
const templates = await fs.readFile('./devTemplates.yaml', 'utf8')
const tags = await fs.readFile('./devTags.json', 'utf8')
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates)))
await fs.writeFile('./tags.json', tags)
console.log('Tags and templates loaded in dev mode...')
} else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text()
const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text()
await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response)))
await fs.writeFile('/app/tags.json', tags)
console.log('Tags and templates loaded...')
}
} catch (error) {
console.log("Couldn't get latest templates.")
console.log(error)
}
}
async function initServer() {
try {
console.log(`Initializing server...`);
await asyncExecShell(`docker network create --attachable coolify`);
} catch (error) { }
try {
@@ -192,10 +253,12 @@ async function initServer() {
}
} catch (error) { }
}
async function getArch() {
try {
const settings = await prisma.setting.findFirst({})
if (settings && !settings.arch) {
console.log(`Getting architecture...`);
await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } })
}
} catch (error) { }
@@ -207,9 +270,247 @@ async function configureRemoteDockers() {
where: { remoteVerified: true, remoteEngine: true }
});
if (remoteDocker.length > 0) {
console.log(`Verifying Remote Docker Engines...`);
for (const docker of remoteDocker) {
await createRemoteEngineConfiguration(docker.id)
console.log('Verifying:', docker.remoteIpAddress)
await verifyRemoteDockerEngineFn(docker.id);
}
}
} catch (error) { }
} catch (error) {
console.log(error)
}
}
async function autoUpdater() {
try {
const { default: got } = await import('got')
const currentVersion = version;
const { coolify } = await got.get('https://get.coollabs.io/versions.json', {
searchParams: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
}).json()
const latestVersion = coolify.main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) {
console.log(error)
}
}
async function checkFluentBit() {
try {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true });
if (!found) {
await asyncExecShell(`env | grep '^COOLIFY' > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
} catch (error) {
console.log(error)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
// const minioInstances = await prisma.minio.findMany({
// where: { publicPort: { not: null } },
// include: { service: { include: { destinationDocker: true } } }
// });
// for (const minio of minioInstances) {
// const { service, publicPort } = minio;
// const { destinationDockerId, destinationDocker, id } = service;
// if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
// await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
// }
// }
} catch (error) {
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function cleanupStorage() {
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) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}

View File

@@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks';
baseDirectory,
publishDirectory,
dockerFileLocation,
dockerComposeConfiguration,
denoMainFile
} = application
const currentHash = crypto
@@ -112,17 +113,6 @@ import * as buildpacks from '../lib/buildPacks';
)
.digest('hex');
const { debug } = settings;
// if (concurrency === 1) {
// await prisma.build.updateMany({
// where: {
// status: { in: ['queued', 'running'] },
// id: { not: buildId },
// applicationId,
// createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) }
// },
// data: { status: 'failed' }
// });
// }
let imageId = applicationId;
let domain = getDomain(fqdn);
const volumes =
@@ -138,6 +128,9 @@ import * as buildpacks from '../lib/buildPacks';
repository = sourceRepository || repository;
}
try {
dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration)
} catch (error) { }
let deployNeeded = true;
let destinationType;
@@ -212,17 +205,37 @@ import * as buildpacks from '../lib/buildPacks';
//
}
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage);
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
if (forceRebuild) deployNeeded = true
if (!imageFound || deployNeeded) {
// if (true) {
if (buildpacks[buildPack])
await buildpacks[buildPack]({
dockerId: destinationDocker.id,
network: destinationDocker.network,
buildId,
applicationId,
domain,
name,
type,
volumes,
labels,
pullmergeRequestId,
buildPack,
repository,
@@ -244,11 +257,12 @@ import * as buildpacks from '../lib/buildPacks';
pythonModule,
pythonVariable,
dockerFileLocation,
dockerComposeConfiguration,
denoMainFile,
denoOptions,
baseImage,
baseBuildImage,
deploymentType
deploymentType,
});
else {
await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
@@ -257,112 +271,137 @@ import * as buildpacks from '../lib/buildPacks';
} else {
await saveBuildLog({ line: 'Build image already available - no rebuild required.', buildId, applicationId });
}
try {
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker stop -t 0 ${imageId}` })
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker rm ${imageId}` })
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
if (buildPack === 'compose') {
try {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) {
//
}
try {
await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
});
}
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
const labels = makeLabelForStandaloneApplication({
applicationId,
fqdn,
name,
type,
pullmergeRequestId,
buildPack,
repository,
branch,
projectId,
port: exposePort ? `${exposePort}:${port}` : port,
commit,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
});
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
// logging: {
// driver: 'fluentd',
// },
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
throw new Error(error);
}
} else {
try {
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0`
})
await executeDockerCmd({
dockerId: destinationDockerId,
command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force`
})
} catch (error) {
//
}
const envs = [
`PORT=${port}`
];
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (pullmergeRequestId) {
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
if (isSecretFound.length > 0) {
envs.push(`${secret.name}=${isSecretFound[0].value}`);
} else {
envs.push(`${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
envs.push(`${secret.name}=${secret.value}`);
}
}
});
}
throw new Error(error);
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
let envFound = false;
try {
envFound = !!(await fs.stat(`${workdir}/.env`));
} catch (error) {
//
}
try {
await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
const composeVolumes = volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
});
const composeFile = {
version: '3.8',
services: {
[imageId]: {
image: `${applicationId}:${tag}`,
container_name: imageId,
volumes,
env_file: envFound ? [`${workdir}/.env`] : [],
labels,
depends_on: [],
expose: [port],
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
...defaultComposeConfiguration(destinationDocker.network),
}
},
networks: {
[destinationDocker.network]: {
external: true
}
},
volumes: Object.assign({}, ...composeVolumes)
};
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` })
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) {
await saveBuildLog({ line: error, buildId, applicationId });
const foundBuild = await prisma.build.findUnique({ where: { id: buildId } })
if (foundBuild) {
await prisma.build.update({
where: { id: buildId },
data: {
status: 'failed'
}
});
}
throw new Error(error);
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
}
await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } });
if (!pullmergeRequestId) await prisma.application.update({
where: { id: applicationId },
data: { configHash: currentHash }
});
}
}
catch (error) {

View File

@@ -1,296 +0,0 @@
import { parentPort } from 'node:worker_threads';
import axios from 'axios';
import { compareVersions } from 'compare-versions';
import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common';
import { checkContainer } from '../lib/docker';
import fs from 'fs/promises'
async function autoUpdater() {
try {
const currentVersion = version;
const { data: versions } = await axios
.get(
`https://get.coollabs.io/versions.json`
, {
params: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
})
const latestVersion = versions['coolify'].main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isUpdateAvailable === 1) {
const activeCount = 0
if (activeCount === 0) {
if (!isDev) {
const { isAutoUpdateEnabled } = await prisma.setting.findFirst();
if (isAutoUpdateEnabled) {
await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`);
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(
`sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env`
);
await asyncExecShell(
`docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"`
);
}
} else {
console.log('Updating (not really in dev mode).');
}
}
}
} catch (error) { }
}
async function checkFluentBit() {
if (!isDev) {
const engine = '/var/run/docker.sock';
const { id } = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify' }
});
const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' });
if (!found) {
await asyncExecShell(`env | grep COOLIFY > .env`);
await asyncExecShell(`docker compose up -d fluent-bit`);
}
}
}
async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) {
try {
await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`)
await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` })
await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` })
} catch (error) {
console.log({ error })
}
}
async function copyLocalCertificates(id: string) {
try {
await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`)
await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`)
await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`)
} catch (error) {
console.log({ error })
}
}
async function copySSLCertificates() {
try {
const pAll = await import('p-all');
const actions = []
const certificates = await prisma.certificate.findMany({ include: { team: true } })
const teamIds = certificates.map(c => c.teamId)
const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } })
for (const certificate of certificates) {
const { id, key, cert } = certificate
const decryptedKey = decrypt(key)
await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey)
await fs.writeFile(`/tmp/${id}-cert.pem`, cert)
for (const destination of destinations) {
if (destination.remoteEngine) {
if (destination.remoteVerified) {
const { id: dockerId, remoteIpAddress } = destination
actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress))
}
} else {
actions.push(async () => copyLocalCertificates(id))
}
}
}
await pAll.default(actions, { concurrency: 1 })
} catch (error) {
console.log(error)
} finally {
await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`)
}
}
async function checkProxies() {
try {
const { default: isReachable } = await import('is-port-reachable');
let portReachable;
const { arch, ipv4, ipv6 } = await listSettings();
// Coolify Proxy local
const engine = '/var/run/docker.sock';
const localDocker = await prisma.destinationDocker.findFirst({
where: { engine, network: 'coolify', isCoolifyProxyUsed: true }
});
if (localDocker) {
portReachable = await isReachable(80, { host: ipv4 || ipv6 })
if (!portReachable) {
await startTraefikProxy(localDocker.id);
}
}
// Coolify Proxy remote
const remoteDocker = await prisma.destinationDocker.findMany({
where: { remoteEngine: true, remoteVerified: true }
});
if (remoteDocker.length > 0) {
for (const docker of remoteDocker) {
if (docker.isCoolifyProxyUsed) {
portReachable = await isReachable(80, { host: docker.remoteIpAddress })
if (!portReachable) {
await startTraefikProxy(docker.id);
}
}
try {
await createRemoteEngineConfiguration(docker.id)
} catch (error) { }
}
}
// TCP Proxies
const databasesWithPublicPort = await prisma.database.findMany({
where: { publicPort: { not: null } },
include: { settings: true, destinationDocker: true }
});
for (const database of databasesWithPublicPort) {
const { destinationDockerId, destinationDocker, publicPort, id } = database;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
const { privatePort } = generateDatabaseConfiguration(database, arch);
await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort);
}
}
const wordpressWithFtp = await prisma.wordpress.findMany({
where: { ftpPublicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const ftp of wordpressWithFtp) {
const { service, ftpPublicPort } = ftp;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp');
}
}
// HTTP Proxies
const minioInstances = await prisma.minio.findMany({
where: { publicPort: { not: null } },
include: { service: { include: { destinationDocker: true } } }
});
for (const minio of minioInstances) {
const { service, publicPort } = minio;
const { destinationDockerId, destinationDocker, id } = service;
if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) {
await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000);
}
}
} catch (error) {
}
}
async function cleanupPrismaEngines() {
if (!isDev) {
try {
const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`)
if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) {
await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`)
}
} catch (error) { }
}
}
async function cleanupStorage() {
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) enginesDone.add(destination.remoteIpAddress)
let lowDiskSpace = false;
try {
let stdout = null
if (!isDev) {
const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` })
stdout = output.stdout;
} else {
const output = await asyncExecShell(
`df -kPT /`
);
stdout = output.stdout;
}
let lines = stdout.trim().split('\n');
let header = lines[0];
let regex =
/^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g;
const boundaries = [];
let match;
while ((match = regex.exec(header))) {
boundaries.push(match[0].length);
}
boundaries[boundaries.length - 1] = -1;
const data = lines.slice(1).map((line) => {
const cl = boundaries.map((boundary) => {
const column = boundary > 0 ? line.slice(0, boundary) : line;
line = line.slice(boundary);
return column.trim();
});
return {
capacity: Number.parseInt(cl[5], 10) / 100
};
});
if (data.length > 0) {
const { capacity } = data[0];
if (capacity > 0.8) {
lowDiskSpace = true;
}
}
} catch (error) { }
await cleanupDockerStorage(destination.id, lowDiskSpace, false)
}
}
(async () => {
let status = {
cleanupStorage: false,
autoUpdater: false,
copySSLCertificates: false,
}
if (parentPort) {
parentPort.on('message', async (message) => {
if (parentPort) {
if (message === 'error') throw new Error('oops');
if (message === 'cancel') {
parentPort.postMessage('cancelled');
process.exit(1);
}
if (message === 'action:cleanupStorage') {
if (!status.autoUpdater) {
status.cleanupStorage = true
await cleanupStorage();
status.cleanupStorage = false
}
return;
}
if (message === 'action:cleanupPrismaEngines') {
await cleanupPrismaEngines();
return;
}
if (message === 'action:checkProxies') {
await checkProxies();
return;
}
if (message === 'action:checkFluentBit') {
await checkFluentBit();
return;
}
if (message === 'action:copySSLCertificates') {
if (!status.copySSLCertificates) {
status.copySSLCertificates = true
await copySSLCertificates();
status.copySSLCertificates = false
}
return;
}
if (message === 'action:autoUpdater') {
if (!status.cleanupStorage) {
status.autoUpdater = true
await autoUpdater();
status.autoUpdater = false
}
return;
}
}
});
} else process.exit(0);
})();

503
apps/api/src/lib.ts Normal file
View File

@@ -0,0 +1,503 @@
import cuid from "cuid";
import { decrypt, encrypt, fixType, generatePassword, prisma } from "./lib/common";
import { getTemplates } from "./lib/services";
export async function migrateServicesToNewTemplate() {
// This function migrates old hardcoded services to the new template based services
try {
let templates = await getTemplates()
const services: any = await prisma.service.findMany({
include: {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true,
hasura: true,
fider: true,
moodle: true,
appwrite: true,
glitchTip: true,
searxng: true,
weblate: true,
taiga: true,
}
})
for (const service of services) {
try {
const { id } = service
if (!service.type) {
continue;
}
let template = templates.find(t => fixType(t.type) === fixType(service.type));
if (template) {
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id))
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics) await plausibleAnalytics(service, template)
if (service.type === 'fider' && service.fider) await fider(service, template)
if (service.type === 'minio' && service.minio) await minio(service, template)
if (service.type === 'vscodeserver' && service.vscodeserver) await vscodeserver(service, template)
if (service.type === 'wordpress' && service.wordpress) await wordpress(service, template)
if (service.type === 'ghost' && service.ghost) await ghost(service, template)
if (service.type === 'meilisearch' && service.meiliSearch) await meilisearch(service, template)
if (service.type === 'umami' && service.umami) await umami(service, template)
if (service.type === 'hasura' && service.hasura) await hasura(service, template)
if (service.type === 'glitchTip' && service.glitchTip) await glitchtip(service, template)
if (service.type === 'searxng' && service.searxng) await searxng(service, template)
if (service.type === 'weblate' && service.weblate) await weblate(service, template)
if (service.type === 'appwrite' && service.appwrite) await appwrite(service, template)
try {
await createVolumes(service, template);
} catch (error) {
console.log(error)
}
if (template.variables.length > 0) {
for (const variable of template.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else {
variable.value = variable.defaultValue || '';
}
}
}
for (const variable of template.variables) {
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
}
}
for (const s of Object.keys(template.services)) {
if (service.type === 'plausibleanalytics') {
continue;
}
if (template.services[s].volumes) {
for (const volume of template.services[s].volumes) {
const [volumeName, path] = volume.split(':')
if (!volumeName.startsWith('/')) {
const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } })
if (!found) {
await prisma.servicePersistentStorage.create({
data: { volumeName, path, containerId: s, predefined: true, service: { connect: { id } } }
});
}
}
}
}
}
await prisma.service.update({ where: { id }, data: { templateVersion: template.templateVersion } })
}
} catch (error) {
console.log(error)
}
}
} catch (error) {
console.log(error)
}
}
async function appwrite(service: any, template: any) {
const { opensslKeyV1, executorSecret, mariadbHost, mariadbPort, mariadbUser, mariadbPassword, mariadbRootUserPassword, mariadbDatabase } = service.appwrite
const secrets = [
`_APP_EXECUTOR_SECRET@@@${executorSecret}`,
`_APP_OPENSSL_KEY_V1@@@${opensslKeyV1}`,
`_APP_DB_PASS@@@${mariadbPassword}`,
`_APP_DB_ROOT_PASS@@@${mariadbRootUserPassword}`,
]
const settings = [
`_APP_DB_HOST@@@${mariadbHost}`,
`_APP_DB_PORT@@@${mariadbPort}`,
`_APP_DB_USER@@@${mariadbUser}`,
`_APP_DB_SCHEMA@@@${mariadbDatabase}`,
]
await migrateSecrets(secrets, service);
await migrateSettings(settings, service, template);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { appwrite: { disconnect: true } } })
}
async function weblate(service: any, template: any) {
const { adminPassword, postgresqlUser, postgresqlPassword, postgresqlDatabase } = service.weblate
const secrets = [
`WEBLATE_ADMIN_PASSWORD@@@${adminPassword}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
]
const settings = [
`WEBLATE_SITE_DOMAIN@@@$$generate_domain`,
`POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DATABASE@@@${postgresqlDatabase}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
`POSTGRES_HOST@@@$$id-postgres`,
`POSTGRES_PORT@@@5432`,
`REDIS_HOST@@@$$id-redis`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { weblate: { disconnect: true } } })
}
async function searxng(service: any, template: any) {
const { secretKey, redisPassword } = service.searxng
const secrets = [
`SECRET_KEY@@@${secretKey}`,
`REDIS_PASSWORD@@@${redisPassword}`,
]
const settings = [
`SEARXNG_BASE_URL@@@$$generate_fqdn`
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { searxng: { disconnect: true } } })
}
async function glitchtip(service: any, template: any) {
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, defaultEmail, defaultUsername, defaultPassword, defaultEmailFrom, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpUseTls, emailSmtpUseSsl, emailBackend, mailgunApiKey, sendgridApiKey, enableOpenUserRegistration } = service.glitchTip
const { id } = service
const secrets = [
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
`SECRET_KEY@@@${secretKeyBase}`,
`MAILGUN_API_KEY@@@${mailgunApiKey}`,
`SENDGRID_API_KEY@@@${sendgridApiKey}`,
`DJANGO_SUPERUSER_PASSWORD@@@${defaultPassword}`,
emailSmtpUser && emailSmtpPassword && emailSmtpHost && emailSmtpPort && `EMAIL_URL@@@${encrypt(`smtp://${emailSmtpUser}:${decrypt(emailSmtpPassword)}@${emailSmtpHost}:${emailSmtpPort}`)} || ''`,
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
`REDIS_URL@@@${encrypt(`redis://${id}-redis:6379`)}`
]
const settings = [
`POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
`DEFAULT_FROM_EMAIL@@@${defaultEmailFrom}`,
`EMAIL_USE_TLS@@@${emailSmtpUseTls}`,
`EMAIL_USE_SSL@@@${emailSmtpUseSsl}`,
`EMAIL_BACKEND@@@${emailBackend}`,
`ENABLE_OPEN_USER_REGISTRATION@@@${enableOpenUserRegistration}`,
`DJANGO_SUPERUSER_EMAIL@@@${defaultEmail}`,
`DJANGO_SUPERUSER_USERNAME@@@${defaultUsername}`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
await prisma.service.update({ where: { id: service.id }, data: { type: 'glitchtip' } })
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { glitchTip: { disconnect: true } } })
}
async function hasura(service: any, template: any) {
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, graphQLAdminPassword } = service.hasura
const { id } = service
const secrets = [
`HASURA_GRAPHQL_ADMIN_PASSWORD@@@${graphQLAdminPassword}`,
`HASURA_GRAPHQL_METADATA_DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
]
const settings = [
`POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { hasura: { disconnect: true } } })
}
async function umami(service: any, template: any) {
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, umamiAdminPassword, hashSalt } = service.umami
const { id } = service
const secrets = [
`HASH_SALT@@@${hashSalt}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
`ADMIN_PASSWORD@@@${umamiAdminPassword}`,
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
]
const settings = [
`DATABASE_TYPE@@@postgresql`,
`POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
await prisma.service.update({ where: { id: service.id }, data: { type: "umami-postgresql" } })
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { umami: { disconnect: true } } })
}
async function meilisearch(service: any, template: any) {
const { masterKey } = service.meiliSearch
const secrets = [
`MEILI_MASTER_KEY@@@${masterKey}`,
]
// await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { meiliSearch: { disconnect: true } } })
}
async function ghost(service: any, template: any) {
const { defaultEmail, defaultPassword, mariadbUser, mariadbPassword, mariadbRootUser, mariadbRootUserPassword, mariadbDatabase } = service.ghost
const { fqdn } = service
const isHttps = fqdn.startsWith('https://');
const secrets = [
`GHOST_PASSWORD@@@${defaultPassword}`,
`MARIADB_PASSWORD@@@${mariadbPassword}`,
`MARIADB_ROOT_PASSWORD@@@${mariadbRootUserPassword}`,
`GHOST_DATABASE_PASSWORD@@@${mariadbPassword}`,
]
const settings = [
`GHOST_EMAIL@@@${defaultEmail}`,
`GHOST_DATABASE_HOST@@@${service.id}-mariadb`,
`GHOST_DATABASE_USER@@@${mariadbUser}`,
`GHOST_DATABASE_NAME@@@${mariadbDatabase}`,
`GHOST_DATABASE_PORT_NUMBER@@@3306`,
`MARIADB_USER@@@${mariadbUser}`,
`MARIADB_DATABASE@@@${mariadbDatabase}`,
`MARIADB_ROOT_USER@@@${mariadbRootUser}`,
`GHOST_HOST@@@$$generate_domain`,
`url@@@$$generate_fqdn`,
`GHOST_ENABLE_HTTPS@@@${isHttps ? 'yes' : 'no'}`
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
await prisma.service.update({ where: { id: service.id }, data: { type: "ghost-mariadb" } })
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { ghost: { disconnect: true } } })
}
async function wordpress(service: any, template: any) {
const { extraConfig, tablePrefix, ownMysql, mysqlHost, mysqlPort, mysqlUser, mysqlPassword, mysqlRootUser, mysqlRootUserPassword, mysqlDatabase, ftpEnabled, ftpUser, ftpPassword, ftpPublicPort, ftpHostKey, ftpHostKeyPrivate } = service.wordpress
let settings = []
let secrets = []
if (ownMysql) {
secrets = [
`WORDPRESS_DB_PASSWORD@@@${mysqlPassword}`,
ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`,
ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`,
ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`,
]
settings = [
`WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`,
`WORDPRESS_DB_HOST@@@${mysqlHost}`,
`WORDPRESS_DB_PORT@@@${mysqlPort}`,
`WORDPRESS_DB_USER@@@${mysqlUser}`,
`WORDPRESS_DB_NAME@@@${mysqlDatabase}`,
]
} else {
secrets = [
`MYSQL_ROOT_PASSWORD@@@${mysqlRootUserPassword}`,
`MYSQL_PASSWORD@@@${mysqlPassword}`,
ftpPassword && `COOLIFY_FTP_PASSWORD@@@${ftpPassword}`,
ftpHostKeyPrivate && `COOLIFY_FTP_HOST_KEY_PRIVATE@@@${ftpHostKeyPrivate}`,
ftpHostKey && `COOLIFY_FTP_HOST_KEY@@@${ftpHostKey}`,
]
settings = [
`MYSQL_ROOT_USER@@@${mysqlRootUser}`,
`MYSQL_USER@@@${mysqlUser}`,
`MYSQL_DATABASE@@@${mysqlDatabase}`,
`MYSQL_HOST@@@${service.id}-mysql`,
`MYSQL_PORT@@@${mysqlPort}`,
`WORDPRESS_CONFIG_EXTRA@@@${extraConfig}`,
`WORDPRESS_TABLE_PREFIX@@@${tablePrefix}`,
`WORDPRESS_DB_HOST@@@${service.id}-mysql`,
`COOLIFY_OWN_DB@@@${ownMysql}`,
`COOLIFY_FTP_ENABLED@@@${ftpEnabled}`,
`COOLIFY_FTP_USER@@@${ftpUser}`,
`COOLIFY_FTP_PUBLIC_PORT@@@${ftpPublicPort}`,
]
}
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
if (ownMysql) {
await prisma.service.update({ where: { id: service.id }, data: { type: "wordpress-only" } })
}
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { wordpress: { disconnect: true } } })
}
async function vscodeserver(service: any, template: any) {
const { password } = service.vscodeserver
const secrets = [
`PASSWORD@@@${password}`,
]
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { vscodeserver: { disconnect: true } } })
}
async function minio(service: any, template: any) {
const { rootUser, rootUserPassword, apiFqdn } = service.minio
const secrets = [
`MINIO_ROOT_PASSWORD@@@${rootUserPassword}`,
]
const settings = [
`MINIO_ROOT_USER@@@${rootUser}`,
`MINIO_SERVER_URL@@@${apiFqdn}`,
`MINIO_BROWSER_REDIRECT_URL@@@$$generate_fqdn`,
`MINIO_DOMAIN@@@$$generate_domain`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { minio: { disconnect: true } } })
}
async function fider(service: any, template: any) {
const { postgresqlUser, postgresqlPassword, postgresqlDatabase, jwtSecret, emailNoreply, emailMailgunApiKey, emailMailgunDomain, emailMailgunRegion, emailSmtpHost, emailSmtpPort, emailSmtpUser, emailSmtpPassword, emailSmtpEnableStartTls } = service.fider
const { id } = service
const secrets = [
`JWT_SECRET@@@${jwtSecret}`,
emailMailgunApiKey && `EMAIL_MAILGUN_API@@@${emailMailgunApiKey}`,
emailSmtpPassword && `EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword}`,
`POSTGRES_PASSWORD@@@${postgresqlPassword}`,
`DATABASE_URL@@@${encrypt(`postgresql://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`)}`
]
const settings = [
`BASE_URL@@@$$generate_fqdn`,
`EMAIL_NOREPLY@@@${emailNoreply || 'noreply@example.com'}`,
`EMAIL_MAILGUN_DOMAIN@@@${emailMailgunDomain || ''}`,
`EMAIL_MAILGUN_REGION@@@${emailMailgunRegion || ''}`,
`EMAIL_SMTP_HOST@@@${emailSmtpHost || ''}`,
`EMAIL_SMTP_PORT@@@${emailSmtpPort || 587}`,
`EMAIL_SMTP_USER@@@${emailSmtpUser || ''}`,
`EMAIL_SMTP_PASSWORD@@@${emailSmtpPassword || ''}`,
`EMAIL_SMTP_ENABLE_STARTTLS@@@${emailSmtpEnableStartTls || 'false'}`,
`POSTGRES_USER@@@${postgresqlUser}`,
`POSTGRES_DB@@@${postgresqlDatabase}`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { fider: { disconnect: true } } })
}
async function plausibleAnalytics(service: any, template: any) {
const { email, username, password, postgresqlUser, postgresqlPassword, postgresqlDatabase, secretKeyBase, scriptName } = service.plausibleAnalytics;
const { id } = service
const settings = [
`BASE_URL@@@$$generate_fqdn`,
`ADMIN_USER_EMAIL@@@${email}`,
`ADMIN_USER_NAME@@@${username}`,
`DISABLE_AUTH@@@false`,
`DISABLE_REGISTRATION@@@true`,
`POSTGRESQL_USERNAME@@@${postgresqlUser}`,
`POSTGRESQL_DATABASE@@@${postgresqlDatabase}`,
`SCRIPT_NAME@@@${scriptName}`,
]
const secrets = [
`ADMIN_USER_PWD@@@${password}`,
`SECRET_KEY_BASE@@@${secretKeyBase}`,
`POSTGRESQL_PASSWORD@@@${postgresqlPassword}`,
`DATABASE_URL@@@${encrypt(`postgres://${postgresqlUser}:${decrypt(postgresqlPassword)}@${id}-postgresql:5432/${postgresqlDatabase}`)}`,
]
await migrateSettings(settings, service, template);
await migrateSecrets(secrets, service);
// Disconnect old service data
// await prisma.service.update({ where: { id: service.id }, data: { plausibleAnalytics: { disconnect: true } } })
}
async function migrateSettings(settings: any[], service: any, template: any) {
for (const setting of settings) {
try {
if (!setting) continue;
let [name, value] = setting.split('@@@')
let minio = name
if (name === 'MINIO_SERVER_URL') {
name = 'coolify_fqdn_minio_console'
}
if (!value || value === 'null') {
continue;
}
let variableName = template.variables.find((v: any) => v.name === name)?.id
if (!variableName) {
variableName = `$$config_${name.toLowerCase()}`
}
// console.log('Migrating setting', name, value, 'for service', service.id, ', service name:', service.name, 'variableName: ', variableName)
await prisma.serviceSetting.findFirst({ where: { name: minio, serviceId: service.id } }) || await prisma.serviceSetting.create({ data: { name: minio, value, variableName, service: { connect: { id: service.id } } } })
} catch(error) {
console.log(error)
}
}
}
async function migrateSecrets(secrets: any[], service: any) {
for (const secret of secrets) {
try {
if (!secret) continue;
let [name, value] = secret.split('@@@')
if (!value || value === 'null') {
continue
}
// console.log('Migrating secret', name, value, 'for service', service.id, ', service name:', service.name)
await prisma.serviceSecret.findFirst({ where: { name, serviceId: service.id } }) || await prisma.serviceSecret.create({ data: { name, value, service: { connect: { id: service.id } } } })
} catch(error) {
console.log(error)
}
}
}
async function createVolumes(service: any, template: any) {
const volumes = [];
for (const s of Object.keys(template.services)) {
if (template.services[s].volumes && template.services[s].volumes.length > 0) {
for (const volume of template.services[s].volumes) {
let volumeName = volume.split(':')[0]
const volumePath = volume.split(':')[1]
let volumeService = s
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
let volumeId = volumeName.split('-')[0]
volumeName = volumeName.replace(volumeId, service.plausibleAnalytics.id)
}
volumes.push(`${volumeName}@@@${volumePath}@@@${volumeService}`)
}
}
}
for (const volume of volumes) {
const [volumeName, path, containerId] = volume.split('@@@')
// console.log('Creating volume', volumeName, path, containerId, 'for service', service.id, ', service name:', service.name)
await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: service.id } }) || await prisma.servicePersistentStorage.create({ data: { volumeName, path, containerId, predefined: true, service: { connect: { id: service.id } } } })
}
}

View File

@@ -468,9 +468,9 @@ export const saveBuildLog = async ({
line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
}
const addTimestamp = `[${generateTimestamp()}] ${line}`;
const fluentBitUrl = isDev ? 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224';
if (isDev) {
if (isDev && !process.env.COOLIFY_CONTAINER_DEV) {
console.debug(`[${applicationId}] ${addTimestamp}`);
}
try {
@@ -480,7 +480,6 @@ export const saveBuildLog = async ({
}
})
} catch (error) {
if (isDev) return
return await prisma.buildLog.create({
data: {
line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId
@@ -580,7 +579,8 @@ export async function buildImage({
dockerId,
isCache = false,
debug = false,
dockerFileLocation = '/Dockerfile'
dockerFileLocation = '/Dockerfile',
commit
}) {
if (isCache) {
await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
@@ -596,7 +596,9 @@ export async function buildImage({
}
const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`
const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} ${workdir}` })
await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` })
const { status } = await prisma.build.findUnique({ where: { id: buildId } })
if (status === 'canceled') {
throw new Error('Deployment canceled.')
@@ -634,6 +636,7 @@ export function makeLabelForStandaloneApplication({
return [
'coolify.managed=true',
`coolify.version=${version}`,
`coolify.applicationId=${applicationId}`,
`coolify.type=standalone-application`,
`coolify.configuration=${base64Encode(
JSON.stringify({
@@ -758,4 +761,4 @@ export async function buildCacheImageWithCargo(data, imageForBuild) {
Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json');
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ ...data, isCache: true });
}
}

View File

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

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import python from './python';
import deno from './deno';
import laravel from './laravel';
import heroku from './heroku';
import compose from './compose'
export {
node,
@@ -35,5 +36,6 @@ export {
python,
deno,
laravel,
heroku
heroku,
compose
};

View File

@@ -14,13 +14,10 @@ import sshConfig from 'ssh-config';
import { checkContainer, removeContainer } from './docker';
import { day } from './dayjs';
import * as serviceFields from './services/serviceFields';
import { saveBuildLog } from './buildPacks/common';
import { scheduler } from './scheduler';
import { supportedServiceTypesAndVersions } from './services/supportedVersions';
import { includeServices } from './services/common';
export const version = '3.10.13';
export const version = '3.11.2';
export const isDev = process.env.NODE_ENV === 'development';
const algorithm = 'aes-256-ctr';
@@ -44,7 +41,7 @@ export function getAPIUrl() {
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
}
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
}
export function getUIUrl() {
@@ -198,7 +195,7 @@ export const encrypt = (text: string) => {
if (text) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
return JSON.stringify({
iv: iv.toString('hex'),
content: encrypted.toString('hex')
@@ -244,7 +241,11 @@ export async function isDNSValid(hostname: any, domain: string): Promise<any> {
}
export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', '');
if (domain) {
return domain?.replace('https://', '').replace('http://', '');
} else {
return '';
}
}
export async function isDomainConfigured({
@@ -264,7 +265,9 @@ export async function isDomainConfigured({
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
],
id: { not: id },
destinationDocker: {
@@ -277,9 +280,7 @@ export async function isDomainConfigured({
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
{ fqdn: { endsWith: `//www.${nakedDomain}` } }
],
id: { not: checkOwn ? undefined : id },
destinationDocker: {
@@ -394,12 +395,6 @@ export function generateTimestamp(): string {
return `${day().format('HH:mm:ss.SSS')}`;
}
export async function listServicesWithIncludes(): Promise<any> {
return await prisma.service.findMany({
include: includeServices,
orderBy: { createdAt: 'desc' }
});
}
export const supportedDatabaseTypesAndVersions = [
{
@@ -509,56 +504,62 @@ export async function createRemoteEngineConfiguration(id: string) {
const localPort = await getFreeSSHLocalPort(id);
const {
sshKey: { privateKey },
network,
remoteIpAddress,
remotePort,
remoteUser
} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
// Needed for remote docker compose
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
);
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
try {
await fs.stat(`/tmp/coolify-ssh-agent.pid`);
await fs.rm(`/tmp/coolify-ssh-agent.pid`);
} catch (error) { }
await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
}
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
// const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
// `ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
// );
// if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
// try {
// await fs.stat(`/tmp/coolify-ssh-agent.pid`);
// await fs.rm(`/tmp/coolify-ssh-agent.pid`);
// } catch (error) { }
// await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
// }
// await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
);
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
try {
await asyncExecShell(
`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
);
} catch (error) { }
}
// const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
// `ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
// );
// if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
// try {
// await asyncExecShell(
// `SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
// );
// } catch (error) { }
// }
const config = sshConfig.parse('');
const foundWildcard = config.find({ Host: '*' });
if (!foundWildcard) {
config.append({
Host: '*',
StrictHostKeyChecking: 'no',
ControlMaster: 'auto',
ControlPath: `${homedir}/.ssh/coolify-%r@%h:%p`,
ControlPersist: '10m'
})
}
const found = config.find({ Host: remoteIpAddress });
if (!found) {
config.append({
Host: remoteIpAddress,
Hostname: 'localhost',
Port: localPort.toString(),
User: remoteUser,
IdentityFile: sshKeyFile,
StrictHostKeyChecking: 'no'
});
}
const Host = `${remoteIpAddress}-remote`
try {
await asyncExecShell(`ssh-keygen -R ${Host}`);
await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`);
await asyncExecShell(`ssh-keygen -R localhost:${localPort}`);
} catch (error) { }
const found = config.find({ Host });
const foundIp = config.find({ Host: remoteIpAddress });
if (found) config.remove({ Host })
if (foundIp) config.remove({ Host: remoteIpAddress })
config.append({
Host,
Hostname: remoteIpAddress,
Port: remotePort.toString(),
User: remoteUser,
StrictHostKeyChecking: 'no',
IdentityFile: sshKeyFile,
ControlMaster: 'auto',
ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`,
ControlPersist: '10m'
});
try {
await fs.stat(`${homedir}/.ssh/`);
@@ -569,27 +570,23 @@ export async function createRemoteEngineConfiguration(id: string) {
}
export async function executeSSHCmd({ dockerId, command }) {
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId)
engine = `ssh://${remoteIpAddress}`
} else {
engine = 'unix:///var/run/docker.sock'
}
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
}
}
command = `ssh ${remoteIpAddress} ${command}`
return await execaCommand(command)
return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`)
}
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
const { execaCommand } = await import('execa')
let { remoteEngine, remoteIpAddress, engine, remoteUser } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
if (remoteEngine) {
await createRemoteEngineConfiguration(dockerId);
engine = `ssh://${remoteIpAddress}`;
engine = `ssh://${remoteIpAddress}-remote`;
} else {
engine = 'unix:///var/run/docker.sock';
}
@@ -598,7 +595,7 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId
command = command.replace(/docker compose/gi, 'docker-compose');
}
}
if (command.startsWith(`docker build --progress plain`) || command.startsWith(`pack build`)) {
if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
}
return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
@@ -1093,6 +1090,7 @@ export const createDirectories = async ({
repository: string;
buildId: string;
}): Promise<{ workdir: string; repodir: string }> => {
repository = repository.replaceAll(' ', '')
const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
let workdirFound = false;
@@ -1397,7 +1395,7 @@ export async function startTraefikTCPProxy(
`--entrypoints.tcp.address=:${publicPort}`,
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
'--providers.http.pollTimeout=2s',
'--providers.http.pollTimeout=10s',
'--log.level=error'
],
ports: [`${publicPort}:${publicPort}`],
@@ -1445,13 +1443,19 @@ export async function getServiceFromDB({
const settings = await prisma.setting.findFirst();
const body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
include: includeServices
include: {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
serviceSetting: true,
wordpress: true,
plausibleAnalytics: true,
}
});
if (!body) {
return null
}
let { type } = body;
type = fixType(type);
// body.type = fixType(body.type);
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
@@ -1459,87 +1463,18 @@ export async function getServiceFromDB({
return s;
});
}
if (body.wordpress) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) };
return { ...body, settings };
}
export function getServiceImage(type: string): string {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImages(type: string): string[] {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export function saveUpdateableFields(type: string, data: any) {
const update = {};
if (type && serviceFields[type]) {
serviceFields[type].map((k) => {
let temp = data[k.name];
if (temp) {
if (k.isEncrypted) {
temp = encrypt(temp);
}
if (k.isLowerCase) {
temp = temp.toLowerCase();
}
if (k.isNumber) {
temp = Number(temp);
}
if (k.isBoolean) {
temp = Boolean(temp);
}
}
if (k.isNumber && temp === '') {
temp = null;
}
update[k.name] = temp;
});
}
return update;
}
export function getUpdateableFields(type: string, data: any) {
const update = {};
if (type && serviceFields[type]) {
serviceFields[type].map((k) => {
let temp = data[k.name];
if (temp) {
if (k.isEncrypted) {
temp = decrypt(temp);
}
update[k.name] = temp;
}
update[k.name] = temp;
});
}
return update;
}
export function fixType(type) {
// Hack to fix the type case sensitivity...
if (type === 'plausibleanalytics') type = 'plausibleAnalytics';
if (type === 'meilisearch') type = 'meiliSearch';
return type;
return type?.replaceAll(' ', '').toLowerCase() || null;
}
export const getServiceMainPort = (service: string) => {
const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service);
if (serviceType) {
return serviceType.ports.main;
}
return null;
};
export function makeLabelForServices(type) {
return [
'coolify.managed=true',
@@ -1679,7 +1614,9 @@ export function persistentVolumes(id, persistentStorage, config) {
for (const [key, value] of Object.entries(config)) {
if (value.volumes) {
for (const volume of value.volumes) {
volumeSet.add(volume);
if (!volume.startsWith('/var/run/docker.sock')) {
volumeSet.add(volume);
}
}
}
}

View File

@@ -87,6 +87,9 @@ export async function removeContainer({
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
if (JSON.parse(stdout).Status === 'exited') {
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
}
} catch (error) {
throw error;
}

View File

@@ -73,6 +73,4 @@ export default async function ({
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', '');
}

View File

@@ -11,15 +11,14 @@ const options: any = {
defaultExtension: 'js',
logger: new Cabin(),
// logger: false,
workerMessageHandler: async ({ name, message }) => {
if (name === 'deployApplication' && message?.deploying) {
if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
scheduler.workers.get('deployApplication').postMessage('cancel')
}
}
},
// workerMessageHandler: async ({ name, message }) => {
// if (name === 'deployApplication' && message?.deploying) {
// if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) {
// scheduler.workers.get('deployApplication').postMessage('cancel')
// }
// }
// },
jobs: [
{ name: 'infrastructure' },
{ name: 'deployApplication' },
],
};

View File

@@ -1,20 +1,170 @@
import { createDirectories, getServiceFromDB, getServiceImage, getServiceMainPort, makeLabelForServices } from "./common";
export async function defaultServiceConfigurations({ id, teamId }) {
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, destinationDocker, type, serviceSecret } = service;
const network = destinationDockerId && destinationDocker.network;
const port = getServiceMainPort(type);
const { workdir } = await createDirectories({ repository: type, buildId: id });
const image = getServiceImage(type);
let secrets = [];
if (serviceSecret.length > 0) {
serviceSecret.forEach((secret) => {
secrets.push(`${secret.name}=${secret.value}`);
});
import { isDev } from "./common";
import fs from 'fs/promises';
export async function getTemplates() {
let templates: any = [];
if (isDev) {
templates = JSON.parse(await (await fs.readFile('./templates.json')).toString())
} else {
templates = JSON.parse(await (await fs.readFile('/app/templates.json')).toString())
}
return { ...service, network, port, workdir, image, secrets }
}
// if (!isDev) {
// templates.push({
// "templateVersion": "1.0.0",
// "defaultVersion": "latest",
// "name": "Test-Fake-Service",
// "description": "",
// "services": {
// "$$id": {
// "name": "Test-Fake-Service",
// "depends_on": [
// "$$id-postgresql",
// "$$id-redis"
// ],
// "image": "weblate/weblate:$$core_version",
// "volumes": [
// "$$id-data:/app/data",
// ],
// "environment": [
// `POSTGRES_SECRET=$$secret_postgres_secret`,
// `WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain`,
// `WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password`,
// `POSTGRES_PASSWORD=$$secret_postgres_password`,
// `POSTGRES_USER=$$config_postgres_user`,
// `POSTGRES_DATABASE=$$config_postgres_db`,
// `POSTGRES_HOST=$$id-postgresql`,
// `POSTGRES_PORT=5432`,
// `REDIS_HOST=$$id-redis`,
// ],
// "ports": [
// "8080"
// ]
// },
// "$$id-postgresql": {
// "name": "PostgreSQL",
// "depends_on": [],
// "image": "postgres:14-alpine",
// "volumes": [
// "$$id-postgresql-data:/var/lib/postgresql/data",
// ],
// "environment": [
// "POSTGRES_USER=$$config_postgres_user",
// "POSTGRES_PASSWORD=$$secret_postgres_password",
// "POSTGRES_DB=$$config_postgres_db",
// ],
// "ports": []
// },
// "$$id-redis": {
// "name": "Redis",
// "depends_on": [],
// "image": "redis:7-alpine",
// "volumes": [
// "$$id-redis-data:/data",
// ],
// "environment": [],
// "ports": [],
// }
// },
// "variables": [
// {
// "id": "$$config_weblate_site_domain",
// "main": "$$id",
// "name": "WEBLATE_SITE_DOMAIN",
// "label": "Weblate Domain",
// "defaultValue": "$$generate_domain",
// "description": "",
// },
// {
// "id": "$$secret_weblate_admin_password",
// "main": "$$id",
// "name": "WEBLATE_ADMIN_PASSWORD",
// "label": "Weblate Admin Password",
// "defaultValue": "$$generate_password",
// "description": "",
// "extras": {
// "isVisibleOnUI": true,
// }
// },
// {
// "id": "$$secret_weblate_admin_password2",
// "name": "WEBLATE_ADMIN_PASSWORD2",
// "label": "Weblate Admin Password2",
// "defaultValue": "$$generate_password",
// "description": "",
// },
// {
// "id": "$$config_postgres_user",
// "main": "$$id-postgresql",
// "name": "POSTGRES_USER",
// "label": "PostgreSQL User",
// "defaultValue": "$$generate_username",
// "description": "",
// },
// {
// "id": "$$secret_postgres_password",
// "main": "$$id-postgresql",
// "name": "POSTGRES_PASSWORD",
// "label": "PostgreSQL Password",
// "defaultValue": "$$generate_password(32)",
// "description": "",
// },
// {
// "id": "$$secret_postgres_password_hex32",
// "name": "POSTGRES_PASSWORD_hex32",
// "label": "PostgreSQL Password hex32",
// "defaultValue": "$$generate_hex(32)",
// "description": "",
// },
// {
// "id": "$$config_postgres_something_hex32",
// "name": "POSTGRES_SOMETHING_HEX32",
// "label": "PostgreSQL Something hex32",
// "defaultValue": "$$generate_hex(32)",
// "description": "",
// },
// {
// "id": "$$config_postgres_db",
// "main": "$$id-postgresql",
// "name": "POSTGRES_DB",
// "label": "PostgreSQL Database",
// "defaultValue": "weblate",
// "description": "",
// },
// {
// "id": "$$secret_postgres_secret",
// "name": "POSTGRES_SECRET",
// "label": "PostgreSQL Secret",
// "defaultValue": "",
// "description": "",
// },
// ]
// })
// }
return templates
}
const compareSemanticVersions = (a: string, b: string) => {
const a1 = a.split('.');
const b1 = b.split('.');
const len = Math.min(a1.length, b1.length);
for (let i = 0; i < len; i++) {
const a2 = +a1[i] || 0;
const b2 = +b1[i] || 0;
if (a2 !== b2) {
return a2 > b2 ? 1 : -1;
}
}
return b1.length - a1.length;
};
export async function getTags(type: string) {
if (type) {
let tags: any = [];
if (isDev) {
tags = JSON.parse(await (await fs.readFile('./tags.json')).toString())
} else {
tags = JSON.parse(await (await fs.readFile('/app/tags.json')).toString())
}
tags = tags.find((tag: any) => tag.name.includes(type))
tags.tags = tags.tags.sort(compareSemanticVersions).reverse();
return tags
}
return []
}

View File

@@ -1,367 +1,9 @@
import cuid from 'cuid';
import { encrypt, generatePassword, prisma } from '../common';
export const includeServices: any = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true,
hasura: true,
fider: true,
moodle: true,
appwrite: true,
glitchTip: true,
searxng: true,
weblate: true,
taiga: true,
};
export async function configureServiceType({
id,
type
}: {
id: string;
type: string;
}): Promise<void> {
if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword({}));
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'plausibleanalytics';
const secretKeyBase = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({
where: { id },
data: {
type,
plausibleAnalytics: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
password,
secretKeyBase
}
}
}
});
} else if (type === 'nocodb') {
await prisma.service.update({
where: { id },
data: { type }
});
} else if (type === 'minio') {
const rootUser = cuid();
const rootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: { type, minio: { create: { rootUser, rootUserPassword } } }
});
} else if (type === 'vscodeserver') {
const password = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: { type, vscodeserver: { create: { password } } }
});
} else if (type === 'wordpress') {
const mysqlUser = cuid();
const mysqlPassword = encrypt(generatePassword({}));
const mysqlRootUser = cuid();
const mysqlRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } }
}
});
} else if (type === 'vaultwarden') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'languagetool') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'n8n') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'uptimekuma') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'ghost') {
const defaultEmail = `${cuid()}@example.com`;
const defaultPassword = encrypt(generatePassword({}));
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword({}));
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
ghost: {
create: {
defaultEmail,
defaultPassword,
mariadbUser,
mariadbPassword,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword({ length: 32 }));
await prisma.service.update({
where: { id },
data: {
type,
meiliSearch: { create: { masterKey } }
}
});
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword({}));
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
} else if (type === 'hasura') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'hasura';
const graphQLAdminPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
hasura: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
graphQLAdminPassword
}
}
}
});
} else if (type === 'fider') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'fider';
const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true }));
await prisma.service.update({
where: { id },
data: {
type,
fider: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
jwtSecret
}
}
}
});
} else if (type === 'moodle') {
const defaultUsername = cuid();
const defaultPassword = encrypt(generatePassword({}));
const defaultEmail = `${cuid()} @example.com`;
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword({}));
const mariadbDatabase = 'moodle_db';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
moodle: {
create: {
defaultUsername,
defaultPassword,
defaultEmail,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else if (type === 'appwrite') {
const opensslKeyV1 = encrypt(generatePassword({}));
const executorSecret = encrypt(generatePassword({}));
const redisPassword = encrypt(generatePassword({}));
const mariadbHost = `${id}-mariadb`
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword({}));
const mariadbDatabase = 'appwrite';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
appwrite: {
create: {
opensslKeyV1,
executorSecret,
redisPassword,
mariadbHost,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else if (type === 'glitchTip') {
const defaultUsername = cuid();
const defaultEmail = `${defaultUsername}@example.com`;
const defaultPassword = encrypt(generatePassword({}));
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'glitchTip';
const secretKeyBase = encrypt(generatePassword({ length: 64 }));
await prisma.service.update({
where: { id },
data: {
type,
glitchTip: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
}
}
}
});
} else if (type === 'searxng') {
const secretKey = encrypt(generatePassword({ length: 32, isHex: true }))
const redisPassword = encrypt(generatePassword({}));
await prisma.service.update({
where: { id },
data: {
type,
searxng: {
create: {
secretKey,
redisPassword,
}
}
}
});
} else if (type === 'weblate') {
const adminPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'weblate';
await prisma.service.update({
where: { id },
data: {
type,
weblate: {
create: {
adminPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else if (type === 'taiga') {
const secretKey = encrypt(generatePassword({}))
const erlangSecret = encrypt(generatePassword({}))
const rabbitMQUser = cuid();
const djangoAdminUser = cuid();
const djangoAdminPassword = encrypt(generatePassword({}))
const rabbitMQPassword = encrypt(generatePassword({}))
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword({}));
const postgresqlDatabase = 'taiga';
await prisma.service.update({
where: { id },
data: {
type,
taiga: {
create: {
secretKey,
erlangSecret,
djangoAdminUser,
djangoAdminPassword,
rabbitMQUser,
rabbitMQPassword,
postgresqlHost: `${id}-postgresql`,
postgresqlPort: 5432,
postgresqlUser,
postgresqlPassword,
postgresqlDatabase,
}
}
}
});
} else {
await prisma.service.update({
where: { id },
data: {
type
}
});
}
}
import { prisma } from '../common';
export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
await prisma.serviceSetting.deleteMany({ where: { serviceId: id } });
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.fider.deleteMany({ where: { serviceId: id } });

File diff suppressed because it is too large Load Diff

View File

@@ -1,258 +0,0 @@
/*
Example of a supported version:
{
// Name used to identify the service internally
name: 'umami',
// Fancier name to show to the user
fancyName: 'Umami',
// Docker base image for the service
baseImage: 'ghcr.io/mikecao/umami',
// Optional: If there is any dependent image, you should list it here
images: [],
// Usable tags
versions: ['postgresql-latest'],
// Which tag is the recommended
recommendedVersion: 'postgresql-latest',
// Application's default port, Umami listens on 3000
ports: {
main: 3000
}
}
*/
export const supportedServiceTypesAndVersions = [
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest', 'stable'],
recommendedVersion: 'stable',
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 2368
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 7700
}
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/umami-software/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
},
{
name: 'hasura',
fancyName: 'Hasura',
baseImage: 'hasura/graphql-engine',
images: ['postgres:12-alpine'],
versions: ['latest', 'v2.10.0', 'v2.5.1'],
recommendedVersion: 'v2.10.0',
ports: {
main: 8080
}
},
{
name: 'fider',
fancyName: 'Fider',
baseImage: 'getfider/fider',
images: ['postgres:12-alpine'],
versions: ['stable'],
recommendedVersion: 'stable',
ports: {
main: 3000
}
},
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '1.0', '0.15.3'],
recommendedVersion: '1.0',
ports: {
main: 80
}
},
// {
// name: 'moodle',
// fancyName: 'Moodle',
// baseImage: 'bitnami/moodle',
// images: [],
// versions: ['latest', 'v4.0.2'],
// recommendedVersion: 'latest',
// ports: {
// main: 8080
// }
// }
{
name: 'glitchTip',
fancyName: 'GlitchTip',
baseImage: 'glitchtip/glitchtip',
images: ['postgres:14-alpine', 'redis:7-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
{
name: 'searxng',
fancyName: 'SearXNG',
baseImage: 'searxng/searxng',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'weblate',
fancyName: 'Weblate',
baseImage: 'weblate/weblate',
images: ['postgres:14-alpine', 'redis:6-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
// {
// name: 'taiga',
// fancyName: 'Taiga',
// baseImage: 'taigaio/taiga-front',
// images: ['postgres:12.3', 'rabbitmq:3.8-management-alpine', 'taigaio/taiga-back', 'taigaio/taiga-events', 'taigaio/taiga-protected'],
// versions: ['latest'],
// recommendedVersion: 'latest',
// ports: {
// main: 80
// }
// },
{
name: 'grafana',
fancyName: 'Grafana',
baseImage: 'grafana/grafana',
images: [],
versions: ['latest', '9.1.3', '9.1.2', '9.0.8', '8.3.11', '8.4.11', '8.5.11'],
recommendedVersion: 'latest',
ports: {
main: 3000
}
},
{
name: 'trilium',
fancyName: 'Trilium Notes',
baseImage: 'zadam/trilium',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
];

View File

@@ -0,0 +1,29 @@
export default async (fastify) => {
fastify.io.use((socket, next) => {
const { token } = socket.handshake.auth;
if (token && fastify.jwt.verify(token)) {
next();
} else {
return next(new Error("unauthorized event"));
}
});
fastify.io.on('connection', (socket: any) => {
const { token } = socket.handshake.auth;
const { teamId } = fastify.jwt.decode(token);
socket.join(teamId);
// console.info('Socket connected!', socket.id)
// console.info('Socket joined team!', teamId)
// socket.on('message', (message) => {
// console.log(message)
// })
// socket.on('error', (err) => {
// console.log(err)
// })
})
// fastify.io.on("error", (err) => {
// if (err && err.message === "unauthorized event") {
// fastify.io.disconnect();
// }
// });
}

View File

@@ -1,16 +1,15 @@
import cuid from 'cuid';
import crypto from 'node:crypto'
import jsonwebtoken from 'jsonwebtoken';
import axios from 'axios';
import { FastifyReply } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import csv from 'csvtojson';
import { day } from '../../../../lib/dayjs';
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
import { setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker';
import type { FastifyRequest } from 'fastify';
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
@@ -110,23 +109,64 @@ export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const { teamId } = request.user
let isRunning = false;
let isExited = false;
let isRestarting = false;
let payload = []
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const containerObj = JSON.parse(container);
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload.push({
name: containerObj.Names,
status: {
isRunning,
isExited,
isRestarting
}
})
}
}
} else {
let isRunning = false;
let isExited = false;
let isRestarting = false;
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
payload.push({
name: id,
status: {
isRunning,
isExited,
isRestarting
}
})
}
}
}
return {
isRunning,
isRestarting,
isExited,
};
return payload
} catch ({ status, message }) {
return errorHandler({ status, message })
}
@@ -289,13 +329,15 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
baseDatabaseBranch
baseDatabaseBranch,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration
} = request.body
if (port) port = Number(port);
if (exposePort) {
exposePort = Number(exposePort);
}
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (denoOptions) denoOptions = denoOptions.trim();
@@ -324,6 +366,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration,
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
}
@@ -342,6 +387,9 @@ export async function saveApplication(request: FastifyRequest<SaveApplication>,
baseImage,
baseBuildImage,
deploymentType,
dockerComposeFile,
dockerComposeFileLocation,
dockerComposeConfiguration,
...defaultConfiguration
}
});
@@ -506,6 +554,21 @@ export async function stopApplication(request: FastifyRequest<OnlyId>, reply: Fa
const application: any = await getApplicationFromDB(id, teamId);
if (application?.destinationDockerId) {
const { id: dockerId } = application.destinationDocker;
if (application.buildPack === 'compose') {
const { stdout: containers } = await executeDockerCmd({
dockerId: application.destinationDocker.id,
command:
`docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
for (const container of containersArray) {
const containerObj = JSON.parse(container);
await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id });
}
}
return
}
const { found } = await checkContainer({ dockerId, container: id });
if (found) {
await removeContainer({ id, dockerId: application.destinationDocker.id });
@@ -613,6 +676,24 @@ export async function getUsage(request) {
return errorHandler({ status, message })
}
}
export async function getUsageByContainer(request) {
try {
const { id, containerId } = request.params
const teamId = request.user?.teamId;
let usage = {};
const application: any = await getApplicationFromDB(id, teamId);
if (application.destinationDockerId) {
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, containerId)]);
}
return {
usage
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
try {
const { id } = request.params
@@ -689,6 +770,7 @@ export async function saveApplicationSource(request: FastifyRequest<SaveApplicat
export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { default: got } = await import('got')
const { id } = request.params
const { teamId } = request.user
const application: any = await getApplicationFromDB(id, teamId);
@@ -700,13 +782,13 @@ export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: Fas
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
algorithm: 'RS256'
});
const { data } = await axios.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {}, {
const { token } = await got.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {
headers: {
Authorization: `Bearer ${githubToken}`
'Authorization': `Bearer ${githubToken}`,
}
})
}).json()
return reply.code(201).send({
token: data.token
token
})
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -737,7 +819,7 @@ export async function saveRepository(request, reply) {
let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
repository = repository.toLowerCase();
branch = branch.toLowerCase();
projectId = Number(projectId);
if (webhookToken) {
await prisma.application.update({
@@ -888,6 +970,10 @@ export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: Fas
try {
const { id } = request.params
const { name, value, isBuildSecret = false } = request.body
const found = await prisma.secret.findMany({ where: { applicationId: id, name } })
if (found.length > 0) {
throw ({ message: 'Secret already exists.' })
}
await prisma.secret.create({
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
});
@@ -1159,7 +1245,7 @@ export async function getPreviews(request: FastifyRequest<OnlyId>) {
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
try {
const { id } = request.params;
const { id, containerId } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
@@ -1170,10 +1256,8 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
});
if (destinationDockerId) {
try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
@@ -1181,7 +1265,10 @@ export async function getApplicationLogs(request: FastifyRequest<GetApplicationL
return { logs: sortedLogs }
// }
} catch (error) {
const { statusCode } = error;
const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true }
}
if (statusCode === 404) {
return {
logs: []

View File

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

View File

@@ -21,7 +21,10 @@ export interface SaveApplication extends OnlyId {
baseImage: string,
baseBuildImage: string,
deploymentType: string,
baseDatabaseBranch: string
baseDatabaseBranch: string,
dockerComposeFile: string,
dockerComposeFileLocation: string,
dockerComposeConfiguration: string
}
}
export interface SaveApplicationSettings extends OnlyId {
@@ -84,7 +87,11 @@ export interface DeleteStorage extends OnlyId {
path: string,
}
}
export interface GetApplicationLogs extends OnlyId {
export interface GetApplicationLogs {
Params: {
id: string,
containerId: string
}
Querystring: {
since: number,
}

View File

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

View File

@@ -4,7 +4,7 @@ import sshConfig from 'ssh-config'
import fs from 'fs/promises'
import os from 'os';
import { asyncExecShell, createRemoteEngineConfiguration, decrypt, errorHandler, executeDockerCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { asyncExecShell, createRemoteEngineConfiguration, decrypt, errorHandler, executeDockerCmd, executeSSHCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common';
import { checkContainer } from '../../../../lib/docker';
import type { OnlyId } from '../../../../types';
@@ -202,25 +202,58 @@ export async function assignSSHKey(request: FastifyRequest) {
return errorHandler({ status, message })
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
export async function verifyRemoteDockerEngineFn(id: string) {
await createRemoteEngineConfiguration(id);
const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
const host = `ssh://${remoteIpAddress}-remote`
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
if (!coolifyNetwork) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
}
if (isCoolifyProxyUsed) await startTraefikProxy(id);
try {
const { id } = request.params;
await createRemoteEngineConfiguration(id);
const { remoteIpAddress, remoteUser, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } })
const host = `ssh://${remoteUser}@${remoteIpAddress}`
const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`);
if (!stdout) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`);
}
const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`);
if (!coolifyNetwork) {
await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`);
}
if (isCoolifyProxyUsed) await startTraefikProxy(id);
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
return reply.code(201).send()
const { stdout: daemonJson } = await executeSSHCmd({ dockerId: id, command: `cat /etc/docker/daemon.json` });
let daemonJsonParsed = JSON.parse(daemonJson);
let isUpdated = false;
if (!daemonJsonParsed['live-restore'] || daemonJsonParsed['live-restore'] !== true) {
isUpdated = true;
daemonJsonParsed['live-restore'] = true
}
if (!daemonJsonParsed?.features?.buildkit) {
isUpdated = true;
daemonJsonParsed.features = {
buildkit: true
}
}
if (isUpdated) {
await executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` });
await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` });
}
} catch (error) {
const daemonJsonParsed = {
"live-restore": true,
"features": {
"buildkit": true
}
}
await executeSSHCmd({ dockerId: id, command: `echo '${JSON.stringify(daemonJsonParsed)}' > /etc/docker/daemon.json` });
await executeSSHCmd({ dockerId: id, command: `systemctl restart docker` });
} finally {
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: true } })
}
}
export async function verifyRemoteDockerEngine(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
const { id } = request.params;
try {
await verifyRemoteDockerEngineFn(id);
return reply.code(201).send()
} catch ({ status, message }) {
await prisma.destinationDocker.update({ where: { id }, data: { remoteVerified: false } })
return errorHandler({ status, message })
}
}

View File

@@ -1,7 +1,8 @@
import axios from "axios";
import { compareVersions } from "compare-versions";
import cuid from "cuid";
import bcrypt from "bcryptjs";
import fs from 'fs/promises';
import yaml from 'js-yaml';
import {
asyncExecShell,
asyncSleep,
@@ -13,7 +14,6 @@ import {
uniqueName,
version,
} from "../../../lib/common";
import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions";
import { scheduler } from "../../../lib/scheduler";
import type { FastifyReply, FastifyRequest } from "fastify";
import type { Login, Update } from ".";
@@ -36,16 +36,59 @@ export async function cleanupManually(request: FastifyRequest) {
return errorHandler({ status, message });
}
}
export async function refreshTags() {
try {
const { default: got } = await import('got')
try {
if (isDev) {
const tags = await fs.readFile('./devTags.json', 'utf8')
await fs.writeFile('./tags.json', tags)
} else {
const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text()
await fs.writeFile('/app/tags.json', tags)
}
} catch (error) {
console.log(error)
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function refreshTemplates() {
try {
const { default: got } = await import('got')
try {
if (isDev) {
const response = await fs.readFile('./devTemplates.yaml', 'utf8')
await fs.writeFile('./templates.json', JSON.stringify(yaml.load(response)))
} else {
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)))
}
} catch (error) {
console.log(error)
}
return {};
} catch ({ status, message }) {
return errorHandler({ status, message });
}
}
export async function checkUpdate(request: FastifyRequest) {
try {
const { default: got } = await import('got')
const isStaging =
request.hostname === "staging.coolify.io" ||
request.hostname === "arm.coolify.io";
const currentVersion = version;
const { data: versions } = await axios.get(
`https://get.coollabs.io/versions.json?appId=${process.env["COOLIFY_APP_ID"]}&version=${currentVersion}`
);
const latestVersion = versions["coolify"].main.version;
const { coolify } = await got.get('https://get.coollabs.io/versions.json', {
searchParams: {
appId: process.env['COOLIFY_APP_ID'] || undefined,
version: currentVersion
}
}).json()
const latestVersion = coolify.main.version;
const isUpdateAvailable = compareVersions(latestVersion, currentVersion);
if (isStaging) {
return {
@@ -146,7 +189,7 @@ export async function showDashboard(request: FastifyRequest) {
let foundUnconfiguredApplication = false;
for (const application of applications) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) {
if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn) && application.buildPack !== "compose") {
foundUnconfiguredApplication = true
}
}
@@ -357,7 +400,6 @@ export async function getCurrentUser(
return {
settings: await prisma.setting.findFirst(),
pendingInvitations,
supportedServiceTypesAndVersions,
token,
...request.user,
};

View File

@@ -1,9 +1,6 @@
import { FastifyPluginAsync } from 'fastify';
import { checkUpdate, login, showDashboard, update, resetQueue, getCurrentUser, cleanupManually, restartCoolify } from './handlers';
import { GetCurrentUser } from './types';
import pump from 'pump'
import fs from 'fs'
import { asyncExecShell, encrypt, errorHandler, prisma } from '../../../lib/common';
export interface Update {
Body: { latestVersion: string }

View File

@@ -8,7 +8,16 @@ export async function listServers(request: FastifyRequest) {
try {
const userId = request.user.userId;
const teamId = request.user.teamId;
const servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } }}, distinct: ['remoteIpAddress', 'engine'] })
let servers = await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, distinct: ['remoteIpAddress', 'engine'] })
servers = servers.filter((server) => {
if (server.remoteEngine) {
if (server.remoteVerified) {
return server
}
} else {
return server
}
})
return {
servers
}
@@ -78,7 +87,7 @@ export async function showUsage(request: FastifyRequest) {
freeMemPercentage: (parsed.totalMemoryKB - parsed.usedMemoryKB) / parsed.totalMemoryKB * 100
},
cpu: {
load: [0,0,0],
load: [0, 0, 0],
usage: cpuUsage,
count: cpus
},

View File

@@ -1,15 +1,17 @@
import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, isContainerExited } from '../../../../lib/docker';
import bcrypt from 'bcryptjs';
import cuid from 'cuid';
import type { OnlyId } from '../../../../types';
import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings } from '../../../../lib/common';
import { day } from '../../../../lib/dayjs';
import { checkContainer, } from '../../../../lib/docker';
import { removeService } from '../../../../lib/services/common';
import { getTags, getTemplates } from '../../../../lib/services';
import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetGlitchTipSettings, SetWordpressSettings } from './types';
import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions';
import { configureServiceType, removeService } from '../../../../lib/services/common';
import type { OnlyId } from '../../../../types';
export async function listServices(request: FastifyRequest) {
try {
@@ -67,30 +69,186 @@ export async function getServiceStatus(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
let isRunning = false;
let isExited = false
let isRestarting = false;
const service = await getServiceFromDB({ id, teamId });
const { destinationDockerId, settings } = service;
let payload = {}
if (destinationDockerId) {
const status = await checkContainer({ dockerId: service.destinationDocker.id, container: id });
if (status?.found) {
isRunning = status.status.isRunning;
isExited = status.status.isExited;
isRestarting = status.status.isRestarting
const { stdout: containers } = await executeDockerCmd({
dockerId: service.destinationDocker.id,
command:
`docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'`
});
const containersArray = containers.trim().split('\n');
if (containersArray.length > 0 && containersArray[0] !== '') {
const templates = await getTemplates();
let template = templates.find(t => t.type === service.type);
template = JSON.parse(JSON.stringify(template).replaceAll('$$id', service.id));
for (const container of containersArray) {
let isRunning = false;
let isExited = false;
let isRestarting = false;
let isExcluded = false;
const containerObj = JSON.parse(container);
const exclude = template.services[containerObj.Names]?.exclude;
if (exclude) {
payload[containerObj.Names] = {
status: {
isExcluded: true,
isRunning: false,
isExited: false,
isRestarting: false,
}
}
continue;
}
const status = containerObj.State
if (status === 'running') {
isRunning = true;
}
if (status === 'exited') {
isExited = true;
}
if (status === 'restarting') {
isRestarting = true;
}
payload[containerObj.Names] = {
status: {
isExcluded,
isRunning,
isExited,
isRestarting
}
}
}
}
}
return {
isRunning,
isExited,
settings
}
return payload
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function parseAndFindServiceTemplates(service: any, workdir?: string, isDeploy: boolean = false) {
const templates = await getTemplates()
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
let parsedTemplate = {}
if (foundTemplate) {
if (!isDeploy) {
for (const [key, value] of Object.entries(foundTemplate.services)) {
const realKey = key.replace('$$id', service.id)
let name = value.name
if (!name) {
if (Object.keys(foundTemplate.services).length === 1) {
name = foundTemplate.name || service.name.toLowerCase()
} else {
if (key === '$$id') {
name = foundTemplate.name || key.replaceAll('$$id-', '') || service.name.toLowerCase()
} else {
name = key.replaceAll('$$id-', '') || service.name.toLowerCase()
}
}
}
parsedTemplate[realKey] = {
name,
documentation: value.documentation || foundTemplate.documentation || 'https://docs.coollabs.io',
image: value.image,
environment: [],
fqdns: [],
proxy: {}
}
if (value.environment?.length > 0) {
for (const env of value.environment) {
let [envKey, ...envValue] = env.split('=')
envValue = envValue.join("=")
const variable = foundTemplate.variables.find(v => v.name === envKey) || foundTemplate.variables.find(v => v.id === envValue)
if (variable) {
const id = variable.id.replaceAll('$$', '')
const label = variable?.label
const description = variable?.description
const defaultValue = variable?.defaultValue
const main = variable?.main || '$$id'
const type = variable?.type || 'input'
const placeholder = variable?.placeholder || ''
const readOnly = variable?.readOnly || false
const required = variable?.required || false
if (envValue.startsWith('$$config') || variable?.showOnConfiguration) {
if (envValue.startsWith('$$config_coolify')) {
continue
}
parsedTemplate[realKey].environment.push(
{ id, name: envKey, value: envValue, main, label, description, defaultValue, type, placeholder, required, readOnly }
)
}
}
}
}
if (value?.proxy && value.proxy.length > 0) {
for (const proxyValue of value.proxy) {
if (proxyValue.domain) {
const variable = foundTemplate.variables.find(v => v.id === proxyValue.domain)
if (variable) {
const { id, name, label, description, defaultValue, required = false } = variable
const found = await prisma.serviceSetting.findFirst({ where: { serviceId: service.id , variableName: proxyValue.domain } })
parsedTemplate[realKey].fqdns.push(
{ id, name, value: found?.value || '', label, description, defaultValue, required }
)
}
}
}
}
}
} else {
parsedTemplate = foundTemplate
}
let strParsedTemplate = JSON.stringify(parsedTemplate)
// replace $$id and $$workdir
strParsedTemplate = strParsedTemplate.replaceAll('$$id', service.id)
strParsedTemplate = strParsedTemplate.replaceAll('$$core_version', service.version || foundTemplate.defaultVersion)
// replace $$fqdn
if (workdir) {
strParsedTemplate = strParsedTemplate.replaceAll('$$workdir', workdir)
}
// replace $$config
if (service.serviceSetting.length > 0) {
for (const setting of service.serviceSetting) {
const { value, variableName } = setting
const regex = new RegExp(`\\$\\$config_${variableName.replace('$$config_', '')}\\"`, 'gi')
if (value === '$$generate_fqdn') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.fqdn + "\"" || '' + "\"")
} else if (value === '$$generate_domain') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, getDomain(service.fqdn) + "\"")
} else if (service.destinationDocker?.network && value === '$$generate_network') {
strParsedTemplate = strParsedTemplate.replaceAll(regex, service.destinationDocker.network + "\"")
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regex, value + "\"")
}
}
}
// replace $$secret
if (service.serviceSecret.length > 0) {
for (const secret of service.serviceSecret) {
const { name, value } = secret
const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\\"`, 'gi')
const regex = new RegExp(`\\$\\$secret_${name}\\"`, 'gi')
if (value) {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + "\"")
} else {
strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, "\"")
strParsedTemplate = strParsedTemplate.replaceAll(regex, "\"")
}
}
}
parsedTemplate = JSON.parse(strParsedTemplate)
}
return parsedTemplate
}
export async function getService(request: FastifyRequest<OnlyId>) {
try {
@@ -100,9 +258,17 @@ export async function getService(request: FastifyRequest<OnlyId>) {
if (!service) {
throw { status: 404, message: 'Service not found.' }
}
let template = {}
let tags = []
if (service.type) {
template = await parseAndFindServiceTemplates(service)
tags = await getTags(service.type)
}
return {
settings: await listSettings(),
service
service,
template,
tags
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -111,7 +277,7 @@ export async function getService(request: FastifyRequest<OnlyId>) {
export async function getServiceType(request: FastifyRequest) {
try {
return {
types: supportedServiceTypesAndVersions
services: await getTemplates()
}
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -121,25 +287,79 @@ export async function saveServiceType(request: FastifyRequest<SaveServiceType>,
try {
const { id } = request.params;
const { type } = request.body;
await configureServiceType({ id, type });
return reply.code(201).send()
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function getServiceVersions(request: FastifyRequest<OnlyId>) {
try {
const teamId = request.user.teamId;
const { id } = request.params;
const { type } = await getServiceFromDB({ id, teamId });
return {
type,
versions: supportedServiceTypesAndVersions.find((name) => name.name === type).versions
const templates = await getTemplates()
let foundTemplate = templates.find(t => fixType(t.type) === fixType(type))
if (foundTemplate) {
foundTemplate = JSON.parse(JSON.stringify(foundTemplate).replaceAll('$$id', id))
if (foundTemplate.variables.length > 0) {
for (const variable of foundTemplate.variables) {
const { defaultValue } = variable;
const regex = /^\$\$.*\((\d+)\)$/g;
const length = Number(regex.exec(defaultValue)?.[1]) || undefined
if (variable.defaultValue.startsWith('$$generate_password')) {
variable.value = generatePassword({ length });
} else if (variable.defaultValue.startsWith('$$generate_hex')) {
variable.value = generatePassword({ length, isHex: true });
} else if (variable.defaultValue.startsWith('$$generate_username')) {
variable.value = cuid();
} else {
variable.value = variable.defaultValue || '';
}
const foundVariableSomewhereElse = foundTemplate.variables.find(v => v.defaultValue.includes(variable.id))
if (foundVariableSomewhereElse) {
foundVariableSomewhereElse.value = foundVariableSomewhereElse.value.replaceAll(variable.id, variable.value)
}
}
}
for (const variable of foundTemplate.variables) {
if (variable.id.startsWith('$$secret_')) {
const found = await prisma.serviceSecret.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSecret.create({
data: { name: variable.name, value: encrypt(variable.value) || '', service: { connect: { id } } }
})
}
}
if (variable.id.startsWith('$$config_')) {
const found = await prisma.serviceSetting.findFirst({ where: { name: variable.name, serviceId: id } })
if (!found) {
await prisma.serviceSetting.create({
data: { name: variable.name, value: variable.value.toString(), variableName: variable.id, service: { connect: { id } } }
})
}
}
}
for (const service of Object.keys(foundTemplate.services)) {
if (foundTemplate.services[service].volumes) {
for (const volume of foundTemplate.services[service].volumes) {
const [volumeName, path] = volume.split(':')
if (!volumeName.startsWith('/')) {
const found = await prisma.servicePersistentStorage.findFirst({ where: { volumeName, serviceId: id } })
if (!found) {
await prisma.servicePersistentStorage.create({
data: { volumeName, path, containerId: service, predefined: true, service: { connect: { id } } }
});
}
}
}
}
}
await prisma.service.update({ where: { id }, data: { type, version: foundTemplate.defaultVersion, templateVersion: foundTemplate.templateVersion } })
if (type.startsWith('wordpress')) {
await prisma.service.update({ where: { id }, data: { wordpress: { create: {} } } })
}
return reply.code(201).send()
} else {
throw { status: 404, message: 'Service type not found.' }
}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function saveServiceVersion(request: FastifyRequest<SaveServiceVersion>, reply: FastifyReply) {
try {
const { id } = request.params;
@@ -186,7 +406,7 @@ export async function getServiceUsage(request: FastifyRequest<OnlyId>) {
}
export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
try {
const { id } = request.params;
const { id, containerId } = request.params;
let { since = 0 } = request.query
if (since !== 0) {
since = day(since).unix();
@@ -197,10 +417,8 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
});
if (destinationDockerId) {
try {
// const found = await checkContainer({ dockerId, container: id })
// if (found) {
const { default: ansi } = await import('strip-ansi')
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` })
const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a);
const logs = stripLogsStderr.concat(stripLogsStdout)
@@ -208,7 +426,10 @@ export async function getServiceLogs(request: FastifyRequest<GetServiceLogs>) {
return { logs: sortedLogs }
// }
} catch (error) {
const { statusCode } = error;
const { statusCode, stderr } = error;
if (stderr.startsWith('Error: No such container')) {
return { logs: [], noContainer: true }
}
if (statusCode === 404) {
return {
logs: []
@@ -258,26 +479,22 @@ export async function checkServiceDomain(request: FastifyRequest<CheckServiceDom
export async function checkService(request: FastifyRequest<CheckService>) {
try {
const { id } = request.params;
let { fqdn, exposePort, forceSave, otherFqdns, dualCerts } = request.body;
let { fqdn, exposePort, forceSave, dualCerts, otherFqdn = false } = request.body;
const domainsList = await prisma.serviceSetting.findMany({ where: { variableName: { startsWith: '$$config_coolify_fqdn' } } })
if (fqdn) fqdn = fqdn.toLowerCase();
if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase());
if (exposePort) exposePort = Number(exposePort);
const { destinationDocker: { remoteIpAddress, remoteEngine, engine }, exposePort: configuredPort } = await prisma.service.findUnique({ where: { id }, include: { destinationDocker: true } })
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
let found = await isDomainConfigured({ id, fqdn, remoteIpAddress, checkOwn: otherFqdn });
if (found) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (otherFqdns && otherFqdns.length > 0) {
for (const ofqdn of otherFqdns) {
found = await isDomainConfigured({ id, fqdn: ofqdn, remoteIpAddress });
if (found) {
throw { status: 500, message: `Domain ${getDomain(ofqdn).replace('www.', '')} is already in use!` }
}
}
if (domainsList.find(d => getDomain(d.value) === getDomain(fqdn))) {
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
}
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
if (isDNSCheckEnabled && !isDev && !forceSave) {
@@ -293,20 +510,33 @@ export async function checkService(request: FastifyRequest<CheckService>) {
export async function saveService(request: FastifyRequest<SaveService>, reply: FastifyReply) {
try {
const { id } = request.params;
let { name, fqdn, exposePort, type } = request.body;
let { name, fqdn, exposePort, type, serviceSetting, version } = request.body;
if (fqdn) fqdn = fqdn.toLowerCase();
if (exposePort) exposePort = Number(exposePort);
type = fixType(type)
const update = saveUpdateableFields(type, request.body[type])
const data = {
fqdn,
name,
exposePort,
version,
}
if (Object.keys(update).length > 0) {
data[type] = { update: update }
const templates = await getTemplates()
const service = await prisma.service.findUnique({ where: { id } })
const foundTemplate = templates.find(t => fixType(t.type) === fixType(service.type))
for (const setting of serviceSetting) {
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting
if (value) {
if (changed) {
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } })
}
if (isNew) {
if (!variableName) {
variableName = foundTemplate.variables.find(v => v.name === name).id
}
await prisma.serviceSetting.create({ data: { name, value, variableName, service: { connect: { id } } } })
}
}
}
await prisma.service.update({
where: { id }, data
@@ -320,11 +550,19 @@ export async function saveService(request: FastifyRequest<SaveService>, reply: F
export async function getServiceSecrets(request: FastifyRequest<OnlyId>) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
let secrets = await prisma.serviceSecret.findMany({
where: { serviceId: id },
orderBy: { createdAt: 'desc' }
});
const templates = await getTemplates()
const foundTemplate = templates.find(t => fixType(t.type) === service.type)
secrets = secrets.map((secret) => {
const foundVariable = foundTemplate?.variables.find(v => v.name === secret.name) || null
if (foundVariable) {
secret.readOnly = foundVariable.readOnly
}
secret.value = decrypt(secret.value);
return secret;
});
@@ -341,7 +579,6 @@ export async function saveServiceSecret(request: FastifyRequest<SaveServiceSecre
try {
const { id } = request.params
let { name, value, isNew } = request.body
if (isNew) {
const found = await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
if (found) {
@@ -400,16 +637,21 @@ export async function getServiceStorages(request: FastifyRequest<OnlyId>) {
export async function saveServiceStorage(request: FastifyRequest<SaveServiceStorage>, reply: FastifyReply) {
try {
const { id } = request.params
const { path, newStorage, storageId } = request.body
const { path, isNewStorage, storageId, containerId } = request.body
if (newStorage) {
if (isNewStorage) {
const volumeName = `${id}-custom${path.replace(/\//gi, '-')}`
const found = await prisma.servicePersistentStorage.findFirst({ where: { path, containerId } });
if (found) {
throw { status: 500, message: 'Persistent storage already exists for this container and path.' }
}
await prisma.servicePersistentStorage.create({
data: { path, service: { connect: { id } } }
data: { path, volumeName, containerId, service: { connect: { id } } }
});
} else {
await prisma.servicePersistentStorage.update({
where: { id: storageId },
data: { path }
data: { path, containerId }
});
}
return reply.code(201).send()
@@ -420,9 +662,8 @@ export async function saveServiceStorage(request: FastifyRequest<SaveServiceStor
export async function deleteServiceStorage(request: FastifyRequest<DeleteServiceStorage>) {
try {
const { id } = request.params
const { path } = request.body
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id, path } });
const { storageId } = request.body
await prisma.servicePersistentStorage.deleteMany({ where: { id: storageId } });
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
@@ -478,14 +719,17 @@ export async function activatePlausibleUsers(request: FastifyRequest<OnlyId>, re
const {
destinationDockerId,
destinationDocker,
plausibleAnalytics: { postgresqlUser, postgresqlPassword, postgresqlDatabase }
serviceSecret
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id}-postgresql psql -H postgresql://${postgresqlUser}:${postgresqlPassword}@localhost:5432/${postgresqlDatabase} -c "UPDATE users SET email_verified = true;"`
})
return await reply.code(201).send()
const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL');
if (databaseUrl) {
await executeDockerCmd({
dockerId: destinationDocker.id,
command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"`
})
return await reply.code(201).send()
}
}
throw { status: 500, message: 'Could not activate users.' }
} catch ({ status, message }) {

View File

@@ -16,7 +16,6 @@ import {
getServiceStorages,
getServiceType,
getServiceUsage,
getServiceVersions,
listServices,
newService,
saveService,
@@ -64,16 +63,15 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
fastify.get('/:id/configuration/type', async (request) => await getServiceType(request));
fastify.post<SaveServiceType>('/:id/configuration/type', async (request, reply) => await saveServiceType(request, reply));
fastify.get<OnlyId>('/:id/configuration/version', async (request) => await getServiceVersions(request));
fastify.post<SaveServiceVersion>('/:id/configuration/version', async (request, reply) => await saveServiceVersion(request, reply));
fastify.post<SaveServiceDestination>('/:id/configuration/destination', async (request, reply) => await saveServiceDestination(request, reply));
fastify.get<OnlyId>('/:id/usage', async (request) => await getServiceUsage(request));
fastify.get<GetServiceLogs>('/:id/logs', async (request) => await getServiceLogs(request));
fastify.get<GetServiceLogs>('/:id/logs/:containerId', async (request) => await getServiceLogs(request));
fastify.post<ServiceStartStop>('/:id/:type/start', async (request) => await startService(request));
fastify.post<ServiceStartStop>('/:id/:type/stop', async (request) => await stopService(request));
fastify.post<ServiceStartStop>('/:id/start', async (request) => await startService(request, fastify));
fastify.post<ServiceStartStop>('/:id/stop', async (request) => await stopService(request));
fastify.post<ServiceStartStop & SetWordpressSettings & SetGlitchTipSettings>('/:id/:type/settings', async (request, reply) => await setSettingsService(request, reply));
fastify.post<OnlyId>('/:id/plausibleanalytics/activate', async (request, reply) => await activatePlausibleUsers(request, reply));

View File

@@ -15,9 +15,13 @@ export interface SaveServiceDestination extends OnlyId {
destinationId: string
}
}
export interface GetServiceLogs extends OnlyId {
export interface GetServiceLogs{
Params: {
id: string,
containerId: string
},
Querystring: {
since: number
since: number,
}
}
export interface SaveServiceSettings extends OnlyId {
@@ -36,7 +40,7 @@ export interface CheckService extends OnlyId {
forceSave: boolean,
dualCerts: boolean,
exposePort: number,
otherFqdns: Array<string>
otherFqdn: boolean
}
}
export interface SaveService extends OnlyId {
@@ -44,6 +48,8 @@ export interface SaveService extends OnlyId {
name: string,
fqdn: string,
exposePort: number,
version: string,
serviceSetting: any
type: string
}
}
@@ -62,14 +68,15 @@ export interface DeleteServiceSecret extends OnlyId {
export interface SaveServiceStorage extends OnlyId {
Body: {
path: string,
newStorage: string,
containerId: string,
storageId: string,
isNewStorage: boolean,
}
}
export interface DeleteServiceStorage extends OnlyId {
Body: {
path: string,
storageId: string,
}
}
export interface ServiceStartStop {

View File

@@ -2,7 +2,7 @@ import { promises as dns } from 'dns';
import { X509Certificate } from 'node:crypto';
import type { FastifyReply, FastifyRequest } from 'fastify';
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { asyncExecShell, checkDomainsIsValidInDNS, decrypt, encrypt, errorHandler, isDev, isDNSValid, isDomainConfigured, listSettings, prisma } from '../../../../lib/common';
import { CheckDNS, CheckDomain, DeleteDomain, OnlyIdInBody, SaveSettings, SaveSSHKey } from './types';
@@ -44,16 +44,18 @@ export async function saveSettings(request: FastifyRequest<SaveSettings>, reply:
maxPort,
isAutoUpdateEnabled,
isDNSCheckEnabled,
DNSServers
DNSServers,
proxyDefaultRedirect
} = request.body
const { id } = await listSettings();
await prisma.setting.update({
where: { id },
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled }
data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled, isDNSCheckEnabled, DNSServers, isAPIDebuggingEnabled, }
});
if (fqdn) {
await prisma.setting.update({ where: { id }, data: { fqdn } });
}
await prisma.setting.update({ where: { id }, data: { proxyDefaultRedirect } });
if (minPort && maxPort) {
await prisma.setting.update({ where: { id }, data: { minPort, maxPort } });
}
@@ -91,7 +93,7 @@ export async function checkDomain(request: FastifyRequest<CheckDomain>) {
if (found) {
throw "Domain already configured";
}
if (isDNSCheckEnabled && !forceSave) {
if (isDNSCheckEnabled && !forceSave && !isDev) {
const hostname = request.hostname.split(':')[0]
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
}

View File

@@ -10,7 +10,8 @@ export interface SaveSettings {
maxPort: number,
isAutoUpdateEnabled: boolean,
isDNSCheckEnabled: boolean,
DNSServers: string
DNSServers: string,
proxyDefaultRedirect: string
}
}
export interface DeleteDomain {

View File

@@ -1,4 +1,3 @@
import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import { encrypt, errorHandler, getDomain, getUIUrl, isDev, prisma } from "../../../lib/common";
@@ -32,13 +31,14 @@ export async function installGithub(request: FastifyRequest<InstallGithub>, repl
}
export async function configureGitHubApp(request, reply) {
try {
const { default: got } = await import('got')
const { code, state } = request.query;
const { apiUrl } = await prisma.gitSource.findFirst({
where: { id: state },
include: { githubApp: true, gitlabApp: true }
});
const { data }: any = await axios.post(`${apiUrl}/app-manifests/${code}/conversions`);
const data: any = await got.post(`${apiUrl}/app-manifests/${code}/conversions`).json()
const { id, client_id, slug, client_secret, pem, webhook_secret } = data
const encryptedClientSecret = encrypt(client_secret);

View File

@@ -1,4 +1,3 @@
import axios from "axios";
import cuid from "cuid";
import crypto from "crypto";
import type { FastifyReply, FastifyRequest } from "fastify";
@@ -10,6 +9,7 @@ import type { ConfigureGitLabApp, GitLabEvents } from "./types";
export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLabApp>, reply: FastifyReply) {
try {
const { default: got } = await import('got')
const { code, state } = request.query;
const { fqdn } = await listSettings();
const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } }: any = await getApplicationFromDB(state, undefined);
@@ -19,19 +19,21 @@ export async function configureGitLabApp(request: FastifyRequest<ConfigureGitLab
if (isDev) {
domain = getAPIUrl();
}
const params = new URLSearchParams({
client_id: appId,
client_secret: appSecret,
code,
state,
grant_type: 'authorization_code',
redirect_uri: `${domain}/webhooks/gitlab`
});
const { data } = await axios.post(`${htmlUrl}/oauth/token`, params)
const { access_token } = await got.post(`${htmlUrl}/oauth/token`, {
searchParams: {
client_id: appId,
client_secret: appSecret,
code,
state,
grant_type: 'authorization_code',
redirect_uri: `${domain}/webhooks/gitlab`
}
}).json()
if (isDev) {
return reply.redirect(`${getUIUrl()}/webhooks/success?token=${data.access_token}`)
return reply.redirect(`${getUIUrl()}/webhooks/success?token=${access_token}`)
}
return reply.redirect(`/webhooks/success?token=${data.access_token}`)
return reply.redirect(`/webhooks/success?token=${access_token}`)
} catch ({ status, message, ...other }) {
return errorHandler({ status, message })
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,4 +1,4 @@
export interface TraefikOtherConfiguration {
export interface OtherProxyConfiguration {
Querystring: {
id: string,
privatePort: number,

View File

@@ -1,4 +1,4 @@
export interface OnlyId {
Params: { id: string },
Params: { id?: string },
}

1
apps/api/tags.json Normal file

File diff suppressed because one or more lines are too long

1
apps/api/templates.json Normal file

File diff suppressed because one or more lines are too long

View File

@@ -14,42 +14,43 @@
"format": "prettier --write --plugin-search-dir=. ."
},
"devDependencies": {
"@floating-ui/dom": "1.0.1",
"@playwright/test": "1.25.1",
"@floating-ui/dom": "1.0.3",
"@playwright/test": "1.27.1",
"@popperjs/core": "2.11.6",
"@sveltejs/kit": "1.0.0-next.405",
"@types/js-cookie": "3.0.2",
"@typescript-eslint/eslint-plugin": "5.36.1",
"@typescript-eslint/parser": "5.36.1",
"autoprefixer": "10.4.8",
"classnames": "2.3.1",
"eslint": "8.23.0",
"@typescript-eslint/eslint-plugin": "5.41.0",
"@typescript-eslint/parser": "5.41.0",
"autoprefixer": "10.4.12",
"classnames": "2.3.2",
"eslint": "8.26.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-svelte3": "4.0.0",
"flowbite": "1.5.2",
"flowbite-svelte": "0.26.2",
"postcss": "8.4.16",
"flowbite": "1.5.3",
"flowbite-svelte": "0.27.11",
"postcss": "8.4.18",
"prettier": "2.7.1",
"prettier-plugin-svelte": "2.7.0",
"svelte": "3.50.0",
"svelte-check": "2.9.0",
"prettier-plugin-svelte": "2.8.0",
"svelte": "3.52.0",
"svelte-check": "2.9.2",
"svelte-preprocess": "4.10.7",
"tailwindcss": "3.1.8",
"tailwindcss": "3.2.1",
"tailwindcss-scrollbar": "0.1.0",
"tslib": "2.4.0",
"typescript": "4.8.2",
"vite": "3.1.0"
"typescript": "4.8.4",
"vite": "3.2.0"
},
"type": "module",
"dependencies": {
"@sveltejs/adapter-static": "1.0.0-next.39",
"@tailwindcss/typography": "^0.5.7",
"@sveltejs/adapter-static": "1.0.0-next.46",
"@tailwindcss/typography": "0.5.7",
"cuid": "2.1.8",
"daisyui": "2.24.2",
"dayjs": "1.11.5",
"daisyui": "2.33.0",
"dayjs": "1.11.6",
"js-cookie": "3.0.1",
"js-yaml": "4.1.0",
"p-limit": "4.0.0",
"svelte-file-dropzone": "^1.0.0",
"socket.io-client": "4.5.3",
"svelte-select": "4.4.7",
"sveltekit-i18n": "2.2.2"
}

View File

@@ -110,7 +110,7 @@ async function send({
if (
response.status === 401 &&
!path.startsWith('https://api.github') &&
!path.includes('/v4/user')
!path.includes('/v4/')
) {
Cookies.remove('token');
}

View File

@@ -38,6 +38,8 @@
class={disabledClass}
class:pr-10={true}
class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
{placeholder}
type="text"
{id}
@@ -54,6 +56,8 @@
type="text"
class:pr-10={true}
class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
{id}
{name}
{required}
@@ -70,6 +74,8 @@
class={disabledClass}
class:pr-10={true}
class:pr-20={value && isHttps}
class:border={required && !value}
class:border-red-500={required && !value}
type="password"
{id}
{name}
@@ -85,6 +91,7 @@
<div class="absolute top-0 right-0 flex justify-center items-center h-full cursor-pointer text-stone-600 hover:text-white mr-3">
<div class="flex space-x-2">
{#if isPasswordField}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={() => (showPassword = !showPassword)}>
{#if showPassword}
<svg
@@ -126,6 +133,7 @@
</div>
{/if}
{#if value && isHttps}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div on:click={copyToClipboard}>
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -1,6 +1,9 @@
<script lang="ts">
import ExternalLink from './ExternalLink.svelte';
import Tooltip from './Tooltip.svelte';
export let url = 'https://docs.coollabs.io';
export let text: any = '';
export let isExternal = false;
let id =
'cool-' +
url
@@ -10,10 +13,32 @@
.slice(-16);
</script>
<a {id} href={url} target="_blank" class="icons inline-block cursor-pointer text-xs mx-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z" />
</svg>
<a
{id}
href={url}
target="_blank noreferrer"
class="flex no-underline inline-block cursor-pointer"
class:icons={!text}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9 5.25h.008v.008H12v-.008z"
/>
</svg>
{text}
{#if isExternal}
<ExternalLink />
{/if}
</a>
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
{#if !text}
<Tooltip triggeredBy={`#${id}`}>See details in the documentation</Tooltip>
{/if}

View File

@@ -3,7 +3,7 @@
// import Tooltip from './Tooltip.svelte';
export let explanation = '';
export let position = 'dropdown-right'
export let position = 'dropdown-right';
// let id: any;
// let self: any;
// onMount(() => {
@@ -12,32 +12,27 @@
</script>
<div class={`dropdown dropdown-end ${position}`}>
<!-- svelte-ignore a11y-label-has-associated-control -->
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<label tabindex="0" class="btn btn-circle btn-ghost btn-xs text-sky-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-4 h-4 stroke-current"><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"></path></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="w-4 h-4 stroke-current"
><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
>
</label>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div tabindex="0" class="card compact dropdown-content shadow bg-coolgray-400 rounded w-64">
<div class="card-body">
<!-- <h2 class="card-title">You needed more info?</h2> -->
<p class="text-xs font-normal">{@html explanation}</p>
</div>
<div class="card-body">
<!-- <h2 class="card-title">You needed more info?</h2> -->
<p class="text-xs font-normal">{@html explanation}</p>
</div>
</div>
</div>
<!-- <div {id} class="inline-block mx-2 cursor-pointer" bind:this={self}>
<svg
fill="none"
height="14"
shape-rendering="geometricPrecision"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.4"
viewBox="0 0 24 24"
width="14"
><path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" /><path
d="M9.09 9a3 3 0 015.83 1c0 2-3 3-3 3"
/><circle cx="12" cy="17" r=".5" />
</svg>
</div>
{#if id}
<Tooltip triggeredBy={`#${id}`}>{@html explanation}</Tooltip>
{/if} -->

View File

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

After

Width:  |  Height:  |  Size: 261 B

View File

@@ -0,0 +1,37 @@
<script lang="ts">
export let id: any;
import { status } from '$lib/store';
let serviceStatus = {
isExcluded: false,
isExited: false,
isRunning: false,
isRestarting: false,
isStopped: false
};
$: if (Object.keys($status.service.statuses).length > 0 && $status.service.statuses[id]?.status) {
let { isExited, isRunning, isRestarting, isExcluded } = $status.service.statuses[id].status;
serviceStatus.isExited = isExited;
serviceStatus.isRunning = isRunning;
serviceStatus.isExcluded = isExcluded;
serviceStatus.isRestarting = isRestarting;
serviceStatus.isStopped = !isExited && !isRunning && !isRestarting;
} else {
serviceStatus.isExited = false;
serviceStatus.isRunning = false;
serviceStatus.isExcluded = false;
serviceStatus.isRestarting = false;
serviceStatus.isStopped = true;
}
</script>
{#if serviceStatus.isExcluded}
<span class="badge font-bold uppercase rounded text-orange-500 mt-2">Excluded</span>
{:else if serviceStatus.isRunning}
<span class="badge font-bold uppercase rounded text-green-500 mt-2">Running</span>
{:else if serviceStatus.isStopped || serviceStatus.isExited}
<span class="badge font-bold uppercase rounded text-red-500 mt-2">Stopped</span>
{:else if serviceStatus.isRestarting}
<span class="badge font-bold uppercase rounded text-yellow-500 mt-2">Restarting</span>
{/if}

View File

@@ -8,7 +8,7 @@
export let setting: any;
export let title: any;
export let isBeta: any = false;
export let description: any;
export let description: any = null;
export let isCenter = true;
export let disabled = false;
export let dataTooltip: any = null;
@@ -31,6 +31,7 @@
</div>
</div>
<div class:text-center={isCenter} class={`flex justify-center ${customClass}`}>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click
aria-pressed="false"

View File

@@ -4,18 +4,19 @@
export let type = 'info';
function success() {
if (type === 'success') {
return 'bg-coollabs';
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 alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`}
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'}
>

View File

@@ -4,11 +4,11 @@
import { dismissToast, pauseToast, resumeToast, toasts } from '$lib/store';
</script>
{#if $toasts}
{#if $toasts.length > 0}
<section>
<article class="toast toast-top toast-end rounded-none px-10" role="alert" >
<article class="toast toast-top toast-center rounded-none w-2/3 lg:w-[20rem]" role="alert">
{#each $toasts as toast (toast.id)}
<Toast
<Toast
type={toast.type}
on:resume={() => resumeToast(toast.id)}
on:pause={() => pauseToast(toast.id)}

View File

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

View File

@@ -68,12 +68,6 @@
</script>
<div class="w-full relative p-5 ">
{#if loading.usage}
<span class="indicator-item badge bg-yellow-500 badge-sm" />
{:else}
<span class="indicator-item badge bg-success badge-sm" />
{/if}
<div class="w-full flex flex-col lg:flex-row space-y-4 lg:space-y-0 space-x-4">
<div class="flex flex-col">
<h1 class="font-bold text-lg lg:text-xl truncate">
@@ -99,6 +93,11 @@
class="btn btn-sm">Cleanup Storage</button
>
{/if}
{#if loading.usage}
<button id="streaming" class=" btn btn-sm bg-transparent border-none loading"
>Getting data...</button
>
{/if}
</div>
<div class="flex lg:flex-row flex-col gap-4">
<div class="flex lg:flex-row flex-col space-x-0 lg:space-x-2 space-y-2 lg:space-y-0" />

View File

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

View File

@@ -0,0 +1,9 @@
<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"
/>

View File

@@ -17,3 +17,4 @@ export { default as Eleventy } from './Eleventy.svelte';
export { default as Deno } from './Deno.svelte';
export { default as Laravel } from './Laravel.svelte';
export { default as Heroku } from './Heroku.svelte';
export { default as Compose } from './Compose.svelte';

File diff suppressed because one or more lines are too long

View File

@@ -1,121 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
viewBox="0 0 700 240"
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-36 absolute top-0 left-0 -m-3 -mt-5' : 'w-full h-10 mx-auto'}
><path fill="#FDBC3D" d="m90.694 107.498-.981.39-20.608 8.23 6.332 6.547z" /><path
fill="#8EC63F"
d="M61.139 77.914 46.632 93 56.9 103.547c8.649-7.169 17.832-10.502 18.653-10.789L61.139 77.914z"
/><path fill="#208ECB" d="M61.139 77.914 46.367 63.247l-14.228 14.8 14.493 14.952z" /><path
fill="#273C8B"
d="m40.767 57.48-6.943 2.79a38.381 38.381 0 0 0-11.742 7.418L32.14 78.047l14.228-14.8-5.601-5.768z"
/><path
fill="#EE4649"
d="m119.074 138.128-.243-.25-5.653 5.675c1.897-1.516 4.287-3.66 5.896-5.425z"
/><path
fill="#F6944E"
d="m102.088 150.087 3.709-1.875a46.26 46.26 0 0 0 7.381-4.659l5.653-5.676-14.311-15.285-14.493 15.072 12.061 12.423z"
/><path fill="#FFC951" d="m90.279 107.926-14.842 14.74 14.589 14.998 14.493-15.072z" /><path
fill="#F6CC18"
d="m69.087 116.125-11.256 4.493c-3.301.973-6.096 2.843-8.434 5.081l11.548 11.892 14.493-14.926-6.35-6.54z"
/><path
fill="#C5D82D"
d="m56.886 103.559-10.253-10.56L32 107.926l11.784 11.991c3.304-6.888 8.174-12.272 13.103-16.358z"
/><path fill="#0D77B3" d="m32.14 78.047-14.507 14.94 14.365 14.939 14.634-14.927z" /><path
fill="#2A377E"
d="M32.14 78.047 22.08 67.688a38.573 38.573 0 0 0-11.093 18.455l6.645 6.843 14.506-14.94z"
/><path
fill="#DA2128"
d="m94.826 162.454-4.87 5.017 14.808 15.397c-.632-1.942-1.606-4.438-2.58-6.307l-7.357-14.107z"
/><path
fill="#F8A561"
d="m91.24 155.575 10.832-5.48-12.046-12.43-14.506 14.939 14.436 14.867 4.87-5.017z"
/><path fill="#FDBC3D" d="m75.437 122.665-14.493 14.926 14.576 15.013 14.506-14.94z" /><path
fill="#FAD412"
d="M49.397 125.7c-6.71 6.472-9.664 16.047-9.664 16.047-.3-4.606.06-8.83.907-12.698l-8.513 8.742 14.311 14.74 14.506-14.94-11.547-11.892z"
/><path
fill="#C4D52D"
d="m43.783 119.917-11.785-11.991-13.29 13.687 3.708 6.178 9.71 10 8.52-8.775a42.699 42.699 0 0 1 3.137-9.099z"
/><path
fill="#1B80C1"
d="m17.633 92.986-7.638 7.72c.65 5.1 2.35 10.3 5.193 15.04l3.52 5.867 13.29-13.687-14.365-14.94z"
/><path
fill="#1A4685"
d="M10.989 86.143c-1.22 4.667-1.597 9.683-.993 14.563l7.638-7.72-6.645-6.843z"
/><path
fill="#B12026"
d="m89.956 197.35 12.502 13.022c4.143-8.355 5.148-18.255 2.307-27.504l-.302-.311-14.507 14.793z"
/><path fill="#E42028" d="M89.956 167.47 75.52 182.484l14.436 14.867 14.506-14.793z" /><path
fill="#F16B4E"
d="m75.52 152.604-14.576 14.867 14.576 15.012 14.436-15.012z"
/><path fill="#FAD412" d="m60.944 137.591-14.506 14.94 14.506 14.94 14.576-14.867z" /><path
fill="#FFC951"
d="m32.127 137.792-2.293 2.36 10.933 18.22 5.671-5.841z"
/><path fill="#FFC951" d="m22.416 127.79 7.418 12.363 2.293-2.361z" /><path
fill="#981C20"
d="M102.458 210.371 89.955 197.35 75.45 212.29l12.918 13.304a36.951 36.951 0 0 0 14.09-15.222z"
/><path
fill="#C92039"
d="m75.52 182.483-12.59 12.823 6.423 10.704 6.097 6.28 14.506-14.94z"
/><path fill="#F05B41" d="m60.944 167.47-9.096 9.369 11.081 18.467 12.59-12.823z" /><path
fill="#F6CC18"
d="m46.438 152.53-5.671 5.842 11.081 18.467 9.096-9.368z"
/><path
fill="#7A1319"
d="m74.01 213.772 8.904 14.838 4.104-2.237c.429-.233.934-.533 1.35-.78L75.45 212.29l-1.44 1.482z"
/><path fill="#981C20" d="m69.353 206.01 4.658 7.762 1.44-1.482z" /><path
fill="#15796E"
d="m147.842 48.094 10.653-10.971a41.81 41.81 0 0 0 .943-6.94l-11.414-11.755-14.48 14.94 14.298 14.726z"
/><path fill="#29B364" d="m133.53 33.354 14.494-14.926-2.737-2.965-20.95 8.422z" /><path
fill="#21A29F"
d="M151.819 52.189c3.057-4.334 5.434-9.932 6.677-15.066l-10.653 10.971 3.976 4.095z"
/><path
fill="#12827F"
d="M159.438 30.183c.307-6.28-.783-12.862-3.488-19.006l-1.41.567-6.516 6.684 11.414 11.755zM154.54 11.744l-9.253 3.72 2.737 2.964z"
/><path fill="#0C6355" d="m133.336 63.034 14.506-14.94-14.311-14.713-14.493 14.926z" /><path
fill="#1B974D"
d="m104.532 33.368 14.506 14.94 14.48-14.94-9.2-9.476-17.363 6.98z"
/><path fill="#16669F" d="m106.955 30.872-3.485 1.401 1.062 1.095z" /><path
fill="#44BFBD"
d="M135.9 65.674A41.696 41.696 0 0 0 151.82 52.19l-3.977-4.095-14.506 14.94 2.564 2.64z"
/><path
fill="#0D5650"
d="m115.71 74.76 11.052-4.956 6.574-6.77-14.298-14.727-14.506 14.94z"
/><path fill="#3FAF49" d="m119.038 48.307-14.506-14.94-14.576 14.868 14.563 14.999z" /><path
fill="#0D77B3"
d="m104.532 33.368-1.062-1.095-20.97 8.43 7.456 7.532z"
/><path
fill="#0C6355"
d="M134.766 66.217c.352-.157.789-.376 1.134-.543l-2.564-2.64-6.574 6.77 8.004-3.587z"
/><path fill="#12827F" d="m115.71 74.76-11.178-11.513-14.506 14.94 5.47 5.633z" /><path
fill="#4EB648"
d="M104.532 63.247 89.956 48.235 75.52 63.247l14.493 14.927z"
/><path fill="#16669F" d="M89.956 48.235 82.5 40.703l-20.868 8.388L75.52 63.247z" /><path
fill="#FBB139"
d="M129.526 119.012c1.902-7.144 2.108-15.019.353-22.538l-11.048 11.379 10.695 11.16z"
/><path
fill="#E2B523"
d="m110.62 99.542 8.21 8.311 11.049-11.38a46.303 46.303 0 0 0-1.186-4.149l-18.074 7.218z"
/><path fill="#189590" d="M90.026 78.186 76.128 92.501l19.367-8.681z" /><path
fill="#8EC63F"
d="m76.083 92.521 13.943-14.335-14.506-14.94-14.381 14.668 14.413 14.844z"
/><path
fill="#0D77B3"
d="M75.52 63.247 61.633 49.09l-2.264.91-13.002 13.246L61.14 77.914z"
/><path fill="#1953A2" d="m59.37 50.002-18.603 7.477 5.6 5.768z" /><path
fill="#ED3551"
d="M119.324 137.84c.885-.988 2.15-2.59 2.942-3.646l-3.17 3.41.228.236z"
/><path
fill="#F8A561"
d="m118.83 137.877 3.437-3.683a46.268 46.268 0 0 0 7.259-15.182l-10.695-11.159-14.311 14.74 14.31 15.284z"
/><path
fill="#E9B520"
d="m90.279 107.926 14.24 14.666 14.312-14.739-8.212-8.311-19.925 7.956z"
/><path
fill="#EE4649"
d="m118.83 137.877.244.251c.085-.095.166-.193.25-.288l-.228-.235-.265.272z"
/></svg
>

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
viewBox="0 0 81 84"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_5273_21928)">
<path
d="M79.7186 28.6019C82.1218 21.073 80.6778 6.03601 76.0158 0.487861C75.4073 -0.238064 74.2624 -0.134361 73.757 0.664158L68.0121 9.72786C66.5887 11.5427 64.0308 11.9575 62.1124 10.6923C55.8827 6.59601 48.4359 4.21082 40.4322 4.21082C32.4285 4.21082 24.9817 6.59601 18.752 10.6923C16.8336 11.9575 14.2757 11.5323 12.8523 9.72786L7.10738 0.664158C6.60199 -0.134361 5.45712 -0.238064 4.84859 0.487861C0.186621 6.03601 -1.25735 21.073 1.14583 28.6019C1.94002 31.1012 2.16693 33.7456 1.69248 36.3279C1.22834 38.879 0.753897 41.9693 0.753897 44.1056C0.753897 66.1323 18.5251 84.0004 40.4322 84.0004C62.3497 84.0004 80.1105 66.1427 80.1105 44.1056C80.1105 41.959 79.6464 38.879 79.1719 36.3279C78.6975 33.7456 78.9244 31.1012 79.7186 28.6019ZM40.4322 75.0819C23.4965 75.0819 9.71684 61.2271 9.71684 44.199C9.71684 43.639 9.73747 43.0893 9.7581 42.5397C10.3769 30.9353 17.3802 21.0108 27.3024 16.2819C31.2836 14.3738 35.7393 13.316 40.4322 13.316C45.1251 13.316 49.5808 14.3842 53.5724 16.2923C63.4945 21.0212 70.4978 30.9456 71.1166 42.5397C71.1476 43.0893 71.1579 43.639 71.1579 44.199C71.1476 61.2271 57.3679 75.0819 40.4322 75.0819Z"
fill="#1EB4D4"
/>
<path
d="M53.7371 56.083L45.8881 42.4044L39.153 30.997C38.9983 30.7274 38.7095 30.5615 38.3898 30.5615H31.9538C31.634 30.5615 31.3452 30.7378 31.1905 31.0074C31.0358 31.2874 31.0358 31.6296 31.2008 31.8993L37.6368 42.7881L28.9936 56.0415C28.8183 56.3111 28.7977 56.6637 28.9524 56.9541C29.1071 57.2444 29.4062 57.4207 29.7259 57.4207H36.2032C36.5023 57.4207 36.7808 57.2652 36.9458 57.0163L41.6181 49.6741L45.8056 56.9748C45.9603 57.2548 46.2594 57.4207 46.5688 57.4207H52.9533C53.273 57.4207 53.5618 57.2548 53.7165 56.9748C53.9022 56.6948 53.9022 56.363 53.7371 56.083Z"
fill="#1EB4D4"
/>
</g>
<defs>
<clipPath id="clip0_5273_21928">
<rect width="81" height="84" fill="white" />
</clipPath>
</defs>
</svg>

View File

@@ -1,27 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
fill="none"
viewBox="0 0 140 140"
data-lt-extension-installed="true"
><g clip-path="url(#clip0)"
><path
fill="#fff"
fill-rule="evenodd"
d="M140 43.602c0-1.662.001-3.324-.01-4.987-.008-1.4-.024-2.8-.062-4.2-.082-3.05-.262-6.126-.805-9.142-.55-3.06-1.448-5.907-2.864-8.688A29.227 29.227 0 0 0 123.476 3.81c-2.783-1.416-5.634-2.314-8.697-2.864-3.016-.542-6.094-.722-9.144-.804-1.4-.038-2.801-.054-4.202-.063C99.77.068 98.107.07 96.444.07L77.135 0H62.694L43.726.07c-1.666 0-3.332-.002-4.998.008-1.404.01-2.807.025-4.21.063-3.058.082-6.142.262-9.166.805-3.067.55-5.922 1.447-8.709 2.862a29.293 29.293 0 0 0-7.419 5.377 29.223 29.223 0 0 0-5.389 7.4c-1.42 2.78-2.32 5.63-2.871 8.691-.543 3.016-.723 6.091-.806 9.14-.038 1.4-.054 2.8-.062 4.2C.086 40.277 0 42.342 0 44.004v33.3l.086 19.102c0 1.665 0 3.33.01 4.994a200.6 200.6 0 0 0 .062 4.205c.083 3.054.263 6.135.807 9.155.551 3.064 1.451 5.916 2.87 8.7a29.294 29.294 0 0 0 12.807 12.794c2.788 1.418 5.645 2.317 8.714 2.868 3.022.542 6.105.722 9.162.804 1.403.038 2.806.054 4.21.063 1.666.01 3.332.009 4.998.009l19.14.001h14.477l19.101-.001c1.663 0 3.326.001 4.989-.009a202.92 202.92 0 0 0 4.202-.063c3.052-.082 6.13-.262 9.148-.805 3.061-.551 5.911-1.45 8.692-2.867a29.215 29.215 0 0 0 7.405-5.384 29.22 29.22 0 0 0 5.378-7.409c1.417-2.785 2.315-5.639 2.866-8.704.542-3.02.722-6.099.804-9.152.038-1.402.054-2.804.062-4.205.011-1.665.01-3.33.01-4.993l-.001-19.103V62.694L140 43.602"
clip-rule="evenodd"
/><path
fill="#000"
fill-rule="evenodd"
d="M39.375 40.188h8.313a6.25 6.25 0 0 1 6.25 6.25v24.25h16.25v8.75h-18.75a6.25 6.25 0 0 1-6.25-6.25v-24.25h-5.813v-8.75zm63.563 6.25v6.5h-8.75v-4h-6.876v30.5h-8.75v-30.5h-6.874v4h-8.75v-6.5a6.25 6.25 0 0 1 6.25-6.25h27.5a6.25 6.25 0 0 1 6.25 6.25z"
clip-rule="evenodd"
/><path
fill="#239AFF"
d="M35.319 102.906l-8.138-5.812c2.39-3.347 4.857-5.936 7.452-7.753 2.884-2.018 5.948-3.091 9.117-3.091 2.942 0 5.491.714 7.768 2.08a17.622 17.622 0 0 1 2.615 1.94c.589.518 1.009.926 1.903 1.82 1.355 1.354 1.917 1.851 2.591 2.255.731.439 1.503.655 2.623.655 1.121 0 1.896-.217 2.631-.657.677-.405 1.245-.905 2.6-2.257l.012-.012c.89-.888 1.314-1.299 1.902-1.817a17.643 17.643 0 0 1 2.61-1.933c2.273-1.362 4.814-2.074 7.745-2.074s5.472.712 7.745 2.074c.916.55 1.758 1.183 2.61 1.933.589.518 1.013.929 1.902 1.817l.013.012c1.354 1.352 1.922 1.852 2.599 2.257.735.44 1.51.657 2.631.657.998 0 2.1-.386 3.383-1.284 1.572-1.1 3.272-2.886 5.048-5.372l8.138 5.812c-2.391 3.347-4.857 5.936-7.452 7.753-2.884 2.018-5.948 3.091-9.117 3.091-2.941 0-5.49-.713-7.769-2.078a17.627 17.627 0 0 1-2.619-1.938c-.59-.519-1.015-.93-1.906-1.82l-.013-.013c-1.351-1.348-1.917-1.846-2.59-2.25-.728-.436-1.494-.651-2.603-.651-1.109 0-1.875.215-2.603.651-.673.404-1.239.902-2.59 2.25l-.012.013c-.892.89-1.317 1.301-1.907 1.82-.855.752-1.7 1.388-2.62 1.938C66.74 104.287 64.192 105 61.25 105c-2.942 0-5.49-.714-7.768-2.08a17.654 17.654 0 0 1-2.615-1.939c-.588-.519-1.009-.927-1.902-1.82-1.355-1.355-1.918-1.852-2.592-2.256-.731-.439-1.503-.655-2.623-.655-.998 0-2.1.386-3.383 1.284-1.572 1.1-3.272 2.886-5.048 5.372z"
/></g
><defs><clipPath id="clip0"><path fill="#fff" d="M0 0h140v140H0z" /></clipPath></defs></svg
>

View File

@@ -1,9 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<img
alt="minio logo"
class={isAbsolute ? 'w-7 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-4 h-8 mx-auto'}
src="/minio.png"
/>

View File

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

View File

@@ -1,24 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-5' : 'w-8 h-8 mx-auto'}
viewBox="0 0 220 105"
>
<g>
<path
fill="#FF6D5A"
d="M183.9,0.2c-9.8,0-18,6.7-20.3,15.8h-29.2c-11.5,0-20.8,9.3-20.8,20.8c0,5.7-4.7,10.4-10.4,10.4H99
c-2.3-9.1-10.5-15.8-20.3-15.8c-9.8,0-18,6.7-20.3,15.8H41.7c-2.3-9.1-10.5-15.8-20.3-15.8c-11.6,0-21,9.4-21,21
c0,11.6,9.4,21,21,21c9.8,0,18-6.7,20.3-15.8h16.7c2.3,9.1,10.5,15.8,20.3,15.8c9.7,0,17.9-6.6,20.3-15.6h4.2
c5.7,0,10.4,4.7,10.4,10.4c0,11.5,9.3,20.8,20.8,20.8h6.8c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21c0-11.6-9.4-21-21-21
c-9.8,0-18,6.7-20.3,15.8h-6.8c-5.7,0-10.4-4.7-10.4-10.4c0-6.3-2.8-11.9-7.2-15.7c4.4-3.8,7.2-9.4,7.2-15.7
c0-5.7,4.7-10.4,10.4-10.4h29.2c2.3,9.1,10.5,15.8,20.3,15.8c11.6,0,21-9.4,21-21C204.9,9.6,195.5,0.2,183.9,0.2z M21.4,63
c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6S32,46.6,32,52.4S27.3,63,21.4,63z M78.7,63c-5.8,0-10.6-4.8-10.6-10.6
s4.8-10.6,10.6-10.6s10.6,4.8,10.6,10.6S84.6,63,78.7,63z M161.5,73.2c5.8,0,10.6,4.8,10.6,10.6s-4.8,10.6-10.6,10.6
s-10.6-4.8-10.6-10.6C150.9,77.9,155.7,73.2,161.5,73.2z M183.9,31.8c-5.8,0-10.6-4.8-10.6-10.6s4.8-10.6,10.6-10.6
s10.6,4.8,10.6,10.6C194.5,27,189.8,31.8,183.9,31.8z"
/>
</g>
</svg>

View File

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

View File

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

View File

@@ -1,56 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
id="svg8"
version="1.1"
viewBox="0 0 92 92"
class={isAbsolute ? 'w-12 h-12 absolute top-0 left-0 -m-3 -mt-5' : 'w-8 h-8 mx-auto'}
>
<defs id="defs2" />
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-40.921303,-17.416526)" id="layer1">
<circle
r="0"
style="fill:none;stroke:#000000;stroke-width:12;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cy="92"
cx="75"
id="path3713"
/>
<circle
r="30"
cy="53.902557"
cx="75.921303"
id="path834"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
<path
d="m 67.514849,37.91524 a 18,18 0 0 1 21.051475,3.312407 18,18 0 0 1 3.137312,21.078282"
id="path852"
style="fill:none;fill-opacity:1;stroke:#3050ff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
<rect
transform="rotate(-46.234709)"
ry="1.8669105e-13"
y="122.08995"
x="3.7063529"
height="39.963303"
width="18.846331"
id="rect912"
style="opacity:1;fill:#3050ff;fill-opacity:1;stroke:none;stroke-width:8;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
/>
</g>
</svg>

View File

@@ -1,49 +1,45 @@
<script lang="ts">
export let type: string;
export let isAbsolute = true;
import * as Icons from '$lib/components/svg/services';
export let isAbsolute = false;
let extension = 'png';
let svgs = [
'languagetool',
'meilisearch',
'n8n',
'glitchtip',
'searxng',
'umami',
'uptimekuma',
'vaultwarden',
'weblate',
'wordpress'
];
const name: any =
type &&
(type[0].toUpperCase() + type.substring(1).toLowerCase())
.replaceAll('.', '')
.replaceAll(' ', '')
.split('-')[0]
.toLowerCase();
if (svgs.includes(name)) {
extension = 'svg';
}
function generateClass() {
switch (name) {
case 'n8n':
return 'w-12 h-12 -mt-3';
case 'weblate':
return 'w-12 h-12 -mt-3';
default:
return isAbsolute ? 'w-10 h-10 absolute -m-4 -mt-9 left-0' : 'w-10 h-10';
}
}
</script>
{#if type === 'plausibleanalytics'}
<Icons.PlausibleAnalytics {isAbsolute} />
{:else if type === 'nocodb'}
<Icons.NocoDb {isAbsolute} />
{:else if type === 'minio'}
<Icons.MinIo {isAbsolute} />
{:else if type === 'vscodeserver'}
<Icons.VsCodeServer {isAbsolute} />
{:else if type === 'wordpress'}
<Icons.Wordpress {isAbsolute} />
{:else if type === 'vaultwarden'}
<Icons.VaultWarden {isAbsolute} />
{:else if type === 'languagetool'}
<Icons.LanguageTool {isAbsolute} />
{:else if type === 'n8n'}
<Icons.N8n {isAbsolute} />
{:else if type === 'uptimekuma'}
<Icons.UptimeKuma {isAbsolute} />
{:else if type === 'ghost'}
<Icons.Ghost {isAbsolute} />
{:else if type === 'meilisearch'}
<Icons.MeiliSearch {isAbsolute} />
{:else if type === 'umami'}
<Icons.Umami {isAbsolute} />
{:else if type === 'hasura'}
<Icons.Hasura {isAbsolute} />
{:else if type === 'fider'}
<Icons.Fider {isAbsolute} />
{:else if type === 'appwrite'}
<Icons.Appwrite {isAbsolute} />
{:else if type === 'moodle'}
<Icons.Moodle {isAbsolute} />
{:else if type === 'glitchTip'}
<Icons.GlitchTip {isAbsolute} />
{:else if type === 'searxng'}
<Icons.Searxng {isAbsolute} />
{:else if type === 'weblate'}
<Icons.Weblate {isAbsolute} />
{:else if type === 'grafana'}
<Icons.Grafana {isAbsolute} />
{:else if type === 'trilium'}
<Icons.Trilium {isAbsolute} />
{#if name}
<img class={generateClass()} src={`/icons/${name}.${extension}`} alt={`Icon of ${name}`} />
{/if}

View File

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

View File

@@ -1,22 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'}
viewBox="0 0 128 128"
>
<path
d="M3.656 45.043s-3.027-2.191.61-5.113l8.468-7.594s2.426-2.559 4.989-.328l78.175 59.328v28.45s-.039 4.468-5.757 3.976zm0 0"
fill="#2489ca"
/><path
d="M23.809 63.379L3.656 81.742s-2.07 1.543 0 4.305l9.356 8.527s2.222 2.395 5.508-.328l21.359-16.238zm0 0"
fill="#1070b3"
/><path
d="M59.184 63.531l36.953-28.285-.239-28.297S94.32.773 89.055 3.99L39.879 48.851zm0 0"
fill="#0877b9"
/><path
d="M90.14 123.797c2.145 2.203 4.747 1.48 4.747 1.48l28.797-14.222c3.687-2.52 3.171-5.645 3.171-5.645V20.465c0-3.735-3.812-5.024-3.812-5.024L98.082 3.38c-5.453-3.379-9.027.61-9.027.61s4.593-3.317 6.843 2.96v112.317c0 .773-.164 1.53-.492 2.214-.656 1.332-2.086 2.57-5.504 2.051zm0 0"
fill="#3c99d4"
/>
</svg>

View File

@@ -1,12 +0,0 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 h-8mx-auto'} viewBox="0 0 128 128">
<path
fill-rule="evenodd"
clip-rule="evenodd"
fill="white"
d="M64.094 126.224c34.275-.052 62.021-27.933 62.021-62.325 0-33.833-27.618-61.697-60.613-62.286C30.85.995 1.894 29.113 1.885 63.21c-.01 35.079 27.612 63.064 62.209 63.014zM63.993 4.63c32.907-.011 59.126 26.725 59.116 60.28-.011 31.679-26.925 58.18-59.092 58.187-32.771.007-59.125-26.563-59.124-59.608.002-32.193 26.766-58.848 59.1-58.859zM39.157 35.896c.538 1.793-.968 2.417-2.569 2.542-1.685.13-3.369.257-5.325.406 6.456 19.234 12.815 38.183 19.325 57.573.464-.759.655-.973.739-1.223 3.574-10.682 7.168-21.357 10.651-32.069.318-.977.16-2.271-.188-3.275-1.843-5.32-4.051-10.524-5.667-15.908-1.105-3.686-2.571-6.071-6.928-5.644-.742.073-1.648-1.524-2.479-2.349 1.005-.6 2.003-1.704 3.017-1.719a849.593 849.593 0 0126.618.008c1.018.017 2.016 1.15 3.021 1.765-.88.804-1.639 2.01-2.668 2.321-1.651.498-3.482.404-5.458.58l19.349 57.56c2.931-9.736 5.658-18.676 8.31-27.639 2.366-8.001.956-15.473-3.322-22.52-1.286-2.119-2.866-4.175-3.595-6.486-.828-2.629-1.516-5.622-1.077-8.259.745-4.469 4.174-6.688 8.814-7.113C74.333.881 34.431 9.317 19.728 34.922c5.66-.261 11.064-.604 16.472-.678 1.022-.013 2.717.851 2.957 1.652zm10.117 77.971c-.118.345-.125.729-.218 1.302 10.943 3.034 21.675 2.815 32.659-.886l-16.78-45.96c-5.37 15.611-10.52 30.575-15.661 45.544zm-8.456-2.078l-25.281-69.35c-11.405 22.278-2.729 56.268 25.281 69.35zm76.428-44.562c.802-10.534-2.832-25.119-5.97-27.125-.35 3.875-.106 8.186-1.218 12.114-2.617 9.255-5.817 18.349-8.899 27.468-3.35 9.912-6.832 19.779-10.257 29.666 16.092-9.539 24.935-23.618 26.344-42.123z"
/>
</svg>

View File

@@ -1,22 +0,0 @@
//@ts-nocheck
export { default as PlausibleAnalytics } from './PlausibleAnalytics.svelte';
export { default as NocoDb } from './NocoDB.svelte';
export { default as MinIo } from './MinIO.svelte';
export { default as VsCodeServer } from './VSCodeServer.svelte';
export { default as Wordpress } from './Wordpress.svelte';
export { default as VaultWarden } from './VaultWarden.svelte';
export { default as LanguageTool } from './LanguageTool.svelte';
export { default as N8n } from './N8n.svelte';
export { default as UptimeKuma } from './UptimeKuma.svelte';
export { default as Ghost } from './Ghost.svelte';
export { default as MeiliSearch } from './MeiliSearch.svelte';
export { default as Umami } from './Umami.svelte';
export { default as Hasura } from './Hasura.svelte';
export { default as Fider } from './Fider.svelte';
export { default as Appwrite } from './Appwrite.svelte';
export { default as Moodle } from './Moodle.svelte';
export { default as GlitchTip } from './GlitchTip.svelte';
export { default as Searxng } from './Searxng.svelte';
export { default as Weblate } from './Weblate.svelte';
export { default as Grafana } from './Grafana.svelte';
export { default as Trilium } from './Trilium.svelte'

View File

@@ -41,7 +41,7 @@
"saving": "Saving...",
"name": "Name",
"value": "Value",
"action": "Action",
"action": "Actions",
"is_required": "is required.",
"add": "Add",
"set": "Set",

View File

@@ -1,7 +1,11 @@
import { dev } from '$app/env';
import cuid from 'cuid';
import Cookies from 'js-cookie';
import { writable, readable, type Writable } from 'svelte/store';
import { io as ioClient } from 'socket.io-client';
const socket = ioClient(dev ? 'http://localhost:3001' : '/', { auth: { token: Cookies.get('token') }, autoConnect: false });
export const io = socket;
interface AppSession {
isRegistrationEnabled: boolean;
ipv4: string | null,
@@ -19,7 +23,6 @@ interface AppSession {
github: string | null,
gitlab: string | null,
},
supportedServiceTypesAndVersions: Array<any>
pendingInvitations: Array<any>
}
interface AddToast {
@@ -48,7 +51,6 @@ export const appSession: Writable<AppSession> = writable({
github: null,
gitlab: null
},
supportedServiceTypesAndVersions: [],
pendingInvitations: []
});
export const disabledButton: Writable<boolean> = writable(false);
@@ -56,6 +58,7 @@ export const isDeploymentEnabled: Writable<boolean> = writable(false);
export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) {
return (
isAdmin &&
(application.buildPack === 'compose') ||
(application.fqdn || application.settings.isBot) &&
application.gitSource &&
application.repository &&
@@ -74,16 +77,16 @@ export function checkIfDeploymentEnabledServices(isAdmin: boolean, service: any)
}
export const status: Writable<any> = writable({
application: {
isRunning: false,
isExited: false,
isRestarting: false,
statuses: [],
overallStatus: 'stopped',
loading: false,
initialLoading: true
},
service: {
isRunning: false,
isExited: false,
statuses: [],
overallStatus: 'stopped',
loading: false,
startup: {},
initialLoading: true
},
database: {
@@ -161,4 +164,11 @@ export const addToast = (toast: AddToast) => {
toasts.update((all: any) => [t, ...all])
}
export const selectedBuildId: any = writable(null)
export const selectedBuildId: any = writable(null)
type State = {
requests: Array<Request>;
};
export const state = writable<State>({
requests: [],
});

View File

@@ -29,7 +29,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: 80
};
}
if (pack === 'docker') {
if (pack === 'docker' || pack === 'compose') {
return {
...metaData,
installCommand: null,
@@ -39,6 +39,7 @@ export function findBuildPack(pack: string, packageManager = 'npm') {
port: null
};
}
if (pack === 'svelte') {
return {
...metaData,
@@ -235,6 +236,14 @@ export const buildPacks = [
color: 'bg-sky-700',
isCoolifyBuildPack: true,
},
{
name: 'compose',
type: 'base',
fancyName: 'Docker Compose',
hoverColor: 'hover:bg-sky-700',
color: 'bg-sky-700',
isCoolifyBuildPack: true,
},
{
name: 'svelte',
type: 'specific',
@@ -349,14 +358,14 @@ export const buildPacks = [
color: 'bg-green-700',
isCoolifyBuildPack: true,
},
{
name: 'heroku',
{
name: 'heroku',
type: 'base',
fancyName: 'Heroku',
hoverColor: 'hover:bg-purple-700',
color: 'bg-purple-700',
isHerokuBuildPack: true,
}
}
];
export const scanningTemplates = {
'@sveltejs/kit': {

View File

@@ -16,9 +16,10 @@
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div class="dropdown dropdown-bottom">
<slot>
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100">
<label for="new" tabindex="0" class="btn btn-sm text-sm bg-coollabs hover:bg-coollabs-100 w-64">
<svg
class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
@@ -35,9 +36,9 @@
</label>
</slot>
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-52">
<ul id="new" tabindex="0" class="dropdown-content menu p-2 shadow bg-coolgray-300 rounded w-64">
<li>
<button on:click={newApplication} class="no-underline hover:bg-applications rounded-none ">
<button on:click={newApplication} class="no-underline hover:bg-applications tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -58,7 +59,7 @@
>
</li>
<li>
<button on:click={newService} class="no-underline hover:bg-services rounded-none ">
<button on:click={newService} class="no-underline hover:bg-services tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -75,7 +76,7 @@
>
</li>
<li>
<button on:click={newDatabase} class="no-underline hover:bg-databases rounded-none ">
<button on:click={newDatabase} class="no-underline hover:bg-databases tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -94,7 +95,7 @@
>
</li>
<li>
<a href="/sources/new" class="no-underline hover:bg-sources rounded-none ">
<a href="/sources/new" class="no-underline hover:bg-sources tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
@@ -116,7 +117,7 @@
>
</li>
<li>
<a href="/destinations/new" class="no-underline hover:bg-destinations rounded-none ">
<a href="/destinations/new" class="no-underline hover:bg-destinations tracking-wide font-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"

View File

@@ -65,7 +65,6 @@
<script lang="ts">
export let baseSettings: any;
export let supportedServiceTypesAndVersions: any;
export let pendingInvitations: any = 0;
$appSession.isRegistrationEnabled = baseSettings.isRegistrationEnabled;
@@ -74,7 +73,6 @@
$appSession.version = baseSettings.version;
$appSession.whiteLabeled = baseSettings.whiteLabeled;
$appSession.whiteLabeledDetails.icon = baseSettings.whiteLabeledIcon;
$appSession.supportedServiceTypesAndVersions = supportedServiceTypesAndVersions;
$appSession.pendingInvitations = pendingInvitations;
@@ -83,6 +81,7 @@
export let permission: string;
export let isAdmin: boolean;
import { status, io } from '$lib/store';
import '../tailwind.css';
import Cookies from 'js-cookie';
import { fade } from 'svelte/transition';
@@ -95,6 +94,7 @@
import { appSession } from '$lib/store';
import Toasts from '$lib/components/Toasts.svelte';
import Tooltip from '$lib/components/Tooltip.svelte';
import { onMount } from 'svelte';
if (userId) $appSession.userId = userId;
if (teamId) $appSession.teamId = teamId;
@@ -109,6 +109,16 @@
return errorNotification(error);
}
}
onMount(async () => {
io.connect();
io.on('start-service', (message) => {
const { serviceId, state } = message;
$status.service.startup[serviceId] = state;
if (state === 0 || state === 1) {
delete $status.service.startup[serviceId];
}
});
});
</script>
<svelte:head>
@@ -259,6 +269,7 @@
</svg>
</a>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
id="logout"
class="icons bg-coolgray-200 hover:text-error cursor-pointer"
@@ -287,7 +298,7 @@
<a
class="text-[10px] no-underline"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
target="_blank noreferrer">v{$appSession.version}</a
>
</div>
</div>
@@ -295,7 +306,7 @@
</nav>
{#if $appSession.whiteLabeled}
<span class="fixed bottom-0 left-[50px] z-50 m-2 px-4 text-xs text-stone-700"
>Powered by <a href="https://coolify.io" target="_blank">Coolify</a></span
>Powered by <a href="https://coolify.io" target="_blank noreferrer">Coolify</a></span
>
{/if}
{/if}
@@ -436,6 +447,7 @@
<UpdateAvailable />
</div>
<li>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="no-underline icons hover:bg-error" on:click={logout}>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -460,7 +472,7 @@
<a
class="text-xs hover:bg-coolgray-200 no-underline hover:text-white text-right"
href={`https://github.com/coollabsio/coolify/releases/tag/v${$appSession.version}`}
target="_blank">v{$appSession.version}</a
target="_blank noreferrer">v{$appSession.version}</a
>
</li>
</ul>

View File

@@ -6,7 +6,7 @@
<ul class="menu border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4">
<li class="menu-title">
<span>Configuration</span>
<span>General</span>
</li>
{#if application.gitSource?.htmlUrl && application.repository && application.branch}
<li>
@@ -86,7 +86,7 @@
<path
d="M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5"
/>
</svg>Build & Deploy</a
</svg>Configuration</a
>
</li>
<li
@@ -160,12 +160,12 @@
<span>Logs</span>
</li>
<li
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus === 'stopped'}
class="rounded"
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/logs`}
>
<a
href={$status.application.isRunning ? `/applications/${$page.params.id}/logs` : ''}
href={$status.application.overallStatus !== 'stopped' ? `/applications/${$page.params.id}/logs` : ''}
class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
@@ -218,10 +218,10 @@
</li>
<li
class="rounded"
class:text-stone-600={!$status.application.isRunning}
class:text-stone-600={$status.application.overallStatus !== 'healthy'}
class:bg-coollabs={$page.url.pathname === `/applications/${$page.params.id}/usage`}
>
<a href={$status.application.isRunning ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
<a href={$status.application.overallStatus === 'healthy' ? `/applications/${$page.params.id}/usage` : ''} class="no-underline w-full"
><svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"

View File

@@ -39,11 +39,11 @@
async function addNewSecret() {
try {
if (!name) return errorNotification({ message: 'Name is required.' });
if (!value) return errorNotification({ message: 'Value is required.' });
if (!name.trim()) return errorNotification({ message: 'Name is required.' });
if (!value.trim()) return errorNotification({ message: 'Value is required.' });
await post(`/applications/${id}/secrets`, {
name,
value,
name: name.trim(),
value: value.trim(),
isBuildSecret
});
cleanupState();
@@ -64,8 +64,8 @@
if (isNewSecret) return;
try {
await put(`/applications/${id}/secrets`, {
name,
value,
name: name.trim(),
value: value.trim(),
isBuildSecret: changeIsBuildSecret ? isBuildSecret : undefined
});
addToast({
@@ -91,7 +91,7 @@
required
placeholder="EXAMPLE_VARIABLE"
readonly={!isNewSecret}
class=" w-full"
class="w-full"
class:bg-coolblack={!isNewSecret}
class:border={!isNewSecret}
class:border-dashed={!isNewSecret}
@@ -166,7 +166,7 @@
</div>
<div class="flex flex-row lg:flex-col lg:items-center items-start">
{#if (index === 0 && !isNewSecret) || length === 0}
<label for="name" class="pb-2 uppercase lg:block hidden">Actions</label>
<label for="name" class="pb-2 uppercase lg:block hidden">Action</label>
{/if}
<div class="flex justify-center h-full items-center pt-3">

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