Compare commits

...

194 Commits

Author SHA1 Message Date
Andras Bacsai
69ebff1a7a Merge pull request #1347 from coollabsio/next
v4.0.0-beta.97
2023-10-20 15:25:50 +02:00
Andras Bacsai
5d9cfc393e add s3 to magicbar 2023-10-20 15:02:40 +02:00
Andras Bacsai
e2a256b31c add api tokens to magic bar 2023-10-20 15:00:57 +02:00
Andras Bacsai
4855af7e57 feat: start all kinds of things 2023-10-20 14:58:00 +02:00
Andras Bacsai
a664174c02 feat: api tokens + deploy webhook 2023-10-20 14:51:01 +02:00
Andras Bacsai
c19c13b4e2 feat: cloning project 2023-10-20 12:34:53 +02:00
Andras Bacsai
266b99bc25 fix: port exposes change, shoud regenerate label 2023-10-20 12:34:25 +02:00
Andras Bacsai
51ef24e1fb fix 2023-10-20 09:38:21 +02:00
Andras Bacsai
33d38ccf40 fix: preselect s3 storage if available 2023-10-20 09:38:13 +02:00
Andras Bacsai
f470ebbbe0 ui: updates 2023-10-20 09:29:09 +02:00
Andras Bacsai
11bd46b200 wip: mongodb backup 2023-10-19 17:17:38 +02:00
Andras Bacsai
53f5674771 wip: mongodb backup 2023-10-19 13:46:15 +02:00
Andras Bacsai
c53d88902c feat: standalone mongodb 2023-10-19 13:32:03 +02:00
Andras Bacsai
e342c4fd65 fix: add PGUSER to prevent HC warning 2023-10-19 11:58:12 +02:00
Andras Bacsai
aab7bd5e28 Merge pull request #1345 from altinselimi/patch-1
Fix spelling error in README.md
2023-10-19 11:49:42 +02:00
Andras Bacsai
3adefb9e49 command: generate services 2023-10-19 11:28:25 +02:00
Altin Selimi
c904441787 Update README.md
Fix image -> imagine spelling error
2023-10-19 11:02:56 +02:00
Andras Bacsai
b7f79ae034 Merge pull request #1333 from theh2so4/main
[+] Templates: BabyBuddy, Code-Server, Dokuwiki, Heimdall, MeTube, SnapDrop and PairDrop
2023-10-19 10:51:24 +02:00
Andras Bacsai
2d63fcdc7f implement new service templates 2023-10-19 10:51:03 +02:00
Andras Bacsai
c1d0cabcfb fix: service docs links 2023-10-19 10:50:52 +02:00
Andras Bacsai
166419b13a update contribution guide 2023-10-19 10:50:47 +02:00
Andras Bacsai
cfc4d3acc7 Merge branch 'main' into next 2023-10-19 09:23:30 +02:00
Andras Bacsai
13a0c2cf43 Merge pull request #1344 from liweiyi88/github-trending-badge
Add github trending badge in readme recognitions
2023-10-19 09:23:01 +02:00
liweiyi88
6ef6975432 add github trending badge in readme recognitions 2023-10-19 09:41:54 +11:00
Andras Bacsai
2c40e93d3b wip: PAT by team 2023-10-18 18:02:09 +02:00
Andras Bacsai
a30ae4fb38 version++ 2023-10-18 15:49:58 +02:00
Andras Bacsai
5b8785d1a9 update prod compose 2023-10-18 15:49:50 +02:00
Andras Bacsai
f6f3364269 Merge pull request #1343 from coollabsio/next
v4.0.0-beta.96
2023-10-18 15:43:26 +02:00
Andras Bacsai
2f93f4450f fix: containerStatus job 2023-10-18 15:43:14 +02:00
Andras Bacsai
2ad7c2b1ce fix: remove custom port from git repo url 2023-10-18 15:33:07 +02:00
Andras Bacsai
6c848199ed fix: add custom port as ssh option to deploy_key based commands 2023-10-18 15:23:43 +02:00
Andras Bacsai
76aab722b8 fix: limit horizon processes to 2 by default 2023-10-18 15:07:04 +02:00
Andras Bacsai
12290304c4 Merge pull request #1342 from coollabsio/next
v4.0.0-beta.95
2023-10-18 14:47:05 +02:00
Andras Bacsai
3a27d13c3e fix 2023-10-18 14:46:26 +02:00
Andras Bacsai
4f588ced96 call handle not matter what 2023-10-18 14:43:48 +02:00
Andras Bacsai
e266c7cdec fix: email channel no recepients 2023-10-18 14:22:09 +02:00
Andras Bacsai
eedc3faba3 fix: labels 2023-10-18 14:14:40 +02:00
Andras Bacsai
2e2c932f07 Merge pull request #1341 from coollabsio/next
v4.0.0-beta.94
2023-10-18 12:49:11 +02:00
Andras Bacsai
e4aed185a2 fix: label generation 2023-10-18 12:48:29 +02:00
Andras Bacsai
dddbe40bbe fix dashboard ui on small screens 2023-10-18 11:35:36 +02:00
Andras Bacsai
59d6818f70 Merge pull request #1339 from coollabsio/next
v4.0.0-beta.93
2023-10-18 11:30:40 +02:00
Andras Bacsai
7678cd47df fix: add config_hash if its null (old deployments) 2023-10-18 11:26:01 +02:00
Andras Bacsai
b101fbacd4 fix: do not show configuration changed if config_hash is null 2023-10-18 11:22:56 +02:00
Andras Bacsai
a61a86dc3b feat: show if config is not applied 2023-10-18 11:20:40 +02:00
Andras Bacsai
0b3cde44c3 feat: able to customize docker labels on applications 2023-10-18 10:32:08 +02:00
TheH2SO4
618d5d837c [+] Template: Dokuwiki
🆕 **New Templates**:

-> ℹ️ **Dokuwiki**: A lightweight and easy-to-use wiki platform for creating and managing documentation and knowledge bases with simplicity and flexibility.
2023-10-18 10:01:37 +02:00
TheH2SO4
d234e8969d [+] Template: MeTube
🆕 **New Template**:

-> ℹ️ **MeTube**: A web GUI for youtube-dl with playlist support. It enables you to effortlessly download videos from YouTube and dozens of other sites.
2023-10-18 09:22:47 +02:00
TheH2SO4
1be77b3fea [+] Template: BabyBuddy
🆕 **New Template**:

-> ℹ️ **Heimdall**: Baby Buddy is an open-source web application that helps parents track their baby's daily activities, growth, and health with ease. It's a handy tool for new parents to keep a close eye on their little one's development.
2023-10-18 09:11:21 +02:00
Andras Bacsai
6b302ab786 Add restarting indicator to resources 2023-10-18 09:03:14 +02:00
TheH2SO4
5831dd6196 Merge pull request #1 from coollabsio/main
4.0.0-beta.92
2023-10-18 08:28:56 +02:00
Andras Bacsai
3c623f13e2 revert version 2023-10-17 20:54:54 +02:00
Andras Bacsai
da54c24e8d fix: setup:dev script & contribution guide 2023-10-17 20:54:26 +02:00
Andras Bacsai
1e39c3d5ab Merge pull request #1338 from coollabsio/next
v4.0.0-beta.92
2023-10-17 19:03:04 +02:00
Andras Bacsai
6071412986 fix: proxy start process 2023-10-17 19:00:23 +02:00
Andras Bacsai
ba7148206a Merge pull request #1336 from coollabsio/next
v4.0.0-beta.91
2023-10-17 15:41:30 +02:00
Andras Bacsai
59c5b22e6c fix: always start proxy if not NONE is selected 2023-10-17 15:40:47 +02:00
Andras Bacsai
be7f2ad9c4 ui: add helper to service domains 2023-10-17 15:34:20 +02:00
Andras Bacsai
62295ef573 Merge pull request #1335 from coollabsio/next
v4.0.0-beta.90
2023-10-17 14:45:26 +02:00
Andras Bacsai
ceb9fcf3b6 service: wordpress 2023-10-17 14:44:25 +02:00
Andras Bacsai
60282f7b6c fix: only include config.json if its exists and a file 2023-10-17 14:23:07 +02:00
Andras Bacsai
f14b0a3411 Merge pull request #1334 from coollabsio/next
v4.0.0-beta.89
2023-10-17 14:06:12 +02:00
Andras Bacsai
30af317bd9 fix: show docker build logs 2023-10-17 14:04:21 +02:00
Andras Bacsai
95faa1c3ad fix: noindex meta tag 2023-10-17 13:28:33 +02:00
TheH2SO4
423d31f227 Merge branch 'main' into main 2023-10-17 13:24:22 +02:00
TheH2SO4
fbb063030d [+] Templates: Heimdall, PairDrop and SnapDrop
🆕 **New Template**:

-> ℹ️ **Heimdall**: Heimdall is a self-hosted dashboard for managing and organizing your server applications, providing a centralized and efficient interface.
-> ℹ️ **PairDrop**: Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.
-> ℹ️ **SnapDrop**: A self-hosted file-sharing service for secure and convenient file transfers, whether on a local network or the internet.
2023-10-17 13:22:37 +02:00
TheH2SO4
1968726cfe [+] Template: Code-Server
🆕 **New Template**:

-> ℹ️ **Code-Server**: Code-Server is a self-hosted, web-based code editor that enables remote coding and collaboration from any device, anywhere.
2023-10-17 12:53:44 +02:00
Andras Bacsai
fb280afe41 Merge pull request #1332 from coollabsio/next
v4.0.0-beta.88
2023-10-17 12:41:45 +02:00
Andras Bacsai
fd488a561a feat: use docker login credentials from server 2023-10-17 12:35:04 +02:00
Andras Bacsai
ab57a5d8ef Merge pull request #1331 from coollabsio/next
v4.0.0-beta.87
2023-10-17 12:14:03 +02:00
Andras Bacsai
f5ae222a6e fix: add internal domain names during build process 2023-10-17 11:23:49 +02:00
Andras Bacsai
5d95d8b79a fix: cancel any deployments + queue next 2023-10-17 11:10:33 +02:00
Andras Bacsai
fbb5f2ca2e fix: generate fqdn if you deleted a service app, but it requires fqdn 2023-10-17 10:37:39 +02:00
Andras Bacsai
16cbca36c1 add trademark policy 2023-10-17 10:37:26 +02:00
Andras Bacsai
24a578bedb Merge pull request #1327 from theh2so4/main
[+] Update
2023-10-17 10:31:25 +02:00
Andras Bacsai
36dc479772 fix: service status check is a bit better 2023-10-17 10:17:03 +02:00
Andras Bacsai
83d6e488e4 Merge pull request #1330 from seii/fix/raspbian-support
Add Raspbian support to install.sh
2023-10-17 09:50:57 +02:00
Seii
a4d358d512 Add Raspbian support to install.sh 2023-10-16 22:11:51 -06:00
TheH2SO4
c0c197101d [+] Update
💄 **Styling**:

-> ℹ️ **Alphabetical order**: Changed the order of the templates and set them on a alphabetical order.
-> ℹ️ **More accurate descriptions**: Created more accurate descriptions for the templates: `Appsmith, Appwrite, Fider, Ghost, Umami` and `Uptime Kuma`.
2023-10-15 23:48:33 +02:00
Andras Bacsai
62e39ccc7f Merge pull request #1326 from coollabsio/next
v4.0.0-beta.86
2023-10-15 16:56:21 +02:00
Andras Bacsai
a88a016137 fix: build image before starting dockerfile buildpacks 2023-10-15 16:54:16 +02:00
Andras Bacsai
f4c8986ab3 Merge pull request #1324 from coollabsio/next
v4.0.0-beta.85
2023-10-14 17:40:08 +02:00
Andras Bacsai
e286eae53b Merge pull request #1322 from scmmishra/fix/redis-default-url
fix: generated redis URL string
2023-10-14 17:36:48 +02:00
Andras Bacsai
bc3e59e4ef fix: delete resource if there was an error
fix: do not refresh on containerStatusJob (db view)
2023-10-14 17:26:16 +02:00
Andras Bacsai
17ebc650c9 Merge pull request #1323 from coollabsio/next
v4.0.0-beta.84
2023-10-14 14:24:20 +02:00
Andras Bacsai
0ef386b4a8 fix: stopping a resource is now job based
ui: show status on project
2023-10-14 14:22:07 +02:00
Shivam Mishra
26fce85bb0 fix: redis URL generated 2023-10-14 12:10:12 +05:30
Andras Bacsai
2a079e3365 Merge pull request #1321 from coollabsio/next
v4.0.0-beta.83
2023-10-13 21:54:26 +02:00
Andras Bacsai
5fb5ed75c4 Merge pull request #1315 from scmmishra/patch-1
fix: docker hub URL for redis
2023-10-13 21:46:46 +02:00
Andras Bacsai
a2008fe9d1 typo 2023-10-13 21:44:04 +02:00
Andras Bacsai
3c96485e3d fix: custom dockerfile location for dockerfile buildpack
fix: able to use custom port for git cloning
2023-10-13 21:39:49 +02:00
Andras Bacsai
1c97d47ea0 fix: turn off static deployment if you switch buildpacks 2023-10-13 21:08:59 +02:00
Shivam Mishra
8f8f5878dd fix: docker hub URL 2023-10-13 22:34:46 +05:30
Andras Bacsai
6e6f39dc1f Merge pull request #1314 from coollabsio/next
v4.0.0-beta.82
2023-10-13 16:38:52 +02:00
Andras Bacsai
d2d1f984e1 fix 2023-10-13 16:32:59 +02:00
Andras Bacsai
d635e5dbae fix: backup database one-by-one. 2023-10-13 15:45:24 +02:00
Andras Bacsai
49c56524e1 fix: dev containerjobs 2023-10-13 15:17:04 +02:00
Andras Bacsai
6ced607f2a fix: timeout for instant remote processes 2023-10-13 15:16:52 +02:00
Andras Bacsai
aaa2febef4 fix: docker cleanup jobs 2023-10-13 15:16:33 +02:00
Andras Bacsai
10e6eddcfe fix: db labels 2023-10-13 15:16:22 +02:00
Andras Bacsai
2639bf92ad fix: traefik dashboard ip 2023-10-13 14:35:02 +02:00
Andras Bacsai
59eae3a44e fix: proxy check for ports, do not kill anything listening on port 80/443 2023-10-13 14:25:30 +02:00
Andras Bacsai
5aa8ccfcf4 link to default redis conf 2023-10-13 09:42:38 +02:00
Andras Bacsai
aea7cc9638 fix: show database logs in case of its not healthy and running 2023-10-13 09:40:37 +02:00
Andras Bacsai
c970907c73 fix: no backup for redis 2023-10-13 09:39:40 +02:00
Andras Bacsai
38c6c1ee40 fix: urls should be password fields 2023-10-13 09:36:37 +02:00
Andras Bacsai
a6118f5daf fix 2023-10-13 09:34:11 +02:00
Andras Bacsai
b196c138d9 fix: server ip could be hostname in self-hosted 2023-10-13 09:31:44 +02:00
Andras Bacsai
8f9949160c feat: add custom redis conf 2023-10-12 17:29:29 +02:00
Andras Bacsai
beae0b545f init: redis 2023-10-12 17:18:33 +02:00
Andras Bacsai
b8dd7704b3 update resource delete command 2023-10-12 13:35:57 +02:00
Andras Bacsai
d913be66e6 Merge pull request #1312 from coollabsio/next
v4.0.0-beta.81
2023-10-12 12:23:03 +02:00
Andras Bacsai
63de538879 seeder update 2023-10-12 12:18:26 +02:00
Andras Bacsai
1d733b2282 seeder 2023-10-12 12:11:54 +02:00
Andras Bacsai
1d0ad51fdf fix: dockerfile location feature 2023-10-12 12:01:09 +02:00
Andras Bacsai
83d00bbe3c fix: make sure to use IP address 2023-10-12 11:41:37 +02:00
Andras Bacsai
c422b4dbcf fix: isCloud in production seeder 2023-10-12 11:36:44 +02:00
Andras Bacsai
767fd334dd Merge pull request #1310 from coollabsio/next
v4.0.0-beta.80
2023-10-12 09:47:39 +02:00
Andras Bacsai
972223f01b disable docker_compose deployments 2023-10-12 09:30:27 +02:00
Andras Bacsai
9318cac189 fix: service status check
fix: containerStatusJob
fix: service form
2023-10-12 09:12:46 +02:00
Andras Bacsai
7aa991fd7c fix: service check status 10 sec 2023-10-12 08:58:08 +02:00
Andras Bacsai
5c27f43b3d move autoupdate job to actions 2023-10-12 08:56:29 +02:00
Andras Bacsai
a2f4d4ed6d fix: make sure proxy wont start in NONE mode 2023-10-12 08:51:32 +02:00
Andras Bacsai
6aca2740fb fix 2023-10-11 15:46:59 +02:00
Andras Bacsai
cd13b5b83e version++ 2023-10-11 15:39:27 +02:00
Andras Bacsai
758dbafbf1 Merge pull request #1308 from coollabsio/next
v4.0.0-beta.79
2023-10-11 15:27:21 +02:00
Andras Bacsai
f6663661df disallow robots 2023-10-11 15:07:00 +02:00
Andras Bacsai
9666099408 commit 2023-10-11 14:43:34 +02:00
Andras Bacsai
d382af6860 fix 2023-10-11 14:40:41 +02:00
Andras Bacsai
4905454269 hm 2023-10-11 14:33:18 +02:00
Andras Bacsai
ed8bd37230 disallow robots 2023-10-11 14:31:59 +02:00
Andras Bacsai
ec1a7aa893 Merge pull request #1307 from coollabsio/next
v4.0.0-beta.78
2023-10-11 14:25:27 +02:00
Andras Bacsai
62adf2c5dc fix: boarding + verification 2023-10-11 14:24:19 +02:00
Andras Bacsai
3e4538de98 update command 2023-10-11 14:12:02 +02:00
Andras Bacsai
5a7b16ea5f command: delete server 2023-10-11 14:04:21 +02:00
Andras Bacsai
aa7bc40f85 fix: send unreachable/revived notifications 2023-10-11 13:52:46 +02:00
Andras Bacsai
4fd83dc727 version++ 2023-10-11 13:51:30 +02:00
Andras Bacsai
0e451f87a9 Merge pull request #1305 from coollabsio/next
v4.0.0-beta.77
2023-10-11 13:50:56 +02:00
Andras Bacsai
0b0ae55f0b fix 2023-10-11 13:47:14 +02:00
Andras Bacsai
40ec3d9753 fix 2023-10-11 13:34:51 +02:00
Andras Bacsai
9535c8df29 fix: check localhost connection 2023-10-11 13:30:36 +02:00
Andras Bacsai
6ca1d36d5d fix: cannot remove localhost 2023-10-11 13:12:29 +02:00
Andras Bacsai
f5d16c46cb version++ 2023-10-11 13:11:52 +02:00
Andras Bacsai
725c3fd547 Merge pull request #1304 from coollabsio/next
v4.0.0-beta.76
2023-10-11 12:57:35 +02:00
Andras Bacsai
dcfcee1db6 fix: public git 2023-10-11 12:56:57 +02:00
Andras Bacsai
9f8c44d96b version++ 2023-10-11 12:33:06 +02:00
Andras Bacsai
5b74fd34f5 fix: instant save build pack change 2023-10-11 12:12:25 +02:00
Andras Bacsai
5a4c9422b2 fix: only require registry image in case of dockerimage bp 2023-10-11 12:10:40 +02:00
Andras Bacsai
81b916724e Merge pull request #1303 from coollabsio/next
v4.0.0-beta.75
2023-10-11 12:06:15 +02:00
Andras Bacsai
9540f60fa2 fix: dashboard goto link 2023-10-11 12:04:30 +02:00
Andras Bacsai
8eb1686125 fix: transactional email link 2023-10-11 12:04:23 +02:00
Andras Bacsai
daf3710a5e fix: add new team button 2023-10-11 12:04:14 +02:00
Andras Bacsai
1067f37e4d fix: deleted team and it is the current one 2023-10-11 12:03:59 +02:00
Andras Bacsai
3b3c0b94e5 version++ 2023-10-11 12:03:39 +02:00
Andras Bacsai
8b0a0d67da Merge pull request #1302 from coollabsio/next
v4.0.0-beta.74
2023-10-11 11:09:19 +02:00
Andras Bacsai
5541c135df feat: proxy logs on the ui 2023-10-11 11:00:40 +02:00
Andras Bacsai
2552cb2208 ui: able to select environment on new resource 2023-10-11 10:19:03 +02:00
Andras Bacsai
f001e9bc34 improve dashboard 2023-10-11 10:08:37 +02:00
Andras Bacsai
f943fdc5be fix: use only ip addresses for servers 2023-10-11 09:57:35 +02:00
Andras Bacsai
a4f1fcba58 move subscription to livewire + show manage subscription button for people already subscribed once 2023-10-11 09:55:05 +02:00
Andras Bacsai
68091b44fc fix: contact link 2023-10-11 09:54:01 +02:00
Andras Bacsai
9f8caac91c dev: coolify proxy access logs exposed in dev 2023-10-11 09:31:30 +02:00
Andras Bacsai
8082dc1a01 fix: use port exposed for reverse proxy 2023-10-11 09:23:31 +02:00
Andras Bacsai
a71cf5bc66 cleanup 2023-10-10 15:36:06 +02:00
Andras Bacsai
0775074509 version++ 2023-10-10 14:34:38 +02:00
Andras Bacsai
242d2fb283 Merge pull request #1300 from coollabsio/next
v4.0.0-beta.73
2023-10-10 14:29:44 +02:00
Andras Bacsai
5646818965 version++ 2023-10-10 14:26:31 +02:00
Andras Bacsai
ffc5320940 fix: backupfailed notification is forced 2023-10-10 14:17:16 +02:00
Andras Bacsai
7c10c55b1c fix: only send email if transactional email set 2023-10-10 14:14:41 +02:00
Andras Bacsai
7c96b6207a Merge pull request #1299 from coollabsio/next
v4.0.0-beta.72
2023-10-10 14:06:11 +02:00
Andras Bacsai
0be8ffbdc9 feat: add dockerfile location 2023-10-10 14:02:43 +02:00
Andras Bacsai
24fa56762e fix: database backups 2023-10-10 13:10:43 +02:00
Andras Bacsai
84d8e35411 fix: tcp proxy for dbs 2023-10-10 11:42:35 +02:00
Andras Bacsai
3d3ccc435c ui: fix 2023-10-10 11:29:33 +02:00
Andras Bacsai
be3b01472e ui: fix 2023-10-10 11:28:57 +02:00
Andras Bacsai
de6f5b1105 fix: goto 2023-10-10 11:27:39 +02:00
Andras Bacsai
14d9c06dcd feat: able to deploy docker images 2023-10-10 11:16:38 +02:00
Andras Bacsai
8abfaa1967 fix: no env goto envs from dashboard 2023-10-10 10:57:56 +02:00
Andras Bacsai
46f7ae9588 ui: updated dashboard 2023-10-10 10:56:11 +02:00
Andras Bacsai
f2c32b9aeb fixes 2023-10-09 20:37:42 +02:00
Andras Bacsai
3dab1eb92e fix: server saving 2023-10-09 20:12:03 +02:00
Andras Bacsai
9c22e01716 wip: dockerimage 2023-10-09 15:49:48 +02:00
Andras Bacsai
d1c47a4062 version++ 2023-10-09 15:22:51 +02:00
Andras Bacsai
6c3f97d9ae Merge pull request #1297 from coollabsio/next
v4.0.0-beta.71
2023-10-09 15:19:35 +02:00
Andras Bacsai
ebd8e2ce40 fix: check connection 2023-10-09 15:08:28 +02:00
Andras Bacsai
b650f3f754 fix: contact docs 2023-10-09 14:52:24 +02:00
Andras Bacsai
f33ba40478 fix help 2023-10-09 14:48:51 +02:00
Andras Bacsai
5cea9c4603 isInstanceAdmin() 2023-10-09 14:38:44 +02:00
Andras Bacsai
d32832fabc update 2023-10-09 14:32:30 +02:00
Andras Bacsai
165f0a3d4a feat: add email verification for cloud 2023-10-09 14:20:55 +02:00
Andras Bacsai
f14995200b fix: do not reset unreachable count 2023-10-09 12:41:21 +02:00
Andras Bacsai
12bb2ecc4a update 2023-10-09 12:13:30 +02:00
Andras Bacsai
933ec5741d fix: server unreachable count 2023-10-09 12:07:42 +02:00
Andras Bacsai
8004a40139 updates 2023-10-09 11:49:38 +02:00
Andras Bacsai
a6209fbe5c package updates 2023-10-09 11:45:23 +02:00
230 changed files with 4707 additions and 1290 deletions

View File

@@ -6,13 +6,14 @@
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## Code Contribution
## 1) Setup your development environment
### 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
## 2) Set your environment variables
### 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
@@ -23,7 +24,14 @@ You can ask for guidance anytime on our
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
## 4) Start development
### 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.
Mails are caught by Mailpit: `localhost:8025`
## New Service Contribution
Check out the docs [here](https://coolify.io/docs/how-to-add-a-service).

View File

@@ -4,7 +4,7 @@ Coolify is an open-source & self-hostable alternative to Heroku / Netlify / Verc
It helps you to manage your servers, applications, databases on your own hardware, all you need is SSH connection. You can manage VPS, Bare Metal, Raspberry PI's anything.
Image if you could have the ease of a cloud but with your own servers. That is **Coolify**.
Imagine if you could have the ease of a cloud but with your own servers. That is **Coolify**.
No vendor lock-in, which means that all the configuration for your applications/databases/etc are saved to your server. So if you decide to stop using Coolify (oh nooo), you could still manage your running resources. You just lose the automations and all the magic. 🪄️
@@ -36,10 +36,11 @@ You can find the installation script [here](./scripts/install.sh).
## Support
Contact us [here](https://docs.coollabs.io/contact).
Contact us [here](https://coolify.io/docs/contact).
## Recognitions
<p>
<a href="https://news.ycombinator.com/item?id=26624341">
<img
style="width: 250px; height: 54px;" width="250" height="54"
@@ -47,9 +48,12 @@ Contact us [here](https://docs.coollabs.io/contact).
src="https://hackernews-badge.vercel.app/api?id=26624341"
/>
</a>
</p>
<a href="https://www.producthunt.com/posts/coolify?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-coolify" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=338273&theme=light" alt="Coolify - An&#0032;open&#0045;source&#0032;&#0038;&#0032;self&#0045;hostable&#0032;Heroku&#0044;&#0032;Netlify&#0032;alternative | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
## 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use App\Notifications\Application\StatusChanged;
use Lorisleiva\Actions\Concerns\AsAction;
class StopApplication
{
use AsAction;
public function handle(Application $application)
{
$server = $application->destination->server;
$containers = getCurrentApplicationContainerStatus($server, $application->id);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$server
);
}
}
// TODO: make notification for application
// $application->environment->project->team->notify(new StatusChanged($application));
}
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class StartDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
$internalPort = null;
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
$internalPort = 6379;
} else if ($database->getMorphClass() === 'App\Models\StandalonePostgresql') {
$internalPort = 5432;
} else if ($database->getMorphClass() === 'App\Models\StandaloneMongodb') {
$internalPort = 27017;
}
$containerName = "{$database->uuid}-proxy";
$configuration_dir = database_proxy_dir($database->uuid);
$nginxconf = <<<EOF
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
stream {
server {
listen $database->public_port;
proxy_pass $database->uuid:$internalPort;
}
}
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF;
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine",
'container_name' => $containerName,
'restart' => RESTART_MODE,
'ports' => [
"$database->public_port:$database->public_port",
],
'networks' => [
$database->destination->network,
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'stat /etc/nginx/nginx.conf || exit 1',
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 3,
'start_period' => '1s'
],
]
],
'networks' => [
$database->destination->network => [
'external' => true,
'name' => $database->destination->network,
'attachable' => true,
]
]
];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([
"mkdir -p $configuration_dir",
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up --build -d",
], $database->destination->server);
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartMongodb
{
use AsAction;
public StandaloneMongodb $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneMongodb $database)
{
$this->database = $database;
$startCommand = "mongod";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_mongo_conf();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'mongo --eval "printjson(db.serverStatus())" | grep uptime | grep -v grep'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf',
'target' => '/etc/mongo/mongod.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' --config /etc/mongo/mongod.conf';
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_USERNAME={$this->database->mongo_initdb_root_username}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_ROOT_PASSWORD={$this->database->mongo_initdb_root_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}
return $environment_variables->all();
}
private function add_custom_mongo_conf()
{
if (is_null($this->database->mongo_conf)) {
return;
}
$filename = 'mongod.conf';
$content = $this->database->mongo_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -2,19 +2,21 @@
namespace App\Actions\Database;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartPostgresql
{
use AsAction;
public StandalonePostgresql $database;
public array $commands = [];
public array $init_scripts = [];
public string $configuration_dir;
public function __invoke(Server $server, StandalonePostgresql $database)
public function handle(StandalonePostgresql $database)
{
$this->database = $database;
$container_name = $this->database->uuid;
@@ -41,6 +43,9 @@ class StartPostgresql
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
@@ -98,7 +103,7 @@ class StartPostgresql
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $server);
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
@@ -139,6 +144,9 @@ class StartPostgresql
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
$environment_variables->push("PGUSER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");

View File

@@ -0,0 +1,159 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneRedis;
use Illuminate\Support\Str;
use Symfony\Component\Yaml\Yaml;
use Lorisleiva\Actions\Concerns\AsAction;
class StartRedis
{
use AsAction;
public StandaloneRedis $database;
public array $commands = [];
public string $configuration_dir;
public function handle(StandaloneRedis $database)
{
$this->database = $database;
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [
"echo '####### Starting {$database->name}.'",
"mkdir -p $this->configuration_dir",
];
$persistent_storages = $this->generate_local_persistent_volumes();
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables();
$this->add_custom_redis();
$docker_compose = [
'version' => '3.8',
'services' => [
$container_name => [
'image' => $this->database->image,
'command' => $startCommand,
'container_name' => $container_name,
'environment' => $environment_variables,
'restart' => RESTART_MODE,
'networks' => [
$this->database->destination->network,
],
'labels' => [
'coolify.managed' => 'true',
],
'healthcheck' => [
'test' => [
'CMD-SHELL',
'redis-cli',
'ping'
],
'interval' => '5s',
'timeout' => '5s',
'retries' => 10,
'start_period' => '5s'
],
'mem_limit' => $this->database->limits_memory,
'memswap_limit' => $this->database->limits_memory_swap,
'mem_swappiness' => $this->database->limits_memory_swappiness,
'mem_reservation' => $this->database->limits_memory_reservation,
'cpus' => $this->database->limits_cpus,
'cpuset' => $this->database->limits_cpuset,
'cpu_shares' => $this->database->limits_cpu_shares,
]
],
'networks' => [
$this->database->destination->network => [
'external' => true,
'name' => $this->database->destination->network,
'attachable' => true,
]
]
];
if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$container_name]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
$docker_compose['services'][$container_name]['command'] = $startCommand . ' /usr/local/etc/redis/redis.conf';
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'";
return remote_process($this->commands, $database->destination->server);
}
private function generate_local_persistent_volumes()
{
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
private function generate_local_persistent_volumes_only_volume_names()
{
$local_persistent_volumes_names = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
private function generate_environment_variables()
{
$environment_variables = collect();
foreach ($this->database->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}
return $environment_variables->all();
}
private function add_custom_redis()
{
if (is_null($this->database->redis_conf)) {
return;
}
$filename = 'redis.conf';
$content = $this->database->redis_conf;
$content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
$server = $database->destination->server;
instant_remote_process(
["docker rm -f {$database->uuid}"],
$server
);
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
// TODO: make notification for services
// $database->environment->project->team->notify(new StatusChanged($database));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabaseProxy
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb $database)
{
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server);
$database->is_public = false;
$database->save();
}
}

View File

@@ -58,6 +58,11 @@ class CreateNewUser implements CreatesNewUsers
'password' => Hash::make($input['password']),
]);
$team = $user->teams()->first();
if (isCloud()) {
$user->sendVerificationEmail();
} else {
$user->markEmailAsVerified();
}
}
// Set session variable
session(['currentTeam' => $user->currentTeam = $team]);

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Actions\Proxy;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckProxy
{
use AsAction;
public function handle(Server $server, $fromUI = false)
{
if (!$server->isProxyShouldRun()) {
if ($fromUI) {
throw new \Exception("Proxy should not run. You selected the Custom Proxy.");
} else {
return false;
}
}
$status = getContainerStatus($server, 'coolify-proxy');
if ($status === 'running') {
$server->proxy->set('status', 'running');
$server->save();
return false;
}
$ip = $server->ip;
if ($server->id === 0) {
$ip = 'host.docker.internal';
}
$connection80 = @fsockopen($ip, '80');
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80);
$port443 = is_resource($connection443) && fclose($connection443);
if ($port80) {
if ($fromUI) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
if ($port443) {
if ($fromUI) {
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a> <br> Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
}
return true;
}
}

View File

@@ -10,56 +10,47 @@ use Spatie\Activitylog\Models\Activity;
class StartProxy
{
use AsAction;
public function handle(Server $server, bool $async = true): Activity|string
public function handle(Server $server, bool $async = true): string|Activity
{
$commands = collect([]);
$proxyType = $server->proxyType();
if ($proxyType === 'none') {
return 'OK';
}
$proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server);
if (!$configuration) {
throw new \Exception("Configuration is not synced");
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
$commands = $commands->merge([
"apt-get update > /dev/null 2>&1 || true",
"command -v lsof >/dev/null || echo '####### Installing lsof.'",
"command -v lsof >/dev/null || apt install -y lsof",
"command -v lsof >/dev/null || command -v fuser >/dev/null || apt install -y psmisc",
"mkdir -p $proxy_path && cd $proxy_path",
"echo '####### Creating Docker Compose file.'",
"echo '####### Pulling docker image.'",
'docker compose pull',
"echo '####### Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"command -v fuser >/dev/null || command -v lsof >/dev/null || echo '####### Could not kill existing processes listening on port 80 & 443. Please stop the process holding these ports...'",
"command -v lsof >/dev/null && lsof -nt -i:80 | xargs -r kill -9 || true",
"command -v lsof >/dev/null && lsof -nt -i:443 | xargs -r kill -9 || true",
"command -v fuser >/dev/null && fuser -k 80/tcp || true",
"command -v fuser >/dev/null && fuser -k 443/tcp || true",
"systemctl disable nginx > /dev/null 2>&1 || true",
"systemctl disable apache2 > /dev/null 2>&1 || true",
"systemctl disable apache > /dev/null 2>&1 || true",
"echo '####### Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo '####### Proxy installed successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
try {
$proxyType = $server->proxyType();
$commands = collect([]);
$proxy_path = get_proxy_path();
$configuration = CheckConfiguration::run($server);
if (!$configuration) {
throw new \Exception("Configuration is not synced");
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return 'OK';
$commands = $commands->merge([
"mkdir -p $proxy_path && cd $proxy_path",
"echo 'Creating required Docker Compose file.'",
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
"docker compose down -v --remove-orphans > /dev/null 2>&1",
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'"
]);
$commands = $commands->merge(connectProxyToNetworks($server));
if ($async) {
$activity = remote_process($commands, $server);
return $activity;
} else {
instant_remote_process($commands, $server);
$server->proxy->set('status', 'running');
$server->proxy->set('type', $proxyType);
$server->save();
return 'OK';
}
} catch(\Throwable $e) {
ray($e);
throw $e;
}
}
}

View File

@@ -2,16 +2,18 @@
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\InstanceSettings;
use App\Models\Server;
class UpdateCoolify
{
use AsAction;
public ?Server $server = null;
public ?string $latestVersion = null;
public ?string $currentVersion = null;
public function __invoke(bool $force)
public function handle(bool $force)
{
try {
$settings = InstanceSettings::get();

View File

@@ -4,6 +4,7 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service;
use App\Notifications\Application\StatusChanged;
class StopService
{
@@ -22,5 +23,7 @@ class StopService
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Symfony\Component\Yaml\Yaml;
class GenerateServiceTemplates extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'services:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate service-templates.yaml based on /templates/compose directory';
/**
* Execute the console command.
*/
public function handle()
{
ray()->clearAll();
$files = array_diff(scandir(base_path('templates/compose')), ['.', '..']);
$files = array_filter($files, function ($file) {
return strpos($file, '.yaml') !== false;
});
$serviceTemplatesJson = [];
foreach ($files as $file) {
$parsed = $this->process_file($file);
if ($parsed) {
$name = data_get($parsed, 'name');
$parsed = data_forget($parsed, 'name');
$serviceTemplatesJson[$name] = $parsed;
}
}
$serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT);
file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson);
}
private function process_file($file)
{
$serviceName = str($file)->before('.yaml')->value();
$content = file_get_contents(base_path("templates/compose/$file"));
// $this->info($content);
$ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values();
if ($ignore->count() > 0) {
$ignore = (bool)str($ignore[0])->after('# ignore:')->trim()->value();
} else {
$ignore = false;
}
if ($ignore) {
$this->info("Ignoring $file");
return;
}
$this->info("Processing $file");
$documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values();
if ($documentation->count() > 0) {
$documentation = str($documentation[0])->after('# documentation:')->trim()->value();
} else {
$documentation = 'https://coolify.io/docs';
}
$slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values();
if ($slogan->count() > 0) {
$slogan = str($slogan[0])->after('# slogan:')->trim()->value();
} else {
$slogan = str($file)->headline()->value();
}
$env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values();
if ($env_file->count() > 0) {
$env_file = str($env_file[0])->after('# env_file:')->trim()->value();
} else {
$env_file = null;
}
$json = Yaml::parse($content);
$yaml = base64_encode(Yaml::dump($json, 10, 2));
$payload = [
'name' => $serviceName,
'documentation' => $documentation,
'slogan' => $slogan,
'compose' => $yaml,
];
if ($env_file) {
$payload['envs'] = $env_file;
}
return $payload;
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Command;
@@ -34,7 +35,7 @@ class ResourcesDelete extends Command
{
$resource = select(
'What resource do you want to delete?',
['Application', 'Database', 'Service'],
['Application', 'Database', 'Service', 'Server'],
);
if ($resource === 'Application') {
$this->deleteApplication();
@@ -42,6 +43,30 @@ class ResourcesDelete extends Command
$this->deleteDatabase();
} elseif ($resource === 'Service') {
$this->deleteService();
} elseif ($resource === 'Server') {
$this->deleteServer();
}
}
private function deleteServer()
{
$servers = Server::all();
if ($servers->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$serversToDelete = multiselect(
label: 'What server do you want to delete?',
options: $servers->pluck('name', 'id')->sortKeys(),
);
foreach ($serversToDelete as $server) {
$toDelete = $servers->where('id', $server)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
break;
}
$toDelete->delete();
}
}
private function deleteApplication()
@@ -53,14 +78,17 @@ class ResourcesDelete extends Command
}
$applicationsToDelete = multiselect(
'What application do you want to delete?',
$applications->pluck('name')->toArray(),
$applications->pluck('name', 'id')->sortKeys(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($applicationsToDelete as $application) {
$toDelete = $applications->where('name', $application)->first();
ray($application);
$toDelete = $applications->where('id', $application)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources? ");
if (!$confirmed) {
break;
}
$toDelete->delete();
}
}
@@ -73,17 +101,18 @@ class ResourcesDelete extends Command
}
$databasesToDelete = multiselect(
'What database do you want to delete?',
$databases->pluck('name')->toArray(),
$databases->pluck('name', 'id')->sortKeys(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first();
$toDelete = $databases->where('id', $database)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete();
}
}
private function deleteService()
{
@@ -94,14 +123,16 @@ class ResourcesDelete extends Command
}
$servicesToDelete = multiselect(
'What service do you want to delete?',
$services->pluck('name')->toArray(),
$services->pluck('name', 'id')->sortKeys(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first();
$toDelete = $services->where('id', $service)->first();
$this->info($toDelete);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
$toDelete->delete();
}
}

View File

@@ -19,20 +19,16 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
if (isDev()) {
// $schedule->job(new ContainerStatusJob(Server::find(0)))->everyTenMinutes()->onOneServer();
// $schedule->command('horizon:snapshot')->everyMinute();
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly();
// $schedule->job(new DockerCleanupJob)->everyOddHour();
// $this->instance_auto_update($schedule);
// $this->check_scheduled_backups($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule);
} else {
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
$schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
// $schedule->job(new DockerCleanupJob)->everyTenMinutes()->onOneServer();
$this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
@@ -69,7 +65,6 @@ class Kernel extends ConsoleKernel
}
private function check_scheduled_backups($schedule)
{
ray('check_scheduled_backups');
$scheduled_backups = ScheduledDatabaseBackup::all();
if ($scheduled_backups->isEmpty()) {
ray('no scheduled backups');

View File

@@ -46,15 +46,6 @@ class Controller extends BaseController
}
return redirect()->route('login')->with('error', 'Invalid credentials.');
}
public function subscription()
{
if (!isCloud()) {
abort(404);
}
return view('subscription.index', [
'settings' => InstanceSettings::get(),
]);
}
public function license()
{

View File

@@ -19,7 +19,7 @@ class DatabaseController extends Controller
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
@@ -37,7 +37,7 @@ class DatabaseController extends Controller
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
@@ -64,10 +64,18 @@ class DatabaseController extends Controller
if (!$environment) {
return redirect()->route('dashboard');
}
$database = $environment->databases->where('uuid', request()->route('database_uuid'))->first();
$database = $environment->databases()->where('uuid', request()->route('database_uuid'))->first();
if (!$database) {
return redirect()->route('dashboard');
}
// No backups for redis
if ($database->getMorphClass() === 'App\Models\StandaloneRedis') {
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $database->uuid,
]);
}
return view('project.database.backups.all', [
'database' => $database,
's3s' => currentTeam()->s3s,

View File

@@ -59,11 +59,17 @@ class ProjectController extends Controller
return redirect()->route('dashboard');
}
if (in_array($type, DATABASE_TYPES)) {
$standalone_postgresql = create_standalone_postgresql($environment->id, $destination_uuid);
if ($type->value() === "postgresql") {
$database = create_standalone_postgresql($environment->id, $destination_uuid);
} else if ($type->value() === 'redis') {
$database = create_standalone_redis($environment->id, $destination_uuid);
} else if ($type->value() === 'mongodb') {
$database = create_standalone_mongodb($environment->id, $destination_uuid);
}
return redirect()->route('project.database.configuration', [
'project_uuid' => $project->uuid,
'environment_name' => $environment->name,
'database_uuid' => $standalone_postgresql->uuid,
'database_uuid' => $database->uuid,
]);
}
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {

View File

@@ -38,8 +38,7 @@ class Kernel extends HttpKernel
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\CheckForcePasswordReset::class,
\App\Http\Middleware\IsSubscriptionValid::class,
\App\Http\Middleware\IsBoardingFlow::class,
\App\Http\Middleware\DecideWhatToDoWithUser::class,
],

View File

@@ -164,7 +164,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
{
$this->validate([
'remoteServerName' => 'required',
'remoteServerHost' => 'required',
'remoteServerHost' => 'required|ip',
'remoteServerPort' => 'required|integer',
'remoteServerUser' => 'required',
]);

View File

@@ -9,21 +9,13 @@ use Livewire\Component;
class Dashboard extends Component
{
public int $projects = 0;
public int $servers = 0;
public int $s3s = 0;
public int $resources = 0;
public $projects = [];
public $servers = [];
public function mount()
{
$this->servers = Server::ownedByCurrentTeam()->get()->count();
$this->s3s = S3Storage::ownedByCurrentTeam()->get()->count();
$projects = Project::ownedByCurrentTeam()->get();
foreach ($projects as $project) {
$this->resources += $project->applications->count();
$this->resources += $project->postgresqls->count();
}
$this->projects = $projects->count();
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
}
// public function getIptables()
// {

View File

@@ -60,12 +60,16 @@ class DeploymentNavbar extends Component
$previous_logs[] = $new_log_entry;
$this->application_deployment_queue->update([
'logs' => json_encode($previous_logs, flags: JSON_THROW_ON_ERROR),
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->application_deployment_queue->update([
'current_process_id' => null,
'status' => ApplicationDeploymentStatus::CANCELLED_BY_USER->value,
]);
queue_next_deployment($this->application);
}
}
}

View File

@@ -3,12 +3,9 @@
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\InstanceSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Livewire\Component;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
class General extends Component
{
@@ -22,6 +19,11 @@ class General extends Component
public string $git_branch;
public ?string $git_commit_sha = null;
public string $build_pack;
public ?string $ports_exposes = null;
public $customLabels;
public bool $labelsChanged = false;
public bool $isConfigurationChanged = false;
public bool $is_static;
public bool $is_git_submodules_enabled;
@@ -49,6 +51,10 @@ class General extends Component
'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable',
'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable',
];
protected $validationAttributes = [
'application.name' => 'name',
@@ -67,12 +73,55 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile',
'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels',
];
public function mount()
{
$this->ports_exposes = $this->application->ports_exposes;
if (str($this->application->status)->startsWith('running') && is_null($this->application->config_hash)) {
$this->application->isConfigurationChanged(true);
}
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if (is_null(data_get($this->application, 'custom_labels'))) {
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
} else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
}
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates();
}
public function updatedApplicationBuildPack()
{
if ($this->application->build_pack !== 'nixpacks') {
$this->application->settings->is_static = $this->is_static = false;
$this->application->settings->save();
}
$this->submit();
}
public function checkLabelUpdates()
{
if (md5($this->application->custom_labels) !== md5(implode(",", generateLabelsApplication($this->application)))) {
$this->labelsChanged = true;
} else {
$this->labelsChanged = false;
}
}
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
@@ -89,36 +138,48 @@ class General extends Component
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain() {
public function getWildcardDomain()
{
$server = data_get($this->application, 'destination.server');
if ($server) {
$fqdn = generateFqdn($server, $this->application->uuid);
ray($fqdn);
$this->application->fqdn = $fqdn;
$this->application->save();
$this->emit('success', 'Application settings updated!');
}
}
public function mount()
public function resetDefaultLabels($showToaster = true)
{
if (data_get($this->application,'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->customLabels = str(implode(",", generateLabelsApplication($this->application)))->replace(',', "\n");
$this->ports_exposes = $this->application->ports_exposes;
$this->submit($showToaster);
}
public function submit()
public function updatedApplicationFqdn()
{
$this->resetDefaultLabels(false);
$this->emit('success', 'Labels reseted to default!');
}
public function submit($showToaster = true)
{
try {
$this->validate();
if ($this->ports_exposes !== $this->application->ports_exposes) {
$this->resetDefaultLabels(false);
}
if (data_get($this->application, 'build_pack') === 'dockerimage') {
$this->validate([
'application.docker_registry_image_name' => 'required',
'application.docker_registry_image_tag' => 'required',
]);
}
if (data_get($this->application, 'fqdn')) {
$domains = Str::of($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
return Str::of($domain)->trim()->lower();
@@ -137,10 +198,17 @@ class General extends Component
if ($this->application->publish_directory && $this->application->publish_directory !== '/') {
$this->application->publish_directory = rtrim($this->application->publish_directory, '/');
}
if (gettype($this->customLabels) === 'string') {
$this->customLabels = str($this->customLabels)->replace(',', "\n");
}
$this->application->custom_labels = $this->customLabels->explode("\n")->implode(',');
$this->application->save();
$this->emit('success', 'Application settings updated!');
$showToaster && $this->emit('success', 'Application settings updated!');
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Project\Application;
use App\Actions\Application\StopApplication;
use App\Jobs\ContainerStatusJob;
use App\Models\Application;
use Livewire\Component;
@@ -21,11 +22,13 @@ class Heading extends Component
public function check_status()
{
dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh();
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
if ($this->application->destination->server->isFunctional()) {
dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh();
$this->application->previews->each(function ($preview) {
$preview->refresh();
});
}
}
public function force_deploy_without_cache()
@@ -57,22 +60,9 @@ class Heading extends Component
public function stop()
{
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() === 0) {
return;
}
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
$this->application->status = 'exited';
$this->application->save();
// $this->application->environment->project->team->notify(new StatusChanged($this->application));
}
}
StopApplication::run($this->application);
$this->application->status = 'exited';
$this->application->save();
$this->application->refresh();
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace App\Http\Livewire\Project;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
class CloneProject extends Component
{
public string $project_uuid;
public string $environment_name;
public int $project_id;
public Project $project;
public $environments;
public $servers;
public ?Environment $environment = null;
public ?int $selectedServer = null;
public ?Server $server = null;
public $resources = [];
public string $newProjectName = '';
protected $messages = [
'selectedServer' => 'Please select a server.',
'newProjectName' => 'Please enter a name for the new project.',
];
public function mount($project_uuid)
{
$this->project_uuid = $project_uuid;
$this->project = Project::where('uuid', $project_uuid)->firstOrFail();
$this->environment = $this->project->environments->where('name', $this->environment_name)->first();
$this->project_id = $this->project->id;
$this->servers = currentTeam()->servers;
$this->newProjectName = $this->project->name . ' (clone)';
}
public function render()
{
return view('livewire.project.clone-project');
}
public function selectServer($server_id)
{
$this->selectedServer = $server_id;
$this->server = $this->servers->where('id', $server_id)->first();
}
public function clone()
{
try {
$this->validate([
'selectedServer' => 'required',
'newProjectName' => 'required',
]);
$newProject = Project::create([
'name' => $this->newProjectName,
'team_id' => currentTeam()->id,
'description' => $this->project->description . ' (clone)',
]);
if ($this->environment->id !== 1) {
$newProject->environments()->create([
'name' => $this->environment->name,
]);
$newProject->environments()->find(1)->delete();
}
$newEnvironment = $newProject->environments->first();
// Clone Applications
$applications = $this->environment->applications;
$databases = $this->environment->databases();
$services = $this->environment->services;
foreach ($applications as $application) {
$uuid = (string)new Cuid2(7);
$newApplication = $application->replicate()->fill([
'uuid' => $uuid,
'fqdn' => generateFqdn($this->server, $uuid),
'status' => 'exited',
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newApplication->environment_id = $newProject->environments->first()->id;
$newApplication->save();
$environmentVaribles = $application->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$newEnvironmentVariable = $environmentVarible->replicate()->fill([
'application_id' => $newApplication->id,
]);
$newEnvironmentVariable->save();
}
$persistentVolumes = $application->persistentStorages()->get();
foreach ($persistentVolumes as $volume) {
$newPersistentVolume = $volume->replicate()->fill([
'name' => $newApplication->uuid . '-' . str($volume->name)->afterLast('-'),
'resource_id' => $newApplication->id,
]);
$newPersistentVolume->save();
}
}
foreach ($databases as $database) {
$uuid = (string)new Cuid2(7);
$newDatabase = $database->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newDatabase->environment_id = $newProject->environments->first()->id;
$newDatabase->save();
$environmentVaribles = $database->environment_variables()->get();
foreach ($environmentVaribles as $environmentVarible) {
$payload = [];
if ($database->type() === 'standalone-postgres') {
$payload['standalone_postgresql_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_redis') {
$payload['standalone_redis_id'] = $newDatabase->id;
} else if ($database->type() === 'standalone_mongodb') {
$payload['standalone_mongodb_id'] = $newDatabase->id;
}
$newEnvironmentVariable = $environmentVarible->replicate()->fill($payload);
$newEnvironmentVariable->save();
}
}
foreach ($services as $service) {
$uuid = (string)new Cuid2(7);
$newService = $service->replicate()->fill([
'uuid' => $uuid,
'environment_id' => $newEnvironment->id,
'destination_id' => $this->selectedServer,
]);
$newService->environment_id = $newProject->environments->first()->id;
$newService->save();
$newService->parse();
}
return redirect()->route('project.resources', [
'project_uuid' => $newProject->uuid,
'environment_name' => $newEnvironment->name,
]);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
}

View File

@@ -8,6 +8,7 @@ class BackupEdit extends Component
{
public $backup;
public $s3s;
public ?string $status = null;
public array $parameters;
protected $rules = [
@@ -16,6 +17,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'required|integer|min:1',
'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable',
];
protected $validationAttributes = [
'backup.enabled' => 'Enabled',
@@ -23,6 +25,7 @@ class BackupEdit extends Component
'backup.number_of_backups_locally' => 'Number of Backups Locally',
'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup',
];
protected $messages = [
'backup.s3_storage_id' => 'Select a S3 Storage',
@@ -36,7 +39,6 @@ class BackupEdit extends Component
}
}
public function delete()
{
// TODO: Delete backup from server and add a confirmation modal
@@ -48,6 +50,7 @@ class BackupEdit extends Component
{
try {
$this->custom_validate();
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');
@@ -70,9 +73,11 @@ class BackupEdit extends Component
public function submit()
{
ray($this->backup->s3_storage_id);
try {
$this->custom_validate();
if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) {
$this->backup->databases_to_backup = null;
}
$this->backup->save();
$this->backup->refresh();
$this->emit('success', 'Backup updated successfully');

View File

@@ -13,6 +13,6 @@ class BackupNow extends Component
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
$this->emit('success', 'Backup queued. It will be available in a few minutes');
$this->emit('success', 'Backup queued. It will be available in a few minutes.');
}
}

View File

@@ -22,6 +22,11 @@ class CreateScheduledBackup extends Component
'frequency' => 'Backup Frequency',
'save_s3' => 'Save to S3',
];
public function mount() {
if ($this->s3s->count() > 0) {
$this->s3_storage_id = $this->s3s->first()->id;
}
}
public function submit(): void
{
@@ -32,7 +37,7 @@ class CreateScheduledBackup extends Component
$this->emit('error', 'Invalid Cron / Human expression.');
return;
}
ScheduledDatabaseBackup::create([
$payload = [
'enabled' => true,
'frequency' => $this->frequency,
'save_s3' => $this->save_s3,
@@ -40,7 +45,11 @@ class CreateScheduledBackup extends Component
'database_id' => $this->database->id,
'database_type' => $this->database->getMorphClass(),
'team_id' => currentTeam()->id,
]);
];
if ($this->database->type() === 'standalone-postgresql') {
$payload['databases_to_backup'] = $this->database->postgres_db;
}
ScheduledDatabaseBackup::create($payload);
$this->emit('refreshScheduledBackups');
} catch (\Throwable $e) {
handleError($e, $this);

View File

@@ -2,7 +2,10 @@
namespace App\Http\Livewire\Project\Database;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Database\StopDatabase;
use App\Jobs\ContainerStatusJob;
use Livewire\Component;
@@ -35,24 +38,24 @@ class Heading extends Component
public function stop()
{
instant_remote_process(
["docker rm -f {$this->database->uuid}"],
$this->database->destination->server
);
if ($this->database->is_public) {
stopPostgresProxy($this->database);
$this->database->is_public = false;
}
StopDatabase::run($this->database);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
// $this->database->environment->project->team->notify(new StatusChanged($this->database));
}
public function start()
{
if ($this->database->type() === 'standalone-postgresql') {
$activity = resolve(StartPostgresql::class)($this->database->destination->server, $this->database);
$activity = StartPostgresql::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-redis') {
$activity = StartRedis::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
if ($this->database->type() === 'standalone-mongodb') {
$activity = StartMongodb::run($this->database);
$this->emit('newMonitorActivity', $activity->id);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Http\Livewire\Project\Database\Mongodb;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneMongodb;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneMongodb $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.mongo_conf' => 'nullable',
'database.mongo_initdb_root_username' => 'required',
'database.mongo_initdb_root_password' => 'required',
'database.mongo_initdb_database' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.mongo_conf' => 'Mongo Configuration',
'database.mongo_initdb_root_username' => 'Root Username',
'database.mongo_initdb_root_password' => 'Root Password',
'database.mongo_initdb_database' => 'Database',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
try {
$this->validate();
if ($this->database->mongo_conf === "") {
$this->database->mongo_conf = null;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render()
{
return view('livewire.project.database.mongodb.general');
}
}

View File

@@ -2,6 +2,8 @@
namespace App\Http\Livewire\Project\Database\Postgresql;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandalonePostgresql;
use Exception;
use Livewire\Component;
@@ -47,15 +49,7 @@ class General extends Component
];
public function mount()
{
$this->getDbUrl();
}
public function getDbUrl() {
if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
}
$this->db_url = $this->database->getDbUrl();
}
public function instantSave()
{
@@ -67,13 +61,13 @@ class General extends Component
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
startPostgresProxy($this->database);
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
stopPostgresProxy($this->database);
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->getDbUrl();
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;

View File

@@ -0,0 +1,86 @@
<?php
namespace App\Http\Livewire\Project\Database\Redis;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy;
use App\Models\StandaloneRedis;
use Exception;
use Livewire\Component;
class General extends Component
{
protected $listeners = ['refresh'];
public StandaloneRedis $database;
public string $db_url;
protected $rules = [
'database.name' => 'required',
'database.description' => 'nullable',
'database.redis_conf' => 'nullable',
'database.redis_password' => 'required',
'database.image' => 'required',
'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer',
];
protected $validationAttributes = [
'database.name' => 'Name',
'database.description' => 'Description',
'database.redis_conf' => 'Redis Configuration',
'database.redis_password' => 'Redis Password',
'database.image' => 'Image',
'database.ports_mappings' => 'Port Mapping',
'database.is_public' => 'Is Public',
'database.public_port' => 'Public Port',
];
public function submit() {
try {
$this->validate();
if ($this->database->redis_conf === "") {
$this->database->redis_conf = null;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
$this->emit('success', 'Starting TCP proxy...');
StartDatabaseProxy::run($this->database);
$this->emit('success', 'Database is now publicly accessible.');
} else {
StopDatabaseProxy::run($this->database);
$this->emit('success', 'Database is no longer publicly accessible.');
}
$this->db_url = $this->database->getDbUrl();
$this->database->save();
} catch(\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
return handleError($e, $this);
}
}
public function refresh(): void
{
$this->database->refresh();
}
public function mount()
{
$this->db_url = $this->database->getDbUrl();
}
public function render()
{
return view('livewire.project.database.redis.general');
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Livewire\Project\New;
use App\Models\Application;
use App\Models\Project;
use App\Models\StandaloneDocker;
use App\Models\SwarmDocker;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
use Illuminate\Support\Str;
class DockerImage extends Component
{
public string $dockerImage = '';
public array $parameters;
public array $query;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
}
public function submit()
{
$this->validate([
'dockerImage' => 'required'
]);
$image = Str::of($this->dockerImage)->before(':');
if (Str::of($this->dockerImage)->contains(':')) {
$tag = Str::of($this->dockerImage)->after(':');
} else {
$tag = 'latest';
}
$destination_uuid = $this->query['destination'];
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first();
}
if (!$destination) {
throw new \Exception('Destination not found. What?!');
}
$destination_class = $destination->getMorphClass();
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
ray($image,$tag);
$application = Application::create([
'name' => 'docker-image-' . new Cuid2(7),
'repository_project_id' => 0,
'git_repository' => "coollabsio/coolify",
'git_branch' => 'main',
'build_pack' => 'dockerimage',
'ports_exposes' => 80,
'docker_registry_image_name' => $image,
'docker_registry_image_tag' => $tag,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'health_check_enabled' => false,
]);
$fqdn = generateFqdn($destination->server, $application->uuid);
$application->update([
'name' => 'docker-image-' . $application->uuid,
'fqdn' => $fqdn
]);
redirect()->route('project.application.configuration', [
'application_uuid' => $application->uuid,
'environment_name' => $environment->name,
'project_uuid' => $project->uuid,
]);
}
public function render()
{
return view('livewire.project.new.docker-image');
}
}

View File

@@ -98,7 +98,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->branch,
'git_full_url' => $this->git_repository,
'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
@@ -112,7 +111,6 @@ class GithubPrivateRepositoryDeployKey extends Component
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->branch,
'git_full_url' => "git@$this->git_host:$this->git_repository.git",
'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
@@ -158,6 +156,8 @@ class GithubPrivateRepositoryDeployKey extends Component
$this->git_host = $this->repository_url_parsed->getHost();
$this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
} else {
$this->git_repository = $this->repository_url;
}
$this->git_source = 'other';
}

View File

@@ -72,10 +72,14 @@ class PublicGitRepository extends Component
public function load_branch()
{
try {
$this->branch_found = false;
$this->validate([
'repository_url' => 'required|url'
]);
} catch (\Throwable $e) {
return handleError($e, $this);
}
try {
$this->branch_found = false;
$this->get_git_source();
$this->get_branch();
$this->selected_branch = $this->git_branch;

View File

@@ -2,12 +2,11 @@
namespace App\Http\Livewire\Project\New;
use App\Models\Project;
use App\Models\Server;
use Countable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Livewire\Component;
class Select extends Component
@@ -24,7 +23,8 @@ class Select extends Component
public Collection|array $services = [];
public bool $loadingServices = true;
public bool $loading = false;
public $environments = [];
public ?string $selectedEnvironment = null;
public ?string $existingPostgresqlUrl = null;
protected $queryString = [
@@ -37,8 +37,18 @@ class Select extends Component
if (isDev()) {
$this->existingPostgresqlUrl = 'postgres://coolify:password@coolify-db:5432';
}
$projectUuid = data_get($this->parameters, 'project_uuid');
$this->environments = Project::whereUuid($projectUuid)->first()->environments;
$this->selectedEnvironment = data_get($this->parameters, 'environment_name');
}
public function updatedSelectedEnvironment()
{
return redirect()->route('project.resources.new', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->selectedEnvironment,
]);
}
// public function addExistingPostgresql()
// {
// try {

View File

@@ -13,13 +13,7 @@ class Index extends Component
public $databases;
public array $parameters;
public array $query;
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
protected $listeners = ["saveCompose"];
protected $listeners = ["refreshStacks","checkStatus"];
public function render()
{
return view('livewire.project.service.index');
@@ -32,17 +26,12 @@ class Index extends Component
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function checkStatus()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->refreshStack();
$this->refreshStacks();
}
public function refreshStack()
public function refreshStacks()
{
$this->applications = $this->service->applications->sort();
$this->applications->each(function ($application) {
@@ -53,21 +42,4 @@ class Index extends Component
$database->refresh();
});
}
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->refreshStack();
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -6,8 +6,8 @@ use Livewire\Component;
class Modal extends Component
{
public function serviceStatusUpdated() {
$this->emit('serviceStatusUpdated');
public function checkStatus() {
$this->emit('checkStatus');
}
public function render()
{

View File

@@ -13,20 +13,15 @@ class Navbar extends Component
public Service $service;
public array $parameters;
public array $query;
protected $listeners = ['serviceStatusUpdated'];
public function render()
{
return view('livewire.project.service.navbar');
}
public function serviceStatusUpdated()
public function checkStatus()
{
$this->check_status();
}
public function check_status()
{
dispatch_sync(new ContainerStatusJob($this->service->server));
$this->service->refresh();
$this->emit('checkStatus');
}
public function deploy()
{
@@ -39,5 +34,6 @@ class Navbar extends Component
StopService::run($this->service);
$this->service->refresh();
$this->emit('success', 'Service stopped successfully.');
$this->checkStatus();
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Livewire\Project\Service;
use Livewire\Component;
class StackForm extends Component
{
public $service;
protected $listeners = ["saveCompose"];
protected $rules = [
'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required',
'service.name' => 'required',
'service.description' => 'nullable',
];
public function saveCompose($raw)
{
$this->service->docker_compose_raw = $raw;
$this->submit();
}
public function submit()
{
try {
$this->validate();
$this->service->save();
$this->service->parse();
$this->service->refresh();
$this->service->saveComposeConfigs();
$this->emit('refreshStacks');
$this->emit('refreshEnvs');
$this->emit('success', 'Service saved successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.service.stack-form');
}
}

View File

@@ -2,7 +2,7 @@
namespace App\Http\Livewire\Project\Shared;
use App\Actions\Service\StopService;
use App\Jobs\StopResourceJob;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,7 +10,7 @@ class Danger extends Component
{
public $resource;
public array $parameters;
public string|null $modalId = null;
public ?string $modalId = null;
public function mount()
{
@@ -20,20 +20,8 @@ class Danger extends Component
public function delete()
{
// Should be queued
try {
if ($this->resource->type() === 'service') {
$server = $this->resource->server;
StopService::run($this->resource);
} else {
$destination = data_get($this->resource, 'destination');
if ($destination) {
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server;
}
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
}
$this->resource->delete();
StopResourceJob::dispatchSync($this->resource);
return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'],
'environment_name' => $this->parameters['environment_name']

View File

@@ -75,6 +75,12 @@ class All extends Component
case 'standalone-postgresql':
$environment->standalone_postgresql_id = $this->resource->id;
break;
case 'standalone-redis':
$environment->standalone_redis_id = $this->resource->id;
break;
case 'standalone-mongodb':
$environment->standalone_mongodb_id = $this->resource->id;
break;
case 'service':
$environment->service_id = $this->resource->id;
break;

View File

@@ -13,6 +13,7 @@ class GetLogs extends Component
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public ?bool $showTimeStamps = true;
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
@@ -24,7 +25,11 @@ class GetLogs extends Component
public function getLogs($refresh = false)
{
if ($this->container) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($this->showTimeStamps) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
} else {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} {$this->container}");
}
if ($refresh) {
$this->outputs = '';
}

View File

@@ -5,13 +5,15 @@ namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|StandalonePostgresql|Service $resource;
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource;
public Server $server;
public ?string $container = null;
public $parameters;
@@ -33,7 +35,18 @@ class Logs extends Component
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail();
$resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneRedis::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
$resource = StandaloneMongodb::where('uuid', $this->parameters['database_uuid'])->first();
if (is_null($resource)) {
abort(404);
}
}
}
$this->resource = $resource;
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Livewire\Security;
use Livewire\Component;
class ApiTokens extends Component
{
public ?string $description = null;
public $tokens = [];
public function render()
{
return view('livewire.security.api-tokens');
}
public function mount()
{
$this->tokens = auth()->user()->tokens;
}
public function addNewToken()
{
try {
$this->validate([
'description' => 'required|min:3|max:255',
]);
$token = auth()->user()->createToken($this->description);
$this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken);
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function revoke(int $id)
{
$token = auth()->user()->tokens()->where('id', $id)->first();
$token->delete();
$this->tokens = auth()->user()->tokens;
}
}

View File

@@ -30,11 +30,11 @@ class Form extends Component
'wildcard_domain' => 'nullable|url',
];
protected $validationAttributes = [
'server.name' => 'name',
'server.description' => 'description',
'server.ip' => 'ip',
'server.user' => 'user',
'server.port' => 'port',
'server.name' => 'Name',
'server.description' => 'Description',
'server.ip' => 'IP address',
'server.user' => 'User',
'server.port' => 'Port',
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
'server.settings.is_reachable' => 'is reachable',
'server.settings.is_part_of_swarm' => 'is part of swarm'
@@ -45,7 +45,8 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
}
public function serverRefresh() {
public function serverRefresh()
{
$this->validateServer();
}
public function instantSave()
@@ -61,7 +62,19 @@ class Form extends Component
$activity = InstallDocker::run($this->server);
$this->emit('newMonitorActivity', $activity->id);
}
public function checkLocalhostConnection()
{
$uptime = $this->server->validateConnection();
if ($uptime) {
$this->emit('success', 'Server is reachable.');
$this->server->settings->is_reachable = true;
$this->server->settings->is_usable = true;
$this->server->settings->save();
} else {
$this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
}
public function validateServer($install = true)
{
try {
@@ -69,7 +82,7 @@ class Form extends Component
if ($uptime) {
$install && $this->emit('success', 'Server is reachable.');
} else {
$install &&$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.');
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
@@ -109,7 +122,14 @@ class Form extends Component
}
public function submit()
{
$this->validate();
if(isCloud() && !isDev()) {
$this->validate();
$this->validate([
'server.ip' => 'required|ip',
]);
} else {
$this->validate();
}
$uniqueIPs = Server::all()->reject(function (Server $server) {
return $server->id === $this->server->id;
})->pluck('ip')->toArray();
@@ -117,6 +137,7 @@ class Form extends Component
$this->emit('error', 'IP address is already in use by another team.');
return;
}
refresh_server_connection($this->server->privateKey);
$this->server->settings->wildcard_domain = $this->wildcard_domain;
$this->server->settings->cleanup_after_percentage = $this->cleanup_after_percentage;
$this->server->settings->save();

View File

@@ -11,13 +11,13 @@ class ByIp extends Component
{
public $private_keys;
public $limit_reached;
public int|null $private_key_id = null;
public ?int $private_key_id = null;
public $new_private_key_name;
public $new_private_key_description;
public $new_private_key_value;
public string $name;
public string|null $description = null;
public ?string $description = null;
public string $ip;
public string $user = 'root';
public int $port = 22;
@@ -26,16 +26,16 @@ class ByIp extends Component
protected $rules = [
'name' => 'required|string',
'description' => 'nullable|string',
'ip' => 'required',
'ip' => 'required|ip',
'user' => 'required|string',
'port' => 'required|integer',
];
protected $validationAttributes = [
'name' => 'name',
'description' => 'description',
'ip' => 'ip',
'user' => 'user',
'port' => 'port',
'name' => 'Name',
'description' => 'Description',
'ip' => 'IP Address',
'user' => 'User',
'port' => 'Port',
];
public function mount()

View File

@@ -15,7 +15,7 @@ class Show extends Component
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first();
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\Server;
use Livewire\Component;
@@ -11,18 +12,40 @@ class Deploy extends Component
public Server $server;
public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated'];
public ?string $serverIp = null;
public function mount() {
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated', "checkProxy", "startProxy"];
public function mount()
{
if ($this->server->id === 0) {
$this->serverIp = base_ip();
} else {
$this->serverIp = $this->server->ip;
}
$this->currentRoute = request()->route()->getName();
}
public function traefikDashboardAvailable(bool $data) {
public function traefikDashboardAvailable(bool $data)
{
$this->traefikDashboardAvailable = $data;
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function ip()
{
}
public function checkProxy()
{
try {
CheckProxy::run($this->server, true);
$this->emit('startProxyPolling');
$this->emit('proxyChecked');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function startProxy()
{
try {

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Server\Proxy;
use App\Models\Server;
use Livewire\Component;
class Logs extends Component
{
public ?Server $server = null;
public $parameters = [];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam(['name', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.proxy.logs');
}
}

View File

@@ -11,8 +11,6 @@ class Modal extends Component
public function proxyStatusUpdated()
{
$this->server->proxy->set('status', 'running');
$this->server->save();
$this->emit('proxyStatusUpdated');
}
}

View File

@@ -9,6 +9,11 @@ class Show extends Component
{
public ?Server $server = null;
public $parameters = [];
protected $listeners = ['proxyStatusUpdated'];
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function mount()
{
$this->parameters = get_route_parameters();

View File

@@ -2,6 +2,7 @@
namespace App\Http\Livewire\Server\Proxy;
use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use Livewire\Component;
@@ -9,12 +10,42 @@ use Livewire\Component;
class Status extends Component
{
public Server $server;
public bool $polling = false;
public int $numberOfPolls = 0;
protected $listeners = ['proxyStatusUpdated'];
protected $listeners = ['proxyStatusUpdated', 'startProxyPolling'];
public function startProxyPolling()
{
$this->polling = true;
}
public function proxyStatusUpdated()
{
$this->server->refresh();
}
public function checkProxy(bool $notification = false)
{
try {
if ($this->polling) {
if ($this->numberOfPolls >= 10) {
$this->polling = false;
$this->numberOfPolls = 0;
$notification && $this->emit('error', 'Proxy is not running.');
return;
}
$this->numberOfPolls++;
}
CheckProxy::run($this->server, true);
$this->emit('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->emit('success', 'Proxy is running.');
} else {
$notification && $this->emit('error', 'Proxy is not running.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function getProxyStatus()
{
try {
@@ -24,11 +55,4 @@ class Status extends Component
return handleError($e, $this);
}
}
public function getProxyStatusWithNoti()
{
if ($this->server->isFunctional()) {
$this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus();
}
}
}

View File

@@ -32,34 +32,18 @@ class ShowPrivateKey extends Component
}
}
public function checkConnection($install = false)
public function checkConnection()
{
try {
$uptime = $this->server->validateConnection();
if ($uptime) {
$install && $this->emit('success', 'Server is reachable.');
$this->emit('success', 'Server is reachable.');
} else {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
} else {
$install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.');
} else {
$install && $this->installDocker();
$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.');
return;
}
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->emit('proxyStatusUpdated');
}
}

View File

@@ -78,10 +78,10 @@ class Backup extends Component
dispatch(new DatabaseBackupJob(
backup: $this->backup
));
$this->emit('success', 'Backup queued. It will be available in a few minutes');
$this->emit('success', 'Backup queued. It will be available in a few minutes.');
}
public function submit()
{
$this->emit('success', 'Backup updated successfully');
$this->emit('success', 'Backup updated successfully.');
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Livewire\Subscription;
use App\Models\InstanceSettings;
use Livewire\Component;
class Show extends Component
{
public InstanceSettings $settings;
public bool $alreadySubscribed = false;
public function mount() {
if (!isCloud()) {
return redirect('/');
}
$this->settings = InstanceSettings::get();
$this->alreadySubscribed = currentTeam()->subscription()->exists();
}
public function stripeCustomerPortal() {
$session = getStripeCustomerPortalSession(currentTeam());
if (is_null($session)) {
return;
}
return redirect($session->url);
}
public function render()
{
return view('livewire.subscription.show')->layout('layouts.subscription');
}
}

View File

@@ -64,7 +64,7 @@ class Create extends Component
}
$this->storage->team_id = currentTeam()->id;
$this->storage->testConnection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->is_usable = true;
$this->storage->save();
return redirect()->route('team.storages.show', $this->storage->uuid);
} catch (\Throwable $e) {

View File

@@ -9,6 +9,7 @@ class Form extends Component
{
public S3Storage $storage;
protected $rules = [
'storage.is_usable' => 'nullable|boolean',
'storage.name' => 'nullable|min:3|max:255',
'storage.description' => 'nullable|min:3|max:255',
'storage.region' => 'required|max:255',
@@ -18,6 +19,7 @@ class Form extends Component
'storage.endpoint' => 'required|url|max:255',
];
protected $validationAttributes = [
'storage.is_usable' => 'Is Usable',
'storage.name' => 'Name',
'storage.description' => 'Description',
'storage.region' => 'Region',

View File

@@ -5,10 +5,11 @@ namespace App\Http\Livewire;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use Livewire\Component;
use Masmerise\Toaster\Toaster;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
class Upgrade extends Component
{
use WithRateLimiting;
public bool $showProgress = false;
public bool $isUpgradeAvailable = false;
public string $latestVersion = '';
@@ -31,11 +32,12 @@ class Upgrade extends Component
public function upgrade()
{
try {
$this->rateLimit(1, 30);
if ($this->showProgress) {
return;
}
$this->showProgress = true;
resolve(UpdateCoolify::class)(true);
UpdateCoolify::run(true);
$this->emit('success', "Upgrading to {$this->latestVersion} version...");
} catch (\Throwable $e) {
return handleError($e, $this);

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
class VerifyEmail extends Component
{
use WithRateLimiting;
public function again() {
try {
$this->rateLimit(1, 300);
auth()->user()->sendVerificationEmail();
$this->emit('success', 'Email verification link sent!');
} catch(\Exception $e) {
ray($e);
return handleError($e,$this);
}
}
public function render()
{
return view('livewire.verify-email');
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Str;
class DecideWhatToDoWithUser
{
public function handle(Request $request, Closure $next): Response
{
if (!auth()->user() || !isCloud() || isInstanceAdmin()) {
if (!isCloud() && showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
return redirect('boarding');
}
return $next($request);
}
if (!auth()->user()->hasVerifiedEmail()) {
if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) {
return $next($request);
}
return redirect('/verify');
}
if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) {
if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('subscription');
}
}
if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) {
if (Str::startsWith($request->path(), 'invitations')) {
return $next($request);
}
return redirect('boarding');
}
if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') {
return redirect('/');
}
if (isSubscriptionActive() && $request->path() === 'subscription') {
return redirect('/');
}
return $next($request);
}
}

View File

@@ -45,15 +45,19 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $commit;
private bool $force_rebuild;
private ?string $dockerImage = null;
private ?string $dockerImageTag = null;
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination;
private Server $server;
private ApplicationPreview|null $preview = null;
private string $container_name;
private string|null $currently_running_container_name = null;
private ?string $currently_running_container_name = null;
private string $basedir;
private string $workdir;
private ?string $build_pack = null;
private string $configuration_dir;
private string $build_image_name;
private string $production_image_name;
@@ -62,17 +66,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_args;
private $docker_compose;
private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private ?string $addHosts = null;
private $log_model;
private Collection $saved_outputs;
private string $serverUser = 'root';
private string $serverUserHomeDir = '/root';
private string $dockerConfigFileExists = 'NOK';
private int $customPort = 22;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
{
ray()->clearScreen();
// ray()->clearScreen();
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -86,13 +98,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server;
$this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
ray($this->basedir, $this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
@@ -132,20 +143,60 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
// Generate custom host<->ip mapping
$allContainers = instant_remote_process(["docker network inspect {$this->destination->network} -f '{{json .Containers}}' "], $this->server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
$this->addHosts = $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
// Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) {
$this->customPort = $matches[0];
$gitHost = str($this->application->git_repository)->before(':');
$gitRepo = str($this->application->git_repository)->after('/');
$this->application->git_repository = "$gitHost:$gitRepo";
}
try {
if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage_buildpack();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile_buildpack();
} else {
if ($this->pull_request_id !== 0) {
$this->deploy_pull_request();
} else {
$this->deploy();
$this->deploy_nixpacks_buildpack();
}
}
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
} catch (Exception $e) {
ray($e);
$this->fail($e);
@@ -173,41 +224,43 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
}
private function deploy_docker_compose()
{
$dockercompose_base64 = base64_encode($this->application->dockercompose);
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->name}.'"
],
);
$this->prepare_builder_image();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
],
);
$this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
$this->save_environment_variables();
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(
["docker rm -f {$containerName}"],
$this->application->destination->server
);
}
}
}
$this->execute_remote_command(
["echo -n 'Starting services (could take a while)...'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
// private function deploy_docker_compose()
// {
// $dockercompose_base64 = base64_encode($this->application->dockercompose);
// $this->execute_remote_command(
// [
// "echo 'Starting deployment of {$this->application->name}.'"
// ],
// );
// $this->prepare_builder_image();
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$dockercompose_base64' | base64 -d > $this->workdir/docker-compose.yaml")
// ],
// );
// $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
// $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
// $this->save_environment_variables();
// $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id);
// ray($containers);
// if ($containers->count() > 0) {
// foreach ($containers as $container) {
// $containerName = data_get($container, 'Names');
// if ($containerName) {
// instant_remote_process(
// ["docker rm -f {$containerName}"],
// $this->application->destination->server
// );
// }
// }
// }
// $this->execute_remote_command(
// ["echo -n 'Starting services (could take a while)...'"],
// [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
// );
// }
private function save_environment_variables()
{
$envs = collect([]);
@@ -245,7 +298,51 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update();
}
private function deploy()
private function deploy_dockerimage_buildpack()
{
$this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
$this->prepare_builder_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function deploy_dockerfile_buildpack()
{
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->clone_repository();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
$this->build_image();
$this->rolling_update();
}
private function deploy_nixpacks_buildpack()
{
$this->execute_remote_command(
[
@@ -268,19 +365,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped...'"
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'",
]);
$this->generate_compose_file();
$this->rolling_update();
return;
}
if ($this->application->isConfigurationChanged()) {
$this->execute_remote_command([
"echo 'Configuration changed. Rebuilding image.'",
]);
}
}
$this->cleanup_git();
if ($this->application->build_pack === 'nixpacks') {
$this->generate_nixpacks_confs();
}
$this->generate_nixpacks_confs();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
@@ -374,7 +474,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->stop_running_container();
$this->execute_remote_command(
["echo -n 'Starting preview deployment.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d"), "hidden" => true],
);
}
@@ -382,7 +482,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{
$pull = "--pull=always";
$helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else {
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
}
$this->execute_remote_command(
[
@@ -398,7 +502,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
);
}
private function set_base_dir() {
private function set_base_dir()
{
$this->execute_remote_command(
[
"echo -n 'Setting base directory to {$this->workdir}.'"
@@ -456,7 +561,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
}
if ($this->application->deploymentType() === 'deploy_key') {
$private_key = base64_encode($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->basedir}";
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -556,6 +661,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$volume_names = $this->generate_local_persistent_volumes_only_volume_names();
$environment_variables = $this->generate_environment_variables($ports);
$labels = generateLabelsApplication($this->application, $this->preview);
if (data_get($this->application, 'custom_labels')) {
$labels = str($this->application->custom_labels)->explode(',')->toArray();
}
$docker_compose = [
'version' => '3.8',
'services' => [
@@ -564,7 +673,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
'container_name' => $this->container_name,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => generateLabelsApplication($this->application, $this->preview),
'labels' => $labels,
'expose' => $ports,
'networks' => [
$this->destination->network,
@@ -608,6 +717,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// if ($this->build_pack === 'dockerfile') {
// $docker_compose['services'][$this->container_name]['build'] = [
// 'context' => $this->workdir,
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
// ];
// }
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -671,7 +786,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_healthcheck_commands()
{
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0';
}
@@ -700,7 +815,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->application->settings->is_static) {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]);
$dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -733,12 +848,12 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$nginx_config}' | base64 -d > {$this->workdir}/nginx.conf")
],
[
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}/Dockerfile-prod {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]
);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --network host -f {$this->workdir}/Dockerfile {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
executeInDocker($this->deployment_uuid, "docker build $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
}
@@ -764,7 +879,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{
$this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
@@ -787,7 +902,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function add_build_env_variables_to_dockerfile()
{
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/Dockerfile"), "hidden" => true, "save" => 'dockerfile'
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
@@ -796,7 +911,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}
$dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/Dockerfile"),
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"),
"hidden" => true
]);
}

View File

@@ -2,6 +2,7 @@
namespace App\Jobs;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Models\ApplicationPreview;
use App\Models\Server;
@@ -11,7 +12,6 @@ use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
@@ -24,51 +24,61 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 120;
public function __construct(public Server $server)
{
$this->handle();
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))->dontRelease()];
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): string
public function uniqueId(): int
{
return $this->server->uuid;
return $this->server->id;
}
public function handle()
public function handle(): void
{
ray("checking server status for {$this->server->id}");
try {
ray("checking server status for {$this->server->name}");
// ray()->clearAll();
$serverUptimeCheckNumber = 0;
$serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3;
while (true) {
ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
send_internal_notification('Server unreachable: ' . $this->server->name);
if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...');
$this->server->team->notify(new Unreachable($this->server));
}
$this->server->settings()->update([
'is_reachable' => false,
]);
// ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...');
$this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]);
return;
}
$result = $this->server->validateConnection();
if ($result) {
break;
}
$serverUptimeCheckNumber++;
sleep(5);
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => 0,
]);
return;
}
$result = $this->server->validateConnection();
if ($result) {
$this->server->settings()->update([
'is_reachable' => true,
]);
$this->server->update([
'unreachable_count' => 0,
]);
} else {
$serverUptimeCheckNumber++;
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
return;
}
if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...');
$this->server->team->notify(new Revived($this->server));
@@ -83,7 +93,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
'is_usable' => true
]);
}
$this->server->validateDockerEngine(true);
// $this->server->validateDockerEngine(true);
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
return;
@@ -100,10 +110,16 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
ray('Proxy not found, starting it...');
if ($this->server->isProxyShouldRun()) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
@@ -283,7 +299,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
return handleError($e);
handleError($e);
}
}
}

View File

@@ -6,6 +6,7 @@ use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
use App\Models\Server;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\Team;
use App\Notifications\Database\BackupFailed;
@@ -27,11 +28,11 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?Team $team = null;
public Server $server;
public ScheduledDatabaseBackup $backup;
public StandalonePostgresql $database;
public StandalonePostgresql|StandaloneMongodb $database;
public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status;
public string $backup_status = 'failed';
public ?string $backup_location = null;
public string $backup_dir;
public string $backup_file;
@@ -66,65 +67,148 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('database not running');
return;
}
$databaseType = $this->database->type();
$databasesToBackup = data_get($this->backup, 'databases_to_backup');
if (is_null($databasesToBackup)) {
if ($databaseType === 'standalone-postgresql') {
$databasesToBackup = [$this->database->postgres_db];
} else if ($databaseType === 'standalone-mongodb') {
$databasesToBackup = ['*'];
} else {
return;
}
} else {
if ($databaseType === 'standalone-postgresql') {
// Format: db1,db2,db3
$databasesToBackup = explode(',', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
} else if ($databaseType === 'standalone-mongodb') {
// Format: db1:collection1,collection2|db2:collection3,collection4
$databasesToBackup = explode('|', $databasesToBackup);
$databasesToBackup = array_map('trim', $databasesToBackup);
ray($databasesToBackup);
} else {
return;
}
}
$this->container_name = $this->database->uuid;
$this->backup_dir = backup_dir() . "/databases/" . Str::of($this->team->name)->slug() . '-' . $this->team->id . '/' . $this->container_name;
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
$this->container_name = "coolify-db";
$ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
}
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
if ($this->database->type() === 'standalone-postgresql') {
$this->backup_standalone_postgresql();
foreach ($databasesToBackup as $database) {
$size = 0;
ray('Backing up ' . $database);
try {
if ($databaseType === 'standalone-postgresql') {
$this->backup_file = "/pg-dump-$database-" . Carbon::now()->timestamp . ".dmp";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_postgresql($database);
} else if ($databaseType === 'standalone-mongodb') {
if ($database === '*') {
$database = 'all';
$databaseName = 'all';
} else {
if (str($database)->contains(':')) {
$databaseName = str($database)->before(':');
} else {
$databaseName = $database;
}
ray($databaseName);
}
$this->backup_file = "/mongo-dump-$databaseName-" . Carbon::now()->timestamp . ".tar.gz";
$this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $databaseName,
'filename' => $this->backup_location,
'scheduled_database_backup_id' => $this->backup->id,
]);
$this->backup_standalone_mongodb($database);
} else {
throw new \Exception('Unsupported database type');
}
$size = $this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_log->update([
'status' => 'success',
'message' => $this->backup_output,
'size' => $size,
]);
} catch (\Throwable $e) {
if ($this->backup_log) {
$this->backup_log->update([
'status' => 'failed',
'message' => $this->backup_output,
'size' => $size,
'filename' => null
]);
}
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e;
}
}
$this->calculate_size();
$this->remove_old_backups();
if ($this->backup->save_s3) {
$this->upload_to_s3();
}
$this->save_backup_logs();
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
throw $e;
}
}
private function backup_standalone_postgresql(): void
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$url = $this->database->getDbUrl();
if ($databaseWithCollections === 'all') {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --gzip --archive > $this->backup_location";
} else {
$collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
$databaseName = str($databaseWithCollections)->before(':');
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=$url --db $databaseName --gzip --excludeCollection " . $collectionsToExclude->implode(' --excludeCollection ') . " --archive > $this->backup_location";
}
ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
$this->backup_status = 'success';
$this->team->notify(new BackupSuccess($this->backup, $this->database));
} catch (\Throwable $e) {
$this->backup_status = 'failed';
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
throw $e;
}
}
private function backup_standalone_postgresql(string $database): void
{
try {
$commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') {
$this->backup_output = null;
}
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
throw $e;
}
}
@@ -137,9 +221,9 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
}
}
private function calculate_size(): void
private function calculate_size()
{
$this->size = instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server);
return instant_remote_process(["du -b $this->backup_location | cut -f1"], $this->server, false);
}
private function remove_old_backups(): void
@@ -163,11 +247,12 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
}
$key = $this->s3->key;
$secret = $this->s3->secret;
// $region = $this->s3->region;
// $region = $this->s3->region;
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection();
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);
@@ -175,19 +260,10 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir);
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
ray($e->getMessage());
throw $e;
} finally {
$command = "docker rm -f backup-of-{$this->backup->uuid}";
instant_remote_process([$command], $this->server);
}
}
private function save_backup_logs(): void
{
$this->backup_log->update([
'status' => $this->backup_status,
'message' => $this->backup_output,
'size' => $this->size,
]);
}
}

View File

@@ -47,20 +47,7 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
if (!$this->server->isFunctional()) {
return;
}
if (isDev()) {
$this->dockerRootFilesystem = "/";
} else {
$this->dockerRootFilesystem = instant_remote_process(
[
"stat --printf=%m $(docker info --format '{{json .DockerRootDir}}'' |sed 's/\"//g')"
],
$this->server,
false
);
}
if (!$this->dockerRootFilesystem) {
return;
}
$this->dockerRootFilesystem = "/";
$this->usageBefore = $this->getFilesystemUsage();
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name)->color('orange');

View File

@@ -23,6 +23,6 @@ class InstanceAutoUpdateJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncr
public function handle(): void
{
resolve(UpdateCoolify::class)($this->force);
UpdateCoolify::run($this->force);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Service\StopService;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandaloneMongodb;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class StopResourceJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb $resource)
{
}
public function handle()
{
try {
$server = $this->resource->destination->server;
if (!$server->isFunctional()) {
return 'Server is not functional';
}
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
break;
case 'standalone-postgresql':
StopDatabase::run($this->resource);
break;
case 'standalone-redis':
StopDatabase::run($this->resource);
break;
case 'standalone-mongodb':
StopDatabase::run($this->resource);
break;
case 'service':
StopService::run($this->resource);
break;
}
} catch (\Throwable $e) {
send_internal_notification('ContainerStoppingJob failed with: ' . $e->getMessage());
throw $e;
} finally {
$this->resource->delete();
}
}
}

View File

@@ -32,14 +32,8 @@ class Application extends BaseModel
]);
});
static::deleting(function ($application) {
// Stop Container
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
$application->settings()->delete();
$storages = $application->persistentStorages()->get();
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
}
@@ -99,7 +93,21 @@ class Application extends BaseModel
}
);
}
public function dockerfileLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/Dockerfile';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
}
);
}
public function baseDirectory(): Attribute
{
return Attribute::make(
@@ -257,15 +265,43 @@ class Application extends BaseModel
if ($this->dockerfile) {
return false;
}
if ($this->build_pack === 'dockerimage') {
return false;
}
return true;
}
public function isHealthcheckDisabled(): bool
{
if (data_get($this, 'dockerfile') || data_get($this, 'build_pack') === 'dockerfile' || data_get($this, 'health_check_enabled') === false) {
ray('dockerfile');
if (data_get($this, 'health_check_enabled') === false) {
return true;
}
return false;
}
public function isConfigurationChanged($save = false)
{
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
if ($this->pull_request_id === 0) {
$newConfigHash .= json_encode($this->environment_variables->all());
} else {
$newConfigHash .= json_encode($this->environment_variables_preview->all());
}
$newConfigHash = md5($newConfigHash);
$oldConfigHash = data_get($this, 'config_hash');
if ($oldConfigHash === null) {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
if ($oldConfigHash === $newConfigHash) {
return false;
} else {
if ($save) {
$this->config_hash = $newConfigHash;
$this->save();
}
return true;
}
}
}

View File

@@ -14,7 +14,11 @@ class Environment extends Model
public function can_delete_environment()
{
return $this->applications()->count() == 0 && $this->postgresqls()->count() == 0 && $this->services()->count() == 0;
return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;
}
public function applications()
@@ -26,10 +30,21 @@ class Environment extends Model
{
return $this->hasMany(StandalonePostgresql::class);
}
public function redis()
{
return $this->hasMany(StandaloneRedis::class);
}
public function mongodbs()
{
return $this->hasMany(StandaloneMongodb::class);
}
public function databases()
{
return $this->postgresqls();
$postgresqls = $this->postgresqls;
$redis = $this->redis;
$mongodbs = $this->mongodbs;
return $postgresqls->concat($redis)->concat($mongodbs);
}
public function project()

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Laravel\Sanctum\PersonalAccessToken as SanctumPersonalAccessToken;
class PersonalAccessToken extends SanctumPersonalAccessToken
{
protected $fillable = [
'name',
'token',
'abilities',
'expires_at',
'team_id',
];
}

View File

@@ -52,4 +52,8 @@ class Project extends BaseModel
{
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
}
public function redis()
{
return $this->hasManyThrough(StandaloneRedis::class, Environment::class);
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Storage;
class S3Storage extends BaseModel
{
@@ -10,6 +12,7 @@ class S3Storage extends BaseModel
protected $guarded = [];
protected $casts = [
'is_usable' => 'boolean',
'key' => 'encrypted',
'secret' => 'encrypted',
];
@@ -19,7 +22,15 @@ class S3Storage extends BaseModel
$selectArray = collect($select)->concat(['id']);
return S3Storage::whereTeamId(currentTeam()->id)->select($selectArray->all())->orderBy('name');
}
public function isUsable()
{
return $this->is_usable;
}
public function team()
{
return $this->belongsTo(Team::class);
}
public function awsUrl()
{
return "{$this->endpoint}/{$this->bucket}";
@@ -27,7 +38,34 @@ class S3Storage extends BaseModel
public function testConnection()
{
set_s3_target($this);
return \Storage::disk('custom-s3')->files();
try {
set_s3_target($this);
Storage::disk('custom-s3')->files();
$this->unusable_email_sent = false;
$this->is_usable = true;
return;
} catch (\Throwable $e) {
$this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
$mail = new MailMessage();
$mail->subject('Coolify: S3 Storage Connection Error');
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('team.storages.show', ['storage_uuid' => $this->uuid])]);
$users = collect([]);
$members = $this->team->members()->get();
foreach ($members as $user) {
if ($user->isAdmin()) {
$users->push($user);
}
}
foreach ($users as $user) {
send_user_an_email($mail, $user->email);
}
$this->unusable_email_sent = true;
}
throw $e;
} finally {
$this->save();
}
}
}

View File

@@ -17,10 +17,14 @@ class Server extends BaseModel
protected static function booted()
{
static::saving(function ($server) {
$server->forceFill([
'ip' => Str::of($server->ip)->trim(),
'user' => Str::of($server->user)->trim(),
]);
$payload = [];
if ($server->user) {
$payload['user'] = Str::of($server->user)->trim();
}
if ($server->ip) {
$payload['ip'] = Str::of($server->ip)->trim();
}
$server->forceFill($payload);
});
static::created(function ($server) {
@@ -89,8 +93,11 @@ class Server extends BaseModel
public function proxyType()
{
$type = $this->proxy->get('type');
if (is_null($type)) {
$proxyType = $this->proxy->get('type');
if ($proxyType === ProxyTypes::NONE->value) {
return $proxyType;
}
if (is_null($proxyType)) {
$this->proxy->type = ProxyTypes::TRAEFIK_V2->value;
$this->proxy->status = ProxyStatus::EXITED->value;
$this->save();
@@ -116,7 +123,9 @@ class Server extends BaseModel
{
return $this->destinations()->map(function ($standaloneDocker) {
$postgresqls = $standaloneDocker->postgresqls;
return $postgresqls?->concat([]) ?? collect([]);
$redis = $standaloneDocker->redis;
$mongodbs = $standaloneDocker->mongodbs;
return $postgresqls->concat($redis)->concat($mongodbs);
})->flatten();
}
public function applications()
@@ -185,23 +194,24 @@ class Server extends BaseModel
}
public function isProxyShouldRun()
{
$shouldRun = false;
if ($this->proxyType() === ProxyTypes::NONE->value) {
return false;
}
foreach ($this->applications() as $application) {
if (data_get($application, 'fqdn')) {
$shouldRun = true;
break;
}
}
if ($this->id === 0) {
$settings = InstanceSettings::get();
if (data_get($settings, 'fqdn')) {
$shouldRun = true;
}
}
return $shouldRun;
// foreach ($this->applications() as $application) {
// if (data_get($application, 'fqdn')) {
// $shouldRun = true;
// break;
// }
// }
// ray($this->services()->get());
// if ($this->id === 0) {
// $settings = InstanceSettings::get();
// if (data_get($settings, 'fqdn')) {
// $shouldRun = true;
// }
// }
return true;
}
public function isFunctional()
{

View File

@@ -19,7 +19,6 @@ class Service extends BaseModel
static::deleting(function ($service) {
$storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
@@ -27,7 +26,6 @@ class Service extends BaseModel
$application->persistentStorages()->delete();
}
foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
$storagesToDelete->push($storage);
@@ -52,7 +50,7 @@ class Service extends BaseModel
public function documentation()
{
$services = Cache::get('services', []);
$services = getServiceTemplates();
$service = data_get($services, Str::of($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url'));
}
@@ -384,7 +382,7 @@ class Service extends BaseModel
$value = Str::of($variable);
}
if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew) {
if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
$fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
@@ -540,7 +538,7 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true));
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
}
}
data_set($service, 'labels', $serviceLabels->toArray());
@@ -570,7 +568,7 @@ class Service extends BaseModel
'networks' => $topLevelNetworks->toArray(),
];
$this->docker_compose_raw = Yaml::dump($yaml, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->docker_compose = Yaml::dump($finalServices, 10, 2);
$this->save();
$this->saveComposeConfigs();
return collect([]);

View File

@@ -16,6 +16,15 @@ class StandaloneDocker extends BaseModel
return $this->morphMany(StandalonePostgresql::class, 'destination');
}
public function redis()
{
return $this->morphMany(StandaloneRedis::class, 'destination');
}
public function mongodbs()
{
return $this->morphMany(StandaloneMongodb::class, 'destination');
}
public function server()
{
return $this->belongsTo(Server::class);

View File

@@ -0,0 +1,99 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneMongodb extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'mongodb-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-mongodb';
}
public function getDbUrl() {
if ($this->is_public) {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->destination->server->getIp}:{$this->public_port}/?directConnection=true";
} else {
return "mongodb://{$this->mongo_initdb_root_username}:{$this->mongo_initdb_root_password}@{$this->uuid}:27017/?directConnection=true";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -29,21 +29,13 @@ class StandalonePostgresql extends BaseModel
]);
});
static::deleting(function ($database) {
// Stop Container
instant_remote_process(
["docker rm -f {$database->uuid}"],
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->scheduledBackups()->delete();
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
});
}
@@ -70,6 +62,14 @@ class StandalonePostgresql extends BaseModel
{
return 'standalone-postgresql';
}
public function getDbUrl(): string
{
if ($this->is_public) {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->destination->server->getIp}:{$this->public_port}/{$this->postgres_db}";
} else {
return "postgres://{$this->postgres_user}:{$this->postgres_password}@{$this->uuid}:5432/{$this->postgres_db}";
}
}
public function environment()
{

View File

@@ -0,0 +1,100 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
class StandaloneRedis extends BaseModel
{
use HasFactory;
protected $guarded = [];
protected static function booted()
{
static::created(function ($database) {
LocalPersistentVolume::create([
'name' => 'redis-data-' . $database->uuid,
'mount_path' => '/data',
'host_path' => null,
'resource_id' => $database->id,
'resource_type' => $database->getMorphClass(),
'is_readonly' => true
]);
});
static::deleting(function ($database) {
$database->scheduledBackups()->delete();
$storages = $database->persistentStorages()->get();
foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $database->destination->server, false);
}
$database->persistentStorages()->delete();
$database->environment_variables()->delete();
});
}
public function portsMappings(): Attribute
{
return Attribute::make(
set: fn ($value) => $value === "" ? null : $value,
);
}
public function portsMappingsArray(): Attribute
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
);
}
public function type(): string
{
return 'standalone-redis';
}
public function getDbUrl(): string {
if ($this->is_public) {
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
} else {
return "redis://:{$this->redis_password}@{$this->uuid}:6379/0";
}
}
public function environment()
{
return $this->belongsTo(Environment::class);
}
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination()
{
return $this->morphTo();
}
public function environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
}
public function persistentStorages()
{
return $this->morphMany(LocalPersistentVolume::class, 'resource');
}
public function scheduledBackups()
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
}

View File

@@ -33,7 +33,7 @@ class Subscription extends Model
}
if (isStripe()) {
if (!$this->stripe_plan_id) {
return 'zero';
return 'zero';
}
$subscription = Subscription::where('id', $this->id)->first();
if (!$subscription) {

View File

@@ -48,6 +48,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
return explode(',', $recipients);
}
public function limits(): Attribute
{
return Attribute::make(
@@ -125,7 +126,7 @@ class Team extends Model implements SendsDiscord, SendsEmail
public function s3s()
{
return $this->hasMany(S3Storage::class);
return $this->hasMany(S3Storage::class)->where('is_usable', true);
}
public function trialEnded() {
foreach ($this->servers as $server) {

View File

@@ -4,12 +4,19 @@ namespace App\Models;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Sanctum\NewAccessToken;
use Illuminate\Support\Str;
class User extends Authenticatable implements SendsEmail
{
@@ -43,7 +50,26 @@ class User extends Authenticatable implements SendsEmail
$user->teams()->attach($new_team, ['role' => 'owner']);
});
}
public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null)
{
ray('asd');
$plainTextToken = sprintf(
'%s%s%s',
config('sanctum.token_prefix', ''),
$tokenEntropy = Str::random(40),
hash('crc32b', $tokenEntropy)
);
$token = $this->tokens()->create([
'name' => $name,
'token' => hash('sha256', $plainTextToken),
'abilities' => $abilities,
'expires_at' => $expiresAt,
'team_id' => session('currentTeam')->id
]);
return new NewAccessToken($token, $token->getKey().'|'.$plainTextToken);
}
public function teams()
{
return $this->belongsToMany(Team::class)->withPivot('role');
@@ -54,6 +80,23 @@ class User extends Authenticatable implements SendsEmail
return $this->email;
}
public function sendVerificationEmail()
{
$mail = new MailMessage();
$url = Url::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $this->getKey(),
'hash' => sha1($this->getEmailForVerification()),
]
);
$mail->view('emails.email-verification', [
'url' => $url,
]);
$mail->subject('Coolify: Verify your email.');
send_user_an_email($mail, $this->email);
}
public function sendPasswordResetNotification($token): void
{
$this->notify(new TransactionalEmailsResetPassword($token));
@@ -61,7 +104,7 @@ class User extends Authenticatable implements SendsEmail
public function isAdmin()
{
return data_get($this->pivot,'role') === 'admin' || data_get($this->pivot,'role') === 'owner';
return data_get($this->pivot, 'role') === 'admin' || data_get($this->pivot, 'role') === 'owner';
}
public function isAdminFromSession()
@@ -79,7 +122,7 @@ class User extends Authenticatable implements SendsEmail
return true;
}
$team = $teams->where('id', session('currentTeam')->id)->first();
$role = data_get($team,'pivot.role');
$role = data_get($team, 'pivot.role');
return $role === 'admin' || $role === 'owner';
}
@@ -96,7 +139,10 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
return Cache::remember('team:' . auth()->user()->id, 3600, function() {
return Cache::remember('team:' . auth()->user()->id, 3600, function () {
if (is_null(data_get(session('currentTeam'), 'id'))) {
return auth()->user()->teams[0];
}
return Team::find(session('currentTeam')->id);
});
}

View File

@@ -52,10 +52,10 @@ class DeploymentFailed extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject(' Deployment failed of ' . $this->application_name . '.');
$mail->subject('Coolify: Deployment failed of ' . $this->application_name . '.');
} else {
$fqdn = $this->preview->fqdn;
$mail->subject(' Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
$mail->subject('Coolify: Deployment failed of pull request #' . $this->preview->pull_request_id . ' of ' . $this->application_name . '.');
}
$mail->view('emails.application-deployment-failed', [
'name' => $this->application_name,
@@ -69,10 +69,10 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toDiscord(): string
{
if ($this->preview) {
$message = ' Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
} else {
$message = ' Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message .= '[View Deployment Logs](' . $this->deployment_url . ')';
}
return $message;
@@ -80,9 +80,9 @@ class DeploymentFailed extends Notification implements ShouldQueue
public function toTelegram(): array
{
if ($this->preview) {
$message = ' Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
$message = 'Coolify: Pull request #' . $this->preview->pull_request_id . ' of **' . $this->application_name . '** (' . $this->preview->fqdn . ') deployment failed: ';
} else {
$message = ' Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
$message = 'Coolify: Deployment failed of **' . $this->application_name . '** (' . $this->fqdn . '): ';
}
return [
"message" => $message,

View File

@@ -52,10 +52,10 @@ class DeploymentSuccess extends Notification implements ShouldQueue
$pull_request_id = data_get($this->preview, 'pull_request_id', 0);
$fqdn = $this->fqdn;
if ($pull_request_id === 0) {
$mail->subject(" New version is deployed of {$this->application_name}");
$mail->subject("Coolify: New version is deployed of {$this->application_name}");
} else {
$fqdn = $this->preview->fqdn;
$mail->subject(" Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");
$mail->subject("Coolify: Pull request #{$pull_request_id} of {$this->application_name} deployed successfully");
}
$mail->view('emails.application-deployment-success', [
'name' => $this->application_name,
@@ -69,7 +69,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toDiscord(): string
{
if ($this->preview) {
$message = ' New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '
';
if ($this->preview->fqdn) {
@@ -77,7 +77,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
}
$message .= '[Deployment logs](' . $this->deployment_url . ')';
} else {
$message = ' New version successfully deployed of ' . $this->application_name . '
$message = 'Coolify: New version successfully deployed of ' . $this->application_name . '
';
if ($this->fqdn) {
@@ -90,7 +90,7 @@ class DeploymentSuccess extends Notification implements ShouldQueue
public function toTelegram(): array
{
if ($this->preview) {
$message = ' New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
$message = 'Coolify: New PR' . $this->preview->pull_request_id . ' version successfully deployed of ' . $this->application_name . '';
if ($this->preview->fqdn) {
$buttons[] = [
"text" => "Open Application",

View File

@@ -15,25 +15,23 @@ class StatusChanged extends Notification implements ShouldQueue
public $tries = 1;
public Application $application;
public string $application_name;
public string $resource_name;
public string $project_uuid;
public string $environment_name;
public ?string $application_url = null;
public ?string $resource_url = null;
public ?string $fqdn;
public function __construct($application)
public function __construct(public Application $resource)
{
$this->application = $application;
$this->application_name = data_get($application, 'name');
$this->project_uuid = data_get($application, 'environment.project.uuid');
$this->environment_name = data_get($application, 'environment.name');
$this->fqdn = data_get($application, 'fqdn', null);
$this->resource_name = data_get($resource, 'name');
$this->project_uuid = data_get($resource, 'environment.project.uuid');
$this->environment_name = data_get($resource, 'environment.name');
$this->fqdn = data_get($resource, 'fqdn', null);
if (Str::of($this->fqdn)->explode(',')->count() > 1) {
$this->fqdn = Str::of($this->fqdn)->explode(',')->first();
}
$this->application_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->application->uuid}";
$this->resource_url = base_url() . "/project/{$this->project_uuid}/{$this->environment_name}/application/{$this->resource->uuid}";
}
public function via(object $notifiable): array
@@ -45,32 +43,32 @@ class StatusChanged extends Notification implements ShouldQueue
{
$mail = new MailMessage();
$fqdn = $this->fqdn;
$mail->subject(" {$this->application_name} has been stopped");
$mail->subject("Coolify: {$this->resource_name} has been stopped");
$mail->view('emails.application-status-changes', [
'name' => $this->application_name,
'name' => $this->resource_name,
'fqdn' => $fqdn,
'application_url' => $this->application_url,
'resource_url' => $this->resource_url,
]);
return $mail;
}
public function toDiscord(): string
{
$message = ' ' . $this->application_name . ' has been stopped.
$message = 'Coolify: ' . $this->resource_name . ' has been stopped.
';
$message .= '[Open Application in Coolify](' . $this->application_url . ')';
$message .= '[Open Application in Coolify](' . $this->resource_url . ')';
return $message;
}
public function toTelegram(): array
{
$message = ' ' . $this->application_name . ' has been stopped.';
$message = 'Coolify: ' . $this->resource_name . ' has been stopped.';
return [
"message" => $message,
"buttons" => [
[
"text" => "Open Application in Coolify",
"url" => $this->application_url
"url" => $this->resource_url
]
],
];

View File

@@ -30,7 +30,12 @@ class EmailChannel
);
} catch (Exception $e) {
ray($e->getMessage());
send_internal_notification("EmailChannel error: {$e->getMessage()}. Failed to send email to: " . implode(', ', $recepients) . " with subject: {$mailMessage->subject}");
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recepients)) {
$message .= implode(', ', $recepients);
}
$message .= " with subject: {$mailMessage->subject}";
send_internal_notification($message);
throw $e;
}
}

View File

@@ -27,7 +27,7 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->subject("Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}");
$mail->view('emails.container-restarted', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -38,12 +38,12 @@ class ContainerRestarted extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = " Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -26,7 +26,7 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Container {$this->name} has been stopped on {$this->server->name}");
$mail->subject("Coolify: Container ({$this->name}) has been stopped on {$this->server->name}");
$mail->view('emails.container-stopped', [
'containerName' => $this->name,
'serverName' => $this->server->name,
@@ -37,12 +37,12 @@ class ContainerStopped extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Container {$this->name} has been stopped on {$this->server->name}";
$message = "Coolify: Container ({$this->name}) has been stopped on {$this->server->name}";
return $message;
}
public function toTelegram(): array
{
$message = " Container ($this->name} has been stopped on {$this->server->name}";
$message = "Coolify: Container ($this->name} has been stopped on {$this->server->name}";
$payload = [
"message" => $message,
];

View File

@@ -3,8 +3,11 @@
namespace App\Notifications\Database;
use App\Models\ScheduledDatabaseBackup;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Channels\MailChannel;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
@@ -24,13 +27,13 @@ class BackupFailed extends Notification implements ShouldQueue
public function via(object $notifiable): array
{
return setNotificationChannels($notifiable, 'database_backups');
return [DiscordChannel::class, TelegramChannel::class, MailChannel::class];
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->subject("Coolify: [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [
'name' => $this->name,
'frequency' => $this->frequency,
@@ -41,11 +44,11 @@ class BackupFailed extends Notification implements ShouldQueue
public function toDiscord(): string
{
return " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
}
public function toTelegram(): array
{
$message = " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return [
"message" => $message,
];

View File

@@ -30,7 +30,7 @@ class BackupSuccess extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Backup successfully done for {$this->database->name}");
$mail->subject("Coolify: Backup successfully done for {$this->database->name}");
$mail->view('emails.backup-success', [
'name' => $this->name,
'frequency' => $this->frequency,
@@ -40,11 +40,11 @@ class BackupSuccess extends Notification implements ShouldQueue
public function toDiscord(): string
{
return " Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
}
public function toTelegram(): array
{
$message = " Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
$message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was successful.";
return [
"message" => $message,
];

View File

@@ -45,7 +45,7 @@ class Revived extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Server ({$this->server->name}) revived.");
$mail->subject("Coolify: Server ({$this->server->name}) revived.");
$mail->view('emails.server-revived', [
'name' => $this->server->name,
]);
@@ -54,13 +54,13 @@ class Revived extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => " Server '{$this->server->name}' revived. All automations & integrations are turned on again!"
"message" => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"
];
}
}

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject(" Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->view('emails.server-lost-connection', [
'name' => $this->server->name,
]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string
{
$message = " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
$message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
"message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
];
}
}

View File

@@ -24,14 +24,14 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Test Email");
$mail->subject("Coolify: Test Email");
$mail->view('emails.test');
return $mail;
}
public function toDiscord(): string
{
$message = 'This is a test Discord notification from Coolify.';
$message = 'Coolify: This is a test Discord notification from Coolify.';
$message .= "\n\n";
$message .= '[Go to your dashboard](' . base_url() . ')';
return $message;
@@ -39,7 +39,7 @@ class Test extends Notification implements ShouldQueue
public function toTelegram(): array
{
return [
"message" => 'This is a test Telegram notification from Coolify.',
"message" => 'Coolify: This is a test Telegram notification from Coolify.',
"buttons" => [
[
"text" => "Go to your dashboard",

View File

@@ -30,7 +30,7 @@ class InvitationLink extends Notification implements ShouldQueue
$invitation_team = Team::find($invitation->team->id);
$mail = new MailMessage();
$mail->subject('Invitation for ' . $invitation_team->name);
$mail->subject('Coolify: Invitation for ' . $invitation_team->name);
$mail->view('emails.invitation-link', [
'team' => $invitation_team->name,
'email' => $this->user->email,

View File

@@ -50,7 +50,7 @@ class ResetPassword extends Notification
protected function buildMailMessage($url)
{
$mail = new MailMessage();
$mail->subject('Reset Password');
$mail->subject('Coolify: Reset Password');
$mail->view('emails.reset-password', ['url' => $url, 'count' => config('auth.passwords.' . config('auth.defaults.passwords') . '.expire')]);
return $mail;
}

View File

@@ -25,7 +25,7 @@ class Test extends Notification implements ShouldQueue
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject('Test Email');
$mail->subject('Coolify: Test Email');
$mail->view('emails.test');
return $mail;
}

View File

@@ -4,6 +4,8 @@ namespace App\Providers;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum;
use App\Models\PersonalAccessToken;
class AppServiceProvider extends ServiceProvider
{
@@ -13,6 +15,8 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
Http::macro('github', function (string $api_url, string|null $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([

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