Compare commits

...

222 Commits

Author SHA1 Message Date
Andras Bacsai
f82207564f Merge pull request #344 from coollabsio/v2.4.8
v2.4.8
2022-04-13 19:19:04 +02:00
Andras Bacsai
991a09838c chore: version++ 2022-04-13 16:08:40 +02:00
Andras Bacsai
25df4bfd85 fix: Remove system wide pw reset 2022-04-13 16:05:26 +02:00
Andras Bacsai
d2f89d001b fix: GitLab typo 2022-04-13 16:05:08 +02:00
Andras Bacsai
1971f227fd fix: Register should happen if coolify proxy cannot be started 2022-04-13 14:23:42 +02:00
Andras Bacsai
c1adffe260 Merge pull request #343 from coollabsio/v2.4.7
v2.4.7
2022-04-13 13:12:35 +02:00
Andras Bacsai
e725887a55 chore:version++ 2022-04-13 13:12:23 +02:00
Andras Bacsai
5bf79b75b0 fix: Destinations to HAProxy 2022-04-13 13:10:04 +02:00
Andras Bacsai
6926975e40 Merge pull request #341 from coollabsio/v2.4.6
v2.4.6
2022-04-13 08:40:10 +02:00
Andras Bacsai
978a01c968 fix: Reverting postgres password for now 2022-04-13 08:35:20 +02:00
Andras Bacsai
f421f5ee84 fix: No permission on first registration 2022-04-12 23:57:08 +02:00
Andras Bacsai
383831c7b8 fix: Restart policy for resources 2022-04-12 23:12:09 +02:00
Andras Bacsai
41329facf7 fix: Try catch me 2022-04-12 22:49:48 +02:00
Andras Bacsai
7d3c644148 fix: DNS check before creating SSL cert 2022-04-12 22:18:54 +02:00
Andras Bacsai
7fab9b5930 fix: ProjectID for Github 2022-04-12 22:18:43 +02:00
Andras Bacsai
58763ef84c fix: Load all branches, not just the first 30 2022-04-12 21:48:50 +02:00
Andras Bacsai
0e6abf172b fix: Meilisearch service 2022-04-12 21:09:38 +02:00
Andras Bacsai
9e681ece41 chore: version++ 2022-04-12 20:58:02 +02:00
Andras Bacsai
28f87a306d fix: Cleanup images older than a day 2022-04-12 20:57:49 +02:00
Andras Bacsai
23e8833208 Merge pull request #339 from coollabsio/v2.4.5
v2.4.5
2022-04-12 19:08:46 +02:00
Andras Bacsai
03962663c2 fix: Timeout values 2022-04-12 18:21:10 +02:00
Andras Bacsai
cc2ec55c4d chore: version++ 2022-04-12 16:50:13 +02:00
Andras Bacsai
ff2c38aa16 fix: Invitations 2022-04-12 16:49:59 +02:00
Andras Bacsai
b5a9a2cea8 fix: Types 2022-04-12 16:49:52 +02:00
Andras Bacsai
cd3f661f7e Merge pull request #336 from coollabsio/v2.4.4
v2.4.4
2022-04-12 11:02:35 +02:00
Andras Bacsai
41bf6b5b86 fixes 2022-04-12 10:47:53 +02:00
Andras Bacsai
a4e7c85184 Add only amd release 2022-04-12 10:14:18 +02:00
Andras Bacsai
19aca9ab35 chore: version++ 2022-04-12 10:13:19 +02:00
Andras Bacsai
08704c289a fix: Proxy 2022-04-12 10:12:46 +02:00
Andras Bacsai
2224c22c6e fix: haproxy build stuffs 2022-04-12 09:22:27 +02:00
Andras Bacsai
b281889acd Merge branch 'main' of github.com:coollabsio/coolify into main 2022-04-12 09:20:12 +02:00
Andras Bacsai
cfc50a27b0 Package.json update 2022-04-12 09:19:48 +02:00
Andras Bacsai
ed5f21da6a Merge pull request #335 from coollabsio/arm
v2.4.3 - ARM!
2022-04-12 09:10:57 +02:00
Andras Bacsai
78f3eb81dd Merge pull request #314 from Mobilpadde/fix-coloured-tooltips
Tooltip with corresponding colours
2022-04-12 07:57:09 +02:00
Andras Bacsai
6a833934ce Merge pull request #293 from dominicbachmann/improve-typing
Started to introduce more typing
2022-04-11 22:40:47 +02:00
Andras Bacsai
45bf6f77d1 Merge branch 'arm' into improve-typing 2022-04-11 22:39:45 +02:00
Andras Bacsai
a1b3b7b687 Merge branch 'arm' of github.com:coollabsio/coolify into arm 2022-04-11 22:31:32 +02:00
Andras Bacsai
7ebcad6abb fix: Update dockerfile 2022-04-11 22:31:27 +02:00
Andras Bacsai
fed6d2bf07 Merge pull request #301 from esdete2/main
Rearrange ARGs in Docker build pack
2022-04-11 22:31:16 +02:00
Andras Bacsai
bea4943e9f chore: update build packages 2022-04-11 20:43:19 +02:00
Andras Bacsai
1979e431b8 chore: update build scripts 2022-04-11 20:40:06 +02:00
Andras Bacsai
9bead1d6b4 chore: Version++ 2022-04-11 20:36:46 +02:00
Andras Bacsai
56c4295e16 chore: Update packages 2022-04-11 20:36:15 +02:00
Andras Bacsai
7c7b5a61e5 fix: Remove unnecessary save button haha 2022-04-11 20:36:03 +02:00
Andras Bacsai
abaa13fda8 Merge branch 'main' into arm 2022-04-11 20:29:29 +02:00
esdete
042bfeddbb Merge branch 'main' into main 2022-04-11 17:47:50 +02:00
Mads Bram Cordes
f45ab067ce Add fuchsia for IAM 2022-04-11 16:58:00 +02:00
Mads Bram Cordes
97a6f04aaa Merge branch 'main' into fix-coloured-tooltips 2022-04-11 16:55:37 +02:00
Andras Bacsai
417c01d6e0 Merge pull request #331 from coollabsio/v2.4.2
v2.4.2
2022-04-10 00:44:22 +02:00
Andras Bacsai
b2e7435d0f chore: version++ 2022-04-10 00:40:12 +02:00
Andras Bacsai
73c9cb1d51 Revert source configuration changes 2022-04-10 00:39:50 +02:00
Andras Bacsai
41c5dd3b53 fix: Show config missing on sources 2022-04-10 00:36:42 +02:00
Andras Bacsai
bb0c93dc2f fix: Return own and other sources better 2022-04-10 00:31:10 +02:00
Andras Bacsai
7953c1df30 fix: Missing install repositories GitHub 2022-04-10 00:30:47 +02:00
esdete
c3f4245164 Merge branch 'main' into main 2022-04-09 15:44:13 +02:00
Andras Bacsai
369001febb Merge pull request #326 from coollabsio/v2.4.1
v2.4.1
2022-04-09 14:17:48 +02:00
Andras Bacsai
7ec296be6b fix: DB Connecting string generator 2022-04-09 13:58:13 +02:00
Andras Bacsai
d2f5a58f3b fix: Able to change postgres user password from ui 2022-04-09 13:33:23 +02:00
Andras Bacsai
f4315144af chore: version++ 2022-04-09 13:26:08 +02:00
Andras Bacsai
e92775887d fix: Postgres root passwor shown and set 2022-04-09 13:26:00 +02:00
Andras Bacsai
a5f1b4b675 fix: Enable https for Ghost 2022-04-09 13:25:46 +02:00
esdete
157e5fd7aa Merge branch 'main' into main 2022-04-08 20:07:43 +02:00
Andras Bacsai
5e7e1c11c7 Merge pull request #307 from coollabsio/v2.4.0
v2.4.0
2022-04-08 15:20:34 +02:00
Andras Bacsai
e8516bc831 ui: fixes 2022-04-08 15:12:10 +02:00
Andras Bacsai
e3f78a1cf9 ui: fixes 2022-04-08 15:02:48 +02:00
Andras Bacsai
3449e0f8fc ui: fix 2022-04-08 14:16:14 +02:00
Andras Bacsai
66af12f9b5 ui: fixes 2022-04-08 14:12:06 +02:00
Andras Bacsai
13acf09dcc ui: fixes 2022-04-08 14:03:21 +02:00
Andras Bacsai
ce71dccbc1 fix: Missing buildpack 2022-04-08 11:19:01 +02:00
Andras Bacsai
d9ba1a0b5c fix: typo 2022-04-08 11:16:07 +02:00
Andras Bacsai
0b709c93a8 fix: html/apiUrls cannot end with / 2022-04-08 10:57:44 +02:00
Andras Bacsai
1657e5a151 fix: no line during buildLog 2022-04-08 10:54:40 +02:00
Andras Bacsai
a165b21950 ui: fix 2022-04-08 10:47:01 +02:00
Andras Bacsai
0d0715a340 ui/fix: Insane amount 2022-04-08 10:35:16 +02:00
Andras Bacsai
76754ded79 docs: Contribution guide 2022-04-08 00:28:10 +02:00
Andras Bacsai
4da27a46a2 fix: self-hosted GitLab URL 2022-04-08 00:16:10 +02:00
Mads Bram Cordes
039953588e Add tooltip colours to correspond with colour of Icon 2022-04-08 00:11:30 +02:00
Andras Bacsai
b8b4f559db fix: Unique storage paths 2022-04-08 00:09:09 +02:00
Andras Bacsai
2b0df270df fix: small typo 2022-04-07 23:58:56 +02:00
Andras Bacsai
b96c1a23ec fix: Ton of updates for users/teams 2022-04-07 23:26:06 +02:00
Andras Bacsai
f779b3bb54 fix: fix for the fix that fixes the fix 2022-04-07 21:29:45 +02:00
Andras Bacsai
6462982d12 fix: www or not-www, that's the question 2022-04-07 21:24:05 +02:00
Andras Bacsai
84b4cc5d54 fix: Last commit 2022-04-07 21:18:36 +02:00
Andras Bacsai
1bd2ccbc16 fix: Possible fix for spikes in CPU usage 2022-04-07 21:12:41 +02:00
Andras Bacsai
3abe1610bf fix: Do not trigger >1 webhooks on GitLab 2022-04-07 20:37:08 +02:00
Andras Bacsai
61716738ed ui: fix 2022-04-07 19:17:03 +02:00
Andras Bacsai
4e819f6eba ui: fix 2022-04-07 19:11:58 +02:00
Andras Bacsai
fedb38f2bc ui: fix 2022-04-07 18:51:52 +02:00
Andras Bacsai
aae108032c ui:fix 2022-04-07 18:45:21 +02:00
Andras Bacsai
020013683b ui: fix 2022-04-07 16:02:02 +02:00
Andras Bacsai
70de2538e2 ui: fix 2022-04-07 16:01:14 +02:00
Andras Bacsai
9f581c82a9 ui: fix 2022-04-07 15:59:01 +02:00
Andras Bacsai
eb2e07afc5 ui: fixes 2022-04-07 15:50:57 +02:00
Andras Bacsai
9c47b8495c ui: fix 2022-04-07 15:40:32 +02:00
Andras Bacsai
2f8d0ee60c ui: Better layout for root team 2022-04-07 15:23:32 +02:00
Andras Bacsai
5bf14f4639 feat: Able to modify database passwords 2022-04-07 14:29:40 +02:00
dominicbachmann
9da08d600b Merged v2.4.0 2022-04-07 01:03:13 +02:00
Andras Bacsai
4d47eab07c fix: Team view for root team 2022-04-06 22:34:56 +02:00
Andras Bacsai
f2061c5c25 fix: Only show proxy for admin team 2022-04-06 22:28:03 +02:00
Andras Bacsai
430fc66ed7 fix: Updated db versions 2022-04-06 22:15:13 +02:00
Andras Bacsai
bcb84b8126 Revert "fix: Do not pull latest image"
This reverts commit dd83e86bc3.
2022-04-06 22:10:48 +02:00
Andras Bacsai
dd83e86bc3 fix: Do not pull latest image 2022-04-06 22:06:14 +02:00
Andras Bacsai
3e8a8364dc feat: Basic white labeled version 2022-04-06 22:01:41 +02:00
dominicbachmann
be41c0dd02 Added types for store 2022-04-06 21:51:19 +02:00
dominicbachmann
a17b7a564e Added types for form 2022-04-06 21:49:43 +02:00
Andras Bacsai
f3cdda29bc code improvement 2022-04-06 21:37:42 +02:00
dominicbachmann
de37ee9f1c Added types for crypto 2022-04-06 21:10:37 +02:00
dominicbachmann
8212868b92 Added types for api 2022-04-06 21:09:15 +02:00
dominicbachmann
b44d8578d9 Added types for queues/sslrenewal 2022-04-06 21:05:36 +02:00
dominicbachmann
0358cf2de2 Added types for queues/ssl 2022-04-06 21:05:12 +02:00
dominicbachmann
94da008a47 Added types for queues/proxy 2022-04-06 21:04:51 +02:00
dominicbachmann
456b1b8074 Added types for queues/logger 2022-04-06 21:04:14 +02:00
dominicbachmann
78e6a7d1d3 Improved code quality of queues/index 2022-04-06 21:03:20 +02:00
dominicbachmann
76dc7ffb68 Added types for queues/cleanup 2022-04-06 21:01:47 +02:00
dominicbachmann
211aff7170 Added types for letsencrypt/index 2022-04-06 20:52:46 +02:00
dominicbachmann
bcacefb841 Added types for importers/gitlab 2022-04-06 20:50:57 +02:00
dominicbachmann
4505ad37d8 Added types for importers/github 2022-04-06 20:50:04 +02:00
dominicbachmann
18cf57f33c Added types for haproxy/index 2022-04-06 20:47:22 +02:00
Andras Bacsai
9f2f5b40c3 Only show teams for root team 2022-04-06 20:44:24 +02:00
dominicbachmann
8a401f50cb Added types for haproxy/configuration 2022-04-06 20:40:25 +02:00
dominicbachmann
51a5b3b602 Added types to database/users 2022-04-06 20:36:51 +02:00
dominicbachmann
68f9bca054 Added types to database/teams 2022-04-06 20:34:22 +02:00
dominicbachmann
e9e92c6e9e Added types to databse/settings 2022-04-06 20:31:51 +02:00
dominicbachmann
008cfdba09 Added types to database/services 2022-04-06 20:30:29 +02:00
dominicbachmann
9973197fa5 Added types for database/secrets 2022-04-06 20:23:27 +02:00
dominicbachmann
ec3b94cf96 added types for database/logs 2022-04-06 20:16:21 +02:00
dominicbachmann
c4cb92c78d Added types for database/gitSource 2022-04-06 20:15:15 +02:00
dominicbachmann
c390f82246 Added types to database/gitlab 2022-04-06 20:01:35 +02:00
dominicbachmann
b4f98e24a1 Added types to database/github 2022-04-06 19:56:47 +02:00
dominicbachmann
e042c5cfde Added types for database/databases 2022-04-06 19:45:47 +02:00
dominicbachmann
faeae8fd6c Added typings for database/destinations 2022-04-06 19:34:17 +02:00
Andras Bacsai
ae4942ba29 feat: Able to change service version/tag 2022-04-06 19:17:28 +02:00
Philip Schmidt
fd652bfce6 write args at the beginning of dockerfile and inherit them for each stage 2022-04-06 18:33:02 +02:00
Andras Bacsai
3d72167721 Merge branch 'v2.4.0' of github.com:coollabsio/coolify into v2.4.0 2022-04-06 17:21:32 +02:00
Andras Bacsai
ba284bef9e Merge pull request #292 from Soneji/patch-1
Add plausible/analytics:stable docker tag
2022-04-06 17:20:50 +02:00
Andras Bacsai
d18bb9cc74 Extend typings 2022-04-06 17:18:25 +02:00
Andras Bacsai
a7ed3e58db Merge pull request #287 from dominicbachmann/dockerfile-typing
Implemented typing for dockerfile configuration
2022-04-06 17:12:17 +02:00
Andras Bacsai
8405ebd28d feat: Admin team sees everything 2022-04-06 15:55:17 +02:00
Andras Bacsai
352bb65125 ui: fix 2022-04-06 14:57:41 +02:00
Andras Bacsai
fe2cc5a99a feat: Working on-demand sftp to wp data 2022-04-06 14:27:51 +02:00
Andras Bacsai
7a2f29f6a3 feat: PHP Composer support 2022-04-06 13:35:53 +02:00
Andras Bacsai
9a05bfa899 fix: Fix for fix haha 2022-04-06 10:30:53 +02:00
Andras Bacsai
39fa64e20d fix: On-demand sFTP for wp 2022-04-06 10:29:42 +02:00
Andras Bacsai
3a835b420e WIP 2022-04-05 23:44:18 +02:00
dominicbachmann
82f7633c3a Improved typing and quality of database/checks and database/common code 2022-04-05 21:15:02 +02:00
dominicbachmann
9fdac2741a Improved typing and quality of applications.ts 2022-04-05 20:48:33 +02:00
dominicbachmann
8fb5260809 Resolved merge conflicts 2022-04-05 20:17:53 +02:00
dominicbachmann
e08ec12d26 Introduced typing for the buildJob and cleaned up common.ts 2022-04-05 20:11:19 +02:00
Dominic Bachmann
1202e00a21 Merge branch 'main' into dockerfile-typing 2022-04-05 19:36:52 +02:00
Andras Bacsai
4ba2205af4 fix: Permission issues 2022-04-05 17:37:03 +02:00
Andras Bacsai
09841ad4cb fix: Add openssl to image 2022-04-05 17:21:40 +02:00
Andras Bacsai
d2dcd0abc8 chore:version++ 2022-04-05 17:15:21 +02:00
Andras Bacsai
fe9d0503fb feat: Finalize on-demand sftp for wp 2022-04-05 17:15:06 +02:00
Andras Bacsai
8e9e6607e5 feat: Wordpress on-demand SFTP 2022-04-05 15:56:25 +02:00
Dhaval Soneji
e1efd9355f Add plausible/analytics:stable docker tag 2022-04-05 14:03:36 +01:00
Andras Bacsai
ca705bbf89 Merge pull request #290 from coollabsio/v2.3.3
v2.3.3
2022-04-05 11:19:07 +02:00
Andras Bacsai
b70fe09d17 fix: Remove asyncUntil 2022-04-05 11:13:13 +02:00
Andras Bacsai
d7d570393f Revert try 2022-04-05 11:09:17 +02:00
Andras Bacsai
41ca265e5a Try to not restart redis? 2022-04-05 11:00:36 +02:00
Andras Bacsai
03cde08d67 fix: Lame fixing 2022-04-05 10:58:47 +02:00
Andras Bacsai
5684674bd7 fix: revert seed 2022-04-05 10:36:28 +02:00
Andras Bacsai
4fe919f2ea Merge branch 'v2.3.3' of github.com:coollabsio/coolify into v2.3.3 2022-04-05 10:36:12 +02:00
Andras Bacsai
c8c23c53ef fix: Update stucked builds 2022-04-05 10:36:09 +02:00
Andras Bacsai
b1c25e98d7 fix: Update stucked builds on startup 2022-04-05 10:34:31 +02:00
Andras Bacsai
7ab5a4bfcf fix: Try to update build status several times 2022-04-05 10:30:18 +02:00
Andras Bacsai
a3ee57995c chore: Version++ 2022-04-05 10:21:46 +02:00
Andras Bacsai
32020fd336 fix: Add git lfs while deploying 2022-04-05 10:21:40 +02:00
dominicbachmann
f1313b6468 Implemented typing for dockerfile configuration 2022-04-05 01:13:25 +02:00
Andras Bacsai
3ef093c7e6 Merge pull request #286 from coollabsio/v2.3.2
v2.3.2
2022-04-04 19:40:39 +02:00
Andras Bacsai
f5dfaa81d3 chore:version++ 2022-04-04 19:35:17 +02:00
Andras Bacsai
fcf206a081 fix: Add default webhook domain for n8n 2022-04-04 19:18:06 +02:00
Andras Bacsai
9790d2b613 fix(php): If .htaccess file found use apache 2022-04-04 18:47:22 +02:00
Andras Bacsai
201fa82efc Merge pull request #284 from coollabsio/v2.3.1
v2.3.1
2022-04-04 14:00:28 +02:00
Andras Bacsai
d28433ee64 fix: Default configuration 2022-04-04 13:55:11 +02:00
Andras Bacsai
cc348bf0f5 chore: Version++ 2022-04-04 13:49:37 +02:00
Andras Bacsai
b023d65fcf fix: Secrets build/runtime coudl be changed after save 2022-04-04 13:49:26 +02:00
Andras Bacsai
305fab488e Merge pull request #276 from coollabsio/v2.3.0
v2.3.0
2022-04-04 11:30:19 +02:00
Andras Bacsai
38f0546f05 fix: Gitlab & Github urls 2022-04-04 11:27:23 +02:00
Andras Bacsai
8cb679711d fix: UI 2022-04-04 11:08:32 +02:00
Andras Bacsai
4d11867500 Merge branch 'main' of github.com:coollabsio/coolify into v2.3.0 2022-04-04 10:41:50 +02:00
Andras Bacsai
8232a7468b fix: Build log 2022-04-03 23:34:17 +02:00
Andras Bacsai
03e7af12be fix: Remove console.log 2022-04-03 23:03:41 +02:00
Andras Bacsai
39f2e28a11 fix: Async progress 2022-04-03 22:53:50 +02:00
Andras Bacsai
53947d805b fix: Lets await! 2022-04-03 22:53:11 +02:00
Andras Bacsai
15f8e44237 Merge pull request #281 from dominicbachmann/fix-husky-pre-commit
Changed the package manager used in the .husky/pre-commit hook
2022-04-03 22:32:36 +02:00
Andras Bacsai
5ce1bc1ec5 Merge pull request #282 from dominicbachmann/fix-contributing-guide
Fixed contributing guide
2022-04-03 22:32:20 +02:00
Andras Bacsai
c36bd34a1a fix: Small UI fix on logs 2022-04-03 22:31:07 +02:00
Andras Bacsai
781d034484 fix: Build log fix attempt #1 2022-04-03 22:18:04 +02:00
Andras Bacsai
5160d0780e fix: No need to paste clear text env for previews 2022-04-03 21:38:58 +02:00
dominicbachmann
b8b57bc48b Fixed contributing guide 2022-04-03 19:15:26 +02:00
dominicbachmann
58406f055e Changed the package manager used in the .husky/pre-commit hook from yarn to pnpm 2022-04-03 19:10:29 +02:00
Andras Bacsai
b049297082 fix: Infinite loop on www domains 2022-04-03 17:49:41 +02:00
Andras Bacsai
45af5cbef8 fix: Rename envs to secrets 2022-04-03 00:03:51 +02:00
Andras Bacsai
463dacbe59 chore: Lock file + fix packages 2022-04-02 23:58:59 +02:00
Andras Bacsai
01e0fb70c9 Merge branch 'v2.3.0' of github.com:coollabsio/coolify into v2.3.0 2022-04-02 23:53:41 +02:00
Andras Bacsai
6ac54e17f4 Merge pull request #277 from SaraVieira/allow-paste-env
feat: Add ability to paste env files
2022-04-02 23:52:53 +02:00
Andras Bacsai
a82805846f fix: If user not found 2022-04-02 23:48:56 +02:00
Sara Vieira
6309074844 clean when done 2022-04-02 23:30:16 +02:00
Sara Vieira
b80e0d15fb feat: Add abilitry to paste env files 2022-04-02 23:25:55 +02:00
Andras Bacsai
c55505af6c feat: MeiliSearch service 2022-04-02 23:08:27 +02:00
Andras Bacsai
5f27fc0770 Merge pull request #275 from SaraVieira/274-fix
Get error correctly in catch
2022-04-02 22:35:13 +02:00
Sara Vieira
b814c6e563 get error correctly; closes 274 2022-04-02 22:21:22 +02:00
Andras Bacsai
7a74ba1796 ui: Improvements 2022-04-02 17:43:24 +02:00
Andras Bacsai
066f5b25e0 fix: Python no wsgi 2022-04-02 17:05:32 +02:00
Andras Bacsai
18f7ab1b95 Merge pull request #268 from restray/main
feat(dev): allow windows users to use pnpm dev
2022-04-02 16:28:44 +02:00
Andras Bacsai
78293340cc chore: version++ 2022-04-02 16:27:44 +02:00
Andras Bacsai
c47457a17f Merge pull request #269 from restray/restray_registre_loading
feat(auth): add loading on register button
2022-04-02 16:26:57 +02:00
Andras Bacsai
d00629b627 Merge pull request #273 from Just-Moh-it/patch-1
Suggested change
2022-04-02 16:26:02 +02:00
Andras Bacsai
ddfbda6f80 feat: initial python support 2022-04-02 16:22:51 +02:00
Andras Bacsai
b60b832426 chore: version++ 2022-04-02 15:53:25 +02:00
Mohit Yadav
adfc976f41 Suggested change
Changed to appropriate word
2022-04-02 17:28:42 +05:30
Andras Bacsai
1b43976ff0 Update proxy build commands 2022-04-02 13:39:24 +02:00
Andras Bacsai
321fb019eb Update dockerfiles for arm 2022-04-01 23:02:23 +02:00
Andras Bacsai
f6858a68e0 Update schema 2022-04-01 22:51:08 +02:00
Restray
809f40dec9 feat: add loading on register button 2022-04-01 21:41:22 +02:00
Restray
f3b5de4697 feat(dev): allow windows users to use pnpm dev 2022-04-01 21:10:24 +02:00
Andras Bacsai
fe17e2eaba Prisma Engine build script 2022-04-01 17:57:37 +02:00
Andras Bacsai
22ef0b5d29 Update packages 2022-04-01 17:46:08 +02:00
Andras Bacsai
823279fb60 Updates 2022-04-01 17:16:11 +02:00
Andras Bacsai
19f661706d fix: Ignore coolify proxy error for now 2022-04-01 15:34:29 +02:00
Andras Bacsai
f56361c0ca updates for ARM 2022-04-01 14:25:55 +02:00
Andras Bacsai
4946ca2d91 Dockerfile for multiarch builds 2022-04-01 00:08:29 +02:00
170 changed files with 5298 additions and 2709 deletions

View File

@@ -2,4 +2,5 @@ COOLIFY_APP_ID=
COOLIFY_SECRET_KEY=12341234123412341234123412341234 COOLIFY_SECRET_KEY=12341234123412341234123412341234
COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_DATABASE_URL=file:../db/dev.db
COOLIFY_SENTRY_DSN= COOLIFY_SENTRY_DSN=
COOLIFY_IS_ON="docker" COOLIFY_IS_ON="docker"
COOLIFY_WHITE_LABELED="false"

View File

@@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.sh" . "$(dirname "$0")/_/husky.sh"
yarn lint-staged pnpm lint-staged

View File

@@ -16,7 +16,7 @@ First of all, thank you for considering to contribute to my project! It means a
- Push to your fork repo - Push to your fork repo
- Create a pull request: https://github.com/coollabsio/compare - Create a pull request: https://github.com/coollabsio/compare
- Write a proper description - Write a proper description
- Click "Change to draft" - Open the pull request to review
# How to start after you set up your local fork? # How to start after you set up your local fork?
@@ -29,7 +29,8 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal
- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. - Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool.
- Install dependencies with `pnpm install`. - Install dependencies with `pnpm install`.
- Need to create a local SQlite database with `pnpm db:push`. - Need to create a local SQlite database with `pnpm db:push`.
- This will apply all migrations and seed the database at `db/dev.db`. - This will apply all migrations at `db/dev.db`.
- Seed the database with base entities with `pnpm db:seed`
- You can start coding after starting `pnpm dev`. - You can start coding after starting `pnpm dev`.
## Database migrations ## Database migrations

View File

@@ -1,31 +1,42 @@
FROM node:16.14.0-alpine FROM node:16.14.2-alpine as install
RUN apk add --no-cache g++ cmake make python3
WORKDIR /app
COPY package*.json .
RUN yarn install
COPY . .
RUN yarn build
FROM node:16.14.0-alpine
WORKDIR /app WORKDIR /app
LABEL coolify.managed true RUN apk add --no-cache curl
RUN apk add --no-cache git openssh-client curl jq cmake sqlite
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6 RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm RUN pnpm add -g pnpm
RUN curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-20.10.9.tgz" | tar -xzvf - docker/docker -C . --strip-components 1 && mv docker /usr/bin/docker COPY package*.json .
RUN mkdir -p ~/.docker/cli-plugins/ RUN pnpm install
RUN curl -SL https://github.com/docker/compose/releases/download/v2.2.2/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose FROM node:16.14.2-alpine
ARG TARGETPLATFORM
WORKDIR /app
ENV PRISMA_QUERY_ENGINE_BINARY=/app/prisma-engines/query-engine \
PRISMA_MIGRATION_ENGINE_BINARY=/app/prisma-engines/migration-engine \
PRISMA_INTROSPECTION_ENGINE_BINARY=/app/prisma-engines/introspection-engine \
PRISMA_FMT_BINARY=/app/prisma-engines/prisma-fmt \
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
PRISMA_CLIENT_ENGINE_TYPE=binary
COPY --from=coollabsio/prisma-engine:latest /prisma-engines/query-engine /prisma-engines/migration-engine /prisma-engines/introspection-engine /prisma-engines/prisma-fmt /app/prisma-engines/
COPY --from=install /app/node_modules ./node_modules
COPY . .
RUN apk add --no-cache git git-lfs openssh-client curl jq cmake sqlite openssl
RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@6
RUN pnpm add -g pnpm
RUN mkdir -p ~/.docker/cli-plugins/
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-20.10.9 -o /usr/bin/docker
RUN curl -SL https://cdn.coollabs.io/bin/$TARGETPLATFORM/docker-compose-linux-2.3.4 -o ~/.docker/cli-plugins/docker-compose
RUN chmod +x ~/.docker/cli-plugins/docker-compose /usr/bin/docker
RUN pnpm prisma generate
RUN pnpm build
COPY --from=0 /app/docker-compose.yaml .
COPY --from=0 /app/build .
COPY --from=0 /app/package.json .
COPY --from=0 /app/node_modules ./node_modules
COPY --from=0 /app/prisma ./prisma
EXPOSE 3000 EXPOSE 3000
CMD ["pnpm", "start"] CMD ["pnpm", "start"]

View File

@@ -6,7 +6,7 @@ An open-source & self-hostable Heroku / Netlify alternative.
https://demo.coolify.io/ https://demo.coolify.io/
(If it is unresponsible, that means someone overloaded the server. 🙃) (If it is unresponsive, that means someone overloaded the server. 🙃)
## How to install ## How to install

View File

@@ -0,0 +1 @@
nohup docker build -t coollabsio/prisma-engine:<arm64/amd64> --push . &

View File

@@ -4,10 +4,10 @@ global
defaults defaults
mode http mode http
log global log global
timeout http-request 60s timeout http-request 120s
timeout connect 10s timeout connect 20s
timeout client 60s timeout client 120s
timeout server 60s timeout server 120s
frontend "${APP}" frontend "${APP}"
mode http mode http

View File

@@ -5,10 +5,10 @@ global
defaults defaults
mode http mode http
log global log global
timeout http-request 60s timeout http-request 120s
timeout connect 10s timeout connect 20s
timeout client 60s timeout client 120s
timeout server 60s timeout server 120s
userlist haproxy-dataplaneapi userlist haproxy-dataplaneapi
user admin insecure-password "${HAPROXY_PASSWORD}" user admin insecure-password "${HAPROXY_PASSWORD}"

View File

@@ -0,0 +1,10 @@
FROM rust:1.58.1-alpine3.14 as prisma
WORKDIR /prisma
ENV RUSTFLAGS="-C target-feature=-crt-static"
RUN apk --no-cache add openssl direnv git musl-dev openssl-dev build-base perl protoc
RUN git clone --depth=1 --branch=3.11.x https://github.com/prisma/prisma-engines.git /prisma
RUN cargo build --release
FROM alpine
WORKDIR /prisma-engines
COPY --from=prisma /prisma/target/release/query-engine /prisma/target/release/migration-engine /prisma/target/release/introspection-engine /prisma/target/release/prisma-fmt /prisma-engines/

View File

@@ -1,14 +1,14 @@
{ {
"name": "coolify", "name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.", "description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.2.7", "version": "2.4.8",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
"dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev", "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev",
"dev:stop": "docker-compose -f docker-compose-dev.yaml down", "dev:stop": "docker-compose -f docker-compose-dev.yaml down",
"dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10", "dev:logs": "docker-compose -f docker-compose-dev.yaml logs -f --tail 10",
"studio": "npx prisma studio", "studio": "npx prisma studio",
"start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node index.js", "start": "npx prisma migrate deploy && npx prisma generate && npx prisma db seed && node build/index.js",
"build": "svelte-kit build", "build": "svelte-kit build",
"preview": "svelte-kit preview", "preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-check --tsconfig ./tsconfig.json",
@@ -17,26 +17,27 @@
"db:push": "prisma db push && prisma generate", "db:push": "prisma db push && prisma generate",
"db:seed": "prisma db seed", "db:seed": "prisma db seed",
"db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name",
"release:staging": "cross-var docker build -t coollabsio/coolify:$npm_package_version . && docker push coollabsio/coolify:$npm_package_version", "release:production:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:pre": "cross-var docker build -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest .", "release:production:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version -t coollabsio/coolify:latest --push .",
"release:coolify": "cross-var yarn release:pre && docker push coollabsio/coolify:$npm_package_version && docker push coollabsio/coolify:latest", "release:staging:all": "cross-var docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify:$npm_package_version --push .",
"release:haproxy": "docker build -f haproxy.Dockerfile -t coollabsio/coolify-haproxy-alpine:1.0.0 -t coollabsio/coolify-haproxy-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-alpine", "release:staging:amd": "cross-var docker build --platform linux/amd64 -t coollabsio/coolify:$npm_package_version --push .",
"release:haproxy:tcp": "docker build -f haproxy-tcp.Dockerfile -t coollabsio/coolify-haproxy-tcp-alpine:1.0.0 -t coollabsio/coolify-haproxy-tcp-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-tcp-alpine", "release:haproxy": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-alpine:latest -t coollabsio/coolify-haproxy-alpine:1.1.0 -f data/haproxy.Dockerfile --push .",
"release:haproxy:http": "docker build -f haproxy-http.Dockerfile -t coollabsio/coolify-haproxy-http-alpine:1.0.0 -t coollabsio/coolify-haproxy-http-alpine:latest . && docker image push --all-tags coollabsio/coolify-haproxy-http-alpine", "release:haproxy:tcp": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-tcp-alpine:latest -t coollabsio/coolify-haproxy-tcp-alpine:1.1.0 -f data/haproxy-tcp.Dockerfile --push .",
"release:haproxy:http": "docker build --platform linux/amd64,linux/arm64 -t coollabsio/coolify-haproxy-http-alpine:latest -t coollabsio/coolify-haproxy-http-alpine:1.1.0 -f data/haproxy-http.Dockerfile --push .",
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-node": "1.0.0-next.73", "@sveltejs/adapter-node": "1.0.0-next.73",
"@sveltejs/kit": "1.0.0-next.303", "@sveltejs/kit": "1.0.0-next.310",
"@types/bcrypt": "5.0.0",
"@types/js-cookie": "3.0.1", "@types/js-cookie": "3.0.1",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "4.0.5",
"@types/node": "17.0.23", "@types/node": "17.0.23",
"@types/node-forge": "1.0.1", "@types/node-forge": "1.0.1",
"@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/eslint-plugin": "4.31.1",
"@typescript-eslint/parser": "4.31.1", "@typescript-eslint/parser": "4.31.1",
"@zerodevx/svelte-toast": "0.7.1", "@zerodevx/svelte-toast": "0.7.1",
"autoprefixer": "10.4.4", "autoprefixer": "10.4.4",
"cross-env": "7.0.3",
"cross-var": "1.1.0", "cross-var": "1.1.0",
"eslint": "7.32.0", "eslint": "7.32.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.5.0",
@@ -44,14 +45,14 @@
"husky": "7.0.4", "husky": "7.0.4",
"lint-staged": "12.3.7", "lint-staged": "12.3.7",
"postcss": "8.4.12", "postcss": "8.4.12",
"prettier": "2.6.1", "prettier": "2.6.2",
"prettier-plugin-svelte": "2.6.0", "prettier-plugin-svelte": "2.7.0",
"prettier-plugin-tailwindcss": "0.1.8", "prettier-plugin-tailwindcss": "0.1.8",
"prisma": "3.11.1", "prisma": "3.11.1",
"svelte": "3.46.4", "svelte": "3.47.0",
"svelte-check": "2.4.6", "svelte-check": "2.6.0",
"svelte-preprocess": "4.10.4", "svelte-preprocess": "4.10.5",
"svelte-select": "^4.4.7", "svelte-select": "4.4.7",
"tailwindcss": "3.0.23", "tailwindcss": "3.0.23",
"ts-node": "10.7.0", "ts-node": "10.7.0",
"tslib": "2.3.1", "tslib": "2.3.1",
@@ -61,26 +62,26 @@
"dependencies": { "dependencies": {
"@iarna/toml": "2.2.5", "@iarna/toml": "2.2.5",
"@prisma/client": "3.11.1", "@prisma/client": "3.11.1",
"@sentry/node": "6.19.2", "@sentry/node": "6.19.6",
"bcrypt": "5.0.1", "bcryptjs": "^2.4.3",
"bullmq": "1.78.1", "bullmq": "1.80.0",
"compare-versions": "4.1.3", "compare-versions": "4.1.3",
"cookie": "0.4.2", "cookie": "0.4.2",
"cooltipz-css": "^2.1.0",
"cuid": "2.1.8", "cuid": "2.1.8",
"dayjs": "1.11.0", "dayjs": "1.11.0",
"dockerode": "3.3.1", "dockerode": "3.3.1",
"dotenv-extended": "2.9.0", "dotenv-extended": "2.9.0",
"generate-password": "1.7.0", "generate-password": "1.7.0",
"get-port": "6.1.2", "get-port": "6.1.2",
"got": "12.0.2", "got": "12.0.3",
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
"mustache": "^4.2.0", "mustache": "4.2.0",
"node-forge": "1.3.0", "node-forge": "1.3.1",
"p-limit": "4.0.0",
"svelte-kit-cookie-session": "2.1.2", "svelte-kit-cookie-session": "2.1.2",
"tailwindcss-scrollbar": "^0.1.0", "tailwindcss-scrollbar": "0.1.0",
"unique-names-generator": "4.7.1" "unique-names-generator": "4.7.1"
}, },
"prisma": { "prisma": {

650
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "Application" ADD COLUMN "pythonModule" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonVariable" TEXT;
ALTER TABLE "Application" ADD COLUMN "pythonWSGI" TEXT;

View File

@@ -0,0 +1,12 @@
-- CreateTable
CREATE TABLE "MeiliSearch" (
"id" TEXT NOT NULL PRIMARY KEY,
"masterKey" TEXT NOT NULL,
"serviceId" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "MeiliSearch_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "MeiliSearch_serviceId_key" ON "MeiliSearch"("serviceId");

View File

@@ -0,0 +1,29 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Wordpress" (
"id" TEXT NOT NULL PRIMARY KEY,
"extraConfig" TEXT,
"tablePrefix" TEXT,
"mysqlUser" TEXT NOT NULL,
"mysqlPassword" TEXT NOT NULL,
"mysqlRootUser" TEXT NOT NULL,
"mysqlRootUserPassword" TEXT NOT NULL,
"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", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "serviceId", "tablePrefix", "updatedAt") SELECT "createdAt", "extraConfig", "id", "mysqlDatabase", "mysqlPassword", "mysqlPublicPort", "mysqlRootUser", "mysqlRootUserPassword", "mysqlUser", "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,5 @@
-- DropIndex
DROP INDEX "ApplicationPersistentStorage_path_key";
-- DropIndex
DROP INDEX "ApplicationPersistentStorage_applicationId_key";

View File

@@ -1,5 +1,6 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
binaryTargets = ["linux-musl"]
} }
datasource db { datasource db {
@@ -87,6 +88,9 @@ model Application {
baseDirectory String? baseDirectory String?
publishDirectory String? publishDirectory String?
phpModules String? phpModules String?
pythonWSGI String?
pythonModule String?
pythonVariable String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
settings ApplicationSettings? settings ApplicationSettings?
@@ -114,8 +118,8 @@ model ApplicationSettings {
model ApplicationPersistentStorage { model ApplicationPersistentStorage {
id String @id @default(cuid()) id String @id @default(cuid())
application Application @relation(fields: [applicationId], references: [id]) application Application @relation(fields: [applicationId], references: [id])
applicationId String @unique applicationId String
path String @unique path String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -280,6 +284,7 @@ model Service {
wordpress Wordpress? wordpress Wordpress?
ghost Ghost? ghost Ghost?
serviceSecret ServiceSecret[] serviceSecret ServiceSecret[]
meiliSearch MeiliSearch?
} }
model PlausibleAnalytics { model PlausibleAnalytics {
@@ -328,6 +333,12 @@ model Wordpress {
mysqlRootUserPassword String mysqlRootUserPassword String
mysqlDatabase String? mysqlDatabase String?
mysqlPublicPort Int? mysqlPublicPort Int?
ftpEnabled Boolean @default(false)
ftpUser String?
ftpPassword String?
ftpPublicPort Int?
ftpHostKey String?
ftpHostKeyPrivate String?
serviceId String @unique serviceId String @unique
service Service @relation(fields: [serviceId], references: [id]) service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
@@ -349,3 +360,12 @@ model Ghost {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }
model MeiliSearch {
id String @id @default(cuid())
masterKey String
serviceId String @unique
service Service @relation(fields: [serviceId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

6
src/app.d.ts vendored
View File

@@ -15,18 +15,20 @@ declare namespace App {
readOnly: boolean; readOnly: boolean;
source: string; source: string;
settings: string; settings: string;
database: Record<string, any>;
versions: string;
privatePort: string;
} }
} }
interface SessionData { interface SessionData {
whiteLabeled: boolean;
version?: string; version?: string;
userId?: string | null; userId?: string | null;
teamId?: string | null; teamId?: string | null;
permission?: string; permission?: string;
isAdmin?: boolean; isAdmin?: boolean;
expires?: string | null; expires?: string | null;
gitlabToken?: string | null;
ghToken?: string | null;
} }
type DateTimeFormatOptions = { type DateTimeFormatOptions = {

View File

@@ -2,7 +2,6 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Coolify</title> <title>Coolify</title>
%svelte.head% %svelte.head%

View File

@@ -7,6 +7,8 @@ import { version } from '$lib/common';
import cookie from 'cookie'; import cookie from 'cookie';
import { dev } from '$app/env'; import { dev } from '$app/env';
const whiteLabeled = process.env['COOLIFY_WHITE_LABELED'] === 'true';
export const handle = handleSession( export const handle = handleSession(
{ {
secret: process.env['COOLIFY_SECRET_KEY'], secret: process.env['COOLIFY_SECRET_KEY'],
@@ -71,6 +73,7 @@ export const handle = handleSession(
export const getSession: GetSession = function ({ locals }) { export const getSession: GetSession = function ({ locals }) {
return { return {
version, version,
whiteLabeled,
...locals.session.data ...locals.session.data
}; };
}; };

View File

@@ -1,9 +1,15 @@
async function send({ method, path, data = {}, headers, timeout = 30000 }) { async function send({
method,
path,
data = {},
headers,
timeout = 120000
}): Promise<Record<string, unknown>> {
const controller = new AbortController(); const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout); const id = setTimeout(() => controller.abort(), timeout);
const opts = { method, headers: {}, body: null, signal: controller.signal }; const opts = { method, headers: {}, body: null, signal: controller.signal };
if (Object.keys(data).length > 0) { if (Object.keys(data).length > 0) {
let parsedData = data; const parsedData = data;
for (const [key, value] of Object.entries(data)) { for (const [key, value] of Object.entries(data)) {
if (value === '') { if (value === '') {
parsedData[key] = null; parsedData[key] = null;
@@ -43,18 +49,33 @@ async function send({ method, path, data = {}, headers, timeout = 30000 }) {
return responseData; return responseData;
} }
export function get(path, headers = {}): Promise<any> { export function get(
path: string,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'GET', path, headers }); return send({ method: 'GET', path, headers });
} }
export function del(path, data = {}, headers = {}): Promise<any> { export function del(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'DELETE', path, data, headers }); return send({ method: 'DELETE', path, data, headers });
} }
export function post(path, data, headers = {}): Promise<any> { export function post(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'POST', path, data, headers }); return send({ method: 'POST', path, data, headers });
} }
export function put(path, data, headers = {}): Promise<any> { export function put(
path: string,
data: Record<string, unknown>,
headers?: Record<string, unknown>
): Promise<Record<string, unknown>> {
return send({ method: 'PUT', path, data, headers }); return send({ method: 'PUT', path, data, headers });
} }

View File

@@ -100,6 +100,7 @@ export const setDefaultConfiguration = async (data) => {
if (buildPack === 'static') port = 80; if (buildPack === 'static') port = 80;
else if (buildPack === 'node') port = 3000; else if (buildPack === 'node') port = 3000;
else if (buildPack === 'php') port = 80; else if (buildPack === 'php') port = 80;
else if (buildPack === 'python') port = 8000;
} }
if (!installCommand) installCommand = template?.installCommand || 'yarn install'; if (!installCommand) installCommand = template?.installCommand || 'yarn install';
if (!startCommand) startCommand = template?.startCommand || 'yarn start'; if (!startCommand) startCommand = template?.startCommand || 'yarn start';
@@ -123,20 +124,13 @@ export const setDefaultConfiguration = async (data) => {
export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId) { export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId) {
try { try {
// TODO: Write full .dockerignore for all deployments!!
if (buildPack === 'php') { if (buildPack === 'php') {
await fs.writeFile(
`${workdir}/.htaccess`,
`
RewriteEngine On
RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]
`
);
await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`); await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`);
saveBuildLog({ line: 'Copied default configuration file for PHP.', buildId, applicationId }); await saveBuildLog({
line: 'Copied default configuration file for PHP.',
buildId,
applicationId
});
} else if (staticDeployments.includes(buildPack)) { } else if (staticDeployments.includes(buildPack)) {
await fs.writeFile( await fs.writeFile(
`${workdir}/nginx.conf`, `${workdir}/nginx.conf`,
@@ -190,7 +184,7 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
} }
` `
); );
saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId }); await saveBuildLog({ line: 'Copied default configuration file.', buildId, applicationId });
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -26,14 +26,17 @@ export default async function ({
if (secrets.length > 0) { if (secrets.length > 0) {
secrets.forEach((secret) => { secrets.forEach((secret) => {
if (secret.isBuildSecret) { if (secret.isBuildSecret) {
if (pullmergeRequestId) { if (
if (secret.isPRMRSecret) { (pullmergeRequestId && secret.isPRMRSecret) ||
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); (!pullmergeRequestId && !secret.isPRMRSecret)
} ) {
} else { Dockerfile.unshift(`ARG ${secret.name}=${secret.value}`);
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`); Dockerfile.forEach((line, index) => {
} if (line.startsWith('FROM')) {
Dockerfile.splice(index + 1, 0, `ARG ${secret.name}`);
}
});
} }
} }
}); });

View File

@@ -12,6 +12,7 @@ import php from './php';
import rust from './rust'; import rust from './rust';
import astro from './static'; import astro from './static';
import eleventy from './static'; import eleventy from './static';
import python from './python';
export { export {
node, node,
@@ -27,5 +28,6 @@ export {
php, php,
rust, rust,
astro, astro,
eleventy eleventy,
python
}; };

View File

@@ -1,23 +1,45 @@
import { buildImage } from '$lib/docker'; import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => { const createDockerfile = async (data, image, htaccessFound): Promise<void> => {
const { workdir, baseDirectory } = data; const { workdir, baseDirectory } = data;
const Dockerfile: Array<string> = []; const Dockerfile: Array<string> = [];
let composerFound = false;
try {
await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`);
composerFound = true;
} catch (error) {}
Dockerfile.push(`FROM ${image}`); Dockerfile.push(`FROM ${image}`);
Dockerfile.push(`LABEL coolify.image=true`); Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('WORKDIR /app'); Dockerfile.push('WORKDIR /app');
Dockerfile.push(`COPY .${baseDirectory || ''} /app`); Dockerfile.push(`COPY .${baseDirectory || ''} /app`);
Dockerfile.push(`COPY /.htaccess .`); if (htaccessFound) {
Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`);
}
if (composerFound) {
Dockerfile.push(`RUN composer install`);
}
Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`);
Dockerfile.push(`EXPOSE 80`); Dockerfile.push(`EXPOSE 80`);
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
}; };
export default async function (data) { export default async function (data) {
const { workdir, baseDirectory } = data;
try { try {
const image = 'webdevops/php-nginx'; let htaccessFound = false;
await createDockerfile(data, image); try {
await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`);
htaccessFound = true;
} catch (e) {
//
}
const image = htaccessFound
? 'webdevops/php-apache:8.0-alpine'
: 'webdevops/php-nginx:8.0-alpine';
await createDockerfile(data, image, htaccessFound);
await buildImage(data); await buildImage(data);
} catch (error) { } catch (error) {
throw error; throw error;

View File

@@ -0,0 +1,71 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const {
workdir,
port,
baseDirectory,
secrets,
pullmergeRequestId,
pythonWSGI,
pythonModule,
pythonVariable
} = data;
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /app');
Dockerfile.push(`LABEL coolify.image=true`);
if (secrets.length > 0) {
secrets.forEach((secret) => {
if (secret.isBuildSecret) {
if (pullmergeRequestId) {
if (secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
} else {
if (!secret.isPRMRSecret) {
Dockerfile.push(`ARG ${secret.name}=${secret.value}`);
}
}
}
});
}
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`RUN pip install gunicorn`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`);
// Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`)
}
try {
await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`);
Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`);
Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`);
} catch (e) {
//
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
Dockerfile.push(`EXPOSE ${port}`);
if (pythonWSGI?.toLowerCase() === 'gunicorn') {
Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`);
} else if (pythonWSGI?.toLowerCase() === 'uwsgi') {
Dockerfile.push(
`CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}`
);
} else {
Dockerfile.push(`CMD python ${pythonModule}`);
}
await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n'));
};
export default async function (data) {
try {
const image = 'python:3-alpine';
await createDockerfile(data, image);
await buildImage(data);
} catch (error) {
throw error;
}
}

View File

@@ -12,7 +12,8 @@ import { version as currentVersion } from '../../package.json';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import Cookie from 'cookie'; import Cookie from 'cookie';
import os from 'os'; import os from 'os';
import cuid from 'cuid'; import type { RequestEvent } from '@sveltejs/kit/types/internal';
import type { Job } from 'bullmq';
try { try {
if (!dev) { if (!dev) {
@@ -45,30 +46,32 @@ const customConfig: Config = {
export const version = currentVersion; export const version = currentVersion;
export const asyncExecShell = util.promisify(child.exec); export const asyncExecShell = util.promisify(child.exec);
export const asyncSleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay)); export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay));
export const sentry = Sentry; export const sentry = Sentry;
export const uniqueName = () => uniqueNamesGenerator(customConfig); export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const saveBuildLog = async ({ line, buildId, applicationId }) => { export const saveBuildLog = async ({
const addTimestamp = `${generateTimestamp()} ${line}`; line,
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId }); buildId,
}; applicationId
}: {
export const isTeamIdTokenAvailable = (request) => { line: string;
const cookie = request.headers.cookie buildId: string;
?.split(';') applicationId: string;
.map((s) => s.trim()) }): Promise<Job> => {
.find((s) => s.startsWith('teamId=')) if (line) {
?.split('=')[1]; if (line.includes('ghs_')) {
if (!cookie) { const regex = /ghs_.*@/g;
return getTeam(request); line = line.replace(regex, '<SENSITIVE_DATA_DELETED>@');
} else { }
return cookie; const addTimestamp = `${generateTimestamp()} ${line}`;
return await buildLogQueue.add(buildId, { buildId, line: addTimestamp, applicationId });
} }
}; };
export const getTeam = (event) => { export const getTeam = (event: RequestEvent): string | null => {
const cookies = Cookie.parse(event.request.headers.get('cookie')); const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies?.teamId) { if (cookies?.teamId) {
return cookies.teamId; return cookies.teamId;
@@ -78,14 +81,28 @@ export const getTeam = (event) => {
return null; return null;
}; };
export const getUserDetails = async (event, isAdminRequired = true) => { export const getUserDetails = async (
event: RequestEvent,
isAdminRequired = true
): Promise<{
teamId: string;
userId: string;
permission: string;
status: number;
body: { message: string };
}> => {
const teamId = getTeam(event); const teamId = getTeam(event);
const userId = event.locals.session.data.userId || null; const userId = event?.locals?.session?.data?.userId || null;
const { permission = 'read' } = await db.prisma.permission.findFirst({ let permission = 'read';
where: { teamId, userId }, if (teamId && userId) {
select: { permission: true }, const data = await db.prisma.permission.findFirst({
rejectOnNotFound: true where: { teamId, userId },
}); select: { permission: true },
rejectOnNotFound: true
});
if (data.permission) permission = data.permission;
}
const payload = { const payload = {
teamId, teamId,
userId, userId,
@@ -95,6 +112,7 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
message: 'OK' message: 'OK'
} }
}; };
if (isAdminRequired && permission !== 'admin' && permission !== 'owner') { if (isAdminRequired && permission !== 'admin' && permission !== 'owner') {
payload.status = 401; payload.status = 401;
payload.body.message = payload.body.message =
@@ -104,11 +122,11 @@ export const getUserDetails = async (event, isAdminRequired = true) => {
return payload; return payload;
}; };
export function getEngine(engine) { export function getEngine(engine: string): string {
return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine; return engine === '/var/run/docker.sock' ? 'unix:///var/run/docker.sock' : engine;
} }
export async function removeContainer(id, engine) { export async function removeContainer(id: string, engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
try { try {
const { stdout } = await asyncExecShell( const { stdout } = await asyncExecShell(
@@ -124,11 +142,23 @@ export async function removeContainer(id, engine) {
} }
} }
export const removeDestinationDocker = async ({ id, engine }) => { export const removeDestinationDocker = async ({
id,
engine
}: {
id: string;
engine: string;
}): Promise<void> => {
return await removeContainer(id, engine); return await removeContainer(id, engine);
}; };
export const createDirectories = async ({ repository, buildId }) => { export const createDirectories = async ({
repository,
buildId
}: {
repository: string;
buildId: string;
}): Promise<{ workdir: string; repodir: string }> => {
const repodir = `/tmp/build-sources/${repository}/`; const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`; const workdir = `/tmp/build-sources/${repository}/${buildId}`;
@@ -140,20 +170,10 @@ export const createDirectories = async ({ repository, buildId }) => {
}; };
}; };
export function generateTimestamp() { export function generateTimestamp(): string {
return `${dayjs().format('HH:mm:ss.SSS')} `; return `${dayjs().format('HH:mm:ss.SSS')} `;
} }
export function getDomain(domain) { export function getDomain(domain: string): string {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');
} }
export function dashify(str: string, options?: any): string {
if (typeof str !== 'string') return str;
return str
.trim()
.replace(/\W/g, (m) => (/[À-ž]/.test(m) ? m : '-'))
.replace(/^-+|-+$/g, '')
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase();
}

View File

@@ -0,0 +1,25 @@
<script>
export let database;
import Clickhouse from './svg/databases/Clickhouse.svelte';
import CouchDb from './svg/databases/CouchDB.svelte';
import MongoDb from './svg/databases/MongoDB.svelte';
import MySql from './svg/databases/MySQL.svelte';
import PostgreSql from './svg/databases/PostgreSQL.svelte';
import Redis from './svg/databases/Redis.svelte';
</script>
<span class="relative">
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>

View File

@@ -0,0 +1,55 @@
<script>
export let service;
import Ghost from './svg/services/Ghost.svelte';
import LanguageTool from './svg/services/LanguageTool.svelte';
import MinIo from './svg/services/MinIO.svelte';
import N8n from './svg/services/N8n.svelte';
import NocoDb from './svg/services/NocoDB.svelte';
import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte';
import UptimeKuma from './svg/services/UptimeKuma.svelte';
import VaultWarden from './svg/services/VaultWarden.svelte';
import VsCodeServer from './svg/services/VSCodeServer.svelte';
import Wordpress from './svg/services/Wordpress.svelte';
</script>
{#if service.type === 'plausibleanalytics'}
<a href="https://plausible.io" target="_blank">
<PlausibleAnalytics />
</a>
{:else if service.type === 'nocodb'}
<a href="https://nocodb.com" target="_blank">
<NocoDb />
</a>
{:else if service.type === 'minio'}
<a href="https://min.io" target="_blank">
<MinIo />
</a>
{:else if service.type === 'vscodeserver'}
<a href="https://coder.com" target="_blank">
<VsCodeServer />
</a>
{:else if service.type === 'wordpress'}
<a href="https://wordpress.org" target="_blank">
<Wordpress />
</a>
{:else if service.type === 'vaultwarden'}
<a href="https://github.com/dani-garcia/vaultwarden" target="_blank">
<VaultWarden />
</a>
{:else if service.type === 'languagetool'}
<a href="https://languagetool.org/dev" target="_blank">
<LanguageTool />
</a>
{:else if service.type === 'n8n'}
<a href="https://n8n.io" target="_blank">
<N8n />
</a>
{:else if service.type === 'uptimekuma'}
<a href="https://github.com/louislam/uptime-kuma" target="_blank">
<UptimeKuma />
</a>
{:else if service.type === 'ghost'}
<a href="https://ghost.org" target="_blank">
<Ghost />
</a>
{/if}

View File

@@ -7,6 +7,7 @@
export let isCenter = true; export let isCenter = true;
export let disabled = false; export let disabled = false;
export let dataTooltip = null; export let dataTooltip = null;
export let loading = false;
</script> </script>
<div class="flex items-center py-4 pr-8"> <div class="flex items-center py-4 pr-8">
@@ -26,9 +27,10 @@
on:click on:click
aria-pressed="false" aria-pressed="false"
class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" class="relative mx-20 inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:opacity-50={disabled} class:opacity-50={disabled || loading}
class:bg-green-600={setting} class:bg-green-600={!loading && setting}
class:bg-stone-700={!setting} class:bg-stone-700={!loading && !setting}
class:bg-yellow-500={loading}
> >
<span class="sr-only">Use setting</span> <span class="sr-only">Use setting</span>
<span <span
@@ -40,6 +42,7 @@
class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in" class=" absolute inset-0 flex h-full w-full items-center justify-center transition-opacity duration-200 ease-in"
class:opacity-0={setting} class:opacity-0={setting}
class:opacity-100={!setting} class:opacity-100={!setting}
class:animate-spin={loading}
aria-hidden="true" aria-hidden="true"
> >
<svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12"> <svg class="h-3 w-3 bg-white text-red-600" fill="none" viewBox="0 0 12 12">
@@ -57,6 +60,7 @@
aria-hidden="true" aria-hidden="true"
class:opacity-100={setting} class:opacity-100={setting}
class:opacity-0={!setting} class:opacity-0={!setting}
class:animate-spin={loading}
> >
<svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12"> <svg class="h-3 w-3 bg-white text-green-600" fill="currentColor" viewBox="0 0 12 12">
<path <path

View File

@@ -19,7 +19,7 @@ export const staticDeployments = [
'astro', 'astro',
'eleventy' 'eleventy'
]; ];
export const notNodeDeployments = ['php', 'docker', 'rust']; export const notNodeDeployments = ['php', 'docker', 'rust', 'python'];
export function getDomain(domain) { export function getDomain(domain) {
return domain?.replace('https://', '').replace('http://', ''); return domain?.replace('https://', '').replace('http://', '');
@@ -37,3 +37,148 @@ export function dashify(str: string, options?: any): string {
.replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m)) .replace(/-{2,}/g, (m) => (options && options.condense ? '-' : m))
.toLowerCase(); .toLowerCase();
} }
export function changeQueryParams(buildId) {
const queryParams = new URLSearchParams(window.location.search);
queryParams.set('buildId', buildId);
return history.pushState(null, null, '?' + queryParams.toString());
}
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0', '4.4', '4.2']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2', '6.0', '5.0']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
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
}
}
];

View File

@@ -0,0 +1,45 @@
<script lang="ts">
export let isAbsolute = false;
</script>
<svg
viewBox="0 0 127 74"
class={isAbsolute ? 'w-10 h-10 absolute top-0 left-0 -m-5' : 'w-8 mx-auto'}
xmlns="http://www.w3.org/2000/svg"
><path
d="M.825 73.993l23.244-59.47A21.85 21.85 0 0144.42.625h14.014L35.19 60.096a21.85 21.85 0 01-20.352 13.897H.825z"
fill="url(#meilisearch_logo_svg__paint0_linear_0_6)"
/><path
d="M34.925 73.993l23.243-59.47A21.85 21.85 0 0178.52.626h14.013L69.29 60.096a21.85 21.85 0 01-20.351 13.897H34.925z"
fill="url(#meilisearch_logo_svg__paint1_linear_0_6)"
/><path
d="M69.026 73.993l23.244-59.47A21.85 21.85 0 01112.621.626h14.014l-23.244 59.47a21.851 21.851 0 01-20.352 13.897H69.026z"
fill="url(#meilisearch_logo_svg__paint2_linear_0_6)"
/><defs
><linearGradient
id="meilisearch_logo_svg__paint0_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint1_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
><linearGradient
id="meilisearch_logo_svg__paint2_linear_0_6"
x1="126.635"
y1="-4.978"
x2="0.825"
y2="66.098"
gradientUnits="userSpaceOnUse"
><stop stop-color="#FF5CAA" /><stop offset="1" stop-color="#FF4E62" /></linearGradient
></defs
></svg
>

View File

@@ -146,6 +146,13 @@ export function findBuildPack(pack, packageManager = 'npm') {
port: 80 port: 80
}; };
} }
if (pack === 'python') {
return {
...metaData,
startCommand: null,
port: 8000
};
}
return { return {
name: 'node', name: 'node',
fancyName: 'Node.js', fancyName: 'Node.js',
@@ -249,6 +256,12 @@ export const buildPacks = [
fancyName: 'Rust', fancyName: 'Rust',
hoverColor: 'hover:bg-pink-700', hoverColor: 'hover:bg-pink-700',
color: 'bg-pink-700' color: 'bg-pink-700'
},
{
name: 'python',
fancyName: 'Python',
hoverColor: 'hover:bg-green-700',
color: 'bg-green-700'
} }
]; ];
export const scanningTemplates = { export const scanningTemplates = {

View File

@@ -1,13 +1,13 @@
import crypto from 'crypto'; import crypto from 'crypto';
const algorithm = 'aes-256-ctr'; const algorithm = 'aes-256-ctr';
export const base64Encode = (text: string) => { export const base64Encode = (text: string): string => {
return Buffer.from(text).toString('base64'); return Buffer.from(text).toString('base64');
}; };
export const base64Decode = (text: string) => { export const base64Decode = (text: string): string => {
return Buffer.from(text, 'base64').toString('ascii'); return Buffer.from(text, 'base64').toString('ascii');
}; };
export const encrypt = (text: string) => { export const encrypt = (text: string): string => {
if (text) { if (text) {
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv); const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
@@ -19,7 +19,7 @@ export const encrypt = (text: string) => {
} }
}; };
export const decrypt = (hashString: string) => { export const decrypt = (hashString: string): string => {
if (hashString) { if (hashString) {
const hash: Hash = JSON.parse(hashString); const hash: Hash = JSON.parse(hashString);
const decipher = crypto.createDecipheriv( const decipher = crypto.createDecipheriv(

View File

@@ -1,14 +1,35 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { getDomain, removeDestinationDocker } from '$lib/common'; import { removeDestinationDocker } from '$lib/common';
import { prisma } from './common'; import { prisma } from './common';
export async function listApplications(teamId) { import type {
return await prisma.application.findMany({ where: { teams: { some: { id: teamId } } } }); DestinationDocker,
GitSource,
Secret,
ApplicationSettings,
Application,
ApplicationPersistentStorage
} from '@prisma/client';
export async function listApplications(teamId: string): Promise<Application[]> {
if (teamId === '0') {
return await prisma.application.findMany({ include: { teams: true } });
}
return await prisma.application.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
} }
export async function newApplication({ name, teamId }) { export async function newApplication({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Application> {
return await prisma.application.create({ return await prisma.application.create({
data: { data: {
name, name,
@@ -18,34 +39,17 @@ export async function newApplication({ name, teamId }) {
}); });
} }
export async function importApplication({ export async function removeApplication({
name, id,
teamId, teamId
fqdn, }: {
port, id: string;
buildCommand, teamId: string;
startCommand, }): Promise<void> {
installCommand const { destinationDockerId, destinationDocker } = await prisma.application.findUnique({
}) {
return await prisma.application.create({
data: {
name,
fqdn,
port,
buildCommand,
startCommand,
installCommand,
teams: { connect: { id: teamId } }
}
});
}
export async function removeApplication({ id, teamId }) {
const { fqdn, destinationDockerId, destinationDocker } = await prisma.application.findUnique({
where: { id }, where: { id },
include: { destinationDocker: true } include: { destinationDocker: true }
}); });
const domain = getDomain(fqdn);
if (destinationDockerId) { if (destinationDockerId) {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
const { stdout: containers } = await asyncExecShell( const { stdout: containers } = await asyncExecShell(
@@ -56,7 +60,6 @@ export async function removeApplication({ id, teamId }) {
for (const container of containersArray) { for (const container of containersArray) {
const containerObj = JSON.parse(container); const containerObj = JSON.parse(container);
const id = containerObj.ID; const id = containerObj.ID;
const preview = containerObj.Image.split('-')[1];
await removeDestinationDocker({ id, engine: destinationDocker.engine }); await removeDestinationDocker({ id, engine: destinationDocker.engine });
} }
} }
@@ -67,12 +70,30 @@ export async function removeApplication({ id, teamId }) {
await prisma.build.deleteMany({ where: { applicationId: id } }); await prisma.build.deleteMany({ where: { applicationId: id } });
await prisma.secret.deleteMany({ where: { applicationId: id } }); await prisma.secret.deleteMany({ where: { applicationId: id } });
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } });
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); if (teamId === '0') {
await prisma.application.deleteMany({ where: { id } });
} else {
await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } });
}
} }
export async function getApplicationWebhook({ projectId, branch }) { export async function getApplicationWebhook({
projectId,
branch
}: {
projectId: number;
branch: string;
}): Promise<
Application & {
destinationDocker: DestinationDocker;
settings: ApplicationSettings;
gitSource: GitSource;
secrets: Secret[];
persistentStorage: ApplicationPersistentStorage[];
}
> {
try { try {
let application = await prisma.application.findFirst({ const application = await prisma.application.findFirst({
where: { projectId, branch, settings: { autodeploy: true } }, where: { projectId, branch, settings: { autodeploy: true } },
include: { include: {
destinationDocker: true, destinationDocker: true,
@@ -121,25 +142,40 @@ export async function getApplicationWebhook({ projectId, branch }) {
throw { status: 404, body: { message: e.message } }; throw { status: 404, body: { message: e.message } };
} }
} }
export async function getApplicationById({ id }) {
const body = await prisma.application.findFirst({
where: { id },
include: { destinationDocker: true }
});
return { ...body }; export async function getApplication({ id, teamId }: { id: string; teamId: string }): Promise<
} Application & {
export async function getApplication({ id, teamId }) { destinationDocker: DestinationDocker;
let body = await prisma.application.findFirst({ settings: ApplicationSettings;
where: { id, teams: { some: { id: teamId } } }, gitSource: GitSource;
include: { secrets: Secret[];
destinationDocker: true, persistentStorage: ApplicationPersistentStorage[];
settings: true, }
gitSource: { include: { githubApp: true, gitlabApp: true } }, > {
secrets: true, let body;
persistentStorage: true if (teamId === '0') {
} body = await prisma.application.findFirst({
}); where: { id },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
} else {
body = await prisma.application.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: {
destinationDocker: true,
settings: true,
gitSource: { include: { githubApp: true, gitlabApp: true } },
secrets: true,
persistentStorage: true
}
});
}
if (body?.gitSource?.githubApp?.clientSecret) { if (body?.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret); body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
@@ -170,7 +206,14 @@ export async function configureGitRepository({
projectId, projectId,
webhookToken, webhookToken,
autodeploy autodeploy
}) { }: {
id: string;
repository: string;
branch: string;
projectId: number;
webhookToken: string;
autodeploy: boolean;
}): Promise<void> {
if (webhookToken) { if (webhookToken) {
const encryptedWebhookToken = encrypt(webhookToken); const encryptedWebhookToken = encrypt(webhookToken);
await prisma.application.update({ await prisma.application.update({
@@ -200,7 +243,10 @@ export async function configureGitRepository({
} }
} }
export async function configureBuildPack({ id, buildPack }) { export async function configureBuildPack({
id,
buildPack
}: Pick<Application, 'id' | 'buildPack'>): Promise<Application> {
return await prisma.application.update({ where: { id }, data: { buildPack } }); return await prisma.application.update({ where: { id }, data: { buildPack } });
} }
@@ -214,11 +260,29 @@ export async function configureApplication({
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
}) { pythonWSGI,
pythonModule,
pythonVariable
}: {
id: string;
buildPack: string;
name: string;
fqdn: string;
port: number;
installCommand: string;
buildCommand: string;
startCommand: string;
baseDirectory: string;
publishDirectory: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
}): Promise<Application> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { data: {
name,
buildPack, buildPack,
fqdn, fqdn,
port, port,
@@ -227,16 +291,31 @@ export async function configureApplication({
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory, publishDirectory,
name pythonWSGI,
pythonModule,
pythonVariable
} }
}); });
} }
export async function checkDoubleBranch(branch, projectId) { export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
const applications = await prisma.application.findMany({ where: { branch, projectId } }); const applications = await prisma.application.findMany({ where: { branch, projectId } });
return applications.length > 1; return applications.length > 1;
} }
export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) {
export async function setApplicationSettings({
id,
debug,
previews,
dualCerts,
autodeploy
}: {
id: string;
debug: boolean;
previews: boolean;
dualCerts: boolean;
autodeploy: boolean;
}): Promise<Application & { destinationDocker: DestinationDocker }> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { settings: { update: { debug, previews, dualCerts, autodeploy } } }, data: { settings: { update: { debug, previews, dualCerts, autodeploy } } },
@@ -244,29 +323,6 @@ export async function setApplicationSettings({ id, debug, previews, dualCerts, a
}); });
} }
export async function createBuild({ export async function getPersistentStorage(id: string): Promise<ApplicationPersistentStorage[]> {
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
type
}) {
return await prisma.build.create({
data: {
id,
applicationId,
destinationDockerId,
gitSourceId,
githubAppId,
gitlabAppId,
status: 'running',
type
}
});
}
export async function getPersistentStorage(id) {
return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } }); return await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
} }

View File

@@ -1,7 +1,16 @@
import { getDomain } from '$lib/common'; import { getDomain } from '$lib/common';
import { prisma } from './common'; import { prisma } from './common';
import type { Application, ServiceSecret, DestinationDocker, Secret } from '@prisma/client';
export async function isBranchAlreadyUsed({ repository, branch, id }) { export async function isBranchAlreadyUsed({
repository,
branch,
id
}: {
id: string;
repository: string;
branch: string;
}): Promise<Application> {
const application = await prisma.application.findUnique({ const application = await prisma.application.findUnique({
where: { id }, where: { id },
include: { gitSource: true } include: { gitSource: true }
@@ -11,18 +20,42 @@ export async function isBranchAlreadyUsed({ repository, branch, id }) {
}); });
} }
export async function isDockerNetworkExists({ network }) { export async function isDockerNetworkExists({
network
}: {
network: string;
}): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({ where: { network } }); return await prisma.destinationDocker.findFirst({ where: { network } });
} }
export async function isServiceSecretExists({ id, name }) { export async function isServiceSecretExists({
id,
name
}: {
id: string;
name: string;
}): Promise<ServiceSecret> {
return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } }); return await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
} }
export async function isSecretExists({ id, name, isPRMRSecret }) { export async function isSecretExists({
id,
name,
isPRMRSecret
}: {
id: string;
name: string;
isPRMRSecret: boolean;
}): Promise<Secret> {
return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } }); return await prisma.secret.findFirst({ where: { name, applicationId: id, isPRMRSecret } });
} }
export async function isDomainConfigured({ id, fqdn }) { export async function isDomainConfigured({
id,
fqdn
}: {
id: string;
fqdn: string;
}): Promise<boolean> {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const nakedDomain = domain.replace('www.', ''); const nakedDomain = domain.replace('www.', '');
const foundApp = await prisma.application.findFirst({ const foundApp = await prisma.application.findFirst({
@@ -55,6 +88,5 @@ export async function isDomainConfigured({ id, fqdn }) {
}, },
select: { fqdn: true } select: { fqdn: true }
}); });
if (foundApp || foundService || coolifyFqdn) return true; return !!(foundApp || foundService || coolifyFqdn);
return false;
} }

View File

@@ -1,12 +1,16 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { sentry } from '$lib/common'; import { sentry } from '$lib/common';
import {
supportedDatabaseTypesAndVersions,
supportedServiceTypesAndVersions
} from '$lib/components/common';
import * as Prisma from '@prisma/client'; import * as Prisma from '@prisma/client';
import { default as ProdPrisma } from '@prisma/client'; import { default as ProdPrisma } from '@prisma/client';
import type { PrismaClientOptions } from '@prisma/client/runtime'; import type { Database, DatabaseSettings } from '@prisma/client';
import generator from 'generate-password'; import generator from 'generate-password';
import forge from 'node-forge'; import forge from 'node-forge';
export function generatePassword(length = 24) { export function generatePassword(length = 24): string {
return generator.generate({ return generator.generate({
length, length,
numbers: true, numbers: true,
@@ -26,8 +30,14 @@ export const prisma = new PrismaClient({
rejectOnNotFound: false rejectOnNotFound: false
}); });
export function ErrorHandler(e) { export function ErrorHandler(e: {
if (e! instanceof Error) { stdout?;
message?: string;
status?: number;
name?: string;
error?: string;
}): { status: number; body: { message: string; error: string } } {
if (e && e instanceof Error) {
e = new Error(e.toString()); e = new Error(e.toString());
} }
let truncatedError = e; let truncatedError = e;
@@ -35,8 +45,7 @@ export function ErrorHandler(e) {
truncatedError = e.stdout; truncatedError = e.stdout;
} }
if (e.message?.includes('docker run')) { if (e.message?.includes('docker run')) {
let truncatedArray = []; const truncatedArray: string[] = truncatedError.message.split('-').filter((line) => {
truncatedArray = truncatedError.message.split('-').filter((line) => {
if (!line.startsWith('e ')) { if (!line.startsWith('e ')) {
return line; return line;
} }
@@ -46,7 +55,9 @@ export function ErrorHandler(e) {
if (e.message?.includes('git clone')) { if (e.message?.includes('git clone')) {
truncatedError.message = 'git clone failed'; truncatedError.message = 'git clone failed';
} }
sentry.captureException(truncatedError); if (!e.message?.includes('Coolify Proxy is not running')) {
sentry.captureException(truncatedError);
}
const payload = { const payload = {
status: truncatedError.status || 500, status: truncatedError.status || 500,
body: { body: {
@@ -62,11 +73,11 @@ export function ErrorHandler(e) {
payload.body.message = 'Already exists. Choose another name.'; payload.body.message = 'Already exists. Choose another name.';
} }
} }
// console.error(e)
return payload; return payload;
} }
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> { export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
return await new Promise(async (resolve, reject) => { return await new Promise((resolve, reject) => {
forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) { forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) {
if (keys) { if (keys) {
resolve({ resolve({
@@ -80,153 +91,93 @@ export async function generateSshKeyPair(): Promise<{ publicKey: string; private
}); });
} }
export const supportedDatabaseTypesAndVersions = [ export function getVersions(type: string): string[] {
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
versions: ['5.0.5', '4.4.11', '4.2.18', '4.0.27']
},
{ name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0.27', '5.7.36'] },
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
versions: ['14.1.0', '13.5.0', '12.9.0', '11.14.0', '10.19.0', '9.6.24']
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
versions: ['6.2.6', '6.0.16', '5.0.14']
},
{ name: 'couchdb', fancyName: 'CouchDB', baseImage: 'bitnami/couchdb', versions: ['3.2.1'] }
];
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'],
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['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'],
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
ports: {
main: 2368
}
}
];
export function getVersions(type) {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
return found.versions; return found.versions;
} }
return []; return [];
} }
export function getDatabaseImage(type) {
export function getDatabaseImage(type: string): string {
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type); const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
return found.baseImage; return found.baseImage;
} }
return ''; return '';
} }
export function getServiceImage(type) {
export function getServiceImage(type: string): string {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type); const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
return found.baseImage; return found.baseImage;
} }
return ''; return '';
} }
export function getServiceImages(type) {
export function getServiceImages(type: string): string[] {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type); const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) { if (found) {
return found.images; return found.images;
} }
return []; return [];
} }
export function generateDatabaseConfiguration(database) {
export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }):
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MYSQL_DATABASE: string;
MYSQL_PASSWORD: string;
MYSQL_ROOT_USER: string;
MYSQL_USER: string;
MYSQL_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
MONGODB_ROOT_USER: string;
MONGODB_ROOT_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
| {
volume: string;
image: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
const { const {
id, id,
dbUser, dbUser,
@@ -241,7 +192,6 @@ export function generateDatabaseConfiguration(database) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type);
if (type === 'mysql') { if (type === 'mysql') {
return { return {
// url: `mysql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 3306}/${defaultDatabase}`,
privatePort: 3306, privatePort: 3306,
environmentVariables: { environmentVariables: {
MYSQL_USER: dbUser, MYSQL_USER: dbUser,
@@ -256,7 +206,6 @@ export function generateDatabaseConfiguration(database) {
}; };
} else if (type === 'mongodb') { } else if (type === 'mongodb') {
return { return {
// url: `mongodb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 27017}/${defaultDatabase}`,
privatePort: 27017, privatePort: 27017,
environmentVariables: { environmentVariables: {
MONGODB_ROOT_USER: rootUser, MONGODB_ROOT_USER: rootUser,
@@ -268,7 +217,6 @@ export function generateDatabaseConfiguration(database) {
}; };
} else if (type === 'postgresql') { } else if (type === 'postgresql') {
return { return {
// url: `psql://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5432}/${defaultDatabase}`,
privatePort: 5432, privatePort: 5432,
environmentVariables: { environmentVariables: {
POSTGRESQL_PASSWORD: dbUserPassword, POSTGRESQL_PASSWORD: dbUserPassword,
@@ -281,7 +229,6 @@ export function generateDatabaseConfiguration(database) {
}; };
} else if (type === 'redis') { } else if (type === 'redis') {
return { return {
// url: `redis://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 6379}/${defaultDatabase}`,
privatePort: 6379, privatePort: 6379,
environmentVariables: { environmentVariables: {
REDIS_PASSWORD: dbUserPassword, REDIS_PASSWORD: dbUserPassword,
@@ -293,7 +240,6 @@ export function generateDatabaseConfiguration(database) {
}; };
} else if (type === 'couchdb') { } else if (type === 'couchdb') {
return { return {
// url: `couchdb://${dbUser}:${dbUserPassword}@${id}:${isPublic ? port : 5984}/${defaultDatabase}`,
privatePort: 5984, privatePort: 5984,
environmentVariables: { environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword, COUCHDB_PASSWORD: dbUserPassword,
@@ -304,18 +250,4 @@ export function generateDatabaseConfiguration(database) {
ulimits: {} ulimits: {}
}; };
} }
// } else if (type === 'clickhouse') {
// return {
// url: `clickhouse://${dbUser}:${dbUserPassword}@${id}:${port}/${defaultDatabase}`,
// privatePort: 9000,
// image: `bitnami/clickhouse-server:${version}`,
// volume: `${id}-${type}-data:/var/lib/clickhouse`,
// ulimits: {
// nofile: {
// soft: 262144,
// hard: 262144
// }
// }
// }
// }
} }

View File

@@ -1,15 +1,28 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import * as db from '$lib/database';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
import { prisma, ErrorHandler } from './common'; import { prisma } from './common';
import getPort, { portNumbers } from 'get-port';
import { asyncExecShell, getEngine, removeContainer } from '$lib/common'; import { asyncExecShell, getEngine, removeContainer } from '$lib/common';
import type { Database, DatabaseSettings, DestinationDocker } from '@prisma/client';
export async function listDatabases(teamId) { export async function listDatabases(teamId: string): Promise<Database[]> {
return await prisma.database.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.database.findMany({ include: { teams: true } });
} else {
return await prisma.database.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
} }
export async function newDatabase({ name, teamId }) {
export async function newDatabase({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Database> {
const dbUser = cuid(); const dbUser = cuid();
const dbUserPassword = encrypt(generatePassword()); const dbUserPassword = encrypt(generatePassword());
const rootUser = cuid(); const rootUser = cuid();
@@ -30,25 +43,44 @@ export async function newDatabase({ name, teamId }) {
}); });
} }
export async function getDatabase({ id, teamId }) { export async function getDatabase({
const body = await prisma.database.findFirst({ id,
where: { id, teams: { some: { id: teamId } } }, teamId
include: { destinationDocker: true, settings: true } }: {
}); id: string;
teamId: string;
}): Promise<Database & { destinationDocker: DestinationDocker; settings: DatabaseSettings }> {
let body;
if (teamId === '0') {
body = await prisma.database.findFirst({
where: { id },
include: { destinationDocker: true, settings: true }
});
} else {
body = await prisma.database.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { destinationDocker: true, settings: true }
});
}
if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword); if (body.dbUserPassword) body.dbUserPassword = decrypt(body.dbUserPassword);
if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword); if (body.rootUserPassword) body.rootUserPassword = decrypt(body.rootUserPassword);
return { ...body }; return body;
} }
export async function removeDatabase({ id }) { export async function removeDatabase({ id }: { id: string }): Promise<void> {
await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); await prisma.databaseSettings.deleteMany({ where: { databaseId: id } });
await prisma.database.delete({ where: { id } }); await prisma.database.delete({ where: { id } });
return; return;
} }
export async function configureDatabaseType({ id, type }) { export async function configureDatabaseType({
id,
type
}: {
id: string;
type: string;
}): Promise<Database> {
return await prisma.database.update({ return await prisma.database.update({
where: { id }, where: { id },
data: { type } data: { type }
@@ -64,7 +96,7 @@ export async function setDatabase({
version?: string; version?: string;
isPublic?: boolean; isPublic?: boolean;
appendOnly?: boolean; appendOnly?: boolean;
}) { }): Promise<Database> {
return await prisma.database.update({ return await prisma.database.update({
where: { id }, where: { id },
data: { data: {
@@ -82,7 +114,16 @@ export async function updateDatabase({
rootUser, rootUser,
rootUserPassword, rootUserPassword,
version version
}) { }: {
id: string;
name: string;
defaultDatabase: string;
dbUser: string;
dbUserPassword: string;
rootUser: string;
rootUserPassword: string;
version: string;
}): Promise<Database> {
const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword); const encryptedDbUserPassword = dbUserPassword && encrypt(dbUserPassword);
const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword); const encryptedRootUserPassword = rootUserPassword && encrypt(rootUserPassword);
return await prisma.database.update({ return await prisma.database.update({
@@ -99,7 +140,9 @@ export async function updateDatabase({
}); });
} }
export async function stopDatabase(database) { export async function stopDatabase(
database: Database & { destinationDocker: DestinationDocker }
): Promise<boolean> {
let everStarted = false; let everStarted = false;
const { const {
id, id,
@@ -122,3 +165,43 @@ export async function stopDatabase(database) {
} }
return everStarted; return everStarted;
} }
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
const {
id,
type,
rootUser,
rootUserPassword,
dbUser,
dbUserPassword,
defaultDatabase,
destinationDockerId,
destinationDocker: { engine }
} = database;
if (destinationDockerId) {
const host = getEngine(engine);
if (type === 'mysql') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
);
} else if (type === 'postgresql') {
if (isRoot) {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
);
} else {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
);
}
} else if (type === 'mongodb') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
);
} else if (type === 'redis') {
await asyncExecShell(
`DOCKER_HOST=${host} docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
);
}
}
}

View File

@@ -1,27 +1,53 @@
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto';
import { dockerInstance } from '$lib/docker'; import { dockerInstance } from '$lib/docker';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy } from '$lib/haproxy';
import { getDatabaseImage } from '.'; import { getDatabaseImage } from '.';
import { prisma } from './common'; import { prisma } from './common';
import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client';
import type { CreateDockerDestination } from '$lib/types/destinations';
export async function listDestinations(teamId) { type DestinationConfigurationObject = {
return await prisma.destinationDocker.findMany({ where: { teams: { some: { id: teamId } } } }); id: string;
destinationId: string;
};
type FindDestinationFromTeam = {
id: string;
teamId: string;
};
export async function listDestinations(teamId: string): Promise<DestinationDocker[]> {
if (teamId === '0') {
return await prisma.destinationDocker.findMany({ include: { teams: true } });
}
return await prisma.destinationDocker.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
} }
export async function configureDestinationForService({ id, destinationId }) { export async function configureDestinationForService({
id,
destinationId
}: DestinationConfigurationObject): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { destinationDocker: { connect: { id: destinationId } } } data: { destinationDocker: { connect: { id: destinationId } } }
}); });
} }
export async function configureDestinationForApplication({ id, destinationId }) { export async function configureDestinationForApplication({
id,
destinationId
}: DestinationConfigurationObject): Promise<Application> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { destinationDocker: { connect: { id: destinationId } } } data: { destinationDocker: { connect: { id: destinationId } } }
}); });
} }
export async function configureDestinationForDatabase({ id, destinationId }) { export async function configureDestinationForDatabase({
id,
destinationId
}: DestinationConfigurationObject): Promise<void> {
await prisma.database.update({ await prisma.database.update({
where: { id }, where: { id },
data: { destinationDocker: { connect: { id: destinationId } } } data: { destinationDocker: { connect: { id: destinationId } } }
@@ -38,13 +64,16 @@ export async function configureDestinationForDatabase({ id, destinationId }) {
const host = getEngine(engine); const host = getEngine(engine);
if (type && version) { if (type && version) {
const baseImage = getDatabaseImage(type); const baseImage = getDatabaseImage(type);
asyncExecShell( asyncExecShell(`DOCKER_HOST=${host} docker pull ${baseImage}:${version}`);
`DOCKER_HOST=${host} docker pull ${baseImage}:${version} && echo "FROM ${baseImage}:${version}" | docker build --label coolify.image="true" -t "${baseImage}:${version}" -`
);
} }
} }
} }
export async function updateDestination({ id, name, engine, network }) { export async function updateDestination({
id,
name,
engine,
network
}: Pick<DestinationDocker, 'id' | 'name' | 'engine' | 'network'>): Promise<DestinationDocker> {
return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } }); return await prisma.destinationDocker.update({ where: { id }, data: { name, engine, network } });
} }
@@ -54,13 +83,8 @@ export async function newRemoteDestination({
engine, engine,
network, network,
isCoolifyProxyUsed, isCoolifyProxyUsed,
remoteEngine, remoteEngine
ipAddress, }: CreateDockerDestination): Promise<string> {
user,
port,
sshPrivateKey
}) {
const encryptedPrivateKey = encrypt(sshPrivateKey);
const destination = await prisma.destinationDocker.create({ const destination = await prisma.destinationDocker.create({
data: { data: {
name, name,
@@ -68,16 +92,18 @@ export async function newRemoteDestination({
engine, engine,
network, network,
isCoolifyProxyUsed, isCoolifyProxyUsed,
remoteEngine, remoteEngine
ipAddress,
user,
port,
sshPrivateKey: encryptedPrivateKey
} }
}); });
return destination.id; return destination.id;
} }
export async function newLocalDestination({ name, teamId, engine, network, isCoolifyProxyUsed }) { export async function newLocalDestination({
name,
teamId,
engine,
network,
isCoolifyProxyUsed
}: CreateDockerDestination): Promise<string> {
const host = getEngine(engine); const host = getEngine(engine);
const docker = dockerInstance({ destinationDocker: { engine, network } }); const docker = dockerInstance({ destinationDocker: { engine, network } });
const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } }); const found = await docker.engine.listNetworks({ filters: { name: [`^${network}$`] } });
@@ -95,18 +121,14 @@ export async function newLocalDestination({ name, teamId, engine, network, isCoo
(destination) => destination.network !== network && destination.isCoolifyProxyUsed === true (destination) => destination.network !== network && destination.isCoolifyProxyUsed === true
); );
if (proxyConfigured) { if (proxyConfigured) {
if (proxyConfigured.isCoolifyProxyUsed) { isCoolifyProxyUsed = !!proxyConfigured.isCoolifyProxyUsed;
isCoolifyProxyUsed = true;
} else {
isCoolifyProxyUsed = false;
}
} }
await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } });
} }
if (isCoolifyProxyUsed) await startCoolifyProxy(engine); if (isCoolifyProxyUsed) await startCoolifyProxy(engine);
return destination.id; return destination.id;
} }
export async function removeDestination({ id }) { export async function removeDestination({ id }: Pick<DestinationDocker, 'id'>): Promise<void> {
const destination = await prisma.destinationDocker.delete({ where: { id } }); const destination = await prisma.destinationDocker.delete({ where: { id } });
if (destination.isCoolifyProxyUsed) { if (destination.isCoolifyProxyUsed) {
const host = getEngine(destination.engine); const host = getEngine(destination.engine);
@@ -123,22 +145,39 @@ export async function removeDestination({ id }) {
} }
} }
export async function getDestination({ id, teamId }) { export async function getDestination({
let destination = await prisma.destinationDocker.findFirst({ id,
where: { id, teams: { some: { id: teamId } } } teamId
}); }: FindDestinationFromTeam): Promise<DestinationDocker & { sshPrivateKey?: string }> {
if (destination.remoteEngine) { let destination;
destination.sshPrivateKey = decrypt(destination.sshPrivateKey); if (teamId === '0') {
destination = await prisma.destinationDocker.findFirst({
where: { id }
});
} else {
destination = await prisma.destinationDocker.findFirst({
where: { id, teams: { some: { id: teamId } } }
});
} }
return destination; return destination;
} }
export async function getDestinationByApplicationId({ id, teamId }) { export async function getDestinationByApplicationId({
id,
teamId
}: FindDestinationFromTeam): Promise<DestinationDocker> {
return await prisma.destinationDocker.findFirst({ return await prisma.destinationDocker.findFirst({
where: { application: { some: { id } }, teams: { some: { id: teamId } } } where: { application: { some: { id } }, teams: { some: { id: teamId } } }
}); });
} }
export async function setDestinationSettings({ engine, isCoolifyProxyUsed }) { export async function setDestinationSettings({
engine,
isCoolifyProxyUsed
}: {
engine: string;
isCoolifyProxyUsed: boolean;
}): Promise<Prisma.BatchPayload> {
return await prisma.destinationDocker.updateMany({ return await prisma.destinationDocker.updateMany({
where: { engine }, where: { engine },
data: { isCoolifyProxyUsed } data: { isCoolifyProxyUsed }

View File

@@ -1,27 +1,36 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
import type { GithubApp, GitlabApp, GitSource, Prisma, Application } from '@prisma/client';
export async function listSources(teamId) { export async function listSources(
teamId: string | Prisma.StringFilter
): Promise<(GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp })[]> {
if (teamId === '0') {
return await prisma.gitSource.findMany({
include: { githubApp: true, gitlabApp: true, teams: true }
});
}
return await prisma.gitSource.findMany({ return await prisma.gitSource.findMany({
where: { teams: { some: { id: teamId } } }, where: { teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true, teams: true }
}); });
} }
export async function newSource({ name, teamId, type, htmlUrl, apiUrl, organization }) { export async function newSource({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<GitSource> {
return await prisma.gitSource.create({ return await prisma.gitSource.create({
data: { data: {
teams: { connect: { id: teamId } },
name, name,
type, teams: { connect: { id: teamId } }
htmlUrl,
apiUrl,
organization
} }
}); });
} }
export async function removeSource({ id }) { export async function removeSource({ id }: { id: string }): Promise<void> {
// TODO: Disconnect application with this sourceId! Maybe not needed?
const source = await prisma.gitSource.delete({ const source = await prisma.gitSource.delete({
where: { id }, where: { id },
include: { githubApp: true, gitlabApp: true } include: { githubApp: true, gitlabApp: true }
@@ -30,11 +39,25 @@ export async function removeSource({ id }) {
if (source.gitlabAppId) await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } }); if (source.gitlabAppId) await prisma.gitlabApp.delete({ where: { id: source.gitlabAppId } });
} }
export async function getSource({ id, teamId }) { export async function getSource({
let body = await prisma.gitSource.findFirst({ id,
where: { id, teams: { some: { id: teamId } } }, teamId
include: { githubApp: true, gitlabApp: true } }: {
}); id: string;
teamId: string;
}): Promise<GitSource & { githubApp: GithubApp; gitlabApp: GitlabApp }> {
let body;
if (teamId === '0') {
body = await prisma.gitSource.findFirst({
where: { id },
include: { githubApp: true, gitlabApp: true }
});
} else {
body = await prisma.gitSource.findFirst({
where: { id, teams: { some: { id: teamId } } },
include: { githubApp: true, gitlabApp: true }
});
}
if (body?.githubApp?.clientSecret) if (body?.githubApp?.clientSecret)
body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret); body.githubApp.clientSecret = decrypt(body.githubApp.clientSecret);
if (body?.githubApp?.webhookSecret) if (body?.githubApp?.webhookSecret)
@@ -43,29 +66,69 @@ export async function getSource({ id, teamId }) {
if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret); if (body?.gitlabApp?.appSecret) body.gitlabApp.appSecret = decrypt(body.gitlabApp.appSecret);
return body; return body;
} }
export async function addSource({ id, appId, teamId, oauthId, groupName, appSecret }) { export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) {
const encrptedAppSecret = encrypt(appSecret); await prisma.gitSource.update({
where: { id },
data: { type, name, htmlUrl, apiUrl, organization }
});
return await prisma.githubApp.create({
data: {
teams: { connect: { id: teamId } },
gitSource: { connect: { id } }
}
});
}
export async function addGitLabSource({
id,
teamId,
type,
name,
htmlUrl,
apiUrl,
oauthId,
appId,
appSecret,
groupName
}) {
const encryptedAppSecret = encrypt(appSecret);
await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name } });
return await prisma.gitlabApp.create({ return await prisma.gitlabApp.create({
data: { data: {
teams: { connect: { id: teamId } }, teams: { connect: { id: teamId } },
appId, appId,
oauthId, oauthId,
groupName, groupName,
appSecret: encrptedAppSecret, appSecret: encryptedAppSecret,
gitSource: { connect: { id } } gitSource: { connect: { id } }
} }
}); });
} }
export async function configureGitsource({ id, gitSourceId }) { export async function configureGitsource({
id,
gitSourceId
}: {
id: string;
gitSourceId: string;
}): Promise<Application> {
return await prisma.application.update({ return await prisma.application.update({
where: { id }, where: { id },
data: { gitSource: { connect: { id: gitSourceId } } } data: { gitSource: { connect: { id: gitSourceId } } }
}); });
} }
export async function updateGitsource({ id, name }) { export async function updateGitsource({
id,
name,
htmlUrl,
apiUrl
}: {
id: string;
name: string;
htmlUrl: string;
apiUrl: string;
}): Promise<GitSource> {
return await prisma.gitSource.update({ return await prisma.gitSource.update({
where: { id }, where: { id },
data: { name } data: { name, htmlUrl, apiUrl }
}); });
} }

View File

@@ -1,7 +1,15 @@
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
import type { GithubApp } from '@prisma/client';
export async function addInstallation({ gitSourceId, installation_id }) { // TODO: We should change installation_id to be camelCase
export async function addInstallation({
gitSourceId,
installation_id
}: {
gitSourceId: string;
installation_id: string;
}): Promise<GithubApp> {
const source = await prisma.gitSource.findUnique({ const source = await prisma.gitSource.findUnique({
where: { id: gitSourceId }, where: { id: gitSourceId },
include: { githubApp: true } include: { githubApp: true }
@@ -12,8 +20,12 @@ export async function addInstallation({ gitSourceId, installation_id }) {
}); });
} }
export async function getUniqueGithubApp({ githubAppId }) { export async function getUniqueGithubApp({
let body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); githubAppId
}: {
githubAppId: string;
}): Promise<GithubApp> {
const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } });
if (body.privateKey) body.privateKey = decrypt(body.privateKey); if (body.privateKey) body.privateKey = decrypt(body.privateKey);
return body; return body;
} }
@@ -26,7 +38,15 @@ export async function createGithubApp({
pem, pem,
webhook_secret, webhook_secret,
state state
}) { }: {
id: number;
client_id: string;
slug: string;
client_secret: string;
pem: string;
webhook_secret: string;
state: string;
}): Promise<GithubApp> {
const encryptedClientSecret = encrypt(client_secret); const encryptedClientSecret = encrypt(client_secret);
const encryptedWebhookSecret = encrypt(webhook_secret); const encryptedWebhookSecret = encrypt(webhook_secret);
const encryptedPem = encrypt(pem); const encryptedPem = encrypt(pem);

View File

@@ -1,7 +1,14 @@
import { encrypt } from '$lib/crypto'; import { encrypt } from '$lib/crypto';
import { generateSshKeyPair, prisma } from './common'; import { generateSshKeyPair, prisma } from './common';
import type { GitlabApp } from '@prisma/client';
export async function updateDeployKey({ id, deployKeyId }) { export async function updateDeployKey({
id,
deployKeyId
}: {
id: string;
deployKeyId: number;
}): Promise<GitlabApp> {
const application = await prisma.application.findUnique({ const application = await prisma.application.findUnique({
where: { id }, where: { id },
include: { gitSource: { include: { gitlabApp: true } } } include: { gitSource: { include: { gitlabApp: true } } }
@@ -11,14 +18,24 @@ export async function updateDeployKey({ id, deployKeyId }) {
data: { deployKeyId } data: { deployKeyId }
}); });
} }
export async function getSshKey({ id }) { export async function getSshKey({
id
}: {
id: string;
}): Promise<{ status: number; body: { publicKey: string } }> {
const application = await prisma.application.findUnique({ const application = await prisma.application.findUnique({
where: { id }, where: { id },
include: { gitSource: { include: { gitlabApp: true } } } include: { gitSource: { include: { gitlabApp: true } } }
}); });
return { status: 200, body: { publicKey: application.gitSource.gitlabApp.publicSshKey } }; return { status: 200, body: { publicKey: application.gitSource.gitlabApp.publicSshKey } };
} }
export async function generateSshKey({ id }) { export async function generateSshKey({
id
}: {
id: string;
}): Promise<
{ status: number; body: { publicKey: string } } | { status: number; body?: undefined }
> {
const application = await prisma.application.findUnique({ const application = await prisma.application.findUnique({
where: { id }, where: { id },
include: { gitSource: { include: { gitlabApp: true } } } include: { gitSource: { include: { gitlabApp: true } } }

View File

@@ -1,6 +1,13 @@
import type { BuildLog } from '@prisma/client';
import { prisma, ErrorHandler } from './common'; import { prisma, ErrorHandler } from './common';
export async function listLogs({ buildId, last = 0 }) { export async function listLogs({
buildId,
last = 0
}: {
buildId: string;
last: number;
}): Promise<BuildLog[] | { status: number; body: { message: string; error: string } }> {
try { try {
const body = await prisma.buildLog.findMany({ const body = await prisma.buildLog.findMany({
where: { buildId, time: { gt: last } }, where: { buildId, time: { gt: last } },

View File

@@ -1,7 +1,8 @@
import { encrypt, decrypt } from '$lib/crypto'; import { encrypt, decrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
import type { ServiceSecret, Secret, Prisma } from '@prisma/client';
export async function listServiceSecrets(serviceId: string) { export async function listServiceSecrets(serviceId: string): Promise<ServiceSecret[]> {
let secrets = await prisma.serviceSecret.findMany({ let secrets = await prisma.serviceSecret.findMany({
where: { serviceId }, where: { serviceId },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
@@ -14,7 +15,7 @@ export async function listServiceSecrets(serviceId: string) {
return secrets; return secrets;
} }
export async function listSecrets(applicationId: string) { export async function listSecrets(applicationId: string): Promise<Secret[]> {
let secrets = await prisma.secret.findMany({ let secrets = await prisma.secret.findMany({
where: { applicationId }, where: { applicationId },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
@@ -27,20 +28,48 @@ export async function listSecrets(applicationId: string) {
return secrets; return secrets;
} }
export async function createServiceSecret({ id, name, value }) { export async function createServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<ServiceSecret> {
value = encrypt(value); value = encrypt(value);
return await prisma.serviceSecret.create({ return await prisma.serviceSecret.create({
data: { name, value, service: { connect: { id } } } data: { name, value, service: { connect: { id } } }
}); });
} }
export async function createSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { export async function createSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Secret> {
value = encrypt(value); value = encrypt(value);
return await prisma.secret.create({ return await prisma.secret.create({
data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } } data: { name, value, isBuildSecret, isPRMRSecret, application: { connect: { id } } }
}); });
} }
export async function updateServiceSecret({ id, name, value }) { export async function updateServiceSecret({
id,
name,
value
}: {
id: string;
name: string;
value: string;
}): Promise<Prisma.BatchPayload | ServiceSecret> {
value = encrypt(value); value = encrypt(value);
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } }); const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
@@ -55,7 +84,19 @@ export async function updateServiceSecret({ id, name, value }) {
}); });
} }
} }
export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecret }) { export async function updateSecret({
id,
name,
value,
isBuildSecret,
isPRMRSecret
}: {
id: string;
name: string;
value: string;
isBuildSecret: boolean;
isPRMRSecret: boolean;
}): Promise<Prisma.BatchPayload | Secret> {
value = encrypt(value); value = encrypt(value);
const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } }); const found = await prisma.secret.findFirst({ where: { applicationId: id, name, isPRMRSecret } });
@@ -71,10 +112,22 @@ export async function updateSecret({ id, name, value, isBuildSecret, isPRMRSecre
} }
} }
export async function removeServiceSecret({ id, name }) { export async function removeServiceSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } }); return await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
} }
export async function removeSecret({ id, name }) { export async function removeSecret({
id,
name
}: {
id: string;
name: string;
}): Promise<Prisma.BatchPayload> {
return await prisma.secret.deleteMany({ where: { applicationId: id, name } }); return await prisma.secret.deleteMany({ where: { applicationId: id, name } });
} }

View File

@@ -1,30 +1,53 @@
import { asyncExecShell, getEngine } from '$lib/common';
import { decrypt, encrypt } from '$lib/crypto'; import { decrypt, encrypt } from '$lib/crypto';
import type { Minio, Service } from '@prisma/client';
import cuid from 'cuid'; import cuid from 'cuid';
import { generatePassword } from '.'; import { generatePassword } from '.';
import { prisma } from './common'; import { prisma } from './common';
export async function listServices(teamId) { export async function listServices(teamId: string): Promise<Service[]> {
return await prisma.service.findMany({ where: { teams: { some: { id: teamId } } } }); if (teamId === '0') {
return await prisma.service.findMany({ include: { teams: true } });
} else {
return await prisma.service.findMany({
where: { teams: { some: { id: teamId } } },
include: { teams: true }
});
}
} }
export async function newService({ name, teamId }) { export async function newService({
name,
teamId
}: {
name: string;
teamId: string;
}): Promise<Service> {
return await prisma.service.create({ data: { name, teams: { connect: { id: teamId } } } }); return await prisma.service.create({ data: { name, teams: { connect: { id: teamId } } } });
} }
export async function getService({ id, teamId }) { export async function getService({ id, teamId }: { id: string; teamId: string }): Promise<Service> {
const body = await prisma.service.findFirst({ let body;
where: { id, teams: { some: { id: teamId } } }, const include = {
include: { destinationDocker: true,
destinationDocker: true, plausibleAnalytics: true,
plausibleAnalytics: true, minio: true,
minio: true, vscodeserver: true,
vscodeserver: true, wordpress: true,
wordpress: true, ghost: true,
ghost: true, serviceSecret: true,
serviceSecret: true meiliSearch: true
} };
}); if (teamId === '0') {
body = await prisma.service.findFirst({
where: { id },
include
});
} else {
body = await prisma.service.findFirst({
where: { id, teams: { some: { id: teamId } } },
include
});
}
if (body.plausibleAnalytics?.postgresqlPassword) if (body.plausibleAnalytics?.postgresqlPassword)
body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword = decrypt(
@@ -50,17 +73,29 @@ export async function getService({ id, teamId }) {
body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword); body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword);
if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword); if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword);
if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey);
if (body?.serviceSecret.length > 0) { if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => { body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value); s.value = decrypt(s.value);
return s; return s;
}); });
} }
if (body.wordpress?.ftpPassword) {
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
}
const settings = await prisma.setting.findFirst();
return { ...body }; return { ...body, settings };
} }
export async function configureServiceType({ id, type }) { export async function configureServiceType({
id,
type
}: {
id: string;
type: string;
}): Promise<void> {
if (type === 'plausibleanalytics') { if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword()); const password = encrypt(generatePassword());
const postgresqlUser = cuid(); const postgresqlUser = cuid();
@@ -142,7 +177,7 @@ export async function configureServiceType({ id, type }) {
} }
}); });
} else if (type === 'ghost') { } else if (type === 'ghost') {
const defaultEmail = `${cuid()}@coolify.io`; const defaultEmail = `${cuid()}@example.com`;
const defaultPassword = encrypt(generatePassword()); const defaultPassword = encrypt(generatePassword());
const mariadbUser = cuid(); const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword()); const mariadbPassword = encrypt(generatePassword());
@@ -165,55 +200,169 @@ export async function configureServiceType({ id, type }) {
} }
} }
}); });
} else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword(32));
await prisma.service.update({
where: { id },
data: {
type,
meiliSearch: { create: { masterKey } }
}
});
} }
} }
export async function setServiceVersion({ id, version }) {
export async function setServiceVersion({
id,
version
}: {
id: string;
version: string;
}): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { version } data: { version }
}); });
} }
export async function setServiceSettings({ id, dualCerts }) { export async function setServiceSettings({
id,
dualCerts
}: {
id: string;
dualCerts: boolean;
}): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { dualCerts } data: { dualCerts }
}); });
} }
export async function updatePlausibleAnalyticsService({ id, fqdn, email, username, name }) { export async function updatePlausibleAnalyticsService({
id,
fqdn,
email,
username,
name
}: {
id: string;
fqdn: string;
name: string;
email: string;
username: string;
}): Promise<void> {
await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } });
await prisma.service.update({ where: { id }, data: { name, fqdn } }); await prisma.service.update({ where: { id }, data: { name, fqdn } });
} }
export async function updateService({ id, fqdn, name }) {
export async function updateService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateLanguageToolService({ id, fqdn, name }) {
export async function updateLanguageToolService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateVaultWardenService({ id, fqdn, name }) {
export async function updateMeiliSearchService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateVsCodeServer({ id, fqdn, name }) {
export async function updateVaultWardenService({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } }); return await prisma.service.update({ where: { id }, data: { fqdn, name } });
} }
export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConfig }) {
export async function updateVsCodeServer({
id,
fqdn,
name
}: {
id: string;
fqdn: string;
name: string;
}): Promise<Service> {
return await prisma.service.update({ where: { id }, data: { fqdn, name } });
}
export async function updateWordpress({
id,
fqdn,
name,
mysqlDatabase,
extraConfig
}: {
id: string;
fqdn: string;
name: string;
mysqlDatabase: string;
extraConfig: string;
}): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } }
}); });
} }
export async function updateMinioService({ id, publicPort }) {
export async function updateMinioService({
id,
publicPort
}: {
id: string;
publicPort: number;
}): Promise<Minio> {
return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } });
} }
export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) {
export async function updateGhostService({
id,
fqdn,
name,
mariadbDatabase
}: {
id: string;
fqdn: string;
name: string;
mariadbDatabase: string;
}): Promise<Service> {
return await prisma.service.update({ return await prisma.service.update({
where: { id }, where: { id },
data: { fqdn, name, ghost: { update: { mariadbDatabase } } } data: { fqdn, name, ghost: { update: { mariadbDatabase } } }
}); });
} }
export async function removeService({ id }) { export async function removeService({ id }: { id: string }): Promise<void> {
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } }); await prisma.minio.deleteMany({ where: { serviceId: id } });

View File

@@ -1,8 +1,9 @@
import { decrypt } from '$lib/crypto'; import { decrypt } from '$lib/crypto';
import { prisma } from './common'; import { prisma } from './common';
import type { Setting } from '@prisma/client';
export async function listSettings() { export async function listSettings(): Promise<Setting> {
let settings = await prisma.setting.findFirst({}); const settings = await prisma.setting.findFirst({});
if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword); if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
return settings; return settings;
} }

View File

@@ -1,9 +1,10 @@
import type { Team, Permission } from '@prisma/client';
import { prisma } from './common'; import { prisma } from './common';
export async function listTeams() { export async function listTeams(): Promise<Team[]> {
return await prisma.team.findMany(); return await prisma.team.findMany();
} }
export async function newTeam({ name, userId }) { export async function newTeam({ name, userId }: { name: string; userId: string }): Promise<Team> {
return await prisma.team.create({ return await prisma.team.create({
data: { data: {
name, name,
@@ -12,7 +13,11 @@ export async function newTeam({ name, userId }) {
} }
}); });
} }
export async function getMyTeams({ userId }) { export async function getMyTeams({
userId
}: {
userId: string;
}): Promise<(Permission & { team: Team & { _count: { users: number } } })[]> {
return await prisma.permission.findMany({ return await prisma.permission.findMany({
where: { userId }, where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } } include: { team: { include: { _count: { select: { users: true } } } } }

View File

@@ -1,16 +1,30 @@
import cuid from 'cuid'; import cuid from 'cuid';
import bcrypt from 'bcrypt'; import bcrypt from 'bcryptjs';
import { prisma } from './common'; import { prisma } from './common';
import { asyncExecShell, uniqueName } from '$lib/common'; import { asyncExecShell, uniqueName } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { startCoolifyProxy } from '$lib/haproxy'; import { startCoolifyProxy } from '$lib/haproxy';
export async function hashPassword(password: string) { import type { User } from '@prisma/client';
export async function hashPassword(password: string): Promise<string> {
const saltRounds = 15; const saltRounds = 15;
return bcrypt.hash(password, saltRounds); return bcrypt.hash(password, saltRounds);
} }
export async function login({ email, password, isLogin }) {
export async function login({
email,
password,
isLogin
}: {
email: string;
password: string;
isLogin: boolean;
}): Promise<{
status: number;
headers: { 'Set-Cookie': string };
body: { userId: string; teamId: string; permission: string; isAdmin: boolean };
}> {
const users = await prisma.user.count(); const users = await prisma.user.count();
const userFound = await prisma.user.findUnique({ const userFound = await prisma.user.findUnique({
where: { email }, where: { email },
@@ -32,26 +46,46 @@ export async function login({ email, password, isLogin }) {
if (users === 0) { if (users === 0) {
await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } }); await prisma.setting.update({ where: { id }, data: { isRegistrationEnabled: false } });
// Create default network & start Coolify Proxy // Create default network & start Coolify Proxy
asyncExecShell(`docker network create --attachable coolify`) try {
.then(() => { await asyncExecShell(`docker network create --attachable coolify`);
console.log('Network created'); } catch (error) {}
}) try {
.catch(() => { await startCoolifyProxy('/var/run/docker.sock');
console.log('Network already exists.'); } catch (error) {}
});
startCoolifyProxy('/var/run/docker.sock')
.then(() => {
console.log('Coolify Proxy started.');
})
.catch((err) => {
console.log(err);
});
uid = '0'; uid = '0';
} }
if (userFound) { if (userFound) {
if (userFound.type === 'email') { if (userFound.type === 'email') {
if (userFound.password === 'RESETME') {
const hashedPassword = await hashPassword(password);
if (userFound.updatedAt < new Date(Date.now() - 1000 * 60 * 10)) {
await prisma.user.update({
where: { email: userFound.email },
data: { password: 'RESETTIMEOUT' }
});
throw {
error: 'Password reset link has expired. Please request a new one.'
};
} else {
await prisma.user.update({
where: { email: userFound.email },
data: { password: hashedPassword }
});
return {
status: 200,
headers: {
'Set-Cookie': `teamId=${uid}; HttpOnly; Path=/; Max-Age=15778800;`
},
body: {
userId: userFound.id,
teamId: userFound.id,
permission: userFound.permission,
isAdmin: true
}
};
}
}
const passwordMatch = await bcrypt.compare(password, userFound.password); const passwordMatch = await bcrypt.compare(password, userFound.password);
if (!passwordMatch) { if (!passwordMatch) {
throw { throw {
@@ -124,6 +158,6 @@ export async function login({ email, password, isLogin }) {
}; };
} }
export async function getUser({ userId }) { export async function getUser({ userId }: { userId: string }): Promise<User> {
return await prisma.user.findUnique({ where: { id: userId } }); return await prisma.user.findUnique({ where: { id: userId } });
} }

View File

@@ -88,12 +88,12 @@ export async function buildImage({
debug = false debug = false
}) { }) {
if (isCache) { if (isCache) {
saveBuildLog({ line: `Building cache image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building cache image started.`, buildId, applicationId });
} else { } else {
saveBuildLog({ line: `Building image started.`, buildId, applicationId }); await saveBuildLog({ line: `Building image started.`, buildId, applicationId });
} }
if (!debug && isCache) { if (!debug && isCache) {
saveBuildLog({ await saveBuildLog({
line: `Debug turned off. To see more details, allow it in the configuration.`, line: `Debug turned off. To see more details, allow it in the configuration.`,
buildId, buildId,
applicationId applicationId
@@ -126,13 +126,17 @@ export async function streamEvents({ stream, docker, buildId, applicationId, deb
if (err) reject(err); if (err) reject(err);
resolve(res); resolve(res);
} }
function onProgress(event) { async function onProgress(event) {
if (event.error) { if (event.error) {
reject(event.error); reject(event.error);
} else if (event.stream) { } else if (event.stream) {
if (event.stream !== '\n') { if (event.stream !== '\n') {
if (debug) if (debug)
saveBuildLog({ line: `${event.stream.replace('\n', '')}`, buildId, applicationId }); await saveBuildLog({
line: `${event.stream.replace('\n', '')}`,
buildId,
applicationId
});
} }
} }
} }

View File

@@ -1,5 +1,6 @@
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
export function errorNotification(message: string) {
export function errorNotification(message: string): void {
console.error(message); console.error(message);
if (typeof message !== 'string') { if (typeof message !== 'string') {
toast.push('Ooops, something is not okay, are you okay?'); toast.push('Ooops, something is not okay, are you okay?');
@@ -30,7 +31,7 @@ export function enhance(
e.preventDefault(); e.preventDefault();
let body = new FormData(form); let body = new FormData(form);
let parsedData = body; const parsedData = body;
body.forEach((data, key) => { body.forEach((data, key) => {
if (data === '' || data === null) parsedData.delete(key); if (data === '' || data === null) parsedData.delete(key);

View File

@@ -1,15 +1,15 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import got from 'got'; import got, { type Got } from 'got';
import * as db from '$lib/database';
import mustache from 'mustache'; import mustache from 'mustache';
import crypto from 'crypto'; import crypto from 'crypto';
import * as db from '$lib/database';
import { checkContainer, checkHAProxy } from '.'; import { checkContainer, checkHAProxy } from '.';
import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { asyncExecShell, getDomain, getEngine } from '$lib/common';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
let template = `program api const template = `program api
command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi command /usr/bin/dataplaneapi -f /usr/local/etc/haproxy/dataplaneapi.hcl --userlist haproxy-dataplaneapi
no option start-on-reload no option start-on-reload
@@ -20,10 +20,10 @@ global
defaults defaults
mode http mode http
log global log global
timeout http-request 60s timeout http-request 120s
timeout connect 10s timeout connect 10s
timeout client 60s timeout client 120s
timeout server 60s timeout server 120s
userlist haproxy-dataplaneapi userlist haproxy-dataplaneapi
user admin insecure-password "\${HAPROXY_PASSWORD}" user admin insecure-password "\${HAPROXY_PASSWORD}"
@@ -127,7 +127,8 @@ backend {{domain}}
server {{id}} {{id}}:{{port}} check fall 10 server {{id}} {{id}}:{{port}} check fall 10
{{/coolify}} {{/coolify}}
`; `;
export async function haproxyInstance() {
export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings(); const { proxyPassword } = await db.listSettings();
return got.extend({ return got.extend({
prefixUrl: url, prefixUrl: url,
@@ -136,113 +137,69 @@ export async function haproxyInstance() {
}); });
} }
export async function configureHAProxy() { export async function configureHAProxy(): Promise<void> {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
try { const data = {
const data = { applications: [],
applications: [], services: [],
services: [], coolify: []
coolify: [] };
}; const applications = await db.prisma.application.findMany({
const applications = await db.prisma.application.findMany({ include: { destinationDocker: true, settings: true }
include: { destinationDocker: true, settings: true } });
}); for (const application of applications) {
for (const application of applications) { const {
const { fqdn,
fqdn, id,
id, port,
port, destinationDocker,
destinationDocker, destinationDockerId,
destinationDockerId, settings: { previews },
settings: { previews }, updatedAt
updatedAt } = application;
} = application; if (destinationDockerId) {
if (destinationDockerId) { const { engine, network } = destinationDocker;
const { engine, network } = destinationDocker; const isRunning = await checkContainer(engine, id);
const isRunning = await checkContainer(engine, id); if (fqdn) {
if (fqdn) { const domain = getDomain(fqdn);
const domain = getDomain(fqdn); const isHttps = fqdn.startsWith('https://');
const isHttps = fqdn.startsWith('https://'); const isWWW = fqdn.includes('www.');
const isWWW = fqdn.includes('www.'); const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; if (isRunning) {
if (isRunning) { data.applications.push({
data.applications.push({ id,
id, port: port || 3000,
port: port || 3000, domain,
domain, isRunning,
isRunning, isHttps,
isHttps, redirectValue,
redirectValue, redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
redirectTo: isWWW ? domain : 'www.' + domain, updatedAt: updatedAt.getTime()
updatedAt: updatedAt.getTime() });
});
}
if (previews) {
const host = getEngine(engine);
const { stdout } = await asyncExecShell(
`DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
);
const containers = stdout
.trim()
.split('\n')
.filter((a) => a)
.map((c) => c.replace(/"/g, ''));
if (containers.length > 0) {
for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`;
data.applications.push({
id: container,
port: port || 3000,
domain: previewDomain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? previewDomain : 'www.' + previewDomain,
updatedAt: updatedAt.getTime()
});
}
}
}
} }
} if (previews) {
} const host = getEngine(engine);
const services = await db.prisma.service.findMany({ const { stdout } = await asyncExecShell(
include: { `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"`
destinationDocker: true, );
minio: true, const containers = stdout
plausibleAnalytics: true, .trim()
vscodeserver: true, .split('\n')
wordpress: true, .filter((a) => a)
ghost: true .map((c) => c.replace(/"/g, ''));
} if (containers.length > 0) {
}); for (const container of containers) {
const previewDomain = `${container.split('-')[1]}.${domain}`;
for (const service of services) { data.applications.push({
const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; id: container,
if (destinationDockerId) { port: port || 3000,
const { engine } = destinationDocker; domain: previewDomain,
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) {
const port = found.ports.main;
const publicPort = service[type]?.publicPort;
const isRunning = await checkContainer(engine, id);
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning, isRunning,
isHttps, isHttps,
redirectValue, redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain, redirectTo: isWWW ? previewDomain.replace('www.', '') : 'www.' + previewDomain,
updatedAt: updatedAt.getTime() updatedAt: updatedAt.getTime()
}); });
} }
@@ -250,37 +207,78 @@ export async function configureHAProxy() {
} }
} }
} }
const { fqdn } = await db.prisma.setting.findFirst(); }
if (fqdn) { const services = await db.prisma.service.findMany({
const domain = getDomain(fqdn); include: {
const isHttps = fqdn.startsWith('https://'); destinationDocker: true,
const isWWW = fqdn.includes('www.'); minio: true,
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`; plausibleAnalytics: true,
data.coolify.push({ vscodeserver: true,
id: dev ? 'host.docker.internal' : 'coolify', wordpress: true,
port: 3000, ghost: true,
domain, meiliSearch: true
isHttps,
redirectValue,
redirectTo: isWWW ? domain : 'www.' + domain
});
} }
const output = mustache.render(template, data); });
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings(); for (const service of services) {
if (proxyHash !== newHash) { const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service;
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } }); if (destinationDockerId) {
await haproxy.post(`v2/services/haproxy/configuration/raw`, { const { engine } = destinationDocker;
searchParams: { const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
skip_version: true if (found) {
}, const port = found.ports.main;
body: output, const publicPort = service[type]?.publicPort;
headers: { const isRunning = await checkContainer(engine, id);
'Content-Type': 'text/plain' if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
if (isRunning) {
data.services.push({
id,
port,
publicPort,
domain,
isRunning,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain,
updatedAt: updatedAt.getTime()
});
}
} }
}); }
} }
} catch (error) { }
throw error; const { fqdn } = await db.prisma.setting.findFirst();
if (fqdn) {
const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
const isWWW = fqdn.includes('www.');
const redirectValue = `${isHttps ? 'https://' : 'http://'}${domain}%[capture.req.uri]`;
data.coolify.push({
id: dev ? 'host.docker.internal' : 'coolify',
port: 3000,
domain,
isHttps,
redirectValue,
redirectTo: isWWW ? domain.replace('www.', '') : 'www.' + domain
});
}
const output = mustache.render(template, data);
const newHash = crypto.createHash('md5').update(output).digest('hex');
const { proxyHash, id } = await db.listSettings();
if (proxyHash !== newHash) {
await db.prisma.setting.update({ where: { id }, data: { proxyHash: newHash } });
await haproxy.post(`v2/services/haproxy/configuration/raw`, {
searchParams: {
skip_version: true
},
body: output,
headers: {
'Content-Type': 'text/plain'
}
});
} }
} }

View File

@@ -1,7 +1,8 @@
import { dev } from '$app/env'; import { dev } from '$app/env';
import { asyncExecShell, getEngine } from '$lib/common'; import { asyncExecShell, getEngine } from '$lib/common';
import got from 'got'; import got, { type Got, type Response } from 'got';
import * as db from '$lib/database'; import * as db from '$lib/database';
import type { DestinationDocker } from '@prisma/client';
const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555';
@@ -9,7 +10,7 @@ export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
export async function haproxyInstance() { export async function haproxyInstance(): Promise<Got> {
const { proxyPassword } = await db.listSettings(); const { proxyPassword } = await db.listSettings();
return got.extend({ return got.extend({
prefixUrl: url, prefixUrl: url,
@@ -17,6 +18,7 @@ export async function haproxyInstance() {
password: proxyPassword password: proxyPassword
}); });
} }
export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> { export async function getRawConfiguration(): Promise<RawHaproxyConfiguration> {
return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json(); return await (await haproxyInstance()).get(`v2/services/haproxy/configuration/raw`).json();
} }
@@ -43,11 +45,12 @@ export async function getNextTransactionId(): Promise<string> {
return newTransaction.id; return newTransaction.id;
} }
export async function completeTransaction(transactionId) { export async function completeTransaction(transactionId: string): Promise<Response<string>> {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`); return await haproxy.put(`v2/services/haproxy/transactions/${transactionId}`);
} }
export async function deleteProxy({ id }) {
export async function deleteProxy({ id }: { id: string }): Promise<void> {
const haproxy = await haproxyInstance(); const haproxy = await haproxyInstance();
await checkHAProxy(haproxy); await checkHAProxy(haproxy);
@@ -77,11 +80,12 @@ export async function deleteProxy({ id }) {
} }
} }
export async function reloadHaproxy(engine) { export async function reloadHaproxy(engine: string): Promise<{ stdout: string; stderr: string }> {
const host = getEngine(engine); const host = getEngine(engine);
return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`); return await asyncExecShell(`DOCKER_HOST=${host} docker exec coolify-haproxy kill -HUP 1`);
} }
export async function checkHAProxy(haproxy?: any) {
export async function checkHAProxy(haproxy?: Got): Promise<void> {
if (!haproxy) haproxy = await haproxyInstance(); if (!haproxy) haproxy = await haproxyInstance();
try { try {
await haproxy.get('v2/info'); await haproxy.get('v2/info');
@@ -93,7 +97,10 @@ export async function checkHAProxy(haproxy?: any) {
} }
} }
export async function stopTcpHttpProxy(destinationDocker, publicPort) { export async function stopTcpHttpProxy(
destinationDocker: DestinationDocker,
publicPort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { engine } = destinationDocker; const { engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
const containerName = `haproxy-for-${publicPort}`; const containerName = `haproxy-for-${publicPort}`;
@@ -108,7 +115,13 @@ export async function stopTcpHttpProxy(destinationDocker, publicPort) {
return error; return error;
} }
} }
export async function startTcpProxy(destinationDocker, id, publicPort, privatePort) { export async function startTcpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number,
volume?: string
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker; const { network, engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
@@ -123,14 +136,22 @@ export async function startTcpProxy(destinationDocker, id, publicPort, privatePo
); );
const ip = JSON.parse(Config)[0].Gateway; const ip = JSON.parse(Config)[0].Gateway;
return await asyncExecShell( return await asyncExecShell(
`DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}` `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${
volume ? `-v ${volume}` : ''
} -d coollabsio/${defaultProxyImageTcp}`
); );
} }
} catch (error) { } catch (error) {
return error; return error;
} }
} }
export async function startHttpProxy(destinationDocker, id, publicPort, privatePort) {
export async function startHttpProxy(
destinationDocker: DestinationDocker,
id: string,
publicPort: number,
privatePort: number
): Promise<{ stdout: string; stderr: string } | Error> {
const { network, engine } = destinationDocker; const { network, engine } = destinationDocker;
const host = getEngine(engine); const host = getEngine(engine);
@@ -152,7 +173,8 @@ export async function startHttpProxy(destinationDocker, id, publicPort, privateP
return error; return error;
} }
} }
export async function startCoolifyProxy(engine) {
export async function startCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy'); const found = await checkContainer(engine, 'coolify-haproxy');
const { proxyPassword, proxyUser, id } = await db.listSettings(); const { proxyPassword, proxyUser, id } = await db.listSettings();
@@ -168,7 +190,8 @@ export async function startCoolifyProxy(engine) {
} }
await configureNetworkCoolifyProxy(engine); await configureNetworkCoolifyProxy(engine);
} }
export async function checkContainer(engine, container) {
export async function checkContainer(engine: string, container: string): Promise<boolean> {
const host = getEngine(engine); const host = getEngine(engine);
let containerFound = false; let containerFound = false;
@@ -178,7 +201,7 @@ export async function checkContainer(engine, container) {
); );
const parsedStdout = JSON.parse(stdout); const parsedStdout = JSON.parse(stdout);
const status = parsedStdout.Status; const status = parsedStdout.Status;
const isRunning = status === 'running' ? true : false; const isRunning = status === 'running';
if (status === 'exited' || status === 'created') { if (status === 'exited' || status === 'created') {
await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`); await asyncExecShell(`DOCKER_HOST="${host}" docker rm ${container}`);
} }
@@ -191,7 +214,9 @@ export async function checkContainer(engine, container) {
return containerFound; return containerFound;
} }
export async function stopCoolifyProxy(engine) { export async function stopCoolifyProxy(
engine: string
): Promise<{ stdout: string; stderr: string } | Error> {
const host = getEngine(engine); const host = getEngine(engine);
const found = await checkContainer(engine, 'coolify-haproxy'); const found = await checkContainer(engine, 'coolify-haproxy');
await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false });
@@ -208,16 +233,18 @@ export async function stopCoolifyProxy(engine) {
} }
} }
export async function configureNetworkCoolifyProxy(engine) { export async function configureNetworkCoolifyProxy(engine: string): Promise<void> {
const host = getEngine(engine); const host = getEngine(engine);
const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } });
destinations.forEach(async (destination) => { const { stdout: networks } = await asyncExecShell(
try { `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-haproxy --format '{{json .Networks}}'`
);
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
for (const destination of destinations) {
if (!configuredNetworks.includes(destination.network)) {
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy` `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-haproxy`
); );
} catch (err) {
// TODO: handle error
} }
}); }
} }

View File

@@ -2,50 +2,55 @@ import { asyncExecShell, saveBuildLog } from '$lib/common';
import got from 'got'; import got from 'got';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database';
export default async function ({ export default async function ({
applicationId, applicationId,
debug,
workdir, workdir,
githubAppId, githubAppId,
repository, repository,
apiUrl,
htmlUrl,
branch, branch,
buildId buildId
}): Promise<any> { }: {
try { applicationId: string;
saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId }); workdir: string;
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId }); githubAppId: string;
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); repository: string;
apiUrl: string;
htmlUrl: string;
branch: string;
buildId: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '');
await saveBuildLog({ line: 'GitHub importer started.', buildId, applicationId });
const { privateKey, appId, installationId } = await db.getUniqueGithubApp({ githubAppId });
const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, '');
const payload = { const payload = {
iat: Math.round(new Date().getTime() / 1000), iat: Math.round(new Date().getTime() / 1000),
exp: Math.round(new Date().getTime() / 1000 + 60), exp: Math.round(new Date().getTime() / 1000 + 60),
iss: appId iss: appId
}; };
const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, { const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, {
algorithm: 'RS256' algorithm: 'RS256'
}); });
const { token } = await got const { token } = await got
.post(`https://api.github.com/app/installations/${installationId}/access_tokens`, { .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, {
headers: { headers: {
Authorization: `Bearer ${jwtToken}`, Authorization: `Bearer ${jwtToken}`,
Accept: 'application/vnd.github.machine-man-preview+json' Accept: 'application/vnd.github.machine-man-preview+json'
} }
}) })
.json(); .json();
saveBuildLog({ await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`, line: `Cloning ${repository}:${branch} branch.`,
buildId, buildId,
applicationId applicationId
}); });
await asyncExecShell( await asyncExecShell(
`git clone -q -b ${branch} https://x-access-token:${token}@github.com/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && cd ..` `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git ${workdir}/ && cd ${workdir} && git submodule update --init --recursive && git lfs pull && cd .. `
); );
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');
} catch (error) {
console.log({ error });
return ErrorHandler(error);
}
} }

View File

@@ -1,28 +1,37 @@
import { asyncExecShell, saveBuildLog } from '$lib/common'; import { asyncExecShell, saveBuildLog } from '$lib/common';
import { ErrorHandler } from '$lib/database';
export default async function ({ export default async function ({
applicationId, applicationId,
debug,
workdir, workdir,
repodir, repodir,
htmlUrl,
repository, repository,
branch, branch,
buildId, buildId,
privateSshKey privateSshKey
}): Promise<any> { }: {
saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId }); applicationId: string;
workdir: string;
repository: string;
htmlUrl: string;
branch: string;
buildId: string;
repodir: string;
privateSshKey: string;
}): Promise<string> {
const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, '');
await saveBuildLog({ line: 'GitLab importer started.', buildId, applicationId });
await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`);
await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); await asyncExecShell(`chmod 600 ${repodir}/id.rsa`);
saveBuildLog({ await saveBuildLog({
line: `Cloning ${repository}:${branch} branch.`, line: `Cloning ${repository}:${branch} branch.`,
buildId, buildId,
applicationId applicationId
}); });
await asyncExecShell( await asyncExecShell(
`git clone -q -b ${branch} git@gitlab.com:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && cd ..` `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git submodule update --init --recursive && git lfs pull && cd .. `
); );
const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`);
return commit.replace('\n', ''); return commit.replace('\n', '');

View File

@@ -3,9 +3,12 @@ import { checkContainer, reloadHaproxy } from '$lib/haproxy';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import cuid from 'cuid'; import cuid from 'cuid';
import fs from 'fs/promises';
import getPort, { portNumbers } from 'get-port'; import getPort, { portNumbers } from 'get-port';
import { supportedServiceTypesAndVersions } from '$lib/components/common';
import { promises as dns } from 'dns';
export async function letsEncrypt(domain, id = null, isCoolify = false) { export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise<void> {
try { try {
const data = await db.prisma.setting.findFirst(); const data = await db.prisma.setting.findFirst();
const { minPort, maxPort } = data; const { minPort, maxPort } = data;
@@ -96,7 +99,7 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) {
} }
} }
export async function generateSSLCerts() { export async function generateSSLCerts(): Promise<void> {
const ssls = []; const ssls = [];
const applications = await db.prisma.application.findMany({ const applications = await db.prisma.application.findMany({
include: { destinationDocker: true, settings: true }, include: { destinationDocker: true, settings: true },
@@ -129,7 +132,7 @@ export async function generateSSLCerts() {
.map((c) => c.replace(/"/g, '')); .map((c) => c.replace(/"/g, ''));
if (containers.length > 0) { if (containers.length > 0) {
for (const container of containers) { for (const container of containers) {
let previewDomain = `${container.split('-')[1]}.${domain}`; const previewDomain = `${container.split('-')[1]}.${domain}`;
if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false });
} }
} }
@@ -146,7 +149,8 @@ export async function generateSSLCerts() {
plausibleAnalytics: true, plausibleAnalytics: true,
vscodeserver: true, vscodeserver: true,
wordpress: true, wordpress: true,
ghost: true ghost: true,
meiliSearch: true
}, },
orderBy: { createdAt: 'desc' } orderBy: { createdAt: 'desc' }
}); });
@@ -160,7 +164,7 @@ export async function generateSSLCerts() {
type, type,
destinationDocker: { engine } destinationDocker: { engine }
} = service; } = service;
const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); const found = supportedServiceTypesAndVersions.find((a) => a.name === type);
if (found) { if (found) {
const domain = getDomain(fqdn); const domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://'); const isHttps = fqdn.startsWith('https://');
@@ -181,12 +185,89 @@ export async function generateSSLCerts() {
if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true }); if (isHttps) ssls.push({ domain, id: 'coolify', isCoolify: true });
} }
if (ssls.length > 0) { if (ssls.length > 0) {
const sslDir = dev ? '/tmp/ssl' : '/app/ssl';
if (dev) {
try {
await asyncExecShell(`mkdir -p ${sslDir}`);
} catch (error) {
//
}
}
const files = await fs.readdir(sslDir);
let certificates = [];
if (files.length > 0) {
for (const file of files) {
file.endsWith('.pem') && certificates.push(file.replace(/\.pem$/, ''));
}
}
const resolver = new dns.Resolver({ timeout: 2000 });
resolver.setServers(['8.8.8.8', '1.1.1.1']);
let ipv4, ipv6;
try {
ipv4 = await (await asyncExecShell(`curl -4s https://ifconfig.io`)).stdout;
} catch (error) {}
try {
ipv6 = await (await asyncExecShell(`curl -6s https://ifconfig.io`)).stdout;
} catch (error) {}
for (const ssl of ssls) { for (const ssl of ssls) {
if (!dev) { if (!dev) {
console.log('Checking SSL for', ssl.domain); if (
await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify); certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
// console.log(`Certificate for ${ssl.domain} already exists`);
} else {
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return await letsEncrypt(ssl.domain, ssl.id, ssl.isCoolify);
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
} else { } else {
console.log('Checking SSL for', ssl.domain); if (
certificates.includes(ssl.domain) ||
certificates.includes(ssl.domain.replace('www.', ''))
) {
console.log(`Certificate for ${ssl.domain} already exists`);
} else {
// Checking DNS entry before generating certificate
if (ipv4 || ipv6) {
let domains4 = [];
let domains6 = [];
try {
domains4 = await resolver.resolve4(ssl.domain);
} catch (error) {}
try {
domains6 = await resolver.resolve6(ssl.domain);
} catch (error) {}
if (domains4.length > 0 || domains6.length > 0) {
if (
(ipv4 && domains4.includes(ipv4.replace('\n', ''))) ||
(ipv6 && domains6.includes(ipv6.replace('\n', '')))
) {
console.log('Generating SSL for', ssl.domain);
return;
}
}
}
console.log('DNS settings is incorrect for', ssl.domain, 'skipping.');
}
} }
} }
} }

View File

@@ -20,30 +20,22 @@ import {
setDefaultConfiguration setDefaultConfiguration
} from '$lib/buildPacks/common'; } from '$lib/buildPacks/common';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import type { Job } from 'bullmq';
import type { BuilderJob } from '$lib/types/builderJob';
export default async function (job) { import type { ComposeFile } from '$lib/types/composeFile';
/*
Edge cases: export default async function (job: Job<BuilderJob, void, string>): Promise<void> {
1 - Change build pack and redeploy, what should happen? const {
*/
let {
id: applicationId, id: applicationId,
repository, repository,
branch,
buildPack,
name, name,
destinationDocker, destinationDocker,
destinationDockerId, destinationDockerId,
gitSource, gitSource,
build_id: buildId, build_id: buildId,
configHash, configHash,
port,
installCommand,
buildCommand,
startCommand,
fqdn, fqdn,
baseDirectory,
publishDirectory,
projectId, projectId,
secrets, secrets,
phpModules, phpModules,
@@ -51,14 +43,27 @@ export default async function (job) {
pullmergeRequestId = null, pullmergeRequestId = null,
sourceBranch = null, sourceBranch = null,
settings, settings,
persistentStorage persistentStorage,
pythonWSGI,
pythonModule,
pythonVariable
} = job.data;
let {
branch,
buildPack,
port,
installCommand,
buildCommand,
startCommand,
baseDirectory,
publishDirectory
} = job.data; } = job.data;
const { debug } = settings; const { debug } = settings;
await asyncSleep(500); await asyncSleep(500);
await db.prisma.build.updateMany({ await db.prisma.build.updateMany({
where: { where: {
status: 'queued', status: { in: ['queued', 'running'] },
id: { not: buildId }, id: { not: buildId },
applicationId, applicationId,
createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) } createdAt: { lt: new Date(new Date().getTime() - 60 * 60 * 1000) }
@@ -67,7 +72,7 @@ export default async function (job) {
}); });
let imageId = applicationId; let imageId = applicationId;
let domain = getDomain(fqdn); let domain = getDomain(fqdn);
let volumes = const volumes =
persistentStorage?.map((storage) => { persistentStorage?.map((storage) => {
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ return `${applicationId}${storage.path.replace(/\//gi, '-')}:${
buildPack !== 'docker' ? '/app' : '' buildPack !== 'docker' ? '/app' : ''
@@ -103,7 +108,7 @@ export default async function (job) {
publishDirectory = configuration.publishDirectory; publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory; baseDirectory = configuration.baseDirectory;
let commit = await importers[gitSource.type]({ const commit = await importers[gitSource.type]({
applicationId, applicationId,
debug, debug,
workdir, workdir,
@@ -114,6 +119,7 @@ export default async function (job) {
branch, branch,
buildId, buildId,
apiUrl: gitSource.apiUrl, apiUrl: gitSource.apiUrl,
htmlUrl: gitSource.htmlUrl,
projectId, projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null, deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null privateSshKey: decrypt(gitSource.gitlabApp?.privateSshKey) || null
@@ -127,7 +133,7 @@ export default async function (job) {
} }
try { try {
db.prisma.build.update({ where: { id: buildId }, data: { commit } }); await db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) { } catch (err) {
console.log(err); console.log(err);
} }
@@ -157,7 +163,7 @@ export default async function (job) {
}); });
deployNeeded = true; deployNeeded = true;
if (configHash) { if (configHash) {
saveBuildLog({ line: 'Configuration changed.', buildId, applicationId }); await saveBuildLog({ line: 'Configuration changed.', buildId, applicationId });
} }
} else { } else {
deployNeeded = false; deployNeeded = false;
@@ -200,16 +206,17 @@ export default async function (job) {
startCommand, startCommand,
baseDirectory, baseDirectory,
secrets, secrets,
phpModules phpModules,
pythonWSGI,
pythonModule,
pythonVariable
}); });
else { else {
saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId });
throw new Error(`Build pack ${buildPack} not found.`); throw new Error(`Build pack ${buildPack} not found.`);
} }
deployNeeded = true;
} else { } else {
deployNeeded = false; await saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
saveBuildLog({ line: 'Nothing changed.', buildId, applicationId });
} }
// Deploy to Docker Engine // Deploy to Docker Engine
@@ -259,15 +266,7 @@ export default async function (job) {
// //
} }
try { try {
saveBuildLog({ line: 'Deployment started.', buildId, applicationId }); await saveBuildLog({ line: 'Deployment started.', buildId, applicationId });
// for await (const volume of volumes) {
// const id = volume.split(':')[0];
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume inspect ${id}`);
// } catch (error) {
// await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}`);
// }
// }
const composeVolumes = volumes.map((volume) => { const composeVolumes = volumes.map((volume) => {
return { return {
[`${volume.split(':')[0]}`]: { [`${volume.split(':')[0]}`]: {
@@ -275,7 +274,7 @@ export default async function (job) {
} }
}; };
}); });
const compose = { const composeFile: ComposeFile = {
version: '3.8', version: '3.8',
services: { services: {
[imageId]: { [imageId]: {
@@ -284,9 +283,17 @@ export default async function (job) {
volumes, volumes,
env_file: envFound ? [`${workdir}/.env`] : [], env_file: envFound ? [`${workdir}/.env`] : [],
networks: [docker.network], networks: [docker.network],
labels: labels, labels,
depends_on: [], depends_on: [],
restart: 'always' restart: 'always',
deploy: {
restart_policy: {
condition: 'on-failure',
delay: '5s',
max_attempts: 3,
window: '120s'
}
}
} }
}, },
networks: { networks: {
@@ -296,23 +303,16 @@ export default async function (job) {
}, },
volumes: Object.assign({}, ...composeVolumes) volumes: Object.assign({}, ...composeVolumes)
}; };
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(compose)); await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
await asyncExecShell( await asyncExecShell(
`DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d` `DOCKER_HOST=${host} docker compose --project-directory ${workdir} up -d`
); );
await saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
// const { stderr } = await asyncExecShell(
// `DOCKER_HOST=${host} docker run ${envFound && `--env-file=${workdir}/.env`} ${labels.join(
// ' '
// )} --name ${imageId} --network ${docker.network} --restart always ${volumes.length > 0 ? volumes : ''
// } -d ${applicationId}:${tag}`
// );
saveBuildLog({ line: 'Deployment successful!', buildId, applicationId });
} catch (error) { } catch (error) {
saveBuildLog({ line: error, buildId, applicationId }); await saveBuildLog({ line: error, buildId, applicationId });
sentry.captureException(error); sentry.captureException(error);
throw new Error(error); throw new Error(error);
} }
saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId }); await saveBuildLog({ line: 'Proxy will be updated shortly.', buildId, applicationId });
} }
} }

View File

@@ -1,8 +1,6 @@
import { dev } from '$app/env';
import { asyncExecShell, getEngine, version } from '$lib/common'; import { asyncExecShell, getEngine, version } from '$lib/common';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import { defaultProxyImageHttp, defaultProxyImageTcp } from '$lib/haproxy'; export default async function (): Promise<void> {
export default async function () {
const destinationDockers = await prisma.destinationDocker.findMany(); const destinationDockers = await prisma.destinationDocker.findMany();
for (const destinationDocker of destinationDockers) { for (const destinationDocker of destinationDockers) {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
@@ -16,56 +14,23 @@ export default async function () {
await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`); await asyncExecShell(`DOCKER_HOST=${host} docker rmi -f ${images}`);
} }
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`); await asyncExecShell(`DOCKER_HOST=${host} docker container prune -f`);
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
} }
try { try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`); await asyncExecShell(`DOCKER_HOST=${host} docker image prune -f --filter "until=2h"`);
} catch (error) { } catch (error) {
console.log(error); //console.log(error);
}
// Cleanup old images older than a day
try {
await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=24h" -a -f`);
} catch (error) {
//console.log(error);
} }
// Tagging images with labels
// try {
// const images = [
// `coollabsio/${defaultProxyImageTcp}`,
// `coollabsio/${defaultProxyImageHttp}`,
// 'certbot/certbot:latest',
// 'node:16.14.0-alpine',
// 'alpine:latest',
// 'nginx:stable-alpine',
// 'node:lts',
// 'php:apache',
// 'rust:latest'
// ];
// for (const image of images) {
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image inspect ${image}`);
// } catch (error) {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker pull ${image} && echo "FROM ${image}" | docker build --label coolify.image="true" -t "${image}" -`
// );
// }
// }
// } catch (error) {}
// if (!dev) {
// // Cleanup images that are not managed by coolify
// try {
// await asyncExecShell(
// `DOCKER_HOST=${host} docker image prune --filter 'label!=coolify.image=true' -a -f`
// );
// } catch (error) {
// console.log(error);
// }
// // Cleanup old images >3 days
// try {
// await asyncExecShell(`DOCKER_HOST=${host} docker image prune --filter "until=72h" -a -f`);
// } catch (error) {
// console.log(error);
// }
// }
} }
} }

View File

@@ -1,6 +1,5 @@
import * as Bullmq from 'bullmq'; import * as Bullmq from 'bullmq';
import { default as ProdBullmq, Job, QueueEvents, QueueScheduler } from 'bullmq'; import { default as ProdBullmq, QueueScheduler } from 'bullmq';
import cuid from 'cuid';
import { dev } from '$app/env'; import { dev } from '$app/env';
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
@@ -28,7 +27,7 @@ const connectionOptions = {
} }
}; };
const cron = async () => { const cron = async (): Promise<void> => {
new QueueScheduler('proxy', connectionOptions); new QueueScheduler('proxy', connectionOptions);
new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('cleanup', connectionOptions);
new QueueScheduler('ssl', connectionOptions); new QueueScheduler('ssl', connectionOptions);
@@ -89,18 +88,6 @@ const cron = async () => {
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = {
proxy: new QueueEvents('proxy', { ...connectionOptions }),
ssl: new QueueEvents('ssl', { ...connectionOptions })
};
events.proxy.on('completed', (data) => {
// console.log(data)
});
events.ssl.on('completed', (data) => {
// console.log(data)
});
}; };
cron().catch((error) => { cron().catch((error) => {
console.log('cron failed to start'); console.log('cron failed to start');
@@ -118,10 +105,14 @@ buildWorker.on('completed', async (job: Bullmq.Job) => {
try { try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} catch (error) { } catch (error) {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
}, 1234);
console.log(error); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`; const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} }
return; return;
}); });
@@ -130,17 +121,21 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
try { try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } }); await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
} catch (error) { } catch (error) {
setTimeout(async () => {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
}, 1234);
console.log(error); console.log(error);
} finally { } finally {
const workdir = `/tmp/build-sources/${job.data.repository}`; const workdir = `/tmp/build-sources/${job.data.repository}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`); if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } });
} }
saveBuildLog({ await saveBuildLog({
line: 'Failed to deploy!', line: 'Failed to deploy!',
buildId: job.data.build_id, buildId: job.data.build_id,
applicationId: job.data.id applicationId: job.data.id
}); });
saveBuildLog({ await saveBuildLog({
line: `Reason: ${failedReason.toString()}`, line: `Reason: ${failedReason.toString()}`,
buildId: job.data.build_id, buildId: job.data.build_id,
applicationId: job.data.id applicationId: job.data.id

View File

@@ -1,7 +1,8 @@
import { prisma } from '$lib/database'; import { prisma } from '$lib/database';
import { dev } from '$app/env'; import { dev } from '$app/env';
import type { Job } from 'bullmq';
export default async function (job) { export default async function (job: Job): Promise<void> {
const { line, applicationId, buildId } = job.data; const { line, applicationId, buildId } = job.data;
if (dev) console.debug(`[${applicationId}] ${line}`); if (dev) console.debug(`[${applicationId}] ${line}`);
await prisma.buildLog.create({ data: { line, buildId, time: Number(job.id), applicationId } }); await prisma.buildLog.create({ data: { line, buildId, time: Number(job.id), applicationId } });

View File

@@ -1,7 +1,10 @@
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import { configureHAProxy } from '$lib/haproxy/configuration'; import { configureHAProxy } from '$lib/haproxy/configuration';
export default async function () { export default async function (): Promise<void | {
status: number;
body: { message: string; error: string };
}> {
try { try {
return await configureHAProxy(); return await configureHAProxy();
} catch (error) { } catch (error) {

View File

@@ -1,6 +1,6 @@
import { generateSSLCerts } from '$lib/letsencrypt'; import { generateSSLCerts } from '$lib/letsencrypt';
export default async function () { export default async function (): Promise<void> {
try { try {
return await generateSSLCerts(); return await generateSSLCerts();
} catch (error) { } catch (error) {

View File

@@ -1,13 +1,9 @@
import { asyncExecShell } from '$lib/common'; import { asyncExecShell } from '$lib/common';
import { reloadHaproxy } from '$lib/haproxy'; import { reloadHaproxy } from '$lib/haproxy';
export default async function () { export default async function (): Promise<void> {
try { await asyncExecShell(
await asyncExecShell( `docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew`
`docker run --rm --name certbot-renewal -v "coolify-letsencrypt:/etc/letsencrypt" certbot/certbot --logs-dir /etc/letsencrypt/logs renew` );
); await reloadHaproxy('unix:///var/run/docker.sock');
await reloadHaproxy('unix:///var/run/docker.sock');
} catch (error) {
throw error;
}
} }

View File

@@ -1,8 +1,6 @@
export const publicPaths = [ export const publicPaths = [
'/login', '/login',
'/register', '/register',
'/reset',
'/reset/password',
'/webhooks/success', '/webhooks/success',
'/webhooks/github', '/webhooks/github',
'/webhooks/github/install', '/webhooks/github/install',

View File

@@ -1,6 +1,7 @@
import { writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
export const gitTokens = writable({ export const gitTokens: Writable<{ githubToken: string | null; gitlabToken: string | null }> =
githubToken: null, writable({
gitlabToken: null githubToken: null,
}); gitlabToken: null
});

View File

@@ -0,0 +1,51 @@
import type { DestinationDocker, GithubApp, GitlabApp, GitSource, Secret } from '@prisma/client';
export type BuilderJob = {
build_id: string;
type: BuildType;
id: string;
name: string;
fqdn: string;
repository: string;
configHash: unknown;
branch: string;
buildPack: BuildPackName;
projectId: number;
port: number;
installCommand: string;
buildCommand?: string;
startCommand?: string;
baseDirectory: string;
publishDirectory: string;
phpModules: string;
pythonWSGI: string;
pythonModule: string;
pythonVariable: string;
createdAt: string;
updatedAt: string;
destinationDockerId: string;
destinationDocker: DestinationDocker;
gitSource: GitSource & { githubApp?: GithubApp; gitlabApp?: GitlabApp };
settings: BuilderJobSettings;
secrets: Secret[];
persistentStorage: { path: string }[];
pullmergeRequestId?: unknown;
sourceBranch?: string;
};
// TODO: Add the other build types
export type BuildType = 'manual';
// TODO: Add the other buildpack names
export type BuildPackName = 'node' | 'docker';
export type BuilderJobSettings = {
id: string;
applicationId: string;
dualCerts: boolean;
debug: boolean;
previews: boolean;
autodeploy: boolean;
createdAt: string;
updatedAt: string;
};

View File

@@ -0,0 +1,61 @@
export type ComposeFile = {
version: ComposerFileVersion;
services: Record<string, ComposeFileService>;
networks: Record<string, ComposeFileNetwork>;
volumes?: Record<string, ComposeFileVolume>;
};
export type ComposeFileService = {
container_name: string;
image?: string;
networks: string[];
environment?: Record<string, unknown>;
volumes?: string[];
ulimits?: unknown;
labels?: string[];
env_file?: string[];
extra_hosts?: string[];
restart: ComposeFileRestartOption;
depends_on?: string[];
command?: string;
build?: {
context: string;
dockerfile: string;
args?: Record<string, unknown>;
};
deploy?: {
restart_policy?: {
condition?: string;
delay?: string;
max_attempts?: number;
window?: string;
};
};
};
export type ComposerFileVersion =
| '3.8'
| '3.7'
| '3.6'
| '3.5'
| '3.4'
| '3.3'
| '3.2'
| '3.1'
| '3.0'
| '2.4'
| '2.3'
| '2.2'
| '2.1'
| '2.0';
export type ComposeFileRestartOption = 'no' | 'always' | 'on-failure' | 'unless-stopped';
export type ComposeFileNetwork = {
external: boolean;
};
export type ComposeFileVolume = {
external?: boolean;
name?: string;
};

View File

@@ -0,0 +1,8 @@
export type CreateDockerDestination = {
name: string;
engine: string;
remoteEngine: boolean;
network: string;
isCoolifyProxyUsed: boolean;
teamId: string;
};

View File

@@ -12,7 +12,7 @@
if (!session.userId) { if (!session.userId) {
return {}; return {};
} }
const endpoint = `/teams.json`; const endpoint = `/dashboard.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
@@ -134,13 +134,18 @@
<svelte:head> <svelte:head>
<title>Coolify</title> <title>Coolify</title>
{#if !$session.whiteLabeled}
<link rel="icon" href="/favicon.png" />
{/if}
</svelte:head> </svelte:head>
<SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} /> <SvelteToast options={{ intro: { y: -64 }, duration: 3000, pausable: true }} />
{#if $session.userId} {#if $session.userId}
<nav class="nav-main"> <nav class="nav-main">
<div class="flex h-screen w-full flex-col items-center transition-all duration-100"> <div class="flex h-screen w-full flex-col items-center transition-all duration-100">
<div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div> {#if !$session.whiteLabeled}
<div class="flex flex-col space-y-4 py-2"> <div class="my-4 h-10 w-10"><img src="/favicon.png" alt="coolLabs logo" /></div>
{/if}
<div class="flex flex-col space-y-4 py-2" class:mt-2={$session.whiteLabeled}>
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/" href="/"
@@ -171,7 +176,7 @@
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/applications" href="/applications"
class="icons tooltip-right bg-coolgray-200 hover:text-green-500" class="icons tooltip-green-500 tooltip-right bg-coolgray-200 hover:text-green-500"
class:text-green-500={$page.url.pathname.startsWith('/applications') || class:text-green-500={$page.url.pathname.startsWith('/applications') ||
$page.url.pathname.startsWith('/new/application')} $page.url.pathname.startsWith('/new/application')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') || class:bg-coolgray-500={$page.url.pathname.startsWith('/applications') ||
@@ -199,7 +204,7 @@
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/sources" href="/sources"
class="icons tooltip-right bg-coolgray-200 hover:text-orange-500" class="icons tooltip-orange-500 tooltip-right bg-coolgray-200 hover:text-orange-500"
class:text-orange-500={$page.url.pathname.startsWith('/sources') || class:text-orange-500={$page.url.pathname.startsWith('/sources') ||
$page.url.pathname.startsWith('/new/source')} $page.url.pathname.startsWith('/new/source')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') || class:bg-coolgray-500={$page.url.pathname.startsWith('/sources') ||
@@ -229,7 +234,7 @@
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/destinations" href="/destinations"
class="icons tooltip-right bg-coolgray-200 hover:text-sky-500" class="icons tooltip-sky-500 tooltip-right bg-coolgray-200 hover:text-sky-500"
class:text-sky-500={$page.url.pathname.startsWith('/destinations') || class:text-sky-500={$page.url.pathname.startsWith('/destinations') ||
$page.url.pathname.startsWith('/new/destination')} $page.url.pathname.startsWith('/new/destination')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') || class:bg-coolgray-500={$page.url.pathname.startsWith('/destinations') ||
@@ -264,7 +269,7 @@
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/databases" href="/databases"
class="icons tooltip-right bg-coolgray-200 hover:text-purple-500" class="icons tooltip-purple-500 tooltip-right bg-coolgray-200 hover:text-purple-500"
class:text-purple-500={$page.url.pathname.startsWith('/databases') || class:text-purple-500={$page.url.pathname.startsWith('/databases') ||
$page.url.pathname.startsWith('/new/database')} $page.url.pathname.startsWith('/new/database')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') || class:bg-coolgray-500={$page.url.pathname.startsWith('/databases') ||
@@ -291,7 +296,7 @@
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/services" href="/services"
class="icons tooltip-right bg-coolgray-200 hover:text-pink-500" class="icons tooltip-pink-500 tooltip-right bg-coolgray-200 hover:text-pink-500"
class:text-pink-500={$page.url.pathname.startsWith('/services') || class:text-pink-500={$page.url.pathname.startsWith('/services') ||
$page.url.pathname.startsWith('/new/service')} $page.url.pathname.startsWith('/new/service')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/services') || class:bg-coolgray-500={$page.url.pathname.startsWith('/services') ||
@@ -312,7 +317,6 @@
<path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" /> <path d="M7 18a4.6 4.4 0 0 1 0 -9a5 4.5 0 0 1 11 2h1a3.5 3.5 0 0 1 0 7h-12" />
</svg> </svg>
</a> </a>
<div class="border-t border-stone-700" />
</div> </div>
<div class="flex-1" /> <div class="flex-1" />
@@ -344,7 +348,7 @@
{:else if updateStatus.success === null} {:else if updateStatus.success === null}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="w-8 h-9" class="h-9 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
@@ -359,7 +363,7 @@
<line x1="16" y1="12" x2="12" y2="8" /> <line x1="16" y1="12" x2="12" y2="8" />
</svg> </svg>
{:else if updateStatus.success} {:else if updateStatus.success}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path ><path
fill="#DD2E44" fill="#DD2E44"
d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z" d="M11.626 7.488c-.112.112-.197.247-.268.395l-.008-.008L.134 33.141l.011.011c-.208.403.14 1.223.853 1.937.713.713 1.533 1.061 1.936.853l.01.01L28.21 24.735l-.008-.009c.147-.07.282-.155.395-.269 1.562-1.562-.971-6.627-5.656-11.313-4.687-4.686-9.752-7.218-11.315-5.656z"
@@ -404,7 +408,7 @@
/></svg /></svg
> >
{:else} {:else}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="w-8 h-9" <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" class="h-9 w-8"
><path ><path
fill="#FFCC4D" fill="#FFCC4D"
d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18" d="M36 18c0 9.941-8.059 18-18 18S0 27.941 0 18 8.059 0 18 0s18 8.059 18 18"
@@ -430,13 +434,12 @@
<div class="flex flex-col space-y-4 py-2"> <div class="flex flex-col space-y-4 py-2">
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/teams" href="/iam"
class="icons tooltip-right bg-coolgray-200 hover:text-cyan-500" class="icons tooltip-fuchsia-500 tooltip-right bg-coolgray-200 hover:text-fuchsia-500"
class:text-cyan-500={$page.url.pathname.startsWith('/teams')} class:text-fuchsia-500={$page.url.pathname.startsWith('/iam')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/teams')} class:bg-coolgray-500={$page.url.pathname.startsWith('/iam')}
data-tooltip="Teams" data-tooltip="IAM"
> ><svg
<svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8" class="h-8 w-8"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -453,11 +456,12 @@
<path d="M21 21v-2a4 4 0 0 0 -3 -3.85" /> <path d="M21 21v-2a4 4 0 0 0 -3 -3.85" />
</svg> </svg>
</a> </a>
{#if $session.teamId === '0'} {#if $session.teamId === '0'}
<a <a
sveltekit:prefetch sveltekit:prefetch
href="/settings" href="/settings"
class="icons tooltip-right bg-coolgray-200 hover:text-yellow-500" class="icons tooltip-yellow-500 tooltip-right bg-coolgray-200 hover:text-yellow-500"
class:text-yellow-500={$page.url.pathname.startsWith('/settings')} class:text-yellow-500={$page.url.pathname.startsWith('/settings')}
class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')} class:bg-coolgray-500={$page.url.pathname.startsWith('/settings')}
data-tooltip="Settings" data-tooltip="Settings"
@@ -480,8 +484,9 @@
</svg> </svg>
</a> </a>
{/if} {/if}
<div <div
class="icons tooltip-right bg-coolgray-200 hover:text-red-500" class="icons tooltip-red-500 tooltip-right bg-coolgray-200 hover:text-red-500"
data-tooltip="Logout" data-tooltip="Logout"
on:click={logout} on:click={logout}
> >
@@ -514,8 +519,14 @@
</div> </div>
</div> </div>
</nav> </nav>
{#if $session.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
>
{/if}
<select <select
class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4" class="fixed right-0 bottom-0 z-50 m-2 w-64 bg-opacity-30 p-2 px-4 hover:bg-opacity-100"
bind:value={selectedTeamId} bind:value={selectedTeamId}
on:change={switchTeam} on:change={switchTeam}
> >

View File

@@ -88,7 +88,6 @@
try { try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application }); const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.'); toast.push('Deployment queued.');
console.log($page.url);
if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) {
return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`);
} else { } else {
@@ -255,9 +254,9 @@
class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`} class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/secrets`}
> >
<button <button
title="Secrets" title="Secret"
class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500" class="icons bg-transparent tooltip-bottom text-sm disabled:text-red-500"
data-tooltip="Secrets" data-tooltip="Secret"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@@ -36,8 +36,15 @@
}); });
} }
async function loadBranchesByPage(page = 0) {
return await get(`${apiUrl}/repos/${selected.repository}/branches?per_page=100&page=${page}`, {
Authorization: `token ${$gitTokens.githubToken}`
});
}
let reposSelectOptions; let reposSelectOptions;
let branchSelectOptions; let branchSelectOptions;
async function loadRepositories() { async function loadRepositories() {
let page = 1; let page = 1;
let reposCount = 0; let reposCount = 0;
@@ -58,24 +65,28 @@
})); }));
} }
async function loadBranches(event) { async function loadBranches(event) {
branches = [];
selected.repository = event.detail.value; selected.repository = event.detail.value;
loading.branches = true;
selected.branch = undefined;
selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id; selected.projectId = repositories.find((repo) => repo.full_name === selected.repository).id;
try { let page = 1;
branches = await get(`${apiUrl}/repos/${selected.repository}/branches`, { let branchCount = 0;
Authorization: `token ${$gitTokens.githubToken}` loading.branches = true;
}); const loadedBranches = await loadBranchesByPage();
branchSelectOptions = branches.map((branch) => ({ branches = branches.concat(loadedBranches);
value: branch.name, branchCount = branches.length;
label: branch.name if (branchCount === 100) {
})); while (branchCount === 100) {
return; page = page + 1;
} catch ({ error }) { const nextBranches = await loadBranchesByPage(page);
return errorNotification(error); branches = branches.concat(nextBranches);
} finally { branchCount = nextBranches.length;
loading.branches = false; }
} }
loading.branches = false;
branchSelectOptions = branches.map((branch) => ({
value: branch.name,
label: branch.name
}));
} }
async function isBranchAlreadyUsed(event) { async function isBranchAlreadyUsed(event) {
selected.branch = event.detail.value; selected.branch = event.detail.value;
@@ -166,30 +177,36 @@
{:else} {:else}
<form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center"> <form on:submit|preventDefault={handleSubmit} class="flex flex-col justify-center text-center">
<div class="flex-col space-y-3 md:space-y-0 space-x-1"> <div class="flex-col space-y-3 md:space-y-0 space-x-1">
<div class="flex gap-4"> <div class="flex-col md:flex gap-4">
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.repositories placeholder={loading.repositories
? 'Loading repositories ...' ? 'Loading repositories...'
: 'Please select a repository'} : 'Please select a repository'}
id="repository" id="repository"
showIndicator={true}
isWaiting={loading.repositories}
on:select={loadBranches} on:select={loadBranches}
items={reposSelectOptions} items={reposSelectOptions}
isDisabled={loading.repositories} isDisabled={loading.repositories}
isClearable={false}
/> />
</div> </div>
<input class="hidden" bind:value={selected.projectId} name="projectId" /> <input class="hidden" bind:value={selected.projectId} name="projectId" />
<div class="custom-select-wrapper"> <div class="custom-select-wrapper">
<Select <Select
placeholder={loading.branches placeholder={loading.branches
? 'Loading branches ...' ? 'Loading branches...'
: !selected.repository : !selected.repository
? 'Please select a repository first' ? 'Please select a repository first'
: 'Please select a branch'} : 'Please select a branch'}
id="repository" isWaiting={loading.branches}
showIndicator={selected.repository}
id="branches"
on:select={isBranchAlreadyUsed} on:select={isBranchAlreadyUsed}
items={branchSelectOptions} items={branchSelectOptions}
isDisabled={loading.branches || !selected.repository} isDisabled={loading.branches || !selected.repository}
isClearable={false}
/> />
</div> </div>
</div> </div>
@@ -202,13 +219,6 @@
class:bg-orange-600={showSave} class:bg-orange-600={showSave}
class:hover:bg-orange-500={showSave}>Save</button class:hover:bg-orange-500={showSave}>Save</button
> >
<!-- <button class="w-40"
><a
class="no-underline"
href="{apiUrl}/apps/{application.gitSource.githubApp.name}/installations/new"
>Modify Repositories</a
></button
> -->
</div> </div>
</form> </form>
{/if} {/if}

View File

@@ -81,6 +81,9 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'blob');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'blob');
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'blob'
);
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
@@ -103,7 +106,7 @@
foundConfig = findBuildPack('python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php'); foundConfig = findBuildPack('php');
} else { } else {
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);
@@ -127,6 +130,9 @@
); );
const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file'); const indexHtml = files.find((file) => file.name === 'index.html' && file.type === 'file');
const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file'); const indexPHP = files.find((file) => file.name === 'index.php' && file.type === 'file');
const composerPHP = files.find(
(file) => file.name === 'composer.json' && file.type === 'file'
);
if (yarnLock) packageManager = 'yarn'; if (yarnLock) packageManager = 'yarn';
if (pnpmLock) packageManager = 'pnpm'; if (pnpmLock) packageManager = 'pnpm';
@@ -146,7 +152,7 @@
foundConfig = findBuildPack('python'); foundConfig = findBuildPack('python');
} else if (indexHtml) { } else if (indexHtml) {
foundConfig = findBuildPack('static', packageManager); foundConfig = findBuildPack('static', packageManager);
} else if (indexPHP) { } else if (indexPHP || composerPHP) {
foundConfig = findBuildPack('php'); foundConfig = findBuildPack('php');
} else { } else {
foundConfig = findBuildPack('node', packageManager); foundConfig = findBuildPack('node', packageManager);

View File

@@ -29,7 +29,7 @@
<script lang="ts"> <script lang="ts">
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { page } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';
@@ -39,6 +39,16 @@
export let destinations: Prisma.DestinationDocker[]; export let destinations: Prisma.DestinationDocker[];
const ownDestinations = destinations.filter((destination) => {
if (destination.teams[0].id === $session.teamId) {
return destination;
}
});
const otherDestinations = destinations.filter((destination) => {
if (destination.teams[0].id !== $session.teamId) {
return destination;
}
});
async function handleSubmit(destinationId) { async function handleSubmit(destinationId) {
try { try {
await post(`/applications/${id}/configuration/destination.json`, { destinationId }); await post(`/applications/${id}/configuration/destination.json`, { destinationId });
@@ -52,8 +62,8 @@
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Configure Destination</div> <div class="mr-4 text-2xl tracking-tight">Configure Destination</div>
</div> </div>
<div class="flex justify-center"> <div class="flex flex-col justify-center">
{#if !destinations || destinations.length === 0} {#if !destinations || ownDestinations.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">No configurable Destination found</div> <div class="pb-2">No configurable Destination found</div>
<div class="flex justify-center"> <div class="flex justify-center">
@@ -75,8 +85,23 @@
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each destinations as destination} {#each ownDestinations as destination}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold">
<div class="font-bold text-xl text-center truncate">{destination.name}</div>
<div class="text-center truncate">{destination.network}</div>
</button>
</form>
</div>
{/each}
</div>
{#if otherDestinations.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Destinations</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherDestinations as destination}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(destination.id)}> <form on:submit|preventDefault={() => handleSubmit(destination.id)}>
<button type="submit" class="box-selection hover:bg-sky-700 font-bold"> <button type="submit" class="box-selection hover:bg-sky-700 font-bold">

View File

@@ -29,7 +29,7 @@
<script lang="ts"> <script lang="ts">
import type Prisma from '@prisma/client'; import type Prisma from '@prisma/client';
import { page } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { post } from '$lib/api'; import { post } from '$lib/api';
@@ -46,6 +46,17 @@
(source.type === 'github' && source.githubAppId && source.githubApp.installationId) || (source.type === 'github' && source.githubAppId && source.githubApp.installationId) ||
(source.type === 'gitlab' && source.gitlabAppId) (source.type === 'gitlab' && source.gitlabAppId)
); );
const ownSources = filteredSources.filter((source) => {
if (source.teams[0].id === $session.teamId) {
return source;
}
});
const otherSources = filteredSources.filter((source) => {
if (source.teams[0].id !== $session.teamId) {
return source;
}
});
async function handleSubmit(gitSourceId) { async function handleSubmit(gitSourceId) {
try { try {
await post(`/applications/${id}/configuration/source.json`, { gitSourceId }); await post(`/applications/${id}/configuration/source.json`, { gitSourceId });
@@ -54,17 +65,21 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function newSource() {
const { id } = await post('/sources/new', {});
return await goto(`/sources/${id}`, { replaceState: true });
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
<div class="mr-4 text-2xl tracking-tight">Select a Git Source</div> <div class="mr-4 text-2xl tracking-tight">Select a Git Source</div>
</div> </div>
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
{#if !filteredSources || filteredSources.length === 0} {#if !filteredSources || ownSources.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="pb-2">No configurable Git Source found</div> <div class="pb-2 text-center">No configurable Git Source found</div>
<div class="flex justify-center"> <div class="flex justify-center">
<a href="/new/source" sveltekit:prefetch class="add-icon bg-orange-600 hover:bg-orange-500"> <button on:click={newSource} class="add-icon bg-orange-600 hover:bg-orange-500">
<svg <svg
class="w-6" class="w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -78,12 +93,39 @@
d="M12 6v6m0 0v6m0-6h6m-6 0H6" d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/></svg /></svg
> >
</a> </button>
</div> </div>
</div> </div>
{:else} {:else}
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each filteredSources as source} {#each ownSources as source}
<div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button
disabled={source.gitlabApp && !source.gitlabAppId}
type="submit"
class="disabled:opacity-95 bg-coolgray-200 disabled:text-white box-selection hover:bg-orange-700 group"
class:border-red-500={source.gitlabApp && !source.gitlabAppId}
class:border-0={source.gitlabApp && !source.gitlabAppId}
class:border-l-4={source.gitlabApp && !source.gitlabAppId}
>
<div class="font-bold text-xl text-center truncate">{source.name}</div>
{#if source.gitlabApp && !source.gitlabAppId}
<div class="font-bold text-center truncate text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</button>
</form>
</div>
{/each}
</div>
{#if otherSources.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Sources</div>
{/if}
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherSources as source}
<div class="p-2"> <div class="p-2">
<form on:submit|preventDefault={() => handleSubmit(source.id)}> <form on:submit|preventDefault={() => handleSubmit(source.id)}>
<button <button

View File

@@ -14,6 +14,7 @@ export const del: RequestHandler = async (event) => {
status: 200 status: 200
}; };
} catch (error) { } catch (error) {
console.log(error);
return ErrorHandler(error); return ErrorHandler(error);
} }
}; };

View File

@@ -5,6 +5,7 @@ import { checkContainer } from '$lib/haproxy';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
import jsonwebtoken from 'jsonwebtoken'; import jsonwebtoken from 'jsonwebtoken';
import { get as getRequest } from '$lib/api'; import { get as getRequest } from '$lib/api';
import { setDefaultConfiguration } from '$lib/buildPacks/common';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
@@ -52,12 +53,23 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable
} = await event.request.json(); } = await event.request.json();
if (port) port = Number(port); if (port) port = Number(port);
try { try {
const defaultConfiguration = await setDefaultConfiguration({
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
});
await db.configureApplication({ await db.configureApplication({
id, id,
buildPack, buildPack,
@@ -68,7 +80,11 @@ export const post: RequestHandler = async (event) => {
buildCommand, buildCommand,
startCommand, startCommand,
baseDirectory, baseDirectory,
publishDirectory publishDirectory,
pythonWSGI,
pythonModule,
pythonVariable,
...defaultConfiguration
}); });
return { status: 201 }; return { status: 201 };
} catch (error) { } catch (error) {

View File

@@ -38,6 +38,7 @@
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Select from 'svelte-select';
import Explainer from '$lib/components/Explainer.svelte'; import Explainer from '$lib/components/Explainer.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@@ -57,6 +58,23 @@
let previews = application.settings.previews; let previews = application.settings.previews;
let dualCerts = application.settings.dualCerts; let dualCerts = application.settings.dualCerts;
let autodeploy = application.settings.autodeploy; let autodeploy = application.settings.autodeploy;
let wsgis = [
{
value: 'None',
label: 'None'
},
{
value: 'Gunicorn',
label: 'Gunicorn'
}
// },
// {
// value: 'uWSGI',
// label: 'uWSGI'
// }
];
if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) {
application.fqdn = `http://${cuid()}.demo.coolify.io`; application.fqdn = `http://${cuid()}.demo.coolify.io`;
} }
@@ -111,7 +129,7 @@
await post(`/applications/${id}.json`, { ...application }); await post(`/applications/${id}.json`, { ...application });
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
if (error.startsWith('DNS not set')) { if (error?.startsWith('DNS not set')) {
forceSave = true; forceSave = true;
} }
return errorNotification(error); return errorNotification(error);
@@ -119,12 +137,19 @@
loading = false; loading = false;
} }
} }
async function selectWSGI(event) {
application.pythonWSGI = event.detail.value;
}
</script> </script>
<div class="flex items-center space-x-2 p-5 px-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block"> <div class="-mb-5 flex-col">
{application.name} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn} {#if application.fqdn}
<a <a
href={application.fqdn} href={application.fqdn}
@@ -282,14 +307,14 @@
<div class="grid grid-flow-row gap-2 px-10"> <div class="grid grid-flow-row gap-2 px-10">
<div class="grid grid-cols-2"> <div class="grid grid-cols-2">
<div class="flex-col"> <div class="flex-col">
<label for="fqdn" class="pt-2 text-base font-bold text-stone-100">Domain (FQDN)</label> <label for="fqdn" class="pt-2 text-base font-bold text-stone-100">URL (FQDN)</label>
{#if browser && window.location.hostname === 'demo.coolify.io'} {#if browser && window.location.hostname === 'demo.coolify.io'}
<Explainer <Explainer
text="<span class='text-white font-bold'>You can use the predefined random domain name or enter your own domain name.</span>" text="<span class='text-white font-bold'>You can use the predefined random url name or enter your own domain name.</span>"
/> />
{/if} {/if}
<Explainer <Explainer
text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the domain, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>" text="If you specify <span class='text-green-500 font-bold'>https</span>, the application will be accessible only over https. SSL certificate will be generated for you.<br>If you specify <span class='text-green-500 font-bold'>www</span>, the application will be redirected (302) from non-www and vice versa.<br><br>To modify the url, you must first stop the application.<br><br><span class='text-white font-bold'>You must set your DNS to point to the server IP in advance.</span>"
/> />
</div> </div>
<input <input
@@ -315,6 +340,39 @@
on:click={() => !isRunning && changeSettings('dualCerts')} on:click={() => !isRunning && changeSettings('dualCerts')}
/> />
</div> </div>
{#if application.buildPack === 'python'}
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">WSGI</label>
<div class="custom-select-wrapper">
<Select id="wsgi" items={wsgis} on:select={selectWSGI} value={application.pythonWSGI} />
</div>
</div>
<div class="grid grid-cols-2 items-center">
<label for="pythonModule" class="text-base font-bold text-stone-100">Module</label>
<input
readonly={!$session.isAdmin}
name="pythonModule"
id="pythonModule"
required
bind:value={application.pythonModule}
placeholder={application.pythonWSGI?.toLowerCase() !== 'gunicorn' ? 'main.py' : 'main'}
/>
</div>
{#if application.pythonWSGI?.toLowerCase() === 'gunicorn'}
<div class="grid grid-cols-2 items-center">
<label for="pythonVariable" class="text-base font-bold text-stone-100">Variable</label>
<input
readonly={!$session.isAdmin}
name="pythonVariable"
id="pythonVariable"
required
bind:value={application.pythonVariable}
placeholder="default: app"
/>
</div>
{/if}
{/if}
{#if !staticDeployments.includes(application.buildPack)} {#if !staticDeployments.includes(application.buildPack)}
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="port" class="text-base font-bold text-stone-100">Port</label> <label for="port" class="text-base font-bold text-stone-100">Port</label>
@@ -323,7 +381,7 @@
name="port" name="port"
id="port" id="port"
bind:value={application.port} bind:value={application.port}
placeholder="default: 3000" placeholder={application.buildPack === 'python' ? '8000' : '3000'}
/> />
</div> </div>
{/if} {/if}

View File

@@ -21,7 +21,7 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import { dateOptions, getDomain } from '$lib/components/common'; import { changeQueryParams, dateOptions, getDomain } from '$lib/components/common';
import BuildLog from './_BuildLog.svelte'; import BuildLog from './_BuildLog.svelte';
import { get } from '$lib/api'; import { get } from '$lib/api';
@@ -79,16 +79,81 @@
noMoreBuilds = true; noMoreBuilds = true;
} }
} }
async function loadBuild(build) { function loadBuild(build) {
buildId = build; buildId = build;
await goto(`/applications/${id}/logs/build?buildId=${buildId}`); return changeQueryParams(buildId);
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Build logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Build Logs</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div> </div>
<div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex"> <div class="block flex-row justify-start space-x-2 px-5 pt-6 sm:px-10 md:flex">
<div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 "> <div class="mb-4 min-w-[16rem] space-y-2 md:mb-0 ">
@@ -130,9 +195,14 @@
</div> </div>
{/each} {/each}
</div> </div>
<div class="flex space-x-2"> {#if !noMoreBuilds}
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button> {#if buildCount > 5}
</div> <div class="flex space-x-2">
<button disabled={noMoreBuilds} class="w-full" on:click={loadMoreBuilds}>Load More</button
>
</div>
{/if}
{/if}
</div> </div>
<div class="flex-1 md:w-96"> <div class="flex-1 md:w-96">
{#if buildId} {#if buildId}

View File

@@ -68,16 +68,83 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Application logs of <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Application Logs
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div> </div>
<div class="flex flex-row justify-center space-x-2 px-10 pt-6"> <div class="flex flex-row justify-center space-x-2 px-10 pt-6">
{#if logs.length === 0} {#if logs.length === 0}
<div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div> <div class="text-xl font-bold tracking-tighter">Waiting for the logs...</div>
{:else} {:else}
<div class="relative"> <div class="relative w-full">
<LoadingLogs /> <LoadingLogs />
<div class="flex justify-end sticky top-0 p-2"> <div class="flex justify-end sticky top-0 p-2">
<button <button
@@ -105,7 +172,7 @@
</button> </button>
</div> </div>
<div <div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200" class="font-mono w-full leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl} bind:this={logsEl}
> >
<div class="px-2"> <div class="px-2">

View File

@@ -11,7 +11,6 @@
} }
}; };
} }
return { return {
status: res.status, status: res.status,
error: new Error(`Could not load ${endpoint}`) error: new Error(`Could not load ${endpoint}`)
@@ -50,14 +49,88 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Previews for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Preview Deployments
</div>
<span class="text-xs">{application.name} </span>
</div> </div>
</div>
{#if applicationSecrets.length !== 0} {#if application.fqdn}
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div>
<div class="mx-auto max-w-6xl px-6 pt-4">
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div>
{#if applicationSecrets.length !== 0}
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@@ -84,16 +157,9 @@
{/each} {/each}
</tbody> </tbody>
</table> </table>
</div> {/if}
{/if}
<div class="flex justify-center py-4 text-center">
<Explainer
customClass="w-full"
text={applicationSecrets.length === 0
? "You can add secrets to PR/MR deployments. Please add secrets to the application first. <br>Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."
: "These values overwrite application secrets in PR/MR deployments. Useful for creating <span class='text-green-500 font-bold'>staging</span> environments."}
/>
</div> </div>
<div class="mx-auto max-w-4xl py-10"> <div class="mx-auto max-w-4xl py-10">
<div class="flex flex-wrap justify-center space-x-2"> <div class="flex flex-wrap justify-center space-x-2">
{#if containers.length > 0} {#if containers.length > 0}

View File

@@ -0,0 +1,48 @@
<script>
export let secrets;
export let refreshSecrets;
export let id;
import { saveSecret } from './utils';
import pLimit from 'p-limit';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
let batchSecrets = '';
function setBatchValue(event) {
batchSecrets = event.target?.value;
}
const limit = pLimit(1);
async function getValues(e) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, value] = secret.split('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew }))
)
);
batchSecrets = '';
refreshSecrets();
}
</script>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
>
</form>

View File

@@ -9,11 +9,12 @@
if (isPRMRSecret) value = PRMRSecret.value; if (isPRMRSecret) value = PRMRSecret.value;
import { page } from '$app/stores'; import { page } from '$app/stores';
import { del, post } from '$lib/api'; import { del } from '$lib/api';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import { errorNotification } from '$lib/form'; import { errorNotification } from '$lib/form';
import { toast } from '@zerodevx/svelte-toast'; import { toast } from '@zerodevx/svelte-toast';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { saveSecret } from './utils';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const { id } = $page.params; const { id } = $page.params;
@@ -30,31 +31,41 @@
return errorNotification(error); return errorNotification(error);
} }
} }
async function saveSecret(isNew = false) {
if (!name) return errorNotification('Name is required.'); async function createSecret(isNew) {
if (!value) return errorNotification('Value is required.'); await saveSecret({
try { isNew,
await post(`/applications/${id}/secrets.json`, { name,
name, value,
value, isBuildSecret,
isBuildSecret, isPRMRSecret,
isPRMRSecret, isNewSecret,
isNew applicationId: id
}); });
dispatch('refresh');
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
toast.push('Secret saved.');
} catch ({ error }) {
return errorNotification(error);
}
}
function setSecretValue() {
if (isNewSecret) { if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
dispatch('refresh');
toast.push('Secret saved');
}
async function setSecretValue() {
if (!isPRMRSecret) {
isBuildSecret = !isBuildSecret; isBuildSecret = !isBuildSecret;
if (!isNewSecret) {
await saveSecret({
isNew: isNewSecret,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId: id
});
toast.push('Secret saved');
}
} }
} }
</script> </script>
@@ -89,9 +100,9 @@
class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out" class="relative inline-flex h-6 w-11 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out"
class:bg-green-600={isBuildSecret} class:bg-green-600={isBuildSecret}
class:bg-stone-700={!isBuildSecret} class:bg-stone-700={!isBuildSecret}
class:opacity-50={!isNewSecret} class:opacity-50={isPRMRSecret}
class:cursor-not-allowed={!isNewSecret} class:cursor-not-allowed={isPRMRSecret}
class:cursor-pointer={isNewSecret} class:cursor-pointer={!isPRMRSecret}
> >
<span class="sr-only">Use isBuildSecret</span> <span class="sr-only">Use isBuildSecret</span>
<span <span
@@ -133,12 +144,14 @@
<td> <td>
{#if isNewSecret} {#if isNewSecret}
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="bg-green-600 hover:bg-green-500" on:click={() => saveSecret(true)}>Add</button> <button class="bg-green-600 hover:bg-green-500" on:click={() => createSecret(true)}
>Add</button
>
</div> </div>
{:else} {:else}
<div class="flex flex-row justify-center space-x-2"> <div class="flex flex-row justify-center space-x-2">
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<button class="" on:click={() => saveSecret(false)}>Set</button> <button class="" on:click={() => createSecret(false)}>Set</button>
</div> </div>
{#if !isPRMRSecret} {#if !isPRMRSecret}
<div class="flex justify-center items-end"> <div class="flex justify-center items-end">

View File

@@ -1,10 +1,10 @@
import { getTeam, getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
@@ -24,7 +24,7 @@ export const get: RequestHandler = async (event) => {
}; };
export const post: RequestHandler = async (event) => { export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
@@ -53,7 +53,7 @@ export const post: RequestHandler = async (event) => {
} }
}; };
export const del: RequestHandler = async (event) => { export const del: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;

View File

@@ -22,25 +22,118 @@
<script lang="ts"> <script lang="ts">
export let secrets; export let secrets;
export let application; export let application;
import pLimit from 'p-limit';
import Secret from './_Secret.svelte'; import Secret from './_Secret.svelte';
import { getDomain } from '$lib/components/common';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { get } from '$lib/api'; import { get } from '$lib/api';
import { saveSecret } from './utils';
import { toast } from '@zerodevx/svelte-toast';
const limit = pLimit(1);
const { id } = $page.params; const { id } = $page.params;
let batchSecrets = '';
async function refreshSecrets() { async function refreshSecrets() {
const data = await get(`/applications/${id}/secrets.json`); const data = await get(`/applications/${id}/secrets.json`);
secrets = [...data.secrets]; secrets = [...data.secrets];
} }
async function getValues(e) {
e.preventDefault();
const eachValuePair = batchSecrets.split('\n');
const batchSecretsPairs = eachValuePair
.filter((secret) => !secret.startsWith('#') && secret)
.map((secret) => {
const [name, value] = secret.split('=');
const cleanValue = value?.replaceAll('"', '') || '';
return {
name,
value: cleanValue,
isNew: !secrets.find((secret) => name === secret.name)
};
});
await Promise.all(
batchSecretsPairs.map(({ name, value, isNew }) =>
limit(() => saveSecret({ name, value, applicationId: id, isNew }))
)
);
batchSecrets = '';
await refreshSecrets();
toast.push('Secrets saved');
}
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Secrets for <a href={application.fqdn} target="_blank">{getDomain(application.fqdn)}</a> <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">Secrets</div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl px-6 pt-4">
<table class="mx-auto border-separate text-left"> <table class="mx-auto border-separate text-left">
<thead> <thead>
<tr class="h-12"> <tr class="h-12">
@@ -68,4 +161,12 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h2 class="title my-6 font-bold">Paste .env file</h2>
<form on:submit|preventDefault={getValues} class="mb-12 w-full">
<textarea bind:value={batchSecrets} class="mb-2 min-h-[200px] w-full" />
<button
class="bg-green-600 hover:bg-green-500 disabled:text-white disabled:opacity-40"
type="submit">Batch add secrets</button
>
</form>
</div> </div>

View File

@@ -0,0 +1,42 @@
import { toast } from '@zerodevx/svelte-toast';
import { errorNotification } from '$lib/form';
import { post } from '$lib/api';
type Props = {
isNew: boolean;
name: string;
value: string;
isBuildSecret?: boolean;
isPRMRSecret?: boolean;
isNewSecret?: boolean;
applicationId: string;
};
export async function saveSecret({
isNew,
name,
value,
isBuildSecret,
isPRMRSecret,
isNewSecret,
applicationId
}: Props): Promise<void> {
if (!name) return errorNotification('Name is required.');
if (!value) return errorNotification('Value is required.');
try {
await post(`/applications/${applicationId}/secrets.json`, {
name,
value,
isBuildSecret,
isPRMRSecret,
isNew: isNew || false
});
if (isNewSecret) {
name = '';
value = '';
isBuildSecret = false;
}
} catch ({ error }) {
return errorNotification(error);
}
}

View File

@@ -36,12 +36,77 @@
} }
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex items-center space-x-2 p-5 px-6 font-bold">
<div class="mr-4 text-2xl tracking-tight"> <div class="-mb-5 flex-col">
Persistent storage for <a href={application.fqdn} target="_blank" <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
>{getDomain(application.fqdn)}</a Persistent Storage
> </div>
<span class="text-xs">{application.name} </span>
</div> </div>
{#if application.fqdn}
<a
href={application.fqdn}
target="_blank"
class="icons tooltip-bottom flex items-center bg-transparent text-sm"
><svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M11 7h-5a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-5" />
<line x1="10" y1="14" x2="20" y2="4" />
<polyline points="15 4 20 4 20 9" />
</svg></a
>
{/if}
<a
href="{application.gitSource.htmlUrl}/{application.repository}/tree/{application.branch}"
target="_blank"
class="w-10"
>
{#if application.gitSource?.type === 'gitlab'}
<svg viewBox="0 0 128 128" class="icons">
<path
fill="#FC6D26"
d="M126.615 72.31l-7.034-21.647L105.64 7.76c-.716-2.206-3.84-2.206-4.556 0l-13.94 42.903H40.856L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664 1.385 72.31a4.792 4.792 0 001.74 5.358L64 121.894l60.874-44.227a4.793 4.793 0 001.74-5.357"
/><path fill="#E24329" d="M64 121.894l23.144-71.23H40.856L64 121.893z" /><path
fill="#FC6D26"
d="M64 121.894l-23.144-71.23H8.42L64 121.893z"
/><path
fill="#FCA326"
d="M8.42 50.663L1.384 72.31a4.79 4.79 0 001.74 5.357L64 121.894 8.42 50.664z"
/><path
fill="#E24329"
d="M8.42 50.663h32.436L26.916 7.76c-.717-2.206-3.84-2.206-4.557 0L8.42 50.664z"
/><path fill="#FC6D26" d="M64 121.894l23.144-71.23h32.437L64 121.893z" /><path
fill="#FCA326"
d="M119.58 50.663l7.035 21.647a4.79 4.79 0 01-1.74 5.357L64 121.894l55.58-71.23z"
/><path
fill="#E24329"
d="M119.58 50.663H87.145l13.94-42.902c.717-2.206 3.84-2.206 4.557 0l13.94 42.903z"
/>
</svg>
{:else if application.gitSource?.type === 'github'}
<svg viewBox="0 0 128 128" class="icons">
<g fill="#ffffff"
><path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/><path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/></g
>
</svg>
{/if}
</a>
</div> </div>
<div class="mx-auto max-w-6xl rounded-xl px-6 pt-4"> <div class="mx-auto max-w-6xl rounded-xl px-6 pt-4">

View File

@@ -1,66 +0,0 @@
<script lang="ts">
export let application;
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
const buildPack = application?.buildPack?.toLowerCase();
</script>
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if buildPack === 'rust'}
<Rust />
{:else if buildPack === 'node'}
<Nodejs />
{:else if buildPack === 'react'}
<React />
{:else if buildPack === 'svelte'}
<Svelte />
{:else if buildPack === 'vuejs'}
<Vuejs />
{:else if buildPack === 'php'}
<PHP />
{:else if buildPack === 'python'}
<Python />
{:else if buildPack === 'static'}
<Static />
{:else if buildPack === 'nestjs'}
<Nestjs />
{:else if buildPack === 'nuxtjs'}
<Nuxtjs />
{:else if buildPack === 'nextjs'}
<Nextjs />
{:else if buildPack === 'gatsby'}
<Gatsby />
{:else if buildPack === 'docker'}
<Docker />
{:else if buildPack === 'astro'}
<Astro />
{:else if buildPack === 'eleventy'}
<Eleventy />
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if application.fqdn}
<div class="truncate text-center">{application.fqdn}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>

View File

@@ -1,13 +1,40 @@
<script lang="ts"> <script lang="ts">
export let applications: Array<Application>; export let applications: Array<Application>;
import { session } from '$app/stores'; import { session } from '$app/stores';
import Application from './_Application.svelte';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Rust from '$lib/components/svg/applications/Rust.svelte';
import Nodejs from '$lib/components/svg/applications/Nodejs.svelte';
import React from '$lib/components/svg/applications/React.svelte';
import Svelte from '$lib/components/svg/applications/Svelte.svelte';
import Vuejs from '$lib/components/svg/applications/Vuejs.svelte';
import PHP from '$lib/components/svg/applications/PHP.svelte';
import Python from '$lib/components/svg/applications/Python.svelte';
import Static from '$lib/components/svg/applications/Static.svelte';
import Nestjs from '$lib/components/svg/applications/Nestjs.svelte';
import Nuxtjs from '$lib/components/svg/applications/Nuxtjs.svelte';
import Nextjs from '$lib/components/svg/applications/Nextjs.svelte';
import Gatsby from '$lib/components/svg/applications/Gatsby.svelte';
import Docker from '$lib/components/svg/applications/Docker.svelte';
import Astro from '$lib/components/svg/applications/Astro.svelte';
import Eleventy from '$lib/components/svg/applications/Eleventy.svelte';
import { getDomain } from '$lib/components/common';
async function newApplication() { async function newApplication() {
const { id } = await post('/applications/new', {}); const { id } = await post('/applications/new', {});
return await goto(`/applications/${id}`, { replaceState: true }); return await goto(`/applications/${id}`, { replaceState: true });
} }
const ownApplications = applications.filter((application) => {
if (application.teams[0].id === $session.teamId) {
return application;
}
});
const otherApplications = applications.filter((application) => {
if (application.teams[0].id !== $session.teamId) {
return application;
}
});
</script> </script>
<div class="flex space-x-1 p-6 font-bold"> <div class="flex space-x-1 p-6 font-bold">
@@ -30,14 +57,125 @@
</div> </div>
{/if} {/if}
</div> </div>
<div class="flex flex-wrap justify-center"> <div class="flex flex-col flex-wrap justify-center">
{#if !applications || applications.length === 0} {#if !applications || ownApplications.length === 0}
<div class="flex-col"> <div class="flex-col">
<div class="text-center text-xl font-bold">No applications found</div> <div class="text-center text-xl font-bold">No applications found</div>
</div> </div>
{:else} {/if}
{#each applications as application} {#if ownApplications.length > 0 || otherApplications.length > 0}
<Application {application} /> <div class="flex flex-col">
{/each} <div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each ownApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{/if}
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $session.teamId === '0' && otherApplications.length > 0}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{#if otherApplications.length > 0 && $session.teamId === '0'}
<div class="px-6 pb-5 pt-10 text-xl font-bold">Other Applications</div>
<div class="flex flex-col flex-wrap justify-center px-2 md:flex-row">
{#each otherApplications as application}
<a href="/applications/{application.id}" class="w-96 p-2 no-underline">
<div class="box-selection group relative hover:bg-green-600">
{#if application.buildPack}
{#if application.buildPack.toLowerCase() === 'rust'}
<Rust />
{:else if application.buildPack.toLowerCase() === 'node'}
<Nodejs />
{:else if application.buildPack.toLowerCase() === 'react'}
<React />
{:else if application.buildPack.toLowerCase() === 'svelte'}
<Svelte />
{:else if application.buildPack.toLowerCase() === 'vuejs'}
<Vuejs />
{:else if application.buildPack.toLowerCase() === 'php'}
<PHP />
{:else if application.buildPack.toLowerCase() === 'python'}
<Python />
{:else if application.buildPack.toLowerCase() === 'static'}
<Static />
{:else if application.buildPack.toLowerCase() === 'nestjs'}
<Nestjs />
{:else if application.buildPack.toLowerCase() === 'nuxtjs'}
<Nuxtjs />
{:else if application.buildPack.toLowerCase() === 'nextjs'}
<Nextjs />
{:else if application.buildPack.toLowerCase() === 'gatsby'}
<Gatsby />
{:else if application.buildPack.toLowerCase() === 'docker'}
<Docker />
{:else if application.buildPack.toLowerCase() === 'astro'}
<Astro />
{:else if application.buildPack.toLowerCase() === 'eleventy'}
<Eleventy />
{/if}
{/if}
<div class="truncate text-center text-xl font-bold">{application.name}</div>
{#if $session.teamId === '0'}
<div class="truncate text-center">Team {application.teams[0].name}</div>
{/if}
{#if application.fqdn}
<div class="truncate text-center">{getDomain(application.fqdn) || ''}</div>
{/if}
{#if !application.gitSourceId || !application.destinationDockerId || !application.fqdn}
<div class="truncate text-center font-bold text-red-500 group-hover:text-white">
Configuration missing
</div>
{/if}
</div>
</a>
{/each}
</div>
{/if}
</div>
{/if} {/if}
</div> </div>

View File

@@ -1,4 +1,4 @@
import { getTeam, getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
@@ -9,23 +9,28 @@ export const get: RequestHandler = async (event) => {
try { try {
const applicationsCount = await db.prisma.application.count({ const applicationsCount = await db.prisma.application.count({
where: { teams: { some: { id: teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const sourcesCount = await db.prisma.gitSource.count({ const sourcesCount = await db.prisma.gitSource.count({
where: { teams: { some: { id: teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const destinationsCount = await db.prisma.destinationDocker.count({ const destinationsCount = await db.prisma.destinationDocker.count({
where: { teams: { some: { id: teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const teamsCount = await db.prisma.permission.count({ where: { userId } }); const teamsCount = await db.prisma.permission.count({ where: { userId } });
const databasesCount = await db.prisma.database.count({ const databasesCount = await db.prisma.database.count({
where: { teams: { some: { id: teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
}); });
const servicesCount = await db.prisma.service.count({ const servicesCount = await db.prisma.service.count({
where: { teams: { some: { id: teamId } } } where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }
});
const teams = await db.prisma.permission.findMany({
where: { userId },
include: { team: { include: { _count: { select: { users: true } } } } }
}); });
return { return {
body: { body: {
teams,
applicationsCount, applicationsCount,
sourcesCount, sourcesCount,
destinationsCount, destinationsCount,

View File

@@ -2,6 +2,8 @@
export let database; export let database;
export let privatePort; export let privatePort;
export let settings; export let settings;
export let isRunning;
import { page, session } from '$app/stores'; import { page, session } from '$app/stores';
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Setting from '$lib/components/Setting.svelte'; import Setting from '$lib/components/Setting.svelte';
@@ -15,27 +17,39 @@
import { browser } from '$app/env'; import { browser } from '$app/env';
import { post } from '$lib/api'; import { post } from '$lib/api';
import { getDomain } from '$lib/components/common'; import { getDomain } from '$lib/components/common';
import { toast } from '@zerodevx/svelte-toast';
const { id } = $page.params; const { id } = $page.params;
let loading = false; let loading = false;
let publicLoading = false;
let isPublic = database.settings.isPublic || false; let isPublic = database.settings.isPublic || false;
let appendOnly = database.settings.appendOnly; let appendOnly = database.settings.appendOnly;
let databaseDefault = database.defaultDatabase; let databaseDefault;
let databaseDbUser = database.dbUser; let databaseDbUser;
let databaseDbUserPassword = database.dbUserPassword; let databaseDbUserPassword;
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false'; generateDbDetails();
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword; function generateDbDetails() {
} else if (database.type === 'redis') { databaseDefault = database.defaultDatabase;
databaseDefault = ''; databaseDbUser = database.dbUser;
databaseDbUser = ''; databaseDbUserPassword = database.dbUserPassword;
if (database.type === 'mongodb') {
databaseDefault = '?readPreference=primary&ssl=false';
databaseDbUser = database.rootUser;
databaseDbUserPassword = database.rootUserPassword;
} else if (database.type === 'redis') {
databaseDefault = '';
databaseDbUser = '';
}
} }
let databaseUrl = generateUrl(); $: databaseUrl = generateUrl();
function generateUrl() { function generateUrl() {
return browser return (databaseUrl = browser
? `${database.type}://${ ? `${database.type}://${
databaseDbUser ? databaseDbUser + ':' : '' databaseDbUser ? databaseDbUser + ':' : ''
}${databaseDbUserPassword}@${ }${databaseDbUserPassword}@${
@@ -45,32 +59,50 @@
: window.location.hostname : window.location.hostname
: database.id : database.id
}:${isPublic ? database.publicPort : privatePort}/${databaseDefault}` }:${isPublic ? database.publicPort : privatePort}/${databaseDefault}`
: 'Loading...'; : 'Loading...');
} }
async function changeSettings(name) { async function changeSettings(name) {
if (publicLoading || !isRunning) return;
publicLoading = true;
let data = {
isPublic,
appendOnly
};
if (name === 'isPublic') { if (name === 'isPublic') {
isPublic = !isPublic; data.isPublic = !isPublic;
} }
if (name === 'appendOnly') { if (name === 'appendOnly') {
appendOnly = !appendOnly; data.appendOnly = !appendOnly;
} }
try { try {
const { publicPort } = await post(`/databases/${id}/settings.json`, { isPublic, appendOnly }); const { publicPort } = await post(`/databases/${id}/settings.json`, {
isPublic: data.isPublic,
appendOnly: data.appendOnly
});
isPublic = data.isPublic;
appendOnly = data.appendOnly;
databaseUrl = generateUrl();
if (isPublic) { if (isPublic) {
database.publicPort = publicPort; database.publicPort = publicPort;
} }
databaseUrl = generateUrl();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
publicLoading = false;
} }
} }
async function handleSubmit() { async function handleSubmit() {
try { try {
await post(`/databases/${id}.json`, { ...database }); loading = true;
return window.location.reload(); await post(`/databases/${id}.json`, { ...database, isRunning });
generateDbDetails();
databaseUrl = generateUrl();
toast.push('Settings saved.');
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -142,21 +174,21 @@
readonly readonly
disabled disabled
name="publicPort" name="publicPort"
value={isPublic ? database.publicPort : privatePort} value={publicLoading ? 'Loading...' : isPublic ? database.publicPort : privatePort}
/> />
</div> </div>
</div> </div>
<div class="grid grid-flow-row gap-2"> <div class="grid grid-flow-row gap-2">
{#if database.type === 'mysql'} {#if database.type === 'mysql'}
<MySql bind:database /> <MySql bind:database {isRunning} />
{:else if database.type === 'postgresql'} {:else if database.type === 'postgresql'}
<PostgreSql bind:database /> <PostgreSql bind:database {isRunning} />
{:else if database.type === 'mongodb'} {:else if database.type === 'mongodb'}
<MongoDb {database} /> <MongoDb bind:database {isRunning} />
{:else if database.type === 'redis'} {:else if database.type === 'redis'}
<Redis {database} /> <Redis bind:database {isRunning} />
{:else if database.type === 'couchdb'} {:else if database.type === 'couchdb'}
<CouchDb bind:database /> <CouchDb {database} />
{/if} {/if}
<div class="grid grid-cols-2 items-center px-10 pb-8"> <div class="grid grid-cols-2 items-center px-10 pb-8">
<label for="url" class="text-base font-bold text-stone-100">Connection String</label> <label for="url" class="text-base font-bold text-stone-100">Connection String</label>
@@ -168,7 +200,7 @@
name="url" name="url"
readonly readonly
disabled disabled
value={databaseUrl} value={publicLoading || loading ? 'Loading...' : generateUrl()}
/> />
</div> </div>
</div> </div>
@@ -179,10 +211,12 @@
<div class="px-10 pb-10"> <div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<Setting <Setting
loading={publicLoading}
bind:setting={isPublic} bind:setting={isPublic}
on:click={() => changeSettings('isPublic')} on:click={() => changeSettings('isPublic')}
title="Set it public" title="Set it public"
description="Your database will be reachable over the internet. <br>Take security seriously in this case!" description="Your database will be reachable over the internet. <br>Take security seriously in this case!"
disabled={!isRunning}
/> />
</div> </div>
{#if database.type === 'redis'} {#if database.type === 'redis'}

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -21,13 +23,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField={true} isPasswordField={true}
readonly
disabled
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -33,14 +35,15 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100">Root User</label> <label for="rootUser" class="text-base font-bold text-stone-100">Root User</label>
@@ -56,13 +59,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label> <label for="rootUserPassword" class="text-base font-bold text-stone-100">Root's Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="rootUserPassword" id="rootUserPassword"
name="rootUserPassword" name="rootUserPassword"
value={database.rootUserPassword} bind:value={database.rootUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -19,6 +21,19 @@
bind:value={database.defaultDatabase} bind:value={database.defaultDatabase}
/> />
</div> </div>
<div class="grid grid-cols-2 items-center">
<label for="rootUser" class="text-base font-bold text-stone-100"
>Root (postgres) User Password</label
>
<CopyPasswordField
disabled={!isRunning}
readonly={!isRunning}
placeholder="Generated automatically after start"
id="rootUserPassword"
name="rootUserPassword"
bind:value={database.rootUserPassword}
/>
</div>
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUser" class="text-base font-bold text-stone-100">User</label> <label for="dbUser" class="text-base font-bold text-stone-100">User</label>
<CopyPasswordField <CopyPasswordField
@@ -33,13 +48,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
readonly disabled={!isRunning}
disabled readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
</div> </div>

View File

@@ -1,6 +1,8 @@
<script> <script>
export let database; export let database;
export let isRunning;
import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte';
import Explainer from '$lib/components/Explainer.svelte';
</script> </script>
<div class="flex space-x-1 py-5 font-bold"> <div class="flex space-x-1 py-5 font-bold">
@@ -10,40 +12,14 @@
<div class="grid grid-cols-2 items-center"> <div class="grid grid-cols-2 items-center">
<label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label> <label for="dbUserPassword" class="text-base font-bold text-stone-100">Password</label>
<CopyPasswordField <CopyPasswordField
disabled disabled={!isRunning}
readonly readonly={!isRunning}
placeholder="Generated automatically after start" placeholder="Generated automatically after start"
isPasswordField isPasswordField
id="dbUserPassword" id="dbUserPassword"
name="dbUserPassword" name="dbUserPassword"
value={database.dbUserPassword} bind:value={database.dbUserPassword}
/> />
<Explainer text="Could be changed while the database is running." />
</div> </div>
<!-- <div class="grid grid-cols-3 items-center">
<label for="rootUser">Root User</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
id="rootUser"
name="rootUser"
value={database.rootUser}
/>
</div>
</div>
<div class="grid grid-cols-3 items-center">
<label for="rootUserPassword">Root's Password</label>
<div class="col-span-2 ">
<CopyPasswordField
disabled
readonly
placeholder="Generated automatically after start"
isPasswordField
id="rootUserPassword"
name="rootUserPassword"
value={database.rootUserPassword}
/>
</div>
</div> -->
</div> </div>

View File

@@ -15,7 +15,7 @@
const endpoint = `/databases/${params.id}.json`; const endpoint = `/databases/${params.id}.json`;
const res = await fetch(endpoint); const res = await fetch(endpoint);
if (res.ok) { if (res.ok) {
const { database, state, versions, privatePort, settings } = await res.json(); const { database, isRunning, versions, privatePort, settings } = await res.json();
if (!database || Object.entries(database).length === 0) { if (!database || Object.entries(database).length === 0) {
return { return {
status: 302, status: 302,
@@ -35,13 +35,13 @@
return { return {
props: { props: {
database, database,
state, isRunning,
versions, versions,
privatePort privatePort
}, },
stuff: { stuff: {
database, database,
state, isRunning,
versions, versions,
privatePort, privatePort,
settings settings
@@ -65,7 +65,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export let database; export let database;
export let state; export let isRunning;
let loading = false; let loading = false;
async function deleteDatabase() { async function deleteDatabase() {
@@ -91,8 +91,6 @@
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
} }
@@ -103,8 +101,6 @@
return window.location.reload(); return window.location.reload();
} catch ({ error }) { } catch ({ error }) {
return errorNotification(error); return errorNotification(error);
} finally {
loading = false;
} }
} }
</script> </script>
@@ -114,7 +110,7 @@
<Loading fullscreen cover /> <Loading fullscreen cover />
{:else} {:else}
{#if database.type && database.destinationDockerId && database.version && database.defaultDatabase} {#if database.type && database.destinationDockerId && database.version && database.defaultDatabase}
{#if state === 'running'} {#if isRunning}
<button <button
on:click={stopDatabase} on:click={stopDatabase}
title="Stop database" title="Stop database"
@@ -140,7 +136,7 @@
<rect x="14" y="5" width="4" height="14" rx="1" /> <rect x="14" y="5" width="4" height="14" rx="1" />
</svg> </svg>
</button> </button>
{:else if state === 'not started'} {:else}
<button <button
on:click={startDatabase} on:click={startDatabase}
title="Start database" title="Start database"

View File

@@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {

View File

@@ -1,6 +1,7 @@
import { getUserDetails } from '$lib/common'; import { getUserDetails } from '$lib/common';
import { supportedDatabaseTypesAndVersions } from '$lib/components/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { ErrorHandler, supportedDatabaseTypesAndVersions } from '$lib/database'; import { ErrorHandler } from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {

View File

@@ -1,6 +1,11 @@
import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { asyncExecShell, getEngine, getUserDetails } from '$lib/common';
import * as db from '$lib/database'; import * as db from '$lib/database';
import { generateDatabaseConfiguration, getVersions, ErrorHandler } from '$lib/database'; import {
generateDatabaseConfiguration,
getVersions,
ErrorHandler,
updatePasswordInDb
} from '$lib/database';
import type { RequestHandler } from '@sveltejs/kit'; import type { RequestHandler } from '@sveltejs/kit';
export const get: RequestHandler = async (event) => { export const get: RequestHandler = async (event) => {
@@ -12,7 +17,7 @@ export const get: RequestHandler = async (event) => {
const database = await db.getDatabase({ id, teamId }); const database = await db.getDatabase({ id, teamId });
const { destinationDockerId, destinationDocker } = database; const { destinationDockerId, destinationDocker } = database;
let state = 'not started'; let isRunning = false;
if (destinationDockerId) { if (destinationDockerId) {
const host = getEngine(destinationDocker.engine); const host = getEngine(destinationDocker.engine);
@@ -22,7 +27,7 @@ export const get: RequestHandler = async (event) => {
); );
if (JSON.parse(stdout).Running) { if (JSON.parse(stdout).Running) {
state = 'running'; isRunning = true;
} }
} catch (error) { } catch (error) {
// //
@@ -34,7 +39,7 @@ export const get: RequestHandler = async (event) => {
body: { body: {
privatePort: configuration?.privatePort, privatePort: configuration?.privatePort,
database, database,
state, isRunning,
versions: getVersions(database.type), versions: getVersions(database.type),
settings settings
} }
@@ -48,10 +53,26 @@ export const post: RequestHandler = async (event) => {
const { teamId, status, body } = await getUserDetails(event); const { teamId, status, body } = await getUserDetails(event);
if (status === 401) return { status, body }; if (status === 401) return { status, body };
const { id } = event.params; const { id } = event.params;
const { name, defaultDatabase, dbUser, dbUserPassword, rootUser, rootUserPassword, version } = const {
await event.request.json(); name,
defaultDatabase,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
version,
isRunning
} = await event.request.json();
try { try {
const database = await db.getDatabase({ id, teamId });
if (isRunning) {
if (database.dbUserPassword !== dbUserPassword) {
await updatePasswordInDb(database, dbUser, dbUserPassword, false);
} else if (database.rootUserPassword !== rootUserPassword) {
await updatePasswordInDb(database, rootUser, rootUserPassword, true);
}
}
await db.updateDatabase({ await db.updateDatabase({
id, id,
name, name,

View File

@@ -8,7 +8,8 @@
database: stuff.database, database: stuff.database,
versions: stuff.versions, versions: stuff.versions,
privatePort: stuff.privatePort, privatePort: stuff.privatePort,
settings: stuff.settings settings: stuff.settings,
isRunning: stuff.isRunning
} }
}; };
} }
@@ -31,37 +32,21 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import DatabaseLinks from '$lib/components/DatabaseLinks.svelte';
import CouchDb from '$lib/components/svg/databases/CouchDB.svelte';
import MongoDb from '$lib/components/svg/databases/MongoDB.svelte';
import MySql from '$lib/components/svg/databases/MySQL.svelte';
import PostgreSql from '$lib/components/svg/databases/PostgreSQL.svelte';
import Redis from '$lib/components/svg/databases/Redis.svelte';
export let database; export let database;
export let settings; export let settings;
export let privatePort; export let privatePort;
export let isRunning;
</script> </script>
<div class="flex items-center space-x-2 p-6 text-2xl font-bold"> <div class="flex items-center space-x-2 p-6 text-2xl font-bold">
<div class="md:max-w-64 truncate text-base tracking-tight md:block md:text-2xl"> <div class="-mb-5 flex-col">
{database.name} <div class="md:max-w-64 truncate text-base tracking-tight md:text-2xl lg:block">
Configuration
</div>
<span class="text-xs">{database.name}</span>
</div> </div>
<span class="relative"> <DatabaseLinks {database} />
{#if database.type === 'clickhouse'}
<Clickhouse />
{:else if database.type === 'couchdb'}
<CouchDb />
{:else if database.type === 'mongodb'}
<MongoDb />
{:else if database.type === 'mysql'}
<MySql />
{:else if database.type === 'postgresql'}
<PostgreSql />
{:else if database.type === 'redis'}
<Redis />
{/if}
</span>
</div> </div>
<Databases bind:database {privatePort} {settings} /> <Databases bind:database {privatePort} {settings} {isRunning} />

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