Compare commits

...

380 Commits

Author SHA1 Message Date
Andras Bacsai
3535dbb98f Merge pull request #3583 from coollabsio/next
v4.0.0-beta.345
2024-09-26 13:21:16 +02:00
Andras Bacsai
84b2af53d8 refactor: Fix modal input closeOutside prop in configuration.blade.php 2024-09-26 13:20:06 +02:00
Andras Bacsai
ba70451765 refactor: Update install.sh script to remove redirection of upgrade output to /dev/null 2024-09-26 13:07:06 +02:00
Andras Bacsai
f3ec4ca4a3 refactor: Improve modal confirmation titles and button labels 2024-09-26 13:07:01 +02:00
Andras Bacsai
174923de98 refactor: Update ProductionSeeder to fix issue with coolify_key assignment 2024-09-26 13:06:56 +02:00
Andras Bacsai
68ab8dc14c refactor: Add localhost as Server if it doesn't exist and not in cloud environment 2024-09-26 12:31:39 +02:00
Andras Bacsai
b218356f2d chore: Update version numbers to 4.0.0-beta.345 2024-09-26 12:31:36 +02:00
Andras Bacsai
e83aecebc3 Merge pull request #3559 from coollabsio/next
v4.0.0-beta.344
2024-09-26 12:22:48 +02:00
Andras Bacsai
0bb1f57ea7 fix: deploy key based deployments 2024-09-26 12:19:49 +02:00
Andras Bacsai
d006edc485 refactor: Add localhost as Server if it doesn't exist and not in cloud environment 2024-09-26 10:37:02 +02:00
Andras Bacsai
aa28c99827 refactor: Update OS_TYPE for Asahi Linux in install.sh script 2024-09-26 10:36:59 +02:00
Andras Bacsai
567bbe9d0b chore: Update version numbers to 4.0.0-beta.344 2024-09-25 10:27:51 +02:00
Andras Bacsai
59d2c9748a fix: make sure to reload window if app status changes 2024-09-25 10:27:23 +02:00
Andras Bacsai
bd2e1ad9fe refactor: Fix typo in execute-container-command.blade.php 2024-09-25 10:25:35 +02:00
Andras Bacsai
43df918e43 Merge pull request #3550 from coollabsio/next
v4.0.0-beta.343
2024-09-25 09:29:54 +02:00
Andras Bacsai
1a8d15390d refactor: Update confirmation label in danger.blade.php template 2024-09-25 09:27:17 +02:00
Andras Bacsai
4af6caa79c refactor: Update checkbox component to support full width option 2024-09-25 09:16:28 +02:00
Andras Bacsai
8e91d958be refactor: Improve layout and add checkbox for task enablement in scheduled task form 2024-09-24 21:29:55 +02:00
Andras Bacsai
17d55630d5 refactor: Group and sort fields in StackForm by service name and password status 2024-09-24 21:17:07 +02:00
Andras Bacsai
70dfa101ef refactor: Improve label positioning in input and checkbox components 2024-09-24 21:04:04 +02:00
Andras Bacsai
d6b4e33db3 fix: exited services statuses 2024-09-24 20:40:41 +02:00
Andras Bacsai
a9670bd6eb refactor: Remove commented out code and improve environment variable handling in newParser function 2024-09-24 18:38:35 +02:00
Andras Bacsai
02165c2b9f chore: Update version numbers to 4.0.0-beta.343 2024-09-24 18:22:06 +02:00
Andras Bacsai
afbdd2eb06 fix: parser 2024-09-24 18:21:31 +02:00
Andras Bacsai
b0e6014e8f Merge pull request #3546 from coollabsio/dependabot/npm_and_yarn/rollup-3.29.5
chore(deps): bump rollup from 3.29.4 to 3.29.5
2024-09-24 14:34:17 +02:00
dependabot[bot]
0f13af2eca chore(deps): bump rollup from 3.29.4 to 3.29.5
Bumps [rollup](https://github.com/rollup/rollup) from 3.29.4 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.29.4...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 12:27:43 +00:00
Andras Bacsai
d4d9268f12 Merge pull request #3506 from coollabsio/next
v4.0.0-beta.342
2024-09-24 14:26:04 +02:00
Andras Bacsai
62459f9a95 refactor: Remove unused code in DatabaseBackupStatusJob and PopulateSshKeysDirectorySeeder 2024-09-24 11:57:13 +02:00
Andras Bacsai
eb80a7e2f7 chore: Update Docker commands to start proxy 2024-09-24 10:53:12 +02:00
Andras Bacsai
57d8930f9e chore: Update cleanup command to use Redis instead of queue 2024-09-23 23:48:12 +02:00
Andras Bacsai
688c27c901 fix: cloudflare tunnel configuration, ui, etc 2024-09-23 23:18:23 +02:00
Andras Bacsai
480ae3de8a Refactor Server.php to improve default 404 redirect setup 2024-09-23 22:57:24 +02:00
Andras Bacsai
21a5b4ce80 chore: Refactor pre-commit hook to improve performance and readability 2024-09-23 22:16:28 +02:00
Andras Bacsai
12a0d71e47 Merge pull request #3543 from peaklabs-dev/cf-production-ready
Feat: Make cloudflare production ready
2024-09-23 21:53:23 +02:00
peaklabs-dev
5ed7ae3d3e remove ray 2024-09-23 21:45:59 +02:00
Andras Bacsai
6dd3adba4d refactor: Simplify SSH key synchronization logic 2024-09-23 21:16:25 +02:00
Andras Bacsai
08d58eb8f8 chore: Update CONTRIBUTING.md with troubleshooting note about database migrations 2024-09-23 20:55:20 +02:00
Andras Bacsai
46a7937761 test 2024-09-23 20:54:56 +02:00
Andras Bacsai
13a5536cff test 2024-09-23 20:54:18 +02:00
Andras Bacsai
2381706465 chore: Refactor pre-commit hook to improve performance and readability 2024-09-23 20:52:13 +02:00
Andras Bacsai
7a0f054f1a test 2024-09-23 20:49:26 +02:00
Andras Bacsai
00e1464185 chore: Add modified files to the commit 2024-09-23 20:48:57 +02:00
Andras Bacsai
fac4a8aaf9 hooks? 2024-09-23 20:45:42 +02:00
Andras Bacsai
2841675691 refactor 2024-09-23 20:31:50 +02:00
Andras Bacsai
573e5c4913 refactor 2024-09-23 20:29:22 +02:00
peaklabs-dev
6934a746f7 Fix: Make helper text more clean to use a FQDN and not an URL 2024-09-23 19:57:42 +02:00
Andras Bacsai
b570ccd7d3 format 2024-09-23 19:51:31 +02:00
Andras Bacsai
68efd4b553 fix: migrations 2024-09-23 19:50:26 +02:00
Andras Bacsai
e00ec2f75b chore: Remove unused migration file for populating SSH keys and clearing mux directory 2024-09-23 19:46:45 +02:00
peaklabs-dev
1bb192f3e2 Fix: Cloudflare tunnel 2024-09-23 19:23:46 +02:00
peaklabs-dev
e039f183da Merge pull request #16 from coollabsio/next
Next
2024-09-23 18:50:19 +02:00
peaklabs-dev
7aab44645f Merge branch 'cf-production-ready' into next 2024-09-23 18:50:08 +02:00
Andras Bacsai
04e504bb8b chore: Refactor DockerCleanupJob to remove unused middleware and uniqueId method 2024-09-23 12:10:46 +02:00
Andras Bacsai
960f970822 chore: Remove unused middleware and uniqueId method in DockerCleanupJob 2024-09-23 12:03:14 +02:00
Andras Bacsai
ae62781c6a fix: cloudflared service 2024-09-23 11:36:57 +02:00
Andras Bacsai
b42c15b2e5 Merge pull request #3483 from djsisson/update-CF-Tunnel-template
Update cloudflared.yaml
2024-09-23 11:36:52 +02:00
Andras Bacsai
c77cc45e0d fix: Logto service 2024-09-23 11:33:53 +02:00
Andras Bacsai
310708f1b6 Merge pull request #3480 from JustMrMendez/patch-1
FIXES: #3322 deploy DB alterations before starting the service
2024-09-23 11:33:55 +02:00
Andras Bacsai
8386aaf95b Merge pull request #3481 from peaklabs-dev/new-pull-request-template
Feat: New pull request template
2024-09-23 11:32:24 +02:00
Andras Bacsai
6d8d91802b chore: Update install.sh version to 1.6 2024-09-23 11:30:57 +02:00
Andras Bacsai
c0a1f15d34 Merge pull request #3472 from disjukr/asahi-linux
Add support to install.sh for Asahi Linux
2024-09-23 11:29:26 +02:00
Andras Bacsai
0222aa137d feat(api): add endpoint to execute a command 2024-09-23 11:09:14 +02:00
Andras Bacsai
4d36bc4742 Merge pull request #3535 from Luca-Sordetti/main
feat(api): add an endpoint to execute a command
2024-09-23 10:53:28 +02:00
Andras Bacsai
6639379ba6 Merge pull request #3522 from InfiniBrains/main
Add hint to "build variable" check in the enviroment variable tab. Remember the user what should be doing in order to make it work.
2024-09-23 10:49:59 +02:00
Andras Bacsai
d60ff107c4 Merge pull request #3539 from peaklabs-dev/fix-dev-error
Fix: Ray error on development
2024-09-23 10:49:39 +02:00
Andras Bacsai
a468ce77f0 refactor: Update shared.php to include predefined ports for services 2024-09-23 10:46:34 +02:00
Andras Bacsai
1fb0ee0db5 refactor: Add Postiz service to compose file (disabled for now) 2024-09-23 10:46:30 +02:00
Andras Bacsai
9281dda03f refactor: Fix typo in subscription URLs 2024-09-23 10:46:19 +02:00
peaklabs-dev
d3485cea8d Update pull_request_template.md 2024-09-23 10:27:48 +02:00
peaklabs-dev
92772f9132 Merge branch 'next' into fix-dev-error 2024-09-23 10:20:26 +02:00
Andras Bacsai
6f8c4a4ce4 refactor: Update select.blade.php to improve trademarks policy display 2024-09-23 09:44:33 +02:00
Andras Bacsai
85ab772acd refactor: Update select.blade.php to improve trademarks policy display 2024-09-23 09:43:17 +02:00
Andras Bacsai
6336125ca2 Merge pull request #3516 from nathanialhenniges/main
Added link for more services to the coolfiy docs
2024-09-23 09:42:49 +02:00
Andras Bacsai
341c7e3598 refactor: Update environment variables for services in compose files 2024-09-23 09:26:53 +02:00
Andras Bacsai
ff9b68b450 refactor: Remove unnecessary code in shared.php file 2024-09-23 09:26:49 +02:00
Andras Bacsai
a072a00926 Merge pull request #3525 from gabrielcossette/fix_ghost_template_domain
Fix Ghost Template domain generation
2024-09-23 09:05:34 +02:00
Andras Bacsai
b84d39ba56 refactor: Update confirmation button text for deletion actions 2024-09-23 08:58:04 +02:00
Luca-Sordetti
4e167dc539 feat(api): add an endpoint to execute a command 2024-09-22 12:38:25 +02:00
peaklabs-dev
b7139f8af8 remove clockwork 2024-09-21 00:20:16 +02:00
peaklabs-dev
a5b3fef1d8 Fix: ray error because port is not uncommented 2024-09-21 00:20:01 +02:00
Gabriel Cossette
b9a026b488 Fix Ghost Template domain generation
Signed-off-by: Gabriel Cossette <gabriel.cossette@mapgears.com>
2024-09-20 14:29:11 -06:00
Alexandre Tolstenko Nogueira
a4d0f1da9e Add hint to "build variable" check in the enviroment variable tab. remember the user what should be doing in order to make it work. #3477 2024-09-20 14:12:15 -04:00
Andras Bacsai
eb9bbf3eda refactor: Improve attribute sanitization in Server model 2024-09-20 18:14:52 +02:00
Andras Bacsai
be42f15711 refactor: Update ServerSeeder and PopulateSshKeysDirectorySeeder 2024-09-20 17:28:55 +02:00
Andras Bacsai
fa375e2fd8 refactor: Update install.sh script to check if coolify-db volume exists before generating SSH key 2024-09-20 14:39:58 +02:00
Andras Bacsai
760cf8aeb5 refactor: Update PrivateKey model to use ownedByCurrentTeam() scope for cleanupUnusedKeys() 2024-09-20 13:05:51 +02:00
Andras Bacsai
f9238ce263 Merge pull request #3509 from coollabsio/delete-unused-ssh-keys
Feat: Delete unused ssh keys button
2024-09-20 13:00:54 +02:00
Andras Bacsai
c0898f0568 refactor: Remove unnecessary code in PrivateKey model 2024-09-20 12:51:02 +02:00
Andras Bacsai
5b00b66f24 refactor: Update PrivateKey model to improve code readability and maintainability 2024-09-20 12:27:55 +02:00
Andras Bacsai
b81f9114d9 chore: Update ProductionSeeder to call OauthSettingSeeder and PopulateSshKeysDirectorySeeder 2024-09-20 11:34:13 +02:00
Andras Bacsai
be8573c828 chore: Update SSH key generation in install.sh script 2024-09-20 11:04:26 +02:00
Andras Bacsai
f1881d5c35 refactor: Update CleanupHelperContainersJob to use more efficient Docker command 2024-09-20 10:08:37 +02:00
Nathanial Henniges
699e76637b Added link for more services to the coolfiy docs 2024-09-20 03:02:31 -05:00
peaklabs-dev
dbc723089b Feat: Delete unused private keys button 2024-09-19 19:27:25 +02:00
Andras Bacsai
fc6f5d82db chore: Add SSH key for localhost in ProductionSeeder 2024-09-19 17:26:59 +02:00
Andras Bacsai
1815c9dccf fix: store original root key in the original location 2024-09-19 16:35:10 +02:00
Andras Bacsai
7d54fe9c18 chore: Update remove-labels-and-assignees-on-close.yml 2024-09-19 13:26:27 +02:00
Andras Bacsai
e9fbb7d2b0 fix: coolify-db should not be in the managed resources 2024-09-19 13:23:48 +02:00
Andras Bacsai
2d94ebf7f1 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-09-19 13:16:10 +02:00
Andras Bacsai
c874261c5b feat: Add nullable constraint to 'fingerprint' column in private_keys table 2024-09-19 13:16:08 +02:00
Andras Bacsai
1c5fd8d668 refactor: Update getAJoke function to use HTTPS for API request 2024-09-19 13:16:05 +02:00
Andras Bacsai
874e04e844 Merge pull request #3505 from peaklabs-dev/main
Fix: Update remove-labels-and-assignees-on-close.yml for PRs
2024-09-19 13:01:40 +02:00
peaklabs-dev
c46715db3c Merge branch 'main' of https://github.com/peaklabs-dev/coolify 2024-09-19 13:01:19 +02:00
peaklabs-dev
aa0fe922ea Update remove-labels-and-assignees-on-close.yml 2024-09-19 13:01:07 +02:00
peaklabs-dev
60e9fcc202 Update remove-labels-and-assignees-on-close.yml 2024-09-19 12:55:20 +02:00
Andras Bacsai
91485b5d4d refactor: Update getAJoke function to exclude offensive jokes 2024-09-19 12:50:51 +02:00
Andras Bacsai
6ec1b9b3f5 disable remove labels for issues 2024-09-19 12:47:35 +02:00
Andras Bacsai
240352f4b2 Merge pull request #3504 from coollabsio/fix-ssh-keys
Fix ssh keys
2024-09-19 12:44:56 +02:00
Andras Bacsai
cfd9ae9817 Merge pull request #3454 from peaklabs-dev/fix-ssh-keys
Fix: SSH Keys, Multiplexing issues and a lot of other small things for dev and prod
2024-09-19 12:44:23 +02:00
Andras Bacsai
f65789bdbb fix: proxy status 2024-09-19 12:32:56 +02:00
Andras Bacsai
9518040d23 refactor: Remove CleanupSshKeysJob from schedule in Kernel.php 2024-09-19 12:06:56 +02:00
Andras Bacsai
631b4e6438 Merge branch 'next' into fix-ssh-keys 2024-09-19 11:45:12 +02:00
Andras Bacsai
d47bd047bf fixes 2024-09-19 11:24:21 +02:00
Andras Bacsai
a65b62332b refactor: Remove commented out code in Navbar.php 2024-09-19 10:57:34 +02:00
Andras Bacsai
eb10bbb888 Merge pull request #3502 from coollabsio/fix-#2546-deletion-issues
Fixes
2024-09-19 10:41:57 +02:00
Andras Bacsai
4b02debf97 Merge pull request #3024 from peaklabs-dev/fix-#2546-deletion-issues
Feat: Fix all deletion issues, add graceful shutdown to everything, new delete confirmation process
2024-09-19 10:40:27 +02:00
Andras Bacsai
430fdcf2e9 chore: Update version numbers to 4.0.0-beta.342 2024-09-19 10:35:54 +02:00
Andras Bacsai
636de24b6c Merge pull request #3501 from coollabsio/#2546
refactor: Update Docker cleanup label in Heading.php and Navbar.php
2024-09-19 10:32:09 +02:00
Andras Bacsai
2e3267ee94 Merge branch 'fix-#2546-deletion-issues' into #2546 2024-09-19 10:32:02 +02:00
Andras Bacsai
98e744e808 refactor: Update Docker cleanup label in Heading.php and Navbar.php 2024-09-19 10:27:44 +02:00
Andras Bacsai
532f5e351e fixes 2024-09-18 21:24:42 +02:00
Andras Bacsai
182087cf1b a few changes here and there 2024-09-18 21:18:47 +02:00
peaklabs-dev
6676c6bd64 Merge branch 'coollabsio:main' into new-pull-request-template 2024-09-18 20:44:42 +02:00
peaklabs-dev
8153e2f63b Merge branch 'coollabsio:main' into fix-ssh-keys 2024-09-18 20:43:59 +02:00
peaklabs-dev
35857de697 Merge branch 'coollabsio:main' into cf-production-ready 2024-09-18 20:42:18 +02:00
Andras Bacsai
5ec45d547a Merge branch 'next' into fix-#2546-deletion-issues 2024-09-18 18:05:06 +02:00
Darren Sisson
d2cf53578a Update cloudflared.yaml
add network mode host to template, otherwise cloudflared has no access to localhost
2024-09-18 16:09:24 +01:00
Mr. Mendez
1fa42ec82d FIXES: #3322 deploy DB alterations before updating
FIXES: #3322 

[Bug]: logto stop working after pulling latest image #3322
2024-09-18 08:59:02 -04:00
Andras Bacsai
e42c7e258c Merge pull request #3479 from coollabsio/next
v4.0.0-beta.341
2024-09-18 14:45:00 +02:00
Andras Bacsai
2c210abf57 Merge pull request #3478 from dennisblume/caddy-terminal-fix
Fix WebSocket connection for Terminal page when using Caddy
2024-09-18 14:36:48 +02:00
Dennis Blume
98ba7ac28c Fix WebSocket connection for Terminal page when using Caddy 2024-09-18 13:33:38 +02:00
Andras Bacsai
5772f2c3d9 feat: Add buddy logo 2024-09-18 13:18:31 +02:00
Andras Bacsai
cb9c569d4b Merge pull request #3470 from Skeyelab/add-buddy-svg
featu update: adding budge logo
2024-09-18 13:18:10 +02:00
Andras Bacsai
5616e588e9 Merge pull request #3473 from coollabsio/dependabot/npm_and_yarn/vite-4.5.5
chore(deps-dev): bump vite from 4.5.3 to 4.5.5
2024-09-18 12:56:34 +02:00
Andras Bacsai
40e844fab4 refactor: Remove unnecessary console.log statements in terminal.blade.php 2024-09-18 12:54:31 +02:00
Andras Bacsai
2e56edd939 refactor: Update WebSocket connection initialization in terminal.blade.php 2024-09-18 09:51:20 +02:00
Andras Bacsai
8179a5c6a3 feat: custom terminal host 2024-09-18 09:43:47 +02:00
Andras Bacsai
d0518153fb fix: generated fqdn for SERVICE_FQDN_APP_3000 magic envs 2024-09-18 09:21:57 +02:00
dependabot[bot]
29236bf101 chore(deps-dev): bump vite from 4.5.3 to 4.5.5
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-18 07:13:17 +00:00
Andras Bacsai
2d306d56ab chore: Update version numbers to 4.0.0-beta.341 2024-09-18 09:13:04 +02:00
JongChan Choi
e5768999e4 chore: Update install.sh to support Asahi Linux 2024-09-18 13:22:07 +09:00
Eric Dahl
b076db2eed update 2024-09-17 18:45:41 -04:00
Eric Dahl
3534424dc8 adding budge logo 2024-09-17 18:41:26 -04:00
peaklabs-dev
3b6c3609c3 Update pull_request_template.md 2024-09-17 22:11:30 +02:00
peaklabs-dev
95a97cf9cd Update pull_request_template.md 2024-09-17 22:09:00 +02:00
peaklabs-dev
7b33ef705b Update pull_request_template.md 2024-09-17 22:07:19 +02:00
peaklabs-dev
39449584ac Update pull_request_template.md 2024-09-17 22:04:19 +02:00
peaklabs-dev
19458e7445 Update pull_request_template.md 2024-09-17 21:47:19 +02:00
Andras Bacsai
2d9c728a64 Merge pull request #3468 from coollabsio/next
v4.0.0-beta.340
2024-09-17 17:31:39 +02:00
Andras Bacsai
12a8e9b0e1 fix: only update helper image in DB 2024-09-17 17:29:42 +02:00
Andras Bacsai
649cc2dac2 chore: Update version numbers to 4.0.0-beta.340 2024-09-17 17:25:38 +02:00
Andras Bacsai
c169f1f64b Merge pull request #3467 from coollabsio/next
v4.0.0-beta.339
2024-09-17 17:20:27 +02:00
Andras Bacsai
5ecf31d1fc refactor: Remove unnecessary code in Terminal.blade.php 2024-09-17 17:20:03 +02:00
Andras Bacsai
e937d30545 fix: move terminal to separate view on services 2024-09-17 17:15:34 +02:00
peaklabs-dev
bf48b33e64 Update pull_request_template.md 2024-09-17 17:14:15 +02:00
Andras Bacsai
595a2414b1 fix: if you exit a container manually, it should close the underlying tty as well 2024-09-17 16:48:58 +02:00
Andras Bacsai
07ed726c88 refactor: Remove unnecessary code in Terminal.php 2024-09-17 16:48:30 +02:00
Andras Bacsai
d373815f98 refactor: Add authorization check in ExecuteContainerCommand mount method 2024-09-17 16:28:28 +02:00
peaklabs-dev
d9181bd00b Fix: Multiplexing do not write file manually 2024-09-17 16:22:53 +02:00
peaklabs-dev
d13e2c0865 Fix: Clear mux directory 2024-09-17 15:57:57 +02:00
peaklabs-dev
42ff7b19a4 Fix: Few multiplexing things 2024-09-17 15:54:22 +02:00
peaklabs-dev
283fcc87a5 Fix: SSH algorhytm text 2024-09-17 15:31:44 +02:00
peaklabs-dev
ea3501ada6 Fix: SSH Multiplexing for Jobs 2024-09-17 15:31:05 +02:00
peaklabs-dev
175f4b9ae1 use shared functions when possible 2024-09-17 14:47:02 +02:00
peaklabs-dev
2bc74c75e1 Remove duplicated code 2024-09-17 14:43:02 +02:00
peaklabs-dev
4ac2758d70 Update .env.development.example 2024-09-17 14:35:08 +02:00
peaklabs-dev
bdc0fc87f0 Fix: UI bug, do not write ssh key to disk in server dialog 2024-09-17 14:33:24 +02:00
peaklabs-dev
845d32c94c Fix: Do not delete SSH Key from disk during server validation error 2024-09-17 14:33:04 +02:00
peaklabs-dev
6a6b947fba Fix: Make sure in use private keys are not deleted 2024-09-17 14:32:44 +02:00
peaklabs-dev
2ec66fd146 FIx: Server ID 0 2024-09-17 14:08:34 +02:00
peaklabs-dev
ccbbfd8908 Fix: ID issues on dev seeders 2024-09-17 14:03:31 +02:00
peaklabs-dev
43895419ff Remove unused code 2024-09-17 13:45:05 +02:00
peaklabs-dev
1c78067386 Feat: Add ssh key fingerprint and generate one for existing keys 2024-09-17 13:20:48 +02:00
peaklabs-dev
871d09bd96 Feat: Move more functions to the PrivateKey Model 2024-09-17 13:20:27 +02:00
peaklabs-dev
2d8bda4fa6 Fix: Private key with ID 2 on dev 2024-09-17 13:06:50 +02:00
peaklabs-dev
95070ab48d Feat: SSH Key cleanup job 2024-09-17 12:57:06 +02:00
Andras Bacsai
8bb8a7faa3 Merge pull request #3464 from peaklabs-dev/remove-labels-assinges-on-close
Feat: GitHub action that removes labels and assignees on close
2024-09-17 12:54:36 +02:00
Andras Bacsai
428c40aab5 chore: Update version numbers to 4.0.0-beta.339 2024-09-17 12:54:23 +02:00
peaklabs-dev
52c4994d44 Feat: remove unused code form multiplexing 2024-09-17 12:44:59 +02:00
Andras Bacsai
6f578855a0 Merge pull request #3463 from coollabsio/next
v4.0.0-beta.388
2024-09-17 12:38:50 +02:00
Andras Bacsai
8967315c49 refactor: terminal / run command 2024-09-17 12:29:36 +02:00
Andras Bacsai
162fb7bfc5 fix: refactor run-command 2024-09-17 12:27:20 +02:00
peaklabs-dev
144508218e Fix: SSH multiplexing 2024-09-17 12:26:11 +02:00
Andras Bacsai
4bdb5c9030 chore: nightly - Update soketi image to version 1.0.1 and versions.json to reflect latest version of realtime container 2024-09-17 12:08:09 +02:00
Andras Bacsai
a2ea8814cf chore: Update soketi image to version 1.0.1 2024-09-17 12:00:57 +02:00
Andras Bacsai
cbc2f1f015 chore: Update versions.json to reflect latest version of realtime container 2024-09-17 11:58:28 +02:00
Andras Bacsai
35b9b7fdf2 fix/feat: able to open terminal to any containers 2024-09-17 11:54:25 +02:00
Andras Bacsai
d92819ab60 fix: Handle WebSocket connection close in terminal.blade.php 2024-09-17 11:30:46 +02:00
Andras Bacsai
ea877f3623 Update version numbers to 4.0.0-beta.338 2024-09-17 11:30:37 +02:00
Andras Bacsai
5818c9cf6b chore: Add validation to prevent selecting 'default' server or container in RunCommand.php 2024-09-17 11:30:29 +02:00
peaklabs-dev
f9375f91ec Feat: Create a Multiplexing Helper 2024-09-16 22:33:43 +02:00
peaklabs-dev
86722939cd Fix. Remove write to SSH key on every remote command execution 2024-09-16 21:34:27 +02:00
peaklabs-dev
70b757df5b remove old function 2024-09-16 19:53:45 +02:00
peaklabs-dev
451272bf11 Fix: Use new function names and logic everywhere 2024-09-16 19:52:55 +02:00
peaklabs-dev
a68fbefadb Fix: Populate SSH keys in dev 2024-09-16 19:34:46 +02:00
peaklabs-dev
b79b4015d7 Feat: Populate SSH key folder 2024-09-16 19:08:15 +02:00
peaklabs-dev
0bfdc1c531 Feat: Store all keys on disk by default 2024-09-16 18:45:08 +02:00
peaklabs-dev
b09017ea46 Feat: new ssh key file name on disk 2024-09-16 18:11:37 +02:00
peaklabs-dev
95fcf38d45 Feat: Add is_sftp and is_server_ssh_key coloums 2024-09-16 18:11:16 +02:00
peaklabs-dev
54c03fae41 Remove ssh key fingerprint as we can just us uuid 2024-09-16 18:10:46 +02:00
Andras Bacsai
b2bab451d3 Merge pull request #3457 from coollabsio/next
v4.0.0-beta.337
2024-09-16 17:29:14 +02:00
Andras Bacsai
7b4559c5e6 fix: install script 2024-09-16 17:28:31 +02:00
peaklabs-dev
ba636a95dc Refactor SSH Keys 2024-09-16 17:24:42 +02:00
Andras Bacsai
682b45a2b5 refactor: Improve Docker network connection command in StartService.php 2024-09-16 16:39:16 +02:00
Andras Bacsai
d44e3a1091 chore: Update docker network connection command in ApplicationDeploymentJob.php 2024-09-16 16:38:34 +02:00
Andras Bacsai
d2d56f136b chore: Update Coolify installer and scripts to include a function for fetching programming jokes 2024-09-16 16:35:52 +02:00
Andras Bacsai
9b48a99798 fix: generate https for minio 2024-09-16 16:35:47 +02:00
Andras Bacsai
1322dc9c23 refactor: Remove unnecessary code in ExecuteContainerCommand.php 2024-09-16 15:43:24 +02:00
Andras Bacsai
f71fb7266d fix: terminal 2024-09-16 15:35:44 +02:00
Andras Bacsai
35f23cfb96 chore: Update version numbers to 4.0.0-beta.337 2024-09-16 15:35:38 +02:00
Andras Bacsai
0c4ce55a15 Merge pull request #3456 from coollabsio/next
chore: Fix syntax error in versions.json
2024-09-16 14:44:08 +02:00
Andras Bacsai
77ee80b562 chore: Fix syntax error in versions.json 2024-09-16 14:43:51 +02:00
Andras Bacsai
2621292dc1 Merge pull request #3425 from coollabsio/next
v4.0.0-beta.336
2024-09-16 14:41:39 +02:00
Andras Bacsai
62a4d7055a fix: update Coolify installer 2024-09-16 14:37:19 +02:00
Andras Bacsai
3fd41c0a92 chore: Update helper version to 1.0.1 2024-09-16 14:30:47 +02:00
Andras Bacsai
0e8291cd86 chore: Update coolify nightly version to 4.0.0-beta.335 2024-09-16 14:30:41 +02:00
Andras Bacsai
175b89ced2 revert: databasebackup 2024-09-16 14:15:06 +02:00
peaklabs-dev
3aee8e030e Fix: Encrypt private SSH keys in the DB 2024-09-16 13:17:39 +02:00
peaklabs-dev
02017334e5 Fix: Make sure invalid private keys can not be added 2024-09-16 13:02:48 +02:00
peaklabs-dev
f9b7841572 Feat: Add a fingerprint to every private key on save, create... 2024-09-16 12:54:48 +02:00
peaklabs-dev
7d39a5089c Feat: Add SSH Key fingerprint to DB 2024-09-16 11:53:26 +02:00
Andras Bacsai
2313fed546 fix: add build.sh to debug logs 2024-09-16 11:50:03 +02:00
Andras Bacsai
e1a6c3e776 chore: Refactor terminal component and select form layout 2024-09-16 11:25:20 +02:00
peaklabs-dev
7037c779e2 Update remove-labels-and-assignees-on-close.yml 2024-09-16 11:13:56 +02:00
Andras Bacsai
f124a1e60d chore: Update terminal button text and layout in application heading view 2024-09-16 10:56:11 +02:00
Andras Bacsai
7ebb0a4579 Merge pull request #3444 from Vahor/trigger-helper-update-in-manual-upgrade
Trigger pull helper image job when upgrading coolify
2024-09-16 10:37:40 +02:00
Andras Bacsai
4962c606bc Merge pull request #3452 from peaklabs-dev/new-issue-onboarding
Feat: New issue onboarding
2024-09-16 10:35:39 +02:00
Andras Bacsai
b728d69ab0 Merge pull request #3451 from peaklabs-dev/add-bounty-issues
Feat: Add Bounty issue type
2024-09-16 10:35:18 +02:00
Andras Bacsai
3d6e53602c Merge pull request #3443 from danielqba/main
Fix tooltip
2024-09-16 10:33:42 +02:00
peaklabs-dev
1c6450da24 Feat: Make sure this action is also triggered on PR issue close 2024-09-16 10:23:05 +02:00
Andras Bacsai
15fe5bd864 chore: Update branch restriction for push event in coolify-helper.yml 2024-09-16 10:22:02 +02:00
Andras Bacsai
08e4afbbc4 fix: keep-alive ws connections 2024-09-16 10:20:57 +02:00
peaklabs-dev
06fd7286da Update ENHANCEMENT_BOUNTY.yml 2024-09-16 09:45:49 +02:00
peaklabs-dev
fce1e34fc8 Update BUG_REPORT.yml 2024-09-16 09:34:31 +02:00
Vahor
0739e0f5e7 trigger pull helper image job when upgrading coolify 2024-09-15 14:23:57 +02:00
Luis Daniel
faf7ba50e6 Fix tootip
Fix the tooltip from the Server Docker cleanup frequency. Previous value was every 10 minutes. New value is every night at midnight according to changes https://github.com/coollabsio/coolify/releases/tag/v4.0.0-beta.332
2024-09-15 07:19:23 -04:00
peaklabs-dev
e8179ec519 Update ENHANCEMENT_BOUNTY.yml 2024-09-13 20:53:31 +02:00
peaklabs-dev
7948a0309f Feat: remove labels and assignees on issue close 2024-09-13 18:42:38 +02:00
peaklabs-dev
47277a68ec Create remove-labels-assignees-on-close.yml 2024-09-13 18:24:37 +02:00
peaklabs-dev
60f75f9deb Update config.yml 2024-09-13 18:06:16 +02:00
peaklabs-dev
e90d1af884 Update ENHANCEMENT_BOUNTY.yml 2024-09-13 17:49:30 +02:00
peaklabs-dev
e3046698a5 Create ENHANCEMENT_BOUNTY.yml 2024-09-13 17:42:19 +02:00
peaklabs-dev
bb773e1118 Update BUG_REPORT.yml 2024-09-13 17:01:31 +02:00
Andras Bacsai
dcf91cc034 Update WebSocket URL in terminal.blade.php to include /ws for consistency with the server configuration. 2024-09-13 16:58:16 +02:00
peaklabs-dev
cddf8476de Update BUG_REPORT.yml 2024-09-13 15:31:14 +02:00
Andras Bacsai
51c43e7457 chore: Rename Command Center to Terminal in code and views 2024-09-13 15:18:00 +02:00
Andras Bacsai
7cac243589 chore: Update Dockerfile and workflow for Coolify Realtime (v4) 2024-09-13 14:05:37 +02:00
Andras Bacsai
0cfd8ed5f0 chore: Update Dockerfile and workflow for Coolify Realtime (v4) 2024-09-13 13:46:10 +02:00
Andras Bacsai
8318598cb5 chore: Update Dockerfile and workflow for Coolify Realtime (v4) 2024-09-13 13:17:19 +02:00
Andras Bacsai
2f692da1c9 chore: Update WebSocket URL in terminal.blade.php 2024-09-13 13:16:14 +02:00
Andras Bacsai
ecd98bfcd5 chore: Update APP_NAME environment variable in docker-compose.prod.yml 2024-09-13 13:13:23 +02:00
Andras Bacsai
ba860398f3 chore: Update .env file and docker-compose configuration 2024-09-13 12:47:46 +02:00
Andras Bacsai
1f9af39fa7 chore: Remove unused entrypoint script and update volume mapping 2024-09-13 12:45:59 +02:00
Andras Bacsai
62b995d26c chore: Update Dockerfile and workflow for Coolify Realtime (v4) 2024-09-13 12:40:17 +02:00
Andras Bacsai
b28d118609 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-09-13 12:37:34 +02:00
Andras Bacsai
81d0589c55 Merge pull request #3427 from peaklabs-dev/new-issue-template
Feat: New issue template
2024-09-13 12:37:35 +02:00
Andras Bacsai
07893b432b chore: Update button text for container connection form 2024-09-13 12:37:32 +02:00
Andras Bacsai
568d47d1dd Merge pull request #3417 from lassejlv/patch-1
Fix spell error
2024-09-13 12:35:07 +02:00
Andras Bacsai
85cce4e453 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-09-13 12:22:01 +02:00
Andras Bacsai
888c1f7697 update files 2024-09-13 12:21:02 +02:00
Andras Bacsai
79c8ce7572 Merge pull request #3429 from coollabsio/feat--terminal-pty
fixes for the new terminal
2024-09-13 12:20:53 +02:00
Andras Bacsai
121afaa18c Merge pull request #2586 from LEstradioto/feat--terminal-pty
[FEAT] fully functioning terminal
2024-09-13 12:20:10 +02:00
Andras Bacsai
e48ad87cdb chore: Update terminal styling for better readability 2024-09-13 12:19:13 +02:00
peaklabs-dev
a587cae251 Update BUG_REPORT.yml 2024-09-13 11:44:07 +02:00
peaklabs-dev
c04c1530f5 Update BUG_REPORT.yml 2024-09-13 11:43:24 +02:00
peaklabs-dev
f4ee61fc6a Update BUG_REPORT.yml 2024-09-13 11:40:00 +02:00
Andras Bacsai
b6080c2c8e Merge branch 'next' into feat--terminal-pty 2024-09-13 11:25:47 +02:00
Andras Bacsai
1738286983 chore: Update shared.php to fix issues with source and network variables 2024-09-13 11:12:28 +02:00
peaklabs-dev
703bf51705 Update BUG_REPORT.yml 2024-09-13 11:00:00 +02:00
Andras Bacsai
aa4980289d feat: make coolify full width by default 2024-09-13 10:51:51 +02:00
Andras Bacsai
dd8a2dd3c1 chore: Update coolify environment variable assignment with double quotes 2024-09-13 08:23:05 +02:00
lasse
3086ef1462 Update actions.blade.php 2024-09-12 21:34:52 +02:00
peaklabs-dev
b9b33c831c Fix: Made help text more clear 2024-09-12 17:17:19 +02:00
peaklabs-dev
9310af0301 Feat: New cf tunnel install flow 2024-09-12 15:50:22 +02:00
peaklabs-dev
0d3d5f40fd Fix: Css issue with advanced settings and remove cf tunnel in onboarding 2024-09-12 14:58:52 +02:00
Luan Estradioto
35dfb1b0f8 fix: grouped process and docker execs are killed with ssh process
fix: run clear command only if exists
fix: link terminal js on dev compose better dx
fix: add error on terminal ux
2024-09-12 01:58:56 -03:00
Luan Estradioto
2edcd01493 Merge remote-tracking branch 'upstream/feat--terminal-pty' into feat--terminal-pty 2024-09-11 20:41:30 -03:00
Andras Bacsai
117fbeb07c fixes for terminal 2024-09-11 12:19:27 +02:00
Andras Bacsai
33e9c9b0f9 Merge branch 'next' into feat--terminal-pty 2024-09-11 10:41:33 +02:00
ayntk-ai
fc3c69f687 Feat: more conformations and fixes 2024-09-05 17:54:32 +02:00
ayntk-ai
08df814408 Feat: delete volume confirmation 2024-09-04 22:33:47 +02:00
ayntk-ai
a7b78dcf41 Feat: Backup job confirmation 2024-09-04 21:29:32 +02:00
ayntk-ai
2b5df8d2fd Feat: Team deletion confirmation 2024-09-04 21:15:46 +02:00
ayntk-ai
f4263ee022 Feat: User deletion confirmation 2024-09-04 21:14:18 +02:00
ayntk-ai
44f3f6001e Feat: Redeploy all confirmation 2024-09-04 21:04:43 +02:00
ayntk-ai
9105c1aa51 Feat: GH app deletion confirmation 2024-09-04 21:00:58 +02:00
ayntk-ai
3e04a7958e Feat/Fix: Proxy stop and restart confirmation 2024-09-04 20:41:17 +02:00
ayntk-ai
505127dae5 Feat: confirm server settings 2024-09-04 20:11:04 +02:00
ayntk-ai
371fe53911 Feat: confirm server deletion 2024-09-04 20:06:22 +02:00
ayntk-ai
9515bc6162 Feat: confirm private key 2024-09-04 20:02:01 +02:00
ayntk-ai
93a4a3e09c Feat: confirm API token 2024-09-04 19:58:36 +02:00
ayntk-ai
bbbd5cbaa1 Feat: confirm scheduled tasks 2024-09-04 19:44:10 +02:00
ayntk-ai
7fe3b78d45 Feat: Environment variabel deletion 2024-09-04 19:41:10 +02:00
ayntk-ai
a29353c1ae Feat: confirm ressource operation 2024-09-04 19:34:47 +02:00
ayntk-ai
c16e914be4 Fix: DB image cleanup 2024-09-04 14:59:44 +02:00
ayntk-ai
bec974dca4 Fix application image cleanup 2024-09-04 14:54:43 +02:00
ayntk-ai
b314b08f25 Feat: stop service confirm 2024-09-04 14:34:46 +02:00
ayntk-ai
9a2d5be354 Feat: confirm file storage 2024-09-03 18:31:06 +02:00
ayntk-ai
d94e39ccc7 Feat: service confirmation 2024-09-03 16:59:51 +02:00
ayntk-ai
d5b7e9ed83 Feat: preview deployments and typos 2024-09-03 16:43:50 +02:00
ayntk-ai
f29bc52fa5 Feat: general confirm 2024-09-03 15:59:30 +02:00
ayntk-ai
3d21f1a2a4 fix checkbox hide label 2024-09-03 14:59:01 +02:00
ayntk-ai
20558d438a remove ray 2024-09-03 00:30:15 +02:00
ayntk-ai
792f6bc163 made wording more clear 2024-09-03 00:01:24 +02:00
ayntk-ai
3d1c730703 Feat: del init script 2024-09-02 23:58:48 +02:00
ayntk-ai
c3188958b4 Feat: DB start, stop confirm 2024-09-02 23:54:16 +02:00
ayntk-ai
c24fec9c45 fix step and password error 2024-09-02 23:48:05 +02:00
ayntk-ai
70043c24cf 100000000x Speed improvement first toast then submit in the background 2024-09-02 23:30:47 +02:00
ayntk-ai
ff1e08cf8b add toast dispatching 2024-09-02 23:25:18 +02:00
ayntk-ai
a4d1ae1341 Feat: ability to hide labels 2024-09-02 21:54:21 +02:00
ayntk-ai
5944ee5524 fix close modal on submit 2024-09-02 21:22:31 +02:00
ayntk-ai
1b0c5f8d69 css fix for long text 2024-09-02 19:49:57 +02:00
ayntk-ai
dfd218ec06 fixes here and there 2024-09-02 19:27:21 +02:00
ayntk-ai
843e3fb599 fix checkboxes in danger 2024-08-31 21:10:44 +02:00
ayntk-ai
a97ccd206c confirm init script 2024-08-31 20:55:56 +02:00
ayntk-ai
776d41613d Fix 3 risk levels 2024-08-31 20:55:43 +02:00
ayntk-ai
f857bbc437 fix submit action 2024-08-31 19:31:23 +02:00
ayntk-ai
f8226cf892 confirm backup deletion 2024-08-31 19:31:01 +02:00
ayntk-ai
d2a0621f93 delete backup folder if empty 2024-08-31 18:38:57 +02:00
ayntk-ai
38845d7eb0 confirm backup and delete all backups of the job 2024-08-31 18:29:19 +02:00
ayntk-ai
3b3bc6c33b fix default checkbox state false or true 2024-08-31 16:19:39 +02:00
ayntk-ai
2adac01034 confirm danger 2024-08-31 16:12:08 +02:00
ayntk-ai
b656cabb33 delete project confirmation 2024-08-31 15:07:50 +02:00
ayntk-ai
830c047ccf delete environment confirmation 2024-08-31 15:06:07 +02:00
ayntk-ai
a3dd48de1d destination and dashboard confirmation 2024-08-31 14:51:59 +02:00
ayntk-ai
b118a627d0 fix password validation and password error 2024-08-31 13:41:08 +02:00
ayntk-ai
bcfca40f3a rest checkboxes on close 2024-08-31 13:10:45 +02:00
ayntk-ai
76cb473db8 fix default checkbox state 2024-08-31 13:06:55 +02:00
ayntk-ai
73dfdb83a7 fix rendering bug, more props 2024-08-31 12:55:17 +02:00
ayntk-ai
bff6964d4a scheduled task deletion 2024-08-30 20:33:12 +02:00
ayntk-ai
b807601d19 delete user confirm 2024-08-30 20:08:14 +02:00
ayntk-ai
9136d7acdc WIP more delete confirmations 2024-08-30 20:00:04 +02:00
ayntk-ai
da0398f35d remove unused code 2024-08-29 21:47:17 +02:00
ayntk-ai
6820fcc084 Fix name for services and DBs 2024-08-29 18:53:49 +02:00
ayntk-ai
d984bec175 Ajust text 2024-08-29 18:08:58 +02:00
ayntk-ai
182af1ec18 Fix application delete 2024-08-29 18:01:16 +02:00
ayntk-ai
a22e757ab7 fix execute action on the last button 2024-08-29 15:19:19 +02:00
ayntk-ai
354c74e920 fix 2024-08-29 12:37:19 +02:00
ayntk-ai
62b7900855 more props and more fixes 2024-08-28 15:40:39 +02:00
ayntk-ai
c92659994d improve button text 2024-08-28 13:41:24 +02:00
ayntk-ai
a2651ab3c1 reset modal on cancel, fix back button when there is no prior step 2024-08-28 13:38:37 +02:00
ayntk-ai
1b51d46b3d more props, nav button fixes 2024-08-28 13:31:09 +02:00
ayntk-ai
141752b9ad fix checkbox actions default display 2024-08-28 13:11:42 +02:00
ayntk-ai
73068aaa75 Refactor: Integrate tow step process in the modal component WIP 2024-08-28 12:52:04 +02:00
ayntk-ai
8d2a02dc3c change title of confirmation popup 2024-08-27 14:29:47 +02:00
ayntk-ai
4726676248 make things more clear 2024-08-27 14:19:37 +02:00
ayntk-ai
9040f5d2a1 confirm with password 2024-08-27 13:44:12 +02:00
ayntk-ai
ac50d8b4d8 fix styling 2024-08-27 12:57:03 +02:00
ayntk-ai
2a581147aa new confirm delete dialog 2024-08-27 12:43:59 +02:00
Luan Estradioto
2b8c9920d8 removed extra container and added new process to soketi container 2024-08-15 20:52:50 -03:00
Luan Estradioto
548fc21e40 added ws authentication 2024-08-15 11:17:18 -03:00
Luan Estradioto
c2ea8996ee feat: fully functional terminal for command center 2024-08-15 11:17:18 -03:00
ayntk-ai
5b54dc8792 Revert "improve CleanupDocker.php"
This reverts commit b5360e5e75.
2024-08-09 23:25:57 +02:00
ayntk-ai
b5360e5e75 improve CleanupDocker.php 2024-08-09 23:12:30 +02:00
ayntk-ai
840e225aa8 formatting and waring text 2024-08-09 22:43:18 +02:00
ayntk-ai
7d1179e7c8 fix cleanup images for databases 2024-08-09 22:25:39 +02:00
ayntk-ai
16a5c601e3 graceful db stop and db deletion fixes 2024-08-09 22:15:45 +02:00
ayntk-ai
c566152f37 improve graceful_shutdown_container 2024-08-09 20:28:57 +02:00
ayntk-ai
2ca6ffb84e fix public function service.php 2024-08-09 19:57:53 +02:00
ayntk-ai
41be1f7666 use public function 2024-08-09 19:39:43 +02:00
ayntk-ai
450351921e added public functions 2024-08-09 19:39:21 +02:00
ayntk-ai
a4bb87d13b simplify DeleteService.php 2024-08-09 19:22:14 +02:00
ayntk-ai
1cfddfd529 fix stop large amount of containers 2024-08-09 19:17:58 +02:00
ayntk-ai
72bcf03cbb graceful service container stop 2024-08-09 18:59:41 +02:00
ayntk-ai
d177e49e62 updated warning message and formating 2024-08-09 18:43:13 +02:00
ayntk-ai
5595853379 WIP database network, image removal 2024-08-09 03:03:40 +02:00
ayntk-ai
97c2bedda2 add delete_connected_networks function to services.php 2024-08-09 03:00:30 +02:00
ayntk-ai
d980c7a425 only run network removal on stop service if it is not a deletion operation 2024-08-09 02:59:41 +02:00
ayntk-ai
53dff4ca4f simplify uuid variabel 2024-08-09 02:58:59 +02:00
ayntk-ai
7722809c52 typo 2024-08-09 02:20:32 +02:00
ayntk-ai
e67e03f73f added comments and removed temp ones 2024-08-09 02:15:40 +02:00
ayntk-ai
86a087056e fix volume deletion for services 2024-08-09 02:11:42 +02:00
ayntk-ai
51071da700 fix order 2024-08-09 01:00:07 +02:00
ayntk-ai
70aa05bde9 order in importance 2024-08-09 00:42:23 +02:00
ayntk-ai
0135e2b5c0 add logic 2024-08-09 00:30:11 +02:00
ayntk-ai
2f95349888 Merge branch 'coollabsio:main' into fix-#2546-deletion-issues 2024-08-08 15:40:13 +02:00
ayntk-ai
4d0acee95c UI options for deletion WIP 2024-08-08 12:31:37 +02:00
ayntk-ai
74bea37b43 Merge branch 'coollabsio:main' into fix-#2546-deletion-issues 2024-08-08 11:50:57 +02:00
ayntk-ai
070daee28e remove networks and cleanup unused images when stoping dockercompose build pack containers 2024-08-08 01:19:17 +02:00
ayntk-ai
df796dffa2 fix delte networks and unused images of services when deleted 2024-08-08 01:02:48 +02:00
228 changed files with 4984 additions and 2605 deletions

View File

@@ -6,7 +6,7 @@ APP_KEY=
APP_URL=http://localhost
APP_PORT=8000
APP_DEBUG=true
SSH_MUX_ENABLED=false
SSH_MUX_ENABLED=true
# PostgreSQL Database Configuration
DB_DATABASE=coolify
@@ -19,11 +19,7 @@ DB_PORT=5432
# Set to true to enable Ray
RAY_ENABLED=false
# Set custom ray port
RAY_PORT=
# Clockwork Configuration
CLOCKWORK_ENABLED=false
CLOCKWORK_QUEUE_COLLECT=true
# RAY_PORT=
# Enable Laravel Telescope for debugging
TELESCOPE_ENABLED=false

View File

@@ -1,46 +1,65 @@
name: Bug report
description: "Create a new bug report."
name: 🐞 Bug Report
description: "File a new bug report."
title: "[Bug]: "
labels: ["🐛 Bug", "🔍 Triage"]
body:
- type: markdown
attributes:
value: >-
# 💎 Bounty program (with
[algora.io](https://console.algora.io/org/coollabsio/bounties/new))
value: |
> [!IMPORTANT]
> **Please ensure you are using the latest version of Coolify before submitting an issue, as the bug may have already been fixed in a recent update.** (Of course, if you're experiencing an issue on the latest version that wasn't present in a previous version, please let us know.)
# 💎 Bounty Program (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
- If you would like to prioritize the issue resolution, consider adding a bounty to this issue through our [Bounty Program](https://console.algora.io/org/coollabsio/bounties/new).
If you would like to prioritize the issue resolution, you can add bounty
to this issue.
Click [here](https://console.algora.io/org/coollabsio/bounties/new) to
get started.
- type: textarea
attributes:
label: Description
description: A clear and concise description of the problem
- type: textarea
attributes:
label: Minimal Reproduction (if possible, example repository)
description: Please provide a step by step guide to reproduce the issue.
label: Error Message and Logs
description: Provide a detailed description of the error or exception you encountered, along with any relevant log output.
validations:
required: true
- type: textarea
attributes:
label: Exception or Error
description: Please provide error logs if possible.
label: Steps to Reproduce
description: Please provide a step-by-step guide to reproduce the issue. Be as detailed as possible, otherwise we may not be able to assist you.
value: |
1.
2.
3.
4.
validations:
required: true
- type: input
attributes:
label: Version
description: Coolify's version (see top of your screen).
label: Example Repository URL
description: If applicable, provide a URL to a repository demonstrating the issue.
- type: input
attributes:
label: Coolify Version
description: Please provide the Coolify version you are using. This can be found in the top left corner of your Coolify dashboard.
placeholder: "v4.0.0-beta.335"
validations:
required: true
- type: checkboxes
- type: dropdown
attributes:
label: Cloud?
description: "Are you using the cloud version of Coolify?"
label: Are you using Coolify Cloud?
options:
- label: 'Yes'
required: false
- label: 'No'
required: false
- "No (self-hosted)"
- "Yes (Coolify Cloud)"
validations:
required: true
- type: input
attributes:
label: Operating System and Version (self-hosted)
description: Run `cat /etc/os-release` or `lsb_release -a` in your terminal and provide the operating system and version.
placeholder: "Ubuntu 22.04"
- type: textarea
attributes:
label: Additional Information
description: Any other relevant details about the issue.

View File

@@ -0,0 +1,31 @@
name: 💎 Enhancement Bounty
description: "Propose a new feature, service, or improvement with an attached bounty."
title: "[Enhancement]: "
labels: ["✨ Enhancement", "🔍 Triage"]
body:
- type: markdown
attributes:
value: |
> [!IMPORTANT]
> **This issue template is exclusively for proposing new features, services, or improvements with an attached bounty.** Enhancements without a bounty can be discussed in the appropriate category of [Github Discussions](https://github.com/coollabsio/coolify/discussions).
# 💎 Add a Bounty (with [algora.io](https://console.algora.io/org/coollabsio/bounties/new))
- [Click here to add the required bounty](https://console.algora.io/org/coollabsio/bounties/new)
- type: dropdown
attributes:
label: Request Type
description: Select the type of request you are making.
options:
- New Feature
- New Service
- Improvement
validations:
required: true
- type: textarea
attributes:
label: Description
description: Provide a detailed description of the feature, improvement, or service you are proposing.
validations:
required: true

View File

@@ -1,8 +1,18 @@
blank_issues_enabled: false
contact_links:
- name: 🤔 Community Support (Chat)
- name: 🤔 Questions and Community Support
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/new-features
about: All feature requests will be discussed here.
about: If you have any questions, reach out to us on Discord inside the "#support" channel.
- name: 💡 Feature Request
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests
about: Suggest a new feature for Coolify.
- name: ⚙️ Service Request
url: https://github.com/coollabsio/coolify/discussions/categories/service-requests
about: Request a new service integration for Coolify.
- name: 🔧 Improvements
url: https://github.com/coollabsio/coolify/discussions/categories/improvements
about: Suggest improvements to existing features for Coolify.

View File

@@ -1 +1,13 @@
> Always use `next` branch as destination branch for PRs, not `main`
## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
- [ ] I have listed all changes in the `Changes` section.
- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
- [ ] I have tested my changes.
- [ ] I have considered backwards compatibility.
- [ ] I have removed this checklist and any unused sections.
## Changes
-
## Issues
- fix #

View File

@@ -2,7 +2,7 @@ name: Coolify Helper Image (v4)
on:
push:
branches: [ "main", "next" ]
branches: [ "main" ]
paths:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile

103
.github/workflows/coolify-realtime.yml vendored Normal file
View File

@@ -0,0 +1,103 @@
name: Coolify Realtime (v4)
on:
push:
branches: [ "main", "next" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -0,0 +1,78 @@
name: Remove Labels and Assignees on Issue Close
on:
issues:
types: [closed]
pull_request:
types: [closed]
pull_request_target:
types: [closed]
jobs:
remove-labels-and-assignees:
runs-on: ubuntu-latest
steps:
- name: Remove labels and assignees
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
async function processIssue(issueNumber) {
try {
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: issueNumber
});
const labelsToKeep = currentLabels
.filter(label => label.name === '⏱︎ Stale')
.map(label => label.name);
await github.rest.issues.setLabels({
owner,
repo,
issue_number: issueNumber,
labels: labelsToKeep
});
const { data: issue } = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber
});
if (issue.assignees && issue.assignees.length > 0) {
await github.rest.issues.removeAssignees({
owner,
repo,
issue_number: issueNumber,
assignees: issue.assignees.map(assignee => assignee.login)
});
}
} catch (error) {
if (error.status !== 404) {
console.error(`Error processing issue ${issueNumber}:`, error);
}
}
}
if (context.eventName === 'issues' || context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const issue = context.payload.issue || context.payload.pull_request;
await processIssue(issue.number);
}
if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
const pr = context.payload.pull_request;
if (pr.body) {
const issueReferences = pr.body.match(/#(\d+)/g);
if (issueReferences) {
for (const reference of issueReferences) {
const issueNumber = parseInt(reference.substring(1));
await processIssue(issueNumber);
}
}
}
}

View File

@@ -6,15 +6,19 @@ You can ask for guidance anytime on our [Discord server](https://coollabs.io/dis
## Table of Contents
1. [Setup Development Environment](#1-setup-development-environment)
2. [Verify Installation](#2-verify-installation-optional)
3. [Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
4. [Set up Environment Variables](#4-set-up-environment-variables)
5. [Start Coolify](#5-start-coolify)
6. [Start Development](#6-start-development)
7. [Development Notes](#7-development-notes)
8. [Create a Pull Request](#8-create-a-pull-request)
9. [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing to Coolify](#contributing-to-coolify)
- [Table of Contents](#table-of-contents)
- [1. Setup Development Environment](#1-setup-development-environment)
- [2. Verify Installation (Optional)](#2-verify-installation-optional)
- [3. Fork and Setup Local Repository](#3-fork-and-setup-local-repository)
- [4. Set up Environment Variables](#4-set-up-environment-variables)
- [5. Start Coolify](#5-start-coolify)
- [6. Start Development](#6-start-development)
- [7. Development Notes](#7-development-notes)
- [8. Create a Pull Request](#8-create-a-pull-request)
- [Additional Contribution Guidelines](#additional-contribution-guidelines)
- [Contributing a New Service](#contributing-a-new-service)
- [Contributing to Documentation](#contributing-to-documentation)
## 1. Setup Development Environment

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Application;
use App\Actions\Server\CleanupDocker;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,44 +10,35 @@ class StopApplication
{
use AsAction;
public function handle(Application $application, bool $previewDeployments = false)
public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
{
if ($application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
try {
$server = $application->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping application: '.$application->name);
if ($server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
return;
}
$servers = collect([]);
$servers->push($application->destination->server);
$application->additional_servers->map(function ($server) use ($servers) {
$servers->push($server);
});
foreach ($servers as $server) {
if (! $server->isFunctional()) {
return 'Server is not functional';
}
if ($previewDeployments) {
$containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
} else {
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
}
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerName = data_get($container, 'Names');
if ($containerName) {
instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
}
}
}
$containersToStop = $application->getContainersToStop($previewDeployments);
$application->stopContainers($containersToStop, $server);
if ($application->build_pack === 'dockercompose') {
// remove network
$uuid = $application->uuid;
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
$application->delete_connected_networks($application->uuid);
}
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Actions\CoolifyTask;
use App\Enums\ActivityTypes;
use App\Enums\ProcessStatus;
use App\Helpers\SshMultiplexingHelper;
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Server;
use Illuminate\Process\ProcessResult;
@@ -137,7 +138,7 @@ class RunRemoteProcess
$command = $this->activity->getExtraProperty('command');
$server = Server::whereUuid($server_uuid)->firstOrFail();
return generateSshCommand($server, $command);
return SshMultiplexingHelper::generateSshCommand($server, $command);
}
protected function handleOutput(string $type, string $output)

View File

@@ -23,7 +23,7 @@ class StartDragonfly
$startCommand = "dragonfly --requirepass {$this->database->dragonfly_password}";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -75,7 +75,7 @@ class StartDragonfly
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -118,10 +118,10 @@ class StartDragonfly
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -152,7 +152,7 @@ class StartDragonfly
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->dragonfly_password}");
}

View File

@@ -24,7 +24,7 @@ class StartKeydb
$startCommand = "keydb-server --requirepass {$this->database->keydb_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -74,7 +74,7 @@ class StartKeydb
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -94,10 +94,10 @@ class StartKeydb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->keydb_conf) || !empty($this->database->keydb_conf)) {
if (! is_null($this->database->keydb_conf) || ! empty($this->database->keydb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/keydb.conf',
'source' => $this->configuration_dir.'/keydb.conf',
'target' => '/etc/keydb/keydb.conf',
'read_only' => true,
];
@@ -125,10 +125,10 @@ class StartKeydb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -159,7 +159,7 @@ class StartKeydb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->keydb_password}");
}

View File

@@ -21,7 +21,7 @@ class StartMariadb
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,7 +69,7 @@ class StartMariadb
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -89,10 +89,10 @@ class StartMariadb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mariadb_conf) || !empty($this->database->mariadb_conf)) {
if (! is_null($this->database->mariadb_conf) || ! empty($this->database->mariadb_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
@@ -120,10 +120,10 @@ class StartMariadb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -154,18 +154,18 @@ class StartMariadb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_ROOT_PASSWORD={$this->database->mariadb_root_password}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_DATABASE'))->isEmpty()) {
$environment_variables->push("MARIADB_DATABASE={$this->database->mariadb_database}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_USER'))->isEmpty()) {
$environment_variables->push("MARIADB_USER={$this->database->mariadb_user}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MARIADB_PASSWORD'))->isEmpty()) {
$environment_variables->push("MARIADB_PASSWORD={$this->database->mariadb_password}");
}

View File

@@ -23,7 +23,7 @@ class StartMongodb
$startCommand = 'mongod';
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -77,7 +77,7 @@ class StartMongodb
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -97,19 +97,19 @@ class StartMongodb
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mongo_conf) || !empty($this->database->mongo_conf)) {
if (! is_null($this->database->mongo_conf) || ! empty($this->database->mongo_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/mongod.conf',
'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['services'][$container_name]['command'] = $startCommand.' --config /etc/mongo/mongod.conf';
}
$this->add_default_database();
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/docker-entrypoint-initdb.d',
'source' => $this->configuration_dir.'/docker-entrypoint-initdb.d',
'target' => '/docker-entrypoint-initdb.d',
'read_only' => true,
];
@@ -136,10 +136,10 @@ class StartMongodb
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -170,15 +170,15 @@ class StartMongodb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MONGO_INITDB_ROOT_USERNAME'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($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($env)->contains('MONGO_INITDB_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($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($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MONGO_INITDB_DATABASE'))->isEmpty()) {
$environment_variables->push("MONGO_INITDB_DATABASE={$this->database->mongo_initdb_database}");
}

View File

@@ -21,7 +21,7 @@ class StartMysql
$this->database = $database;
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -69,7 +69,7 @@ class StartMysql
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -89,10 +89,10 @@ class StartMysql
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->mysql_conf) || !empty($this->database->mysql_conf)) {
if (! is_null($this->database->mysql_conf) || ! empty($this->database->mysql_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/custom-config.cnf',
'source' => $this->configuration_dir.'/custom-config.cnf',
'target' => '/etc/mysql/conf.d/custom-config.cnf',
'read_only' => true,
];
@@ -120,10 +120,10 @@ class StartMysql
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -154,18 +154,18 @@ class StartMysql
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_ROOT_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_ROOT_PASSWORD={$this->database->mysql_root_password}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_DATABASE'))->isEmpty()) {
$environment_variables->push("MYSQL_DATABASE={$this->database->mysql_database}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_USER'))->isEmpty()) {
$environment_variables->push("MYSQL_USER={$this->database->mysql_user}");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('MYSQL_PASSWORD'))->isEmpty()) {
$environment_variables->push("MYSQL_PASSWORD={$this->database->mysql_password}");
}

View File

@@ -37,7 +37,6 @@ class StartPostgresql
$this->generate_init_scripts();
$this->add_custom_conf();
$docker_compose = [
'services' => [
$container_name => [

View File

@@ -24,7 +24,7 @@ class StartRedis
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
$container_name = $this->database->uuid;
$this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
$this->commands = [
"echo 'Starting {$database->name}.'",
@@ -78,7 +78,7 @@ class StartRedis
],
],
];
if (!is_null($this->database->limits_cpuset)) {
if (! is_null($this->database->limits_cpuset)) {
data_set($docker_compose, "services.{$container_name}.cpuset", $this->database->limits_cpuset);
}
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
@@ -98,10 +98,10 @@ class StartRedis
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
if (!is_null($this->database->redis_conf) || !empty($this->database->redis_conf)) {
if (! is_null($this->database->redis_conf) || ! empty($this->database->redis_conf)) {
$docker_compose['services'][$container_name]['volumes'][] = [
'type' => 'bind',
'source' => $this->configuration_dir . '/redis.conf',
'source' => $this->configuration_dir.'/redis.conf',
'target' => '/usr/local/etc/redis/redis.conf',
'read_only' => true,
];
@@ -130,10 +130,10 @@ class StartRedis
$local_persistent_volumes = [];
foreach ($this->database->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path !== '' && $persistentStorage->host_path !== null) {
$local_persistent_volumes[] = $persistentStorage->host_path . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $persistentStorage->host_path.':'.$persistentStorage->mount_path;
} else {
$volume_name = $persistentStorage->name;
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
$local_persistent_volumes[] = $volume_name.':'.$persistentStorage->mount_path;
}
}
@@ -164,7 +164,7 @@ class StartRedis
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Database;
use App\Actions\Server\CleanupDocker;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction;
class StopDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
$this->stopContainer($database, $database->uuid, 300);
if (! $isDeleteOperation) {
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
return 'Database stopped successfully';
}
private function stopContainer($database, string $containerName, int $timeout = 300): void
{
$server = $database->destination->server;
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
}
private function forceStopContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
}
private function removeContainer(string $containerName, $server): void
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
private function deleteConnectedNetworks($uuid, $server)
{
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
}

View File

@@ -543,7 +543,7 @@ class GetContainersStatus
}
}
}
$exitedServices = $exitedServices->unique('id');
$exitedServices = $exitedServices->unique('uuid');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;

View File

@@ -26,7 +26,7 @@ class CheckProxy
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
return false;
}
['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
if (! $uptime) {
throw new \Exception($error);
}

View File

@@ -47,7 +47,8 @@ class StartProxy
"echo 'Pulling docker image.'",
'docker compose pull',
"echo 'Stopping existing coolify-proxy.'",
'docker compose down -v --remove-orphans > /dev/null 2>&1',
'docker stop -t 10 coolify-proxy || true',
'docker rm coolify-proxy || true',
"echo 'Starting coolify-proxy.'",
'docker compose up -d --remove-orphans',
"echo 'Proxy started successfully.'",

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Events\CloudflareTunnelConfigured;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -40,12 +41,17 @@ class ConfigureCloudflared
instant_remote_process($commands, $server);
} catch (\Throwable $e) {
ray($e);
$server->settings->is_cloudflare_tunnel = false;
$server->settings->save();
throw $e;
} finally {
CloudflareTunnelConfigured::dispatch($server->team_id);
$commands = collect([
'rm -fr /tmp/cloudflared',
]);
instant_remote_process($commands, $server);
}
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -55,6 +56,13 @@ class UpdateCoolify
return;
}
$all_servers = Server::all();
$servers = $all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
PullHelperImageJob::dispatch($server);
}
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$this->latestVersion}"], $this->server, false);
remote_process([

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,11 +10,11 @@ class DeleteService
{
use AsAction;
public function handle(Service $service)
public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
{
try {
$server = data_get($service, 'server');
if ($server->isFunctional()) {
if ($deleteVolumes && $server->isFunctional()) {
$storagesToDelete = collect([]);
$service->environment_variables()->delete();
@@ -33,13 +34,29 @@ class DeleteService
foreach ($storagesToDelete as $storage) {
$commands[] = "docker volume rm -f $storage->name";
}
$commands[] = "docker rm -f $service->uuid";
instant_remote_process($commands, $server, false);
// Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
if (! empty($commands)) {
foreach ($commands as $command) {
$result = instant_remote_process([$command], $server, false);
if ($result !== 0) {
ray("Failed to execute: $command");
}
}
}
}
if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid);
}
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
} finally {
if ($deleteConfigurations) {
$service->delete_configurations();
}
foreach ($service->applications()->get() as $application) {
$application->forceDelete();
}
@@ -50,6 +67,11 @@ class DeleteService
$task->delete();
}
$service->tags()->detach();
$service->forceDelete();
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
}
}
}

View File

@@ -16,7 +16,7 @@ class StartService
$service->saveComposeConfigs();
$commands[] = 'cd '.$service->workdir();
$commands[] = "echo 'Saved configuration files to {$service->workdir()}.'";
if($service->networks()->count() > 0){
if ($service->networks()->count() > 0) {
$commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network inspect $service->uuid >/dev/null 2>&1 || docker network create --attachable $service->uuid";
}
@@ -31,7 +31,7 @@ class StartService
$network = $service->destination->network;
$serviceNames = data_get(Yaml::parse($compose), 'services', []);
foreach ($serviceNames as $serviceName => $serviceConfig) {
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} || true";
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true";
}
}
$activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged');

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Service;
use App\Actions\Server\CleanupDocker;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
@@ -9,40 +10,27 @@ class StopService
{
use AsAction;
public function handle(Service $service)
public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
{
try {
$server = $service->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
if ($applications->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
}
instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
$application->update(['status' => 'exited']);
}
$dbs = $service->databases()->get();
foreach ($dbs as $db) {
if ($dbs->count() < 6) {
instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$containersToStop = $service->getContainersToStop();
$service->stopContainers($containersToStop, $server);
if (! $isDeleteOperation) {
$service->delete_connected_networks($service->uuid);
if ($dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
$db->update(['status' => 'exited']);
}
instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
} catch (\Exception $e) {
ray($e->getMessage());
return $e->getMessage();
}
}
}

View File

@@ -1,24 +0,0 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupQueue extends Command
{
protected $signature = 'cleanup:queue';
protected $description = 'Cleanup Queue';
public function handle()
{
echo "Running queue cleanup...\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
foreach ($keys as $key) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
}
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class CleanupRedis extends Command
{
protected $signature = 'cleanup:redis';
protected $description = 'Cleanup Redis';
public function handle()
{
echo "Cleanup Redis keys.\n";
$prefix = config('database.redis.options.prefix');
$keys = Redis::connection()->keys('*:laravel*');
collect($keys)->each(function ($key) use ($prefix) {
$keyWithoutPrefix = str_replace($prefix, '', $key);
Redis::connection()->del($keyWithoutPrefix);
});
$queueOverlaps = Redis::connection()->keys('*laravel-queue-overlap*');
collect($queueOverlaps)->each(function ($key) {
Redis::connection()->del($key);
});
}
}

View File

@@ -2,10 +2,12 @@
namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application;
use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
@@ -35,6 +37,16 @@ class CleanupStuckedResources extends Command
private function cleanup_stucked_resources()
{
try {
$servers = Server::all()->filter(function ($server) {
return $server->isFunctional();
});
foreach ($servers as $server) {
CleanupHelperContainersJob::dispatch($server);
}
} catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
}
try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) {

View File

@@ -5,7 +5,6 @@ namespace App\Console\Commands;
use App\Actions\Server\StopSentinel;
use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\InstanceSettings;
@@ -18,7 +17,7 @@ use Illuminate\Support\Facades\Http;
class Init extends Command
{
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
protected $signature = 'app:init {--force-cloud}';
protected $description = 'Cleanup instance related stuffs';
@@ -26,9 +25,63 @@ class Init extends Command
public function handle()
{
if (isCloud() && ! $this->option('force-cloud')) {
echo "Skipping init as we are on cloud and --force-cloud option is not set\n";
return;
}
$this->servers = Server::all();
$this->alive();
if (isCloud()) {
} else {
$this->send_alive_signal();
get_public_ips();
}
// Backward compatibility
$this->disable_metrics();
$this->replace_slash_in_environment_name();
$this->restore_coolify_db_backup();
//
$this->update_traefik_labels();
if (! isCloud() || $this->option('force-cloud')) {
$this->cleanup_unused_network_from_coolify_proxy();
}
if (isCloud()) {
$this->cleanup_unnecessary_dynamic_proxy_configuration();
} else {
$this->cleanup_in_progress_application_deployments();
}
$this->call('cleanup:redis');
$this->call('cleanup:stucked-resources');
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
} else {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
}
}
private function disable_metrics()
{
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
@@ -39,62 +92,6 @@ class Init extends Command
}
}
}
$full_cleanup = $this->option('full-cleanup');
$cleanup_deployments = $this->option('cleanup-deployments');
$cleanup_proxy_networks = $this->option('cleanup-proxy-networks');
$this->replace_slash_in_environment_name();
if ($cleanup_deployments) {
echo "Running cleanup deployments.\n";
$this->cleanup_in_progress_application_deployments();
return;
}
if ($cleanup_proxy_networks) {
echo "Running cleanup proxy networks.\n";
$this->cleanup_unused_network_from_coolify_proxy();
return;
}
if ($full_cleanup) {
// Required for falsely deleted coolify db
$this->restore_coolify_db_backup();
$this->update_traefik_labels();
$this->cleanup_unused_network_from_coolify_proxy();
$this->cleanup_unnecessary_dynamic_proxy_configuration();
$this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:queue');
$this->call('cleanup:stucked-resources');
if (! isCloud()) {
try {
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
}
$settings = InstanceSettings::get();
if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]);
} else {
$settings->update(['is_auto_update_enabled' => false]);
}
}
if (isCloud()) {
$response = Http::retry(3, 1000)->get(config('constants.services.official'));
if ($response->successful()) {
$services = $response->json();
File::put(base_path('templates/service-templates.json'), json_encode($services));
}
}
return;
}
$this->cleanup_stucked_helper_containers();
$this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
@@ -108,7 +105,6 @@ class Init extends Command
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
if (isCloud()) {
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
@@ -128,13 +124,9 @@ class Init extends Command
}
}
}
private function cleanup_unused_network_from_coolify_proxy()
{
if (isCloud()) {
return;
}
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;
@@ -175,6 +167,7 @@ class Init extends Command
private function restore_coolify_db_backup()
{
if (version_compare('4.0.0-beta.179', config('version'), '<=')) {
try {
$database = StandalonePostgresql::withTrashed()->find(0);
if ($database && $database->trashed()) {
@@ -197,17 +190,9 @@ class Init extends Command
echo "Error in restoring coolify db backup: {$e->getMessage()}\n";
}
}
private function cleanup_stucked_helper_containers()
{
foreach ($this->servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
}
private function alive()
private function send_alive_signal()
{
$id = config('app.id');
$version = config('version');
@@ -225,23 +210,7 @@ class Init extends Command
echo "Error in alive: {$e->getMessage()}\n";
}
}
// private function cleanup_ssh()
// {
// TODO: it will cleanup id.root@host.docker.internal
// try {
// $files = Storage::allFiles('ssh/keys');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// $files = Storage::allFiles('ssh/mux');
// foreach ($files as $file) {
// Storage::delete($file);
// }
// } catch (\Throwable $e) {
// echo "Error in cleaning ssh: {$e->getMessage()}\n";
// }
// }
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
@@ -263,6 +232,7 @@ class Init extends Command
private function replace_slash_in_environment_name()
{
if (version_compare('4.0.0-beta.298', config('version'), '<=')) {
$environments = Environment::all();
foreach ($environments as $environment) {
if (str_contains($environment->name, '/')) {
@@ -271,4 +241,5 @@ class Init extends Command
}
}
}
}
}

View File

@@ -43,6 +43,8 @@ class Kernel extends ConsoleKernel
$schedule->command('uploads:clear')->everyTwoMinutes();
$schedule->command('telescope:prune')->daily();
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
@@ -77,12 +79,12 @@ class Kernel extends ConsoleKernel
}
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))
}
$schedule->job(new PullHelperImageJob)
->cron($settings->update_check_frequency)
->timezone($settings->instance_timezone)
->onOneServer();
}
}
private function schedule_updates($schedule)
{

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class CloudflareTunnelConfigured implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
if (is_null($teamId)) {
$teamId = auth()->user()->currentTeam()->id ?? null;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
}
$this->teamId = $teamId;
}
public function broadcastOn(): array
{
return [
new PrivateChannel("team.{$this->teamId}"),
];
}
}

View File

@@ -0,0 +1,184 @@
<?php
namespace App\Helpers;
use App\Models\PrivateKey;
use App\Models\Server;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Process;
class SshMultiplexingHelper
{
public static function serverSshConfiguration(Server $server)
{
$privateKey = PrivateKey::findOrFail($server->private_key_id);
$sshKeyLocation = $privateKey->getKeyLocation();
$muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
return [
'sshKeyLocation' => $sshKeyLocation,
'muxFilename' => $muxFilename,
];
}
public static function ensureMultiplexedConnection(Server $server)
{
if (! self::isMultiplexingEnabled()) {
return;
}
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$sshKeyLocation = $sshConfig['sshKeyLocation'];
self::validateSshKey($sshKeyLocation);
$checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$checkCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$checkCommand .= "{$server->user}@{$server->ip}";
$process = Process::run($checkCommand);
if ($process->exitCode() !== 0) {
self::establishNewMultiplexedConnection($server);
}
}
public static function establishNewMultiplexedConnection(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$connectionTimeout = config('constants.ssh.connection_timeout');
$serverInterval = config('constants.ssh.server_interval');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$establishCommand .= ' -o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$establishCommand .= self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval);
$establishCommand .= "{$server->user}@{$server->ip}";
$establishProcess = Process::run($establishCommand);
if ($establishProcess->exitCode() !== 0) {
throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
}
}
public static function removeMuxFile(Server $server)
{
$sshConfig = self::serverSshConfiguration($server);
$muxSocket = $sshConfig['muxFilename'];
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket ";
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$closeCommand .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$closeCommand .= "{$server->user}@{$server->ip}";
Process::run($closeCommand);
}
public static function generateScpCommand(Server $server, string $source, string $dest)
{
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp ";
if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$scp_command .= '-o ProxyCommand="cloudflared access ssh --hostname %h" ';
}
$scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
$scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
return $scp_command;
}
public static function generateSshCommand(Server $server, string $command)
{
if ($server->settings->force_disabled) {
throw new \RuntimeException('Server is disabled.');
}
$sshConfig = self::serverSshConfiguration($server);
$sshKeyLocation = $sshConfig['sshKeyLocation'];
$muxSocket = $sshConfig['muxFilename'];
$timeout = config('constants.ssh.command_timeout');
$muxPersistTime = config('constants.ssh.mux_persist_time');
$ssh_command = "timeout $timeout ssh ";
if (self::isMultiplexingEnabled()) {
$ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server);
}
if (data_get($server, 'settings.is_cloudflare_tunnel')) {
$ssh_command .= "-o ProxyCommand='cloudflared access ssh --hostname %h' ";
}
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command);
$command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
.$command.PHP_EOL
.$delimiter;
return $ssh_command;
}
private static function isMultiplexingEnabled(): bool
{
return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
}
private static function validateSshKey(string $sshKeyLocation): void
{
$checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
$keyCheckProcess = Process::run($checkKeyCommand);
if ($keyCheckProcess->exitCode() !== 0) {
throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
}
}
private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
{
$options = "-i {$sshKeyLocation} "
.'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
.'-o PasswordAuthentication=no '
."-o ConnectTimeout=$connectionTimeout "
."-o ServerAliveInterval=$serverInterval "
.'-o RequestTTY=no '
.'-o LogLevel=ERROR ';
// Bruh
if ($isScp) {
$options .= "-P {$server->port} ";
} else {
$options .= "-p {$server->port} ";
}
return $options;
}
}

View File

@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
}
#[OA\Post(
summary: 'Execute Command',
description: "Execute a command on the application's current container.",
path: '/applications/{uuid}/execute',
operationId: 'execute-command-application',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
requestBody: new OA\RequestBody(
required: true,
description: 'Command to execute.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'command' => ['type' => 'string', 'description' => 'Command to execute.'],
],
),
),
),
responses: [
new OA\Response(
response: 200,
description: "Execute a command on the application's current container.",
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Command executed.'],
'response' => ['type' => 'string'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function execute_command_by_uuid(Request $request)
{
// TODO: Need to review this from security perspective, to not allow arbitrary command execution
$allowedFields = ['command'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json(['message' => 'Application not found.'], 404);
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'command' => 'string|required',
]);
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
if ($validator->fails() || ! empty($extraFields)) {
$errors = $validator->errors();
if (! empty($extraFields)) {
foreach ($extraFields as $field) {
$errors->add($field, 'This field is not allowed.');
}
}
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
$container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
$status = getContainerStatus($application->destination->server, $container['Names']);
if ($status !== 'running') {
return response()->json([
'message' => 'Application is not running.',
], 400);
}
$commands = collect([
executeInDocker($container['Names'], $request->command),
]);
$res = instant_remote_process(command: $commands, server: $application->destination->server);
return response()->json([
'message' => 'Command executed.',
'response' => $res,
]);
}
private function validateDataApplications(Request $request, Server $server)
{
$teamId = getTeamIdFromToken();

View File

@@ -27,6 +27,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep;
use Illuminate\Support\Str;
use RuntimeException;
@@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
ray('New container name: ', $this->container_name)->green();
savePrivateKeyToFs($this->server);
$this->saved_outputs = collect();
// Set preview fqdn
@@ -514,7 +514,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
'hidden' => true,
'ignore_errors' => true,
], [
"docker network connect {$networkId} coolify-proxy || true",
"docker network connect {$networkId} coolify-proxy >/dev/null 2>&1 || true",
'hidden' => true,
'ignore_errors' => true,
]);
@@ -919,10 +919,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables_preview->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
}
}
@@ -978,10 +978,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
if ($this->application->build_pack !== 'dockercompose' || $this->application->compose_parsing_version === '1' || $this->application->compose_parsing_version === '2') {
if ($this->application->environment_variables->where('key', 'COOLIFY_BRANCH')->isEmpty()) {
$envs->push("COOLIFY_BRANCH={$local_branch}");
$envs->push("COOLIFY_BRANCH=\"{$local_branch}\"");
}
if ($this->application->environment_variables->where('key', 'COOLIFY_CONTAINER_NAME')->isEmpty()) {
$envs->push("COOLIFY_CONTAINER_NAME={$this->container_name}");
$envs->push("COOLIFY_CONTAINER_NAME=\"{$this->container_name}\"");
}
}
@@ -1456,10 +1456,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
],
[
executeInDocker($this->deployment_uuid, "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 ls-remote {$this->fullRepoUrl} {$local_branch}"),
executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
'hidden' => true,
'save' => 'git_commit_sha',
],
]
);
} else {
$this->execute_remote_command(
@@ -2049,6 +2049,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2068,6 +2072,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2110,6 +2118,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2129,6 +2141,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2157,6 +2173,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2176,6 +2196,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
executeInDocker($this->deployment_uuid, "echo '{$base64_build_command}' | base64 -d | tee /artifacts/build.sh > /dev/null"),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'cat /artifacts/build.sh'),
'hidden' => true,
],
[
executeInDocker($this->deployment_uuid, 'bash /artifacts/build.sh'),
'hidden' => true,
@@ -2187,20 +2211,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->addLogEntry('Building docker image completed.');
}
/**
* @param int $timeout in seconds
*/
private function graceful_shutdown_container(string $containerName, int $timeout = 30)
private function graceful_shutdown_container(string $containerName, int $timeout = 300)
{
try {
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
} catch (\Exception $error) {
// report error if needed
break;
}
usleep(100000);
}
$isRunning = $this->execute_remote_command(
["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
) === 'true';
if ($isRunning) {
$this->execute_remote_command(
["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
} catch (\Exception $error) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr');
}
$this->remove_container($containerName);
}
private function remove_container(string $containerName)
{
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);

View File

@@ -9,8 +9,8 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{

View File

@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
{
try {
ray('Cleaning up helper containers on '.$this->server->name);
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
$containers = format_docker_command_output_to_json($containers);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerId = data_get($container, 'ID');
$containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
$containerIds = collect(json_decode($containers))->pluck('ID');
if ($containerIds->count() > 0) {
foreach ($containerIds as $containerId) {
ray('Removing container '.$containerId);
instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
}

View File

@@ -3,12 +3,14 @@
namespace App\Jobs;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
class CleanupStaleMultiplexedConnections implements ShouldQueue
{
@@ -16,22 +18,65 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
public function handle()
{
Server::chunk(100, function ($servers) {
foreach ($servers as $server) {
$this->cleanupStaleConnection($server);
}
});
$this->cleanupStaleConnections();
$this->cleanupNonExistentServerConnections();
}
private function cleanupStaleConnection(Server $server)
private function cleanupStaleConnections()
{
$muxSocket = "/tmp/mux_{$server->id}";
$checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
$muxFiles = Storage::disk('ssh-mux')->files();
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
$server = Server::where('uuid', $serverUuid)->first();
if (! $server) {
$this->removeMultiplexFile($muxFile);
continue;
}
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
$checkProcess = Process::run($checkCommand);
if ($checkProcess->exitCode() !== 0) {
$closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
Process::run($closeCommand);
$this->removeMultiplexFile($muxFile);
} else {
$muxContent = Storage::disk('ssh-mux')->get($muxFile);
$establishedAt = Carbon::parse(substr($muxContent, 37));
$expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
if (Carbon::now()->isAfter($expirationTime)) {
$this->removeMultiplexFile($muxFile);
}
}
}
}
private function cleanupNonExistentServerConnections()
{
$muxFiles = Storage::disk('ssh-mux')->files();
$existingServerUuids = Server::pluck('uuid')->toArray();
foreach ($muxFiles as $muxFile) {
$serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
if (! in_array($serverUuid, $existingServerUuids)) {
$this->removeMultiplexFile($muxFile);
}
}
}
private function extractServerUuidFromMuxFile($muxFile)
{
return substr($muxFile, 4);
}
private function removeMultiplexFile($muxFile)
{
$muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
$closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
Process::run($closeCommand);
Storage::disk('ssh-mux')->delete($muxFile);
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -25,16 +24,6 @@ class ContainerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
GetContainersStatus::run($this->server);

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated;
use App\Models\InstanceSettings;
use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution;
@@ -22,7 +23,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
@@ -79,16 +79,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
public function middleware(): array
{
return [new WithoutOverlapping($this->backup->id)];
}
public function uniqueId(): int
{
return $this->backup->id;
}
public function handle(): void
{
try {
@@ -478,10 +468,37 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
}
}
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void
{
try {
ray($this->backup_location);
if (is_null($this->s3)) {
return;
}
@@ -491,20 +508,64 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint;
$this->s3->testConnection(shouldSave: true);
$configName = new Cuid2;
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$network = $this->database->service->destination->network;
} else {
$network = $this->database->destination->network;
}
$s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
$commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
$commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
$this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$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);
$this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage());
throw $e;
} finally {
$removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
$removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
instant_remote_process($removeConfigCommands, $this->server, false);
$command = "docker rm -f backup-of-{$this->backup->uuid}";
instant_remote_process([$command], $this->server);
}
}
private function ensureHelperImageAvailable(): void
{
$fullImageName = $this->getFullImageName();
$imageExists = $this->checkImageExists($fullImageName);
if (! $imageExists) {
$this->pullHelperImage($fullImageName);
}
}
private function checkImageExists(string $fullImageName): bool
{
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
return trim($result) === 'exists';
}
private function pullHelperImage(string $fullImageName): void
{
try {
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
} catch (\Exception $e) {
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
$this->add_to_backup_output($errorMessage);
throw new \RuntimeException($errorMessage);
}
}
private function getFullImageName(): string
{
$settings = InstanceSettings::get();
$helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version;
return "{$helperImage}:{$latestVersion}";
}
}

View File

@@ -1,62 +0,0 @@
<?php
namespace App\Jobs;
use App\Models\ScheduledDatabaseBackup;
use App\Models\Team;
use App\Notifications\Database\DailyBackup;
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 DatabaseBackupStatusJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public function __construct() {}
public function handle()
{
// $teams = Team::all();
// foreach ($teams as $team) {
// $scheduled_backups = $team->scheduledDatabaseBackups()->get();
// if ($scheduled_backups->isEmpty()) {
// continue;
// }
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// }
// }
// $scheduled_backups = ScheduledDatabaseBackup::all();
// $databases = collect();
// $teams = collect();
// foreach ($scheduled_backups as $scheduled_backup) {
// $last_days_backups = $scheduled_backup->get_last_days_backup_status();
// if ($last_days_backups->isEmpty()) {
// continue;
// }
// $failed = $last_days_backups->where('status', 'failed');
// $database = $scheduled_backup->database;
// $team = $database->team();
// $teams->put($team->id, $team);
// $databases->put("{$team->id}:{$database->name}", [
// 'failed_count' => $failed->count(),
// ]);
// }
// foreach ($databases as $name => $database) {
// [$team_id, $name] = explode(':', $name);
// $team = $teams->get($team_id);
// $team?->notify(new DailyBackup($databases));
// }
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Actions\Application\StopApplication;
use App\Actions\Database\StopDatabase;
use App\Actions\Server\CleanupDocker;
use App\Actions\Service\DeleteService;
use App\Actions\Service\StopService;
use App\Models\Application;
@@ -30,8 +31,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false,
public bool $deleteVolumes = false) {}
public bool $deleteConfigurations,
public bool $deleteVolumes,
public bool $dockerCleanup,
public bool $deleteConnectedNetworks
) {}
public function handle()
{
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource);
StopDatabase::run($this->resource, true);
break;
case 'service':
StopService::run($this->resource);
DeleteService::run($this->resource);
StopService::run($this->resource, true);
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
$isDatabase = $this->resource instanceof StandalonePostgresql
|| $this->resource instanceof StandaloneRedis
|| $this->resource instanceof StandaloneMongodb
|| $this->resource instanceof StandaloneMysql
|| $this->resource instanceof StandaloneMariadb
|| $this->resource instanceof StandaloneKeydb
|| $this->resource instanceof StandaloneDragonfly
|| $this->resource instanceof StandaloneClickhouse;
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if (($this->dockerCleanup || $isDatabase) && $server) {
CleanupDocker::dispatch($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
}
} catch (\Throwable $e) {
ray($e->getMessage());
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
if ($this->dockerCleanup) {
CleanupDocker::dispatch($server, true);
}
Artisan::queue('cleanup:stucked-resources');
}
}

View File

@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [new WithoutOverlapping($this->server->id)];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -25,16 +24,6 @@ class GithubAppPermissionJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public GithubApp $github_app) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->github_app->uuid))];
}
public function uniqueId(): int
{
return $this->github_app->uuid;
}
public function handle()
{
try {

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
@@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function __construct() {}
public function handle(): void
{
@@ -42,8 +31,8 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
$current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$helperImage = config('coolify.helper_image');
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
// $helperImage = config('coolify.helper_image');
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
$settings->update(['helper_version' => $latest_version]);
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
@@ -18,16 +17,6 @@ class PullSentinelImageJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 1000;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) {}
public function handle(): void

View File

@@ -13,7 +13,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ScheduledTaskJob implements ShouldQueue
@@ -56,24 +55,17 @@ class ScheduledTaskJob implements ShouldQueue
{
if ($this->resource instanceof Application) {
$timezone = $this->resource->destination->server->settings->server_timezone;
return $timezone;
} elseif ($this->resource instanceof Service) {
$timezone = $this->resource->server->settings->server_timezone;
return $timezone;
}
return 'UTC';
}
public function middleware(): array
{
return [new WithoutOverlapping($this->task->id)];
}
public function uniqueId(): int
{
return $this->task->id;
}
public function handle(): void
{
@@ -94,12 +86,12 @@ class ScheduledTaskJob implements ShouldQueue
} elseif ($this->resource->type() == 'service') {
$this->resource->applications()->get()->each(function ($application) {
if (str(data_get($application, 'status'))->contains('running')) {
$this->containers[] = data_get($application, 'name') . '-' . data_get($this->resource, 'uuid');
$this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid');
}
});
$this->resource->databases()->get()->each(function ($database) {
if (str(data_get($database, 'status'))->contains('running')) {
$this->containers[] = data_get($database, 'name') . '-' . data_get($this->resource, 'uuid');
$this->containers[] = data_get($database, 'name').'-'.data_get($this->resource, 'uuid');
}
});
}
@@ -112,8 +104,8 @@ class ScheduledTaskJob implements ShouldQueue
}
foreach ($this->containers as $containerName) {
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container . '-' . $this->resource->uuid)) {
$cmd = "sh -c '" . str_replace("'", "'\''", $this->task->command) . "'";
if (count($this->containers) == 1 || str_starts_with($containerName, $this->task->container.'-'.$this->resource->uuid)) {
$cmd = "sh -c '".str_replace("'", "'\''", $this->task->command)."'";
$exec = "docker exec {$containerName} {$cmd}";
$this->task_output = instant_remote_process([$exec], $this->server, true);
$this->task_log->update([

View File

@@ -16,7 +16,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
@@ -24,7 +23,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $tries = 1;
public $timeout = 60;
@@ -45,16 +44,6 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle()
{
try {
@@ -93,7 +82,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection();
['uptime' => $uptime] = $this->server->validateConnection(false);
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);

View File

@@ -10,7 +10,7 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\Middleware\;
use Illuminate\Queue\SerializesModels;
class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +26,6 @@ class ServerLimitCheckJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Team $team) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->team->uuid))];
}
public function uniqueId(): int
{
return $this->team->uuid;
}
public function handle()
{
try {

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
@@ -26,16 +25,6 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
public function __construct(public Server $server) {}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): int
{
return $this->server->uuid;
}
public function handle()
{
if (! $this->server->isServerReady($this->tries)) {

View File

@@ -141,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
@@ -175,7 +175,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
@@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function savePrivateKey()
{
$this->validate([
'privateKeyName' => 'required',
'privateKey' => 'required',
'privateKeyName' => 'required|string|max:255',
'privateKeyDescription' => 'nullable|string|max:255',
'privateKey' => 'required|string',
]);
$this->createdPrivateKey = PrivateKey::create([
try {
$privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id,
]);
$this->createdPrivateKey->save();
$this->createdPrivateKey = $privateKey;
$this->currentState = 'create-server';
} catch (\Exception $e) {
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
}
}
public function saveServer()

View File

@@ -1,21 +0,0 @@
<?php
namespace App\Livewire\CommandCenter;
use App\Models\Server;
use Livewire\Component;
class Index extends Component
{
public $servers = [];
public function mount()
{
$this->servers = Server::isReachable()->get();
}
public function render()
{
return view('livewire.command-center.index');
}
}

View File

@@ -30,7 +30,6 @@ class Dashboard extends Component
public function cleanup_queue()
{
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id,
]);

View File

@@ -38,7 +38,7 @@ class Form extends Component
}
$this->destination->delete();
return redirect()->route('dashboard');
return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,13 +2,28 @@
namespace App\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
public function delete()
public $team;
public function mount()
{
$this->team = currentTeam()->name;
}
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$currentTeam = currentTeam();
$currentTeam->delete();

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component

View File

@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
public bool $docker_cleanup = true;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop()
{
StopApplication::run($this->application);
StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
public function render()
{
return view('livewire.project.application.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
}

View File

@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -184,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
}
}
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
$this->dispatch('reloadWindow');
GetContainersStatus::run($server);
$this->application->refresh();
$this->dispatch('containerStatusUpdated');
$this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -203,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id)
{
try {
$server = $this->application->destination->server;
$timeout = 300;
if ($this->application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
}
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
ApplicationPreview::where('application_id', $this->application->id)
->where('pull_request_id', $pull_request_id)
->first()
->delete();
$this->application->refresh();
$this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ class Previews extends Component
return handleError($e, $this);
}
}
private function stopContainers(array $containers, $server, int $timeout)
{
$processes = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@@ -12,6 +14,12 @@ class BackupEdit extends Component
public $s3s;
public bool $delete_associated_backups_locally = false;
public bool $delete_associated_backups_s3 = false;
public bool $delete_associated_backups_sftp = false;
public ?string $status = null;
public array $parameters;
@@ -46,10 +54,24 @@ class BackupEdit extends Component
}
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
if ($this->delete_associated_backups_locally) {
$this->deleteAssociatedBackupsLocally();
}
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
}
$this->backup->delete();
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
@@ -104,4 +126,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage());
}
}
public function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (! $backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if ($backupFolder) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
public function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
public function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -3,18 +3,28 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
public $database;
public $executions = [];
public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_sftp = true;
public function getListeners()
{
$userId = auth()->user()->id;
$userId = Auth::id();
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +41,36 @@ class BackupExecutions extends Component
}
}
public function deleteBackup($exeuctionId)
#[On('deleteBackup')]
public function deleteBackup($executionId, $password)
{
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
if ($this->delete_backup_s3) {
// Add logic to delete from S3
}
if ($this->delete_backup_sftp) {
// Add logic to delete from SFTP
}
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
@@ -82,16 +109,18 @@ class BackupExecutions extends Component
return $server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -104,6 +133,17 @@ class BackupExecutions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
public function render()
{
return view('livewire.project.database.backup-executions', [
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
],
]);
}
}

View File

@@ -56,7 +56,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -73,14 +73,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -95,7 +95,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -54,7 +54,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters;
public $docker_cleanup = true;
public function getListeners()
{
$userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop()
{
StopDatabase::run($this->database);
StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
public function render()
{
return view('livewire.project.database.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
],
]);
}
}

View File

@@ -57,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -94,14 +94,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -116,7 +116,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -63,7 +63,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -100,14 +100,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -122,7 +122,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -61,7 +61,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -101,14 +101,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -123,7 +123,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -62,7 +62,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -99,14 +99,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -121,7 +121,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -57,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false;
public string $environmentName = '';
public function mount()
{
$this->parameters = get_route_parameters();
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
}
public function delete()

View File

@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false;
public string $projectName = '';
public function mount()
{
$this->parameters = get_route_parameters();
$this->projectName = Project::findOrFail($this->project_id)->name;
}
public function delete()

View File

@@ -52,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id);
if ($application) {
$application->restart();
$this->dispatch('success', 'Application restarted successfully.');
$this->dispatch('success', 'Service application restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -65,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id);
if ($database) {
$database->restart();
$this->dispatch('success', 'Database restarted successfully.');
$this->dispatch('success', 'Service database restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);

View File

@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
@@ -83,8 +85,14 @@ class FileStorage extends Component
}
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$message = 'File deleted.';
if ($this->fileStorage->is_directory) {
@@ -129,6 +137,13 @@ class FileStorage extends Component
public function render()
{
return view('livewire.project.service.file-storage');
return view('livewire.project.service.file-storage', [
'directoryDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
],
'fileDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
],
]);
}
}

View File

@@ -20,6 +20,10 @@ class Navbar extends Component
public $isDeploymentProgress = false;
public $docker_cleanup = true;
public $title = 'Configuration';
public function mount()
{
if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) {
@@ -40,7 +44,7 @@ class Navbar extends Component
public function serviceStarted()
{
$this->dispatch('success', 'Service status changed.');
// $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@@ -60,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.');
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments()
{
try {
@@ -95,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id);
}
public function stop(bool $forceCleanup = false)
public function stop()
{
StopService::run($this->service);
if ($forceCleanup) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
}
@@ -121,4 +115,13 @@ class Navbar extends Component
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function render()
{
return view('livewire.project.service.navbar', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters;
public $docker_cleanup = true;
public $delete_volumes = true;
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
public function render()
{
return view('livewire.project.service.service-application-view');
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose');
}
}
public function render()
{
return view('livewire.project.service.service-application-view', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,7 +47,15 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
$this->fields = $this->fields->sortBy('name');
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
return data_get($field, 'isPassword') ? 1 : 0;
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
})->flatMap(function ($group) {
return $group;
});
}
public function saveCompose($raw)

View File

@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{
public $resource;
public $resourceName;
public $projectUuid;
public $environmentName;
@@ -18,22 +25,93 @@ class Danger extends Component
public bool $delete_volumes = true;
public bool $docker_cleanup = true;
public bool $delete_connected_networks = true;
public ?string $modalId = null;
public string $resourceDomain = '';
public function mount()
{
$this->modalId = new Cuid2;
$parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
if ($this->resource === null) {
if (isset($parameters['service_uuid'])) {
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
} elseif (isset($parameters['stack_service_uuid'])) {
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
}
}
public function delete()
if ($this->resource === null) {
$this->resourceName = 'Unknown Resource';
return;
}
if (! method_exists($this->resource, 'type')) {
$this->resourceName = 'Unknown Resource';
return;
}
switch ($this->resource->type()) {
case 'application':
$this->resourceName = $this->resource->name ?? 'Application';
break;
case 'standalone-postgresql':
case 'standalone-redis':
case 'standalone-mongodb':
case 'standalone-mysql':
case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$this->resourceName = $this->resource->name ?? 'Database';
break;
case 'service':
$this->resourceName = $this->resource->name ?? 'Service';
break;
case 'service-application':
$this->resourceName = $this->resource->name ?? 'Service Application';
break;
case 'service-database':
$this->resourceName = $this->resource->name ?? 'Service Database';
break;
default:
$this->resourceName = 'Unknown Resource';
}
}
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! $this->resource) {
$this->addError('resource', 'Resource not found.');
return;
}
try {
// $this->authorize('delete', $this->resource);
$this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
DeleteResourceJob::dispatch(
$this->resource,
$this->delete_configurations,
$this->delete_volumes,
$this->docker_cleanup,
$this->delete_connected_networks
);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
@@ -43,4 +121,19 @@ class Danger extends Component
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.danger', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -115,8 +117,14 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
public function removeServer(int $network_id, int $server_id)
public function removeServer(int $network_id, int $server_id, $password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');

View File

@@ -2,18 +2,16 @@
namespace App\Livewire\Project\Shared;
use App\Actions\Server\RunCommand;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Support\Collection;
use Livewire\Attributes\On;
use Livewire\Component;
class ExecuteContainerCommand extends Component
{
public string $command;
public string $container;
public $container;
public Collection $containers;
@@ -23,8 +21,6 @@ class ExecuteContainerCommand extends Component
public string $type;
public string $workDir = '';
public Server $server;
public Collection $servers;
@@ -33,11 +29,13 @@ class ExecuteContainerCommand extends Component
'server' => 'required',
'container' => 'required',
'command' => 'required',
'workDir' => 'nullable',
];
public function mount()
{
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->parameters = get_route_parameters();
$this->containers = collect();
$this->servers = collect();
@@ -62,24 +60,13 @@ class ExecuteContainerCommand extends Component
if ($this->resource->destination->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->destination->server);
}
$this->container = $this->resource->uuid;
$this->containers->push($this->container);
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->resource->applications()->get()->each(function ($application) {
$this->containers->push(data_get($application, 'name').'-'.data_get($this->resource, 'uuid'));
});
$this->resource->databases()->get()->each(function ($database) {
$this->containers->push(data_get($database, 'name').'-'.data_get($this->resource, 'uuid'));
});
if ($this->resource->server->isFunctional()) {
$this->servers = $this->servers->push($this->resource->server);
}
}
if ($this->containers->count() > 0) {
$this->container = $this->containers->first();
}
}
public function loadContainers()
@@ -102,44 +89,65 @@ class ExecuteContainerCommand extends Component
];
$this->containers = $this->containers->push($payload);
}
} elseif (data_get($this->parameters, 'database_uuid')) {
if ($this->resource->isRunning()) {
$this->containers = $this->containers->push([
'server' => $server,
'container' => [
'Names' => $this->resource->uuid,
],
]);
}
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->resource->applications()->get()->each(function ($application) {
ray($application);
if ($application->isRunning()) {
$this->containers->push([
'server' => $this->resource->server,
'container' => [
'Names' => data_get($application, 'name').'-'.data_get($this->resource, 'uuid'),
],
]);
}
});
$this->resource->databases()->get()->each(function ($database) {
if ($database->isRunning()) {
$this->containers->push([
'server' => $this->resource->server,
'container' => [
'Names' => data_get($database, 'name').'-'.data_get($this->resource, 'uuid'),
],
]);
}
});
}
}
if ($this->containers->count() > 0) {
if (data_get($this->parameters, 'application_uuid')) {
$this->container = data_get($this->containers->first(), 'container.Names');
} elseif (data_get($this->parameters, 'database_uuid')) {
$this->container = $this->containers->first();
} elseif (data_get($this->parameters, 'service_uuid')) {
$this->container = $this->containers->first();
}
}
}
public function runCommand()
#[On('connectToContainer')]
public function connectToContainer()
{
try {
if (data_get($this->parameters, 'application_uuid')) {
$container = $this->containers->where('container.Names', $this->container)->first();
$container_name = data_get($container, 'container.Names');
if (is_null($container)) {
$container_name = data_get($this->container, 'container.Names');
if (is_null($container_name)) {
throw new \RuntimeException('Container not found.');
}
$server = data_get($container, 'server');
} else {
$container_name = $this->container;
$server = $this->servers->first();
}
$server = data_get($this->container, 'server');
if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.');
}
$cmd = "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; ".str_replace("'", "'\''", $this->command)."'";
if (! empty($this->workDir)) {
$exec = "docker exec -w {$this->workDir} {$container_name} {$cmd}";
} else {
$exec = "docker exec {$container_name} {$cmd}";
}
$activity = RunCommand::run(server: $server, command: $exec);
$this->dispatch('activityMonitor', $activity->id);
$this->dispatch('send-terminal-command',
true,
$container_name,
$server->uuid,
);
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -108,14 +109,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
} else {
if ($this->server->isSwarm()) {
@@ -124,14 +125,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
}
if ($refresh) {

View File

@@ -7,7 +7,9 @@ use Livewire\Component;
class Executions extends Component
{
public $executions = [];
public $selectedKey;
public $task;
public function getListeners()
@@ -29,7 +31,7 @@ class Executions extends Component
public function server()
{
if (!$this->task) {
if (! $this->task) {
return null;
}
@@ -42,16 +44,18 @@ class Executions extends Component
return $this->task->service->destination->server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -64,6 +68,7 @@ class Executions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -20,6 +20,8 @@ class Show extends Component
public string $type;
public string $scheduledTaskName;
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
$this->scheduledTaskName = $this->task->name;
}
public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete();
if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters);
return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else {
return redirect()->route('project.service.configuration', $this->parameters);
return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
}
} catch (\Exception $e) {
return handleError($e);

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Show extends Component
@@ -36,8 +38,14 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully');
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$this->storage->delete();
$this->dispatch('refreshStorages');
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;
class Terminal extends Component
{
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
];
}
public function closeTerminal()
{
$this->dispatch('reloadWindow');
}
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
if ($isContainer) {
$status = getContainerStatus($server, $identifier);
if ($status !== 'running') {
return;
}
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
}
// ssh command is sent back to frontend then to websocket
// this is done because the websocket connection is not available here
// a better solution would be to remove websocket on NodeJS and work with something like
// 1. Laravel Pusher/Echo connection (not possible without a sdk)
// 2. Ratchet / Revolt / ReactPHP / Event Loop (possible but hard to implement and huge dependencies)
// 3. Just found out about this https://github.com/sirn-se/websocket-php, perhaps it can be used
// 4. Follow-up discussions here:
// - https://github.com/coollabsio/coolify/issues/2298
// - https://github.com/coollabsio/coolify/discussions/3362
$this->dispatch('send-back-command', $command);
}
public function render()
{
return view('livewire.project.shared.terminal');
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace App\Livewire;
use App\Actions\Server\RunCommand as ServerRunCommand;
use App\Models\Server;
use Livewire\Component;
class RunCommand extends Component
{
public string $command;
public $server;
public $servers = [];
protected $rules = [
'server' => 'required',
'command' => 'required',
];
protected $validationAttributes = [
'server' => 'server',
'command' => 'command',
];
public function mount($servers)
{
$this->servers = $servers;
$this->server = $servers[0]->uuid;
}
public function runCommand()
{
$this->validate();
try {
$activity = ServerRunCommand::run(server: Server::where('uuid', $this->server)->first(), command: $this->command);
$this->dispatch('activityMonitor', $activity->id);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -3,17 +3,13 @@
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component
{
use WithRateLimiting;
public string $name = '';
public string $name;
public string $value;
public string $value = '';
public ?string $from = null;
@@ -26,72 +22,69 @@ class Create extends Component
'value' => 'required|string',
];
protected $validationAttributes = [
'name' => 'name',
'value' => 'private Key',
];
public function generateNewRSAKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->generateNewKey('rsa');
}
public function generateNewEDKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->generateNewKey('ed25519');
}
public function updated($updateProperty)
private function generateNewKey($type)
{
if ($updateProperty === 'value') {
try {
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
if ($this->$updateProperty === '') {
$this->publicKey = '';
} else {
$this->publicKey = 'Invalid private key';
$keyData = PrivateKey::generateNewKeyPair($type);
$this->setKeyData($keyData);
}
public function updated($property)
{
if ($property === 'value') {
$this->validatePrivateKey();
}
}
$this->validateOnly($updateProperty);
}
public function createPrivateKey()
{
$this->validate();
try {
$this->value = trim($this->value);
if (! str_ends_with($this->value, "\n")) {
$this->value .= "\n";
}
$private_key = PrivateKey::create([
$privateKey = PrivateKey::createAndStore([
'name' => $this->name,
'description' => $this->description,
'private_key' => $this->value,
'private_key' => trim($this->value)."\n",
'team_id' => currentTeam()->id,
]);
if ($this->from === 'server') {
return redirect()->route('dashboard');
}
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
return $this->redirectAfterCreation($privateKey);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function setKeyData(array $keyData)
{
$this->name = $keyData['name'];
$this->description = $keyData['description'];
$this->value = $keyData['private_key'];
$this->publicKey = $keyData['public_key'];
}
private function validatePrivateKey()
{
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
$this->publicKey = $validationResult['publicKey'];
if (! $validationResult['isValid']) {
$this->addError('value', 'Invalid private key');
}
}
private function redirectAfterCreation(PrivateKey $privateKey)
{
return $this->from === 'server'
? redirect()->route('dashboard')
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use Livewire\Component;
class Index extends Component
{
public function render()
{
$privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
return view('livewire.security.private-key.index', [
'privateKeys' => $privateKeys,
])->layout('components.layout');
}
public function cleanupUnusedKeys()
{
PrivateKey::cleanupUnusedKeys();
$this->dispatch('success', 'Unused keys have been cleaned up.');
}
}

View File

@@ -29,25 +29,27 @@ class Show extends Component
try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
abort(404);
}
}
public function loadPublicKey()
{
$this->public_key = $this->private_key->publicKey();
$this->public_key = $this->private_key->getPublicKey();
if ($this->public_key === 'Error loading private key') {
$this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
}
}
public function delete()
{
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
$this->private_key->safeDelete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('security.private-key.index');
}
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -56,8 +58,9 @@ class Show extends Component
public function changePrivateKey()
{
try {
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
$this->private_key->updatePrivateKey([
'private_key' => formatPrivateKey($this->private_key->private_key),
]);
refresh_server_connection($this->private_key);
$this->dispatch('success', 'Private key updated.');
} catch (\Throwable $e) {

View File

@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token);
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
@@ -11,8 +13,13 @@ class Delete extends Component
public $server;
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {

View File

@@ -24,11 +24,16 @@ class Form extends Component
public $timezones;
protected $listeners = [
'serverInstalled',
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh',
];
}
protected $rules = [
'server.name' => 'required',
@@ -96,6 +101,12 @@ class Form extends Component
}
}
public function cloudflareTunnelConfigured()
{
$this->serverInstalled();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled()
{
$this->server->refresh();
@@ -238,4 +249,12 @@ class Form extends Component
$this->server->settings->save();
$this->dispatch('success', 'Server timezone updated.');
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
}

View File

@@ -39,6 +39,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyChanged');
}
public function selectProxy($proxy_type)
@@ -47,7 +48,7 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
if ($this->selectedProxy !== 'NONE') {
if ($this->server->proxySet()) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');

View File

@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -29,6 +31,7 @@ class Deploy extends Component
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
];
}
@@ -94,21 +97,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
if ($this->server->isSwarm()) {
instant_remote_process([
'docker service rm coolify-proxy_traefik',
], $this->server);
} else {
instant_remote_process([
'docker rm -f coolify-proxy',
], $this->server);
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$timeout = 30;
$process = $this->stopContainer($containerName, $timeout);
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
}
$this->server->proxy->status = 'exited';
$this->server->proxy->force_stop = $forceStop;
$this->server->save();
$this->dispatch('proxyStatusUpdated');
usleep(100000);
}
$this->removeContainer($containerName);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->server->proxy->force_stop = $forceStop;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->dispatch('proxyStatusUpdated');
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName)
{
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
}
private function removeContainer(string $containerName)
{
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
}
}

View File

@@ -11,7 +11,7 @@ class Show extends Component
public $parameters = [];
protected $listeners = ['proxyStatusUpdated'];
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
public function proxyStatusUpdated()
{

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
public $parameters;
public function setPrivateKey($newPrivateKeyId)
public function setPrivateKey($privateKeyId)
{
try {
$oldPrivateKeyId = $this->server->private_key_id;
refresh_server_connection($this->server->privateKey);
$this->server->update([
'private_key_id' => $newPrivateKeyId,
]);
$privateKey = PrivateKey::findOrFail($privateKeyId);
$this->server->update(['private_key_id' => $privateKey->id]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
$this->checkConnection();
} catch (\Throwable $e) {
$this->server->update([
'private_key_id' => $oldPrivateKeyId,
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
return handleError($e, $this);
$this->dispatch('success', 'Private key updated successfully.');
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
}
}
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
$this->dispatch('success', 'Server is reachable.');
} else {
ray($error);
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
return;
}

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
@@ -73,8 +75,13 @@ class AdminView extends Component
$team->delete();
}
public function delete($id)
public function delete($id, $password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}

View File

@@ -0,0 +1,76 @@
<?php
namespace App\Livewire\Terminal;
use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;
class Index extends Component
{
public $selected_uuid = 'default';
public $servers = [];
public $containers = [];
public function mount()
{
if (! auth()->user()->isAdmin()) {
abort(403);
}
$this->servers = Server::isReachable()->get();
$this->containers = $this->getAllActiveContainers();
}
private function getAllActiveContainers()
{
return collect($this->servers)->flatMap(function ($server) {
if (! $server->isFunctional()) {
return [];
}
return $server->loadAllContainers()->map(function ($container) use ($server) {
$state = data_get_str($container, 'State')->lower();
if ($state->contains('running')) {
return [
'name' => data_get($container, 'Names'),
'connection_name' => data_get($container, 'Names'),
'uuid' => data_get($container, 'Names'),
'status' => data_get_str($container, 'State')->lower(),
'server' => $server,
'server_uuid' => $server->uuid,
];
}
return null;
})->filter();
});
}
public function updatedSelectedUuid()
{
$this->connectToContainer();
}
#[On('connectToContainer')]
public function connectToContainer()
{
if ($this->selected_uuid === 'default') {
$this->dispatch('error', 'Please select a server or a container.');
return;
}
$container = collect($this->containers)->firstWhere('uuid', $this->selected_uuid);
$this->dispatch('send-terminal-command',
isset($container),
$container['connection_name'] ?? $this->selected_uuid,
$container['server_uuid'] ?? $this->selected_uuid
);
}
public function render()
{
return view('livewire.terminal.index');
}
}

View File

@@ -6,7 +6,9 @@ use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Str;
use OpenApi\Attributes as OA;
use RuntimeException;
@@ -149,12 +151,64 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
: getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
return $containers->pluck('Names')->toArray();
}
public function stopContainers(array $containerNames, $server, int $timeout = 600)
{
$processes = [];
foreach ($containerNames as $containerName) {
$processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach ($finishedProcesses as $containerName => $process) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
public function removeContainer(string $containerName, $server)
{
instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
}
public function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
public function delete_configurations()
{
$server = data_get($this, 'destination.server');
$workdir = $this->workdir();
if (str($workdir)->endsWith($this->uuid)) {
ray('Deleting workdir');
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
}
}
@@ -176,6 +230,13 @@ class Application extends BaseModel
}
}
public function delete_connected_networks($uuid)
{
$server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false);
}
public function additional_servers()
{
return $this->belongsToMany(Server::class, 'additional_destinations')
@@ -1034,6 +1095,7 @@ class Application extends BaseModel
throw new \Exception($e->getMessage());
}
$services = data_get($yaml, 'services');
$commands = collect([]);
$services = collect($services)->map(function ($service) use ($commands) {
$serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -1166,7 +1228,6 @@ class Application extends BaseModel
} else {
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
}
}
public function parseContainerLabels(?ApplicationPreview $preview = null)

View File

@@ -2,6 +2,9 @@
namespace App\Models;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Support\Facades\Storage;
use Illuminate\Validation\ValidationException;
use OpenApi\Attributes as OA;
use phpseclib3\Crypt\PublicKeyLoader;
@@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader;
)]
class PrivateKey extends BaseModel
{
use WithRateLimiting;
protected $fillable = [
'name',
'description',
'private_key',
'is_git_related',
'team_id',
'fingerprint',
];
protected $casts = [
'private_key' => 'encrypted',
];
protected static function booted()
{
static::saving(function ($key) {
$privateKey = data_get($key, 'private_key');
if (substr($privateKey, -1) !== "\n") {
$key->private_key = $privateKey."\n";
$key->private_key = formatPrivateKey($key->private_key);
if (! self::validatePrivateKey($key->private_key)) {
throw ValidationException::withMessages([
'private_key' => ['The private key is invalid.'],
]);
}
$key->fingerprint = self::generateFingerprint($key->private_key);
if (self::fingerprintExists($key->fingerprint, $key->id)) {
throw ValidationException::withMessages([
'private_key' => ['This private key already exists.'],
]);
}
});
static::deleted(function ($key) {
self::deleteFromStorage($key);
});
}
public function getPublicKey()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
}
public static function ownedByCurrentTeam(array $select = ['*'])
{
$selectArray = collect($select)->concat(['id']);
return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
}
public function publicKey()
public static function validatePrivateKey($privateKey)
{
try {
return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
return 'Error loading private key';
}
}
PublicKeyLoader::load($privateKey);
public function isEmpty()
{
if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
return true;
} catch (\Throwable $e) {
return false;
}
}
return false;
public static function createAndStore(array $data)
{
$privateKey = new self($data);
$privateKey->save();
$privateKey->storeInFileSystem();
return $privateKey;
}
public static function generateNewKeyPair($type = 'rsa')
{
try {
$instance = new self;
$instance->rateLimit(10);
$name = generate_random_name();
$description = 'Created by Coolify';
$keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
return [
'name' => $name,
'description' => $description,
'private_key' => $keyPair['private'],
'public_key' => $keyPair['public'],
];
} catch (\Throwable $e) {
throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
}
}
public static function extractPublicKeyFromPrivate($privateKey)
{
try {
$key = PublicKeyLoader::load($privateKey);
return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
return null;
}
}
public static function validateAndExtractPublicKey($privateKey)
{
$isValid = self::validatePrivateKey($privateKey);
$publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
return [
'isValid' => $isValid,
'publicKey' => $publicKey,
];
}
public function storeInFileSystem()
{
$filename = "ssh_key@{$this->uuid}";
Storage::disk('ssh-keys')->put($filename, $this->private_key);
return "/var/www/html/storage/app/ssh/keys/{$filename}";
}
public static function deleteFromStorage(self $privateKey)
{
$filename = "ssh_key@{$privateKey->uuid}";
Storage::disk('ssh-keys')->delete($filename);
}
public function getKeyLocation()
{
return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
}
public function updatePrivateKey(array $data)
{
$this->update($data);
$this->storeInFileSystem();
return $this;
}
public function servers()
@@ -85,4 +184,53 @@ class PrivateKey extends BaseModel
{
return $this->hasMany(GitlabApp::class);
}
public function isInUse()
{
return $this->servers()->exists()
|| $this->applications()->exists()
|| $this->githubApps()->exists()
|| $this->gitlabApps()->exists();
}
public function safeDelete()
{
if (! $this->isInUse()) {
$this->delete();
return true;
}
return false;
}
public static function generateFingerprint($privateKey)
{
try {
$key = PublicKeyLoader::load($privateKey);
$publicKey = $key->getPublicKey();
return $publicKey->getFingerprint('sha256');
} catch (\Throwable $e) {
return null;
}
}
private static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::where('fingerprint', $fingerprint);
if (! is_null($excludeId)) {
$query->where('id', '!=', $excludeId);
}
return $query->exists();
}
public static function cleanupUnusedKeys()
{
self::ownedByCurrentTeam()->each(function ($privateKey) {
$privateKey->safeDelete();
});
}
}

View File

@@ -35,14 +35,17 @@ class ScheduledDatabaseBackup extends BaseModel
{
return $this->hasMany(ScheduledDatabaseBackupExecution::class)->where('created_at', '>=', now()->subDays($days))->get();
}
public function server()
{
if ($this->database) {
if ($this->database->destination && $this->database->destination->server) {
$server = $this->database->destination->server;
return $server;
}
}
return null;
}
}

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