Compare commits

...

445 Commits

Author SHA1 Message Date
Andras Bacsai
671b22e964 Merge pull request #2941 from coollabsio/next
v4.0.0-beta.320
2024-08-08 11:27:29 +02:00
Andras Bacsai
777bff6553 refactor: Update Traefik labels on init and cleanup unnecessary dynamic proxy configuration 2024-08-08 11:15:33 +02:00
Andras Bacsai
d3b3c2f7fd fix: Add missing middleware for server check job 2024-08-08 10:28:34 +02:00
Andras Bacsai
0b000919cf feat: able to generate only the required labels for resources 2024-08-07 18:27:50 +02:00
Andras Bacsai
92f90d4e52 Merge pull request #2863 from lorenzomigliorero/feat/labels-by-proxy-type
Generate labels by proxy type
2024-08-07 18:06:51 +02:00
Andras Bacsai
59702c6dbc refactor: Update ProxyTypes enum values to use TRAEFIK instead of TRAEFIK_V2 2024-08-07 17:52:51 +02:00
Andras Bacsai
13e8d3c17c fix: update Traefik labels on init 2024-08-07 17:52:43 +02:00
Andras Bacsai
5d384b1149 feat: Support map fields in Directus 2024-08-07 17:43:41 +02:00
Andras Bacsai
7b31955409 Merge pull request #2906 from Loque-/Loque--patch-1
Support map fields in Directus
2024-08-07 17:43:03 +02:00
Andras Bacsai
efb4049966 feat: Improve homepage service template 2024-08-07 12:58:36 +02:00
Andras Bacsai
6da359bc60 fix: empty string content should be saved as a file 2024-08-07 12:58:32 +02:00
Andras Bacsai
f5f3c77d9e Merge pull request #3010 from Telokis/improvate-homepage-template
feat: Improve homepage service template
2024-08-07 12:17:46 +02:00
Andras Bacsai
dc5c324f9c feat: Update Upgrade component to check for latest version of Coolify 2024-08-07 12:16:06 +02:00
Andras Bacsai
d283be4917 disable php code styling for now 2024-08-07 12:12:09 +02:00
Andras Bacsai
f0278bc33d fix: show latest version on manual update view 2024-08-07 12:10:31 +02:00
Andras Bacsai
052565f4e8 Merge pull request #3011 from stooit/feat/enhance-services-api
Return applications & databases in service API.
2024-08-07 12:02:18 +02:00
Andras Bacsai
64146a46fc chore: Update UpdateCoolifyJob to dispatch CheckForUpdatesJob synchronously 2024-08-07 11:58:53 +02:00
Andras Bacsai
5a82395bb7 feat: Update auto update and update check frequencies in settings 2024-08-07 11:55:16 +02:00
Andras Bacsai
d3085e1ade feat: Add manual update check functionality to settings page 2024-08-07 11:42:55 +02:00
Andras Bacsai
af41ed26ba chore: Update UpdateCoolifyJob timeout to 10 minutes 2024-08-07 11:10:02 +02:00
Andras Bacsai
961ba49d89 fix: Handle null and empty disk usage in DockerCleanupJob 2024-08-07 10:54:22 +02:00
Andras Bacsai
a2150f2f7d refactor: Remove unused code and optimize CheckForUpdatesJob 2024-08-07 10:50:12 +02:00
Andras Bacsai
46b549ab8d chore: Update workflows to include PR build and merge manifest steps 2024-08-07 10:02:01 +02:00
Andras Bacsai
e4c3d61b76 fixes 2024-08-07 09:50:29 +02:00
Andras Bacsai
869f0878c2 fix (cloud): cloud instance should get latest servics on update (all servers) 2024-08-07 09:50:12 +02:00
Stuart Rowlands
dfb6d4da3d Return applications & databases in service API. 2024-08-07 17:20:55 +10:00
Telokis
5d4a379e8d Improve homepage service template 2024-08-07 01:06:54 +02:00
Andras Bacsai
73a265107a Merge branch 'next' of github.com:coollabsio/coolify into next 2024-08-06 14:38:41 +02:00
Andras Bacsai
b77171d2f2 fix: settings view
feat: add separate views for settings
2024-08-06 14:36:56 +02:00
andrasbacsai
8b817dad87 Fix styling 2024-08-06 12:04:23 +00:00
Andras Bacsai
c4436aadfa Merge pull request #3000 from ayntk-ai/update-settings-improvements
Feat: Updated Check Frequency and Auto Update Frequency settings
2024-08-06 14:03:41 +02:00
Andras Bacsai
c9a7af0ffa chore: Update navbar and configuration to improve service status check functionality 2024-08-06 14:02:24 +02:00
Andras Bacsai
c648e0dff9 chore: Update navbar to include service status check 2024-08-06 13:50:32 +02:00
Andras Bacsai
e897eb2999 fix: Stop resources gracefully 2024-08-06 13:27:06 +02:00
Andras Bacsai
35e62a3003 chore: Update server form to use password input for IP Address/Domain field 2024-08-06 13:18:57 +02:00
Andras Bacsai
b66c511160 refactor: Update timeout for graceful_shutdown_container in ApplicationDeploymentJob 2024-08-06 13:07:32 +02:00
Andras Bacsai
a866bf437d Merge branch 'next' of github.com:coollabsio/coolify into next 2024-08-06 13:07:03 +02:00
Andras Bacsai
a4f107e191 fix: graceful shutdown 2024-08-06 13:05:34 +02:00
andrasbacsai
f22d7741a3 Fix styling 2024-08-06 10:13:34 +00:00
Andras Bacsai
f4d64e121c Merge pull request #2969 from alexzvn/feat/graceful-shutdown
feat: add graceful shutdown
2024-08-06 12:13:00 +02:00
Andras Bacsai
818d5e1159 fix: minecraft server template fixed 2024-08-06 12:10:29 +02:00
ayntk-ai
7eacdf23f9 fix database migration 2024-08-06 12:00:34 +02:00
ayntk-ai
0459baa55e Update Kernel.php 2024-08-06 11:48:43 +02:00
ayntk-ai
a426c00a03 should work :) 2024-08-06 11:47:48 +02:00
ayntk-ai
fd36e143e0 final conflict hopfully :) 2024-08-06 11:46:26 +02:00
ayntk-ai
2243a3304b remove duplicates 2024-08-06 11:43:55 +02:00
ayntk-ai
bb23141138 add backup line twice 2024-08-06 11:42:01 +02:00
ayntk-ai
5f1e1c0ac4 fix another conflict 2024-08-06 11:41:24 +02:00
Andras Bacsai
ef8be5f133 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-08-06 11:40:16 +02:00
Andras Bacsai
f205c0ab9e Merge pull request #2981 from kunumigab/main
Added minecraft-server (java) as template
2024-08-06 11:40:30 +02:00
ayntk-ai
c4cf116e6e more fixes 2024-08-06 11:38:30 +02:00
ayntk-ai
f7b1aaca92 fix fix 2024-08-06 11:35:22 +02:00
Andras Bacsai
0ce41d2c1c chore: Update registration view to display a notice for first user that it will be an admin 2024-08-06 11:34:51 +02:00
ayntk-ai
d9edb1c72f fix 2024-08-06 11:32:37 +02:00
ayntk-ai
93322dc3cf updated helper text 2024-08-06 11:29:02 +02:00
Andras Bacsai
22f04e4708 refactor: Remove unnecessary debug statement in ApplicationDeploymentJob 2024-08-06 11:27:10 +02:00
ayntk-ai
d9a079c289 fix conflict in kernel.php 2024-08-06 11:25:57 +02:00
Andras Bacsai
f4690bf15a Merge pull request #2958 from filiabel/formbricks-template-update
chore: update Formbricks template with additional env variables
2024-08-06 11:25:46 +02:00
Andras Bacsai
a81faa68b8 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-08-06 11:16:55 +02:00
Andras Bacsai
4b388b463d Merge pull request #2960 from J-o-n-a-t-h-a-n-M-u-e-l-l-e-r/fix-plausible-totp-key
Fixes Plausible TOTP key by changing it from BASE64 to REALBASE64_32
2024-08-06 11:16:58 +02:00
Andras Bacsai
3c98b558f6 fix: do not use port exposes on docker compose buildpacks 2024-08-06 11:16:49 +02:00
Andras Bacsai
e3c7c615c6 refactor: Cleanup unnecessary dynamic proxy configuration in Init command 2024-08-06 10:53:13 +02:00
Andras Bacsai
74e8a4a703 refactor: Simplify log drain installation and stop log drain if necessary 2024-08-06 10:52:47 +02:00
Andras Bacsai
c5de1a25c3 refactor: Remove unnecessary debug statement in ServerCheckJob 2024-08-06 09:45:48 +02:00
ayntk-ai
d4cb7e25dc renamed database migration file 2024-08-05 21:04:47 +02:00
ayntk-ai
50ede5cab9 remove comments and remove duplicated scheduling 2024-08-05 20:57:27 +02:00
ayntk-ai
b64d4881cb made helper more clear 2024-08-05 20:33:20 +02:00
ayntk-ai
4dfec6771c added defaults, remove duplicated cron validation 2024-08-05 20:31:06 +02:00
Andras Bacsai
92ebc3f0c6 refactor 2024-08-05 20:08:37 +02:00
Andras Bacsai
3e0821e471 refactor 2024-08-05 20:07:08 +02:00
ayntk-ai
38976dac12 fixes and check for valid cron expressions 2024-08-05 20:05:38 +02:00
Andras Bacsai
ea5101c814 fix: file storages (dir/file mount) handled properly 2024-08-05 20:00:57 +02:00
Andras Bacsai
8133a8b770 fix: dir mounts should have proper dirs 2024-08-05 18:38:13 +02:00
Andras Bacsai
3269ca3eb8 chore: Add comment explaining the purpose of disconnecting the network in cleanup_unused_network_from_coolify_proxy() 2024-08-05 18:27:54 +02:00
Andras Bacsai
012d660886 fix: remove network if it is only connected to coolify proxy itself 2024-08-05 18:16:29 +02:00
Andras Bacsai
ddb4b4d215 refactor: Simplify log drain installation in ServerCheckJob 2024-08-05 16:45:30 +02:00
Andras Bacsai
2725a93bfd fix: only run logdrain if necessary 2024-08-05 16:44:50 +02:00
ayntk-ai
27e82f0bde implement logic, jobs and add DB migrate 2024-08-05 16:31:41 +02:00
Andras Bacsai
5755965b4f chore: add pull_request image builds to GH actions 2024-08-05 16:09:49 +02:00
Andras Bacsai
e4b92bb660 feat: new server checking job
feat: show if the server  has problems on ui
2024-08-05 15:48:15 +02:00
Andras Bacsai
bc48b42ff1 refactor: Remove unused code for checking server status in Heading.php 2024-08-05 15:02:33 +02:00
Andras Bacsai
94b2d67a6e refactor: Update livewire polling interval in heading.blade.php 2024-08-05 15:02:23 +02:00
ayntk-ai
7d6a895449 Updated Configuration Settings 2024-08-05 14:44:20 +02:00
Andras Bacsai
39e3ea9f07 fix: async remote command 2024-08-05 13:51:34 +02:00
Andras Bacsai
465b254813 fix: if usagebefore cannot be determined, cleanup docker with force 2024-08-05 13:45:53 +02:00
Andras Bacsai
e2bc3f4841 refactor: Remove commented out code for cleaning up networks in CleanupDocker.php 2024-08-05 13:45:33 +02:00
Andras Bacsai
b38a651a08 feat: coolify init should cleanup stuck networks in proxy 2024-08-05 13:45:24 +02:00
Andras Bacsai
76c39a987c fix: service_url should not have a trailing slash 2024-08-05 13:42:51 +02:00
Andras Bacsai
1d8c496906 feat: delete team in cloud without subscription 2024-08-05 12:03:36 +02:00
Andras Bacsai
820693ac22 fix: plausible template 2024-08-05 11:50:52 +02:00
Andras Bacsai
d7b45a6dd2 fix: remove lazy load from scheduled tasks 2024-08-05 11:30:48 +02:00
Gabriel Peralta
cb2fc68dde Update minecraft.yaml 2024-08-03 21:13:42 -04:00
Andras Bacsai
e6e48c5812 fix: only append docker network if service/app is running 2024-08-01 13:47:58 +02:00
Andras Bacsai
fd855847ff refactor: Adjust keep_days in CleanupDatabase.php based on environment 2024-08-01 09:07:54 +02:00
Andras Bacsai
35dbced3c5 refactor: Update CleanupDatabase.php to adjust keep_days based on environment 2024-08-01 08:52:58 +02:00
Andras Bacsai
e8b2d8bf03 chore: Update version to 4.0.0-beta.320 2024-08-01 08:52:13 +02:00
Gabriel Peralta
9887d5eedd Added minecraft-server (java) as template 2024-07-31 00:50:13 -04:00
Alexzvn
1feb8488a3 cleanup 2024-07-29 08:09:27 +00:00
Alexzvn
342ef4d367 add graceful shutdown 2024-07-29 07:57:13 +00:00
Andras Bacsai
85d080a042 update servicetemplates 2024-07-26 20:32:29 +02:00
Andras Bacsai
69c48a511e Merge branch 'main' into next 2024-07-26 20:31:48 +02:00
Andras Bacsai
10b9c4bcfa Merge pull request #2961 from coollabsio/fixservicesenvparse
v4.0.0-beta.319
2024-07-26 20:28:43 +02:00
Andras Bacsai
38d914076e fix: update env on ui 2024-07-26 20:17:40 +02:00
Andras Bacsai
102dd6bfb1 fix: activity type invalid 2024-07-26 20:07:39 +02:00
Andras Bacsai
04379b76f2 chore: collect/create/update volumes in parseDockerComposeFile function 2024-07-26 20:04:41 +02:00
Andras Bacsai
d6fb54f3c3 fix: service env variables 2024-07-26 20:01:23 +02:00
Andras Bacsai
1d419c6ab8 fix: service env parsing 2024-07-26 20:00:37 +02:00
Andras Bacsai
8a4e958663 fix: parse docker composer 2024-07-26 19:58:52 +02:00
Andras Bacsai
b04f7686fd update servicetemplates 2024-07-26 19:54:29 +02:00
Andras Bacsai
5aabdefaa7 quickfix: service env parsing 2024-07-26 19:53:40 +02:00
Jonathan Müller
a2e439686d Fixes Plausible TOTP key by changing it from BASE64 to REALBASE64_32 2024-07-26 16:30:36 +00:00
Filip Gornitzka Abelson
219b21767b chore: update formbricks template 2024-07-26 17:27:20 +02:00
Andras Bacsai
281c6e39a5 fix: members of root team should not see instance admin stuff 2024-07-26 14:54:24 +02:00
Andras Bacsai
95548b9d9b enhance: Add required CRON_SECRET to Formbricks template 2024-07-26 14:05:48 +02:00
Andras Bacsai
2ed1529b95 Merge pull request #2954 from LaurenceJJones/formbricks_env_var_required
enhance: Formbricks template add required CRON_SECRET
2024-07-26 14:05:34 +02:00
Laurence Jones
85f037702a Update formbricks.yaml 2024-07-26 13:03:28 +01:00
Laurence Jones
2caa91d772 enhance: Formbricks template add required CRON_SECRET
formbricks/formbricks#2874

A new required env var has been added, using special magic to generate a random password as the value to abstract away from user.
2024-07-26 12:25:12 +01:00
Andras Bacsai
d7350fad76 fix: several shared environment variables in one value, parsed correctly 2024-07-26 13:22:44 +02:00
Andras Bacsai
dcfb716711 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-07-26 11:58:43 +02:00
Andras Bacsai
0e0c81c32b fix: update SERVICE_FQDN/URL env variables when you change the domain 2024-07-26 11:58:40 +02:00
Andras Bacsai
802cab4a70 fix: domain update on services on the UI 2024-07-26 11:58:14 +02:00
Andras Bacsai
2c0b40ac8c fix: by default volumes that we cannot determine if they are directories or files are treated as directories 2024-07-26 11:57:49 +02:00
Andras Bacsai
001e8493ae Merge pull request #2946 from vitalybaev/fixes-link-to-feature-requests
Fixes link to feature requests in GitHub's issue template
2024-07-26 10:01:30 +02:00
Vitaly Baev
e9256152e8 Fixes link to feature requests in GitHub's issue template 2024-07-26 00:06:12 +02:00
Andras Bacsai
25410cb31a workaround for null bytes 2024-07-25 21:51:53 +02:00
Andras Bacsai
3dedf5548f feat: new server check job 2024-07-25 20:30:22 +02:00
Andras Bacsai
2d2bb23708 Merge pull request #2879 from lorenzomigliorero/fix/bitbucket-repository-url
fix: bitbucket repos url
2024-07-25 14:23:58 +02:00
Andras Bacsai
6afc0b6303 fix: gitlab logo 2024-07-25 14:17:26 +02:00
Andras Bacsai
f558f01ffd fix: gitlab service 2024-07-25 14:14:00 +02:00
Andras Bacsai
47fa955e71 Merge pull request #2638 from GunniBusch/main
[TEMPLATE] Add GitLab template
2024-07-25 13:49:40 +02:00
andrasbacsai
96c4f5b8da Fix styling 2024-07-25 11:31:59 +00:00
Andras Bacsai
6a4aa492c0 fix: random generated uuid will be full length (not 7 characters) 2024-07-25 13:31:01 +02:00
Andras Bacsai
debd2a3433 fix: link in task failed job notifications 2024-07-25 13:30:38 +02:00
Andras Bacsai
192bc0f13b fix: handle array env vars 2024-07-25 12:38:31 +02:00
Andras Bacsai
8756141f1d fix: remove pull always when uploading backup to s3 2024-07-25 12:11:47 +02:00
Andras Bacsai
b97abc600e chore: remove commented out code for docker image pruning 2024-07-25 12:00:14 +02:00
Andras Bacsai
4b29636b42 fix: api -> application patch endpoint 2024-07-25 11:53:57 +02:00
Andras Bacsai
321c51f8ed feat: check custom internal name through server's applications. 2024-07-25 11:30:31 +02:00
Andras Bacsai
69d8f706cf fix: raw compose deployment .env not found 2024-07-25 11:07:32 +02:00
Andras Bacsai
98f67c5c6c feat: update Caddy button label in proxy.blade.php 2024-07-25 11:07:18 +02:00
Andras Bacsai
1cf8d0b886 chore: update version numbers to 4.0.0-beta.319 2024-07-24 21:12:22 +02:00
andrasbacsai
613830e6a6 Fix styling 2024-07-24 19:11:12 +00:00
Andras Bacsai
2ea146333e fix: uuid in api docs type 2024-07-24 21:10:32 +02:00
andrasbacsai
f76d45b826 Fix styling 2024-07-24 12:27:21 +00:00
Andras Bacsai
6cc86a3c82 Merge pull request #2881 from coollabsio/next
v4.0.0-beta.318
2024-07-24 14:26:39 +02:00
Andras Bacsai
f1e5b61970 feat: update API endpoint summaries 2024-07-23 14:36:44 +02:00
Andras Bacsai
65380646f7 feat: add branddev logo to README.md 2024-07-23 14:23:58 +02:00
Andras Bacsai
189a8347ed feat: add server api endpoints 2024-07-23 14:20:53 +02:00
Andras Bacsai
e96e8f6fec feat: add patch request to projects 2024-07-23 11:48:38 +02:00
Andras Bacsai
38299ab507 feat: create/delete project endpoints 2024-07-23 11:36:05 +02:00
Andras Bacsai
f134171855 fix: restart proxy does not work + status indicator on the UI 2024-07-23 11:11:54 +02:00
Andras Bacsai
320204d854 fix: directory will be created by default for compose host mounts 2024-07-22 15:10:07 +02:00
Andras Bacsai
b68199a482 fix: Fix issue with deployment start command in ApplicationDeploymentJob 2024-07-22 15:09:50 +02:00
Andras Bacsai
6f4436fd5e fix: plane service images 2024-07-22 14:32:58 +02:00
Andras Bacsai
0d8cc19698 fix: deleting application should delete preview deployments 2024-07-22 14:13:56 +02:00
Andras Bacsai
a3a1ff69e1 fix: preview deployments should be stopped properly via gh webhook 2024-07-22 13:06:03 +02:00
Andras Bacsai
5df7e23aa4 chore: Update resource-limits.blade.php with improved input field helpers 2024-07-22 11:33:25 +02:00
Andras Bacsai
35d9691b3f chore: Update APP_BASE_URL to use SERVICE_FQDN_PLANE 2024-07-22 11:16:50 +02:00
Andras Bacsai
465f649641 Merge pull request #2903 from MrAlexand0r/bugfix/2860_plane_images
#2860 - [PLANE] Fixed image uploads not working
2024-07-22 10:30:49 +02:00
Andras Bacsai
d909e7d802 update service template 2024-07-22 10:02:28 +02:00
Andras Bacsai
06db6b8502 Merge branch 'main' into next 2024-07-22 10:02:03 +02:00
Andras Bacsai
12261b9082 chore: Remove commented out code for sending internal notification 2024-07-22 09:50:49 +02:00
Andras Bacsai
583ec432e8 Merge branch 'next' into bugfix/2860_plane_images 2024-07-22 09:43:16 +02:00
Andras Bacsai
8ffbccf7db fix: create file storage even if content is empty 2024-07-22 09:18:15 +02:00
Toby Brancher
097ca209bc Support map fields in Directus
To support the map fields in Directus with Postgres you need the Postgis extension installed, with docker we can just change the image to use the equivalent alpine postgis image, source latest stable alpine from here (perhaps there is a better ref to use than one with a specific version number?): https://registry.hub.docker.com/r/postgis/postgis/

If you are already running Directus without postgis, and would like to make use of the mapping fields after updating the docker compose file you will need to go to `execute command`, select the postgres container and run the following command filling in the appropriate env vars;

`psql -U ${SERVICE_USER_POSTGRESQL} -d ${POSTGRESQL_DATABASE} -c "CREATE EXTENSION postgis;"`
2024-07-21 23:39:22 +01:00
Alexander G.
439fe43a04 #2860 - plane service: fixed image uploads not working because credentials were unset 2024-07-21 15:54:42 +02:00
Andras Bacsai
7fd9a799b5 Update BUG_REPORT.yml 2024-07-20 14:17:51 +02:00
Andras Bacsai
7459ab22d1 remove file 2024-07-20 13:17:09 +02:00
Andras Bacsai
133a68f3eb update suapbase 2024-07-20 12:31:05 +02:00
Andras Bacsai
3224110583 fix: supabase 2024-07-20 12:30:32 +02:00
Andras Bacsai
810488b115 fix: volume detection (dir or file) is fixed 2024-07-19 17:06:30 +02:00
Andras Bacsai
b2276147ad chore: Disable health check by default 2024-07-19 15:40:44 +02:00
Andras Bacsai
6c1293c63e chore: Update helper message with link to documentation 2024-07-19 15:40:36 +02:00
Andras Bacsai
526d675272 refactor: Disable health check for Rust applications during deployment 2024-07-19 15:40:33 +02:00
Andras Bacsai
14b2442d40 chore: Update version to 4.0.0-beta.318 2024-07-19 15:04:18 +02:00
Andras Bacsai
d6d194d414 Merge pull request #2880 from coollabsio/next
v4.0.0-beta.317
2024-07-19 14:57:29 +02:00
Andras Bacsai
0e99f97855 oops 2024-07-19 14:56:18 +02:00
Andras Bacsai
14dc933219 fix: missing input for api endpoint 2024-07-19 14:40:01 +02:00
Andras Bacsai
9497f123b4 revert: advanced dropdown 2024-07-19 14:38:47 +02:00
Andras Bacsai
6feb439d0a chore: Update version to 4.0.0-beta.317 2024-07-19 14:34:21 +02:00
Andras Bacsai
e4ca5ee5f5 chore: Update Traefik image version to v2.11 2024-07-19 14:34:19 +02:00
Andras Bacsai
f21c12f39b Merge pull request #2856 from coollabsio/next
v4.0.0-beta.316
2024-07-19 13:45:51 +02:00
Andras Bacsai
6c1e50a914 fix: backup downloads 2024-07-19 13:45:04 +02:00
Lorenzo Migliorero
2dbba366b7 fix: bitbucket repository url 2024-07-19 13:41:01 +02:00
Andras Bacsai
da064def7a update service-templates 2024-07-19 10:06:26 +02:00
Andras Bacsai
3af3fa5773 refactor: Update DockerCleanupJob to use server settings for force cleanup 2024-07-19 09:59:09 +02:00
Andras Bacsai
005bd55fb2 refactor: Update DockerCleanupJob to use server settings for force cleanup 2024-07-18 15:12:52 +02:00
Andras Bacsai
82a5b4c55d refactor: server status job and docker cleanup job 2024-07-18 14:43:21 +02:00
Andras Bacsai
b8e95b2099 feat: force cleanup server 2024-07-18 14:38:56 +02:00
Andras Bacsai
8ea50dc029 refactor: Update DockerCleanupJob to handle nullable usageBefore property 2024-07-18 14:28:33 +02:00
Andras Bacsai
ec191af874 chore: Handle JSON parsing errors in format_docker_command_output_to_json 2024-07-18 14:23:15 +02:00
Andras Bacsai
d98c742aff chore: update general page of apps 2024-07-18 14:20:22 +02:00
Andras Bacsai
2529496594 feat: preserve git repository 2024-07-18 13:14:07 +02:00
Andras Bacsai
1b6114036a chore: Update checkbox labels in general.blade.php 2024-07-18 12:40:17 +02:00
Andras Bacsai
b33fb6c39a chore: Update width of container in general.blade.php 2024-07-18 12:39:49 +02:00
Andras Bacsai
0a6826af58 remove ray 2024-07-18 12:32:33 +02:00
Andras Bacsai
1c7034ff78 fix: if git limit reached, ignore it and continue with a default selection 2024-07-18 12:30:45 +02:00
Andras Bacsai
7e11698c55 chore: Update repository form with simplified URL input field 2024-07-18 12:13:23 +02:00
Andras Bacsai
1c4eb31d59 fix: handle custom_internal_name check in ApplicationDeploymentJob.php 2024-07-18 12:10:59 +02:00
Andras Bacsai
b4b6a4294a chore: Update bug report template
Update the bug report template to include a checkbox for indicating whether the user is using the cloud version of Coolify.
2024-07-18 12:07:44 +02:00
Andras Bacsai
4c031a7c05 fix: handle / in preselecting branches 2024-07-18 12:03:48 +02:00
Lorenzo Migliorero
1b4a8aa58f Merge branch 'feat/labels-by-proxy-type' of github.com:lorenzomigliorero/coolify into feat/labels-by-proxy-type 2024-07-18 11:42:52 +02:00
Lorenzo Migliorero
be6d74a6a3 style: linting 2024-07-18 11:42:41 +02:00
Andras Bacsai
997a262b6c Merge pull request #2840 from Pjort/next
Update supabase.yaml
2024-07-18 10:38:36 +02:00
Lorenzo Migliorero
5aae65f62f Merge branch 'next' into feat/labels-by-proxy-type 2024-07-17 21:21:09 +02:00
Lorenzo Migliorero
3be06ced92 feat: labels by proxy type 2024-07-17 21:06:56 +02:00
Andras Bacsai
c0e88df3e8 feat: add readonly labels 2024-07-17 14:52:40 +02:00
Andras Bacsai
85e1cbad53 chore: Update version to 4.0.0-beta.316 2024-07-17 09:17:02 +02:00
Andras Bacsai
c37398af72 Merge pull request #2853 from coollabsio/next
v4.0.0-beta.315
2024-07-17 08:45:33 +02:00
Andras Bacsai
19cfe4e514 fix: new docker compose parsing 2024-07-17 08:09:33 +02:00
Andras Bacsai
23a1b1925f fix: tag deployments 2024-07-17 07:59:12 +02:00
Andras Bacsai
1fb8d1e14c revert: pull policy 2024-07-17 07:59:06 +02:00
Andras Bacsai
804c70b575 chore: Update version to 4.0.0-beta.315 2024-07-17 07:58:45 +02:00
Pjort
548c4a4c64 Update supabase.yaml
Fixes problem related to emails sent for invite and forgotten password, that then doesn't actually use the external URL instead uses the hardcoded: http://supabase-kong:8000
2024-07-15 17:47:35 +02:00
Andras Bacsai
2978042162 Merge pull request #2835 from coollabsio/next
v4.0.0-beta.314
2024-07-15 16:42:04 +02:00
Andras Bacsai
4225ec7060 feat: Improve error handling in loadComposeFile method 2024-07-15 16:39:40 +02:00
Andras Bacsai
893339fc8e refactor: Update Docker Compose build command to include --pull flag 2024-07-15 16:39:28 +02:00
Andras Bacsai
356e7b57d2 improvement: add basedir + compose file in new compose based apps 2024-07-15 16:39:22 +02:00
Andras Bacsai
4ee1f1a507 fix: improve github source creation 2024-07-15 15:33:46 +02:00
Andras Bacsai
7d64df60cd fix: drupal 2024-07-15 13:59:33 +02:00
Andras Bacsai
eb3a4ca157 Merge pull request #2463 from emircanerkul/main
Add drupal-with-postgresql service template
2024-07-15 13:51:43 +02:00
Andras Bacsai
a7b5157fa6 fix: docmost template 2024-07-15 12:58:29 +02:00
Andras Bacsai
793e6d19eb Merge pull request #2747 from alfinauzikri/main
[TEMPLATE] Add Docmost Template
2024-07-15 12:54:36 +02:00
Andras Bacsai
674fa4d09c fix: vikunja 2024-07-15 12:51:04 +02:00
Andras Bacsai
0089e86dd1 refactor: Remove unused code and fix storage form layout 2024-07-15 12:23:06 +02:00
Andras Bacsai
e1d802b507 Merge pull request #2817 from luckydonald/patch-2
[TEMPLATE] fix vikunja, add postgres variant.
2024-07-15 12:18:21 +02:00
Andras Bacsai
9927b71af9 fix: plane service template 2024-07-15 12:13:34 +02:00
Andras Bacsai
b1c0f105ab fix: update docker compose pull command with --policy always 2024-07-15 12:13:21 +02:00
Andras Bacsai
35cae1d4dc Merge pull request #2831 from MrAlexand0r/main
[Feature] #2354 - Add Plane Service
2024-07-15 11:48:53 +02:00
Andras Bacsai
3dab3365e2 fix service-templates 2024-07-15 11:40:12 +02:00
Andras Bacsai
1bdc7c87ba Delete templates/service-templates.json 2024-07-15 11:34:40 +02:00
Andras Bacsai
cab8ad0ca0 Merge pull request #2826 from truemiller/patch-1
Fix typo in "Is Literal?" checkbox in Environment Variables
2024-07-15 11:32:08 +02:00
Andras Bacsai
43409f3ff0 fix: add validation for missing docker compose file 2024-07-15 11:31:18 +02:00
Andras Bacsai
a5dd4cab52 fix: update minio hc in services 2024-07-15 11:31:13 +02:00
Andras Bacsai
a815240f4e Merge pull request #2827 from Megumiso/fix-placement-constraints
fix placement constraints were ignored
2024-07-15 11:18:27 +02:00
Andras Bacsai
2a44e7c5bd Merge pull request #2829 from mateusfmello/fix-minio-healthcheck
fix(MinIO): error in healthcheck command
2024-07-15 11:14:55 +02:00
Andras Bacsai
28c7e439b1 fix: service domains and envs are properly updated 2024-07-15 10:55:04 +02:00
Andras Bacsai
4396c786b4 refactor: Update version numbers to 4.0.0-beta.314 2024-07-15 10:54:50 +02:00
Alexander Gratzl
b67bb8595f removed health checks none 2024-07-14 00:36:14 +02:00
Mateus Fernandes
bec47487dd fix(MinIO): new command healthcheck
MinIO container were not available, as they do not contain the CURL or WGET commands, but MinIO has its own verification command:
https://github.com/minio/minio/issues/18389
2024-07-13 12:33:54 -03:00
Mateus Fernandes
b110d0c12b fix(reactive-resume): new healthcheck command for MinIO
MinIO container were not available, as they do not contain the CURL or WGET commands, but MinIO has its own verification command:
https://github.com/minio/minio/issues/18389
2024-07-13 12:33:12 -03:00
Alexander G
ae425475b4 #2354 added healthchecks for the services 2024-07-13 10:41:04 +02:00
Megumiso
dc6aee44b3 changed variable name for better readability 2024-07-13 13:26:51 +09:00
Megumiso
4ffea311e8 placement constraints is now working 2024-07-13 13:15:17 +09:00
Alexander
77a6a6e46a Merge branch 'main' of https://github.com/coollabsio/coolify
# Conflicts:
#	templates/service-templates.json
2024-07-13 01:08:17 +02:00
Alexander
2278ba31e7 #2354 WIP plane feature 2024-07-13 00:38:41 +02:00
Josh Miller
aaeec3d340 fix: env is_literal helper text typo 2024-07-12 19:00:20 +01:00
Josh Miller
2cbe530b7e fix: typo in is_literal helper 2024-07-12 18:59:06 +01:00
Andras Bacsai
6ada6d145c Merge pull request #2821 from coollabsio/next
v4.0.0-beta.313
2024-07-12 15:46:08 +02:00
Andras Bacsai
0f55e83591 revert: instancesettings 2024-07-12 15:45:36 +02:00
Andras Bacsai
4017ea7b65 Merge pull request #2819 from coollabsio/next
v4.0.0-beta.312
2024-07-12 15:06:15 +02:00
Andras Bacsai
a85066c644 fix: disable sentinel until a few bugs are fixed 2024-07-12 15:05:12 +02:00
Andras Bacsai
b08d38f339 refactor: Update version numbers to 4.0.0-beta.312 2024-07-12 14:54:54 +02:00
Andras Bacsai
d4f4632461 Merge pull request #2812 from coollabsio/next
v4.0.0-beta.311
2024-07-12 14:15:15 +02:00
Andras Bacsai
666aa041f4 refactor: Update metrics.blade.php to improve alert message clarity 2024-07-12 14:12:44 +02:00
Andras Bacsai
1c565fd502 refactor: Add lazy loading to tags in Livewire configuration view 2024-07-12 14:00:39 +02:00
Luckydonald
7de2b8cbd7 Create vikunja-with-postgres.yaml
to have a db variant.
2024-07-12 13:58:14 +02:00
Luckydonald
852e906736 Update vikunja.yaml, follow recommended docker-compose. 2024-07-12 13:55:37 +02:00
Andras Bacsai
5778466947 refactor: Update Webhooks.php to use nullable type for webhook URLs 2024-07-12 13:54:12 +02:00
Andras Bacsai
7006239b0d refactor: Update Livewire configuration views 2024-07-12 13:40:11 +02:00
Andras Bacsai
49d011574d refactor: Remove unnecessary code in AppServiceProvider.php 2024-07-12 13:34:48 +02:00
Andras Bacsai
046a358ae0 refactor: Update Dockerfile to set CI environment variable to true 2024-07-12 13:02:37 +02:00
Andras Bacsai
d23f5af957 hmmm 2024-07-12 12:59:53 +02:00
Andras Bacsai
20a3f4b200 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-07-12 12:53:09 +02:00
Andras Bacsai
73acda833e feat: Enable legacy model binding in Livewire configuration 2024-07-12 12:53:07 +02:00
andrasbacsai
fa895db76e Fix styling 2024-07-12 10:53:07 +00:00
Andras Bacsai
88f33be5b6 refactor: only get instanceSettings once from db 2024-07-12 12:51:55 +02:00
Andras Bacsai
21612cccf7 refactor: tags view 2024-07-12 12:51:13 +02:00
Andras Bacsai
39a7332343 refactored: webhooks view 2024-07-12 11:52:32 +02:00
Andras Bacsai
21825876fb fix: service status changed event 2024-07-12 11:27:08 +02:00
Andras Bacsai
aaee887d3e fix: respect top-level configs and secrets 2024-07-12 11:21:22 +02:00
Andras Bacsai
cb44373eff chore: Bump version to 4.0.0-beta.311 2024-07-12 11:20:44 +02:00
Andras Bacsai
4e6ea4f584 Merge pull request #2809 from coollabsio/next
Another hoopsy
2024-07-12 10:39:05 +02:00
Andras Bacsai
62a93d3e51 feat: Add new logo for Latitude 2024-07-12 10:38:16 +02:00
Andras Bacsai
f60c281e80 Merge pull request #2808 from coollabsio/next
Forgot to commit, oopsy
2024-07-12 10:36:37 +02:00
Andras Bacsai
43c40cdb09 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-07-12 10:35:52 +02:00
Andras Bacsai
c851262d81 refactor: Reset default labels when docker_compose_domains is modified 2024-07-12 10:35:50 +02:00
Andras Bacsai
91783ccc3e Merge pull request #2805 from coollabsio/next
v4.0.0-beta.310
2024-07-12 10:35:47 +02:00
Andras Bacsai
6ba3d5f86e Merge pull request #2737 from DerrikMilligan/patch-1
fix: Add arch as supported os
2024-07-12 10:00:48 +02:00
Andras Bacsai
a9a20755a9 Merge pull request #2799 from janbiasi/template-twenty-update-env
[TEMPLATE] feat: add security and storage access key env to twenty template
2024-07-12 10:00:29 +02:00
Andras Bacsai
d2693c1ac8 chore: Add new logo for Latitude 2024-07-12 09:39:06 +02:00
Jan Biasi
aaa6f434a9 feat: add security and storage access key env to twenty template 2024-07-12 09:23:51 +02:00
Andras Bacsai
314a3ac83f chore: update composer dependencies 2024-07-12 09:05:31 +02:00
Andras Bacsai
36e177479e chore: update version to 4.0.0-beta.310 2024-07-12 09:05:25 +02:00
Andras Bacsai
cbeebed6c9 Merge pull request #2798 from coollabsio/next
v4.0.0-beta.309
2024-07-11 14:12:58 +02:00
Andras Bacsai
4b905dbfad fix: update redirect URL in unauthenticated exception handler 2024-07-11 14:12:28 +02:00
Andras Bacsai
6072e7efc7 Merge pull request #2797 from coollabsio/next
refactor: comment out unused code for network cleanup
2024-07-11 13:04:36 +02:00
Andras Bacsai
19097c6692 refactor: comment out unused code for network cleanup 2024-07-11 13:04:01 +02:00
Andras Bacsai
d37f63c63c Merge pull request #2789 from coollabsio/next
v4.0.0-beta.308
2024-07-11 13:01:13 +02:00
Andras Bacsai
574bafd950 fix: cleanup parameter 2024-07-11 12:50:12 +02:00
Andras Bacsai
2b805f869a fix/feat: better volume cleanups 2024-07-11 12:38:54 +02:00
Andras Bacsai
36c4be1d17 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-07-11 11:30:23 +02:00
Andras Bacsai
f2d82e16d6 fix: remove volumes as well 2024-07-11 11:30:20 +02:00
andrasbacsai
c6658e1ac7 Fix styling 2024-07-11 09:20:09 +00:00
Andras Bacsai
22a7d85e58 Merge pull request #2760 from KobyW/next
fix: prevent instance fqdn persisting to other servers dynamic proxy config
2024-07-11 11:19:29 +02:00
Andras Bacsai
b3421b47b6 Merge pull request #2762 from Xiloe/gitea-pr-preview
fix: Gitea PR preview not working as intended
2024-07-11 11:17:35 +02:00
Andras Bacsai
b5247f77ec Merge pull request #2795 from alexzvn/feat/display-rollback-interval
feat: display time interval for rollback images
2024-07-11 11:16:05 +02:00
Andras Bacsai
e63e806572 fix: always set project name during app deployments 2024-07-11 11:14:20 +02:00
Andras Bacsai
62b84add36 feat: compose parser v2 2024-07-11 10:55:15 +02:00
Andras Bacsai
858ae1266f chore: Update storage.blade.php view for livewire project service 2024-07-11 10:55:04 +02:00
Andras Bacsai
3ae990aa40 fix: api 2024-07-11 10:17:20 +02:00
Andras Bacsai
deb4b16ae1 feat: cleanup unused docker networks from proxy 2024-07-11 10:17:15 +02:00
Andras Bacsai
b37dc4c73e fix: remove networks when deleting a docker compose based app 2024-07-11 10:16:56 +02:00
Andras Bacsai
6b08100819 chore: Refactor checkIfDomainIsAlreadyUsed function 2024-07-11 10:02:35 +02:00
Alexzvn
2c45e7146b feat: display time interval for rollback images 2024-07-11 02:56:31 +00:00
Andras Bacsai
7c4a722d72 refactor: Add force parameter to StartProxy handle method 2024-07-10 15:53:56 +02:00
Andras Bacsai
f4bccefaba chore: Update livewire/livewire dependency to version 3.4.9 2024-07-10 15:53:52 +02:00
Andras Bacsai
491bb93e95 fix: do not overwrite hardcoded variables if they rely on another variable 2024-07-10 15:53:46 +02:00
Andras Bacsai
f35700c9ee chore: Update Plausible docker compose template to Plausible 2.1.0 2024-07-10 14:02:05 +02:00
Andras Bacsai
bd26aca3d9 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-07-10 13:58:56 +02:00
Andras Bacsai
71d24773b6 chore: Update Plausible docker compose template to Plausible 2.1.0 2024-07-10 13:58:53 +02:00
Andras Bacsai
781bd29f40 Merge pull request #2689 from mtctonyhkhk2010/add-traditional-chinese-translation
Add Traditional Chinese translation
2024-07-10 13:30:31 +02:00
Andras Bacsai
fd4dd1edfa Merge pull request #2688 from ari-party/next
Multiple CSS changes
2024-07-10 13:29:04 +02:00
Andras Bacsai
d2db26cb5e Merge pull request #2575 from Nsbx/patch-1
Update reactive-resume.yaml
2024-07-10 13:25:55 +02:00
Andras Bacsai
01977839f7 Merge branch 'next' into patch-1 2024-07-10 13:25:42 +02:00
Andras Bacsai
0116892b6b Merge pull request #2774 from OlegWock/fix-gitea-installations
Update volumes for Gitea with DB templates
2024-07-10 13:24:55 +02:00
Andras Bacsai
4d88873524 Merge pull request #2779 from Lukyrouge3/patch-1
Fixing supabase service
2024-07-10 13:19:45 +02:00
Andras Bacsai
8e46c0186d Merge pull request #2785 from saeedesmaili/patch-1
Update Plausible docker compose template to Plausible 2.1.0
2024-07-10 13:18:13 +02:00
Andras Bacsai
1b0e589aab update packages 2024-07-10 13:10:21 +02:00
Saeed Esmaili
02ba149e26 Update to community-edition:v2.1.1 2024-07-10 11:53:49 +02:00
Saeed Esmaili
2981aa876c Update plausible.yaml to plausible v2.1.0 2024-07-10 11:43:35 +02:00
Andras Bacsai
82057e1f50 Merge pull request #2681 from coollabsio/next
v4.0.0-beta.307
2024-07-10 11:20:04 +02:00
Andras Bacsai
4ce36631e0 Refactor deployment API response structure 2024-07-10 11:15:43 +02:00
Andras Bacsai
995324d6b3 chore: Refactor shared.php helper functions 2024-07-10 11:09:29 +02:00
Andras Bacsai
c61ad9cd95 feat: Add schema for uuid property in app update response 2024-07-10 10:30:11 +02:00
Andras Bacsai
26f4bcc77e fix: return data of app update 2024-07-10 10:29:52 +02:00
Andras Bacsai
c2b2d06e47 fix: remove own app from domain checks 2024-07-10 10:29:19 +02:00
Torrenté Florian
cbcc7f6d88 Update supabase.yaml
Based on solution here:
https://github.com/coollabsio/coolify/issues/2696
Tested and working !
2024-07-10 00:10:17 +02:00
Andras Bacsai
d05e23264b fix: database input validators 2024-07-09 15:23:53 +02:00
Andras Bacsai
db9faed184 update openapi.yaml 2024-07-09 14:12:52 +02:00
Andras Bacsai
e7feac848a descriptions 2024-07-09 14:12:36 +02:00
Andras Bacsai
33b965d9db chore: more details 2024-07-09 13:59:54 +02:00
Andras Bacsai
6c33bd9c72 openapi services 2024-07-09 13:30:13 +02:00
Andras Bacsai
c72fd2fc9d openapi databases 2024-07-09 13:19:21 +02:00
OlegWock
906a3dc9b4 Update volumes for Gitea with DB templates 2024-07-09 11:36:24 +02:00
Andras Bacsai
2d3a6a4528 openapi work work 2024-07-09 10:45:10 +02:00
Tom Ferriere
01abc26316 removed redundant if statement 2024-07-07 10:45:44 +02:00
Xiloe
2dfe43fc3c Fix styling 2024-07-07 08:02:36 +00:00
Tom Ferriere
f71861300a fix: gitea pr previews 2024-07-07 10:01:11 +02:00
Koby Wood
52d7841334 fix: prevent instance fqdn persisting to other servers dynamic proxy configs
fixes: 2650
2024-07-06 19:33:42 -04:00
Andras Bacsai
9c821e2480 init openapi generator 2024-07-06 14:34:15 +02:00
Andras Bacsai
f8f0aa171c dev command updated 2024-07-06 14:33:59 +02:00
Andras Bacsai
38d9999814 refactor: Simplify code for retrieving subscription in Stripe webhook 2024-07-06 13:47:43 +02:00
Andras Bacsai
920305432b feat: Improve internal notification message for early fraud warning webhook 2024-07-05 20:31:19 +02:00
Andras Bacsai
42fb8ab379 feat: early fraud warning webhook 2024-07-05 20:25:53 +02:00
Andras Bacsai
88ab385100 test openapi 2024-07-05 16:08:01 +02:00
Andras Bacsai
479a3540ec remove tag name uniqueness 2024-07-05 14:04:52 +02:00
Andras Bacsai
47f5a0de81 fix: Add validation for webhook endpoint selection 2024-07-05 13:35:57 +02:00
Andras Bacsai
311c118834 fix: Add newline character to private key before saving 2024-07-05 13:35:51 +02:00
Alfin Auzikri
0c40c0d795 Add files via upload 2024-07-04 23:38:52 +07:00
Alfin Auzikri
25f0a8f0b7 Create docmost.yaml 2024-07-04 23:38:16 +07:00
Andras Bacsai
f58a1a9ecf feat: Rename CloudCleanupSubs to CloudCleanupSubscriptions 2024-07-04 14:28:01 +02:00
Andras Bacsai
efa2ae5177 api api api api 2024-07-04 13:45:06 +02:00
Andras Bacsai
5e55c799ec api api api 2024-07-03 17:10:00 +02:00
Andras Bacsai
46e61cb409 fix: yaml everywhere 2024-07-03 16:27:28 +02:00
Andras Bacsai
b24a489c77 fix: api updates 2024-07-03 13:13:38 +02:00
Derrik Milligan
65a618d019 Add arch as supported os
Update `SUPPORTED_OS` to include the id `arch`. The install script supports `arch` but you can't proceed with a server install because `arch` isn't a `SUPPORTED_OS`
2024-07-02 12:02:38 -06:00
Andras Bacsai
4459c9f73d feat: api api api api api api 2024-07-02 16:12:04 +02:00
Andras Bacsai
3c13f1ff61 feat: restart database
feat: public dbs stay public after restart
feat: patch database conf
2024-07-02 13:39:44 +02:00
Andras Bacsai
c39d6dd407 feat: token permissions
feat: handle sensitive data
feat: handle read-only data
2024-07-02 12:15:58 +02:00
Andras Bacsai
1249b1ece9 fix: custom container name will be the container name, not just internal network name 2024-07-02 10:02:43 +02:00
Andras Bacsai
da6f2da3d0 feat: lots of api endpoints 2024-07-01 16:26:50 +02:00
Andras Bacsai
dbc235d84a fix: check domain on new app via api 2024-07-01 11:39:10 +02:00
Andras Bacsai
b86924bc0e feat: private gh deployments through api 2024-06-30 11:30:31 +02:00
Andras Bacsai
0fb8cf4241 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-06-28 15:05:39 +02:00
Andras Bacsai
30b7e831c0 feat: new app API endpoint 2024-06-28 15:05:37 +02:00
Andras Bacsai
f1b4ebcde2 Merge pull request #2706 from therumbler/patch-3
fix minor typo in backup.blade.php
2024-06-28 12:35:32 +02:00
andrasbacsai
e3c4ebb121 Fix styling 2024-06-28 10:04:28 +00:00
Andras Bacsai
2dd17cfac5 fix: force cleanup on busy servers 2024-06-28 12:03:38 +02:00
Andras Bacsai
93d04ef426 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-06-28 11:00:05 +02:00
Andras Bacsai
70bfd4dd8a fix: show keydbs/dragonflies/clickhouses 2024-06-28 11:00:02 +02:00
Benjamin Rumble
ca917d9d21 fix minor typo in backup.blade.php
~add as a database~ -> add a database
2024-06-27 11:30:28 -04:00
Andras Bacsai
be633f0560 fix: only run cloud clean on cloud + remove root team 2024-06-27 15:07:41 +02:00
Andras Bacsai
613e980267 fix: cleanup subs in cloud 2024-06-27 12:48:37 +02:00
Andras Bacsai
4fb37054df feat: Update server settings metrics history days to 7 2024-06-26 13:59:41 +02:00
Andras Bacsai
07508df8fd fix: remove both option for api endpoints. it just makes things complicated 2024-06-26 13:57:04 +02:00
Andras Bacsai
2a52fb5872 feat: bulk env update api endpoint 2024-06-26 13:32:36 +02:00
Andras Bacsai
f45b3cab55 feat: more API endpoints 2024-06-26 13:00:36 +02:00
Andras Bacsai
eb76d63117 extend application put api 2024-06-25 21:22:23 +02:00
Andras Bacsai
0964c7a338 remove unnecessary things from application table 2024-06-25 21:22:14 +02:00
Bruce Mak
7af151d44e add Traditional Chinese translation 2024-06-26 00:05:52 +08:00
Astrid
7028391e57 remove unused li element? 2024-06-25 17:21:23 +02:00
Astrid
cbae0845e7 h2 instead of h3 as element is child of h2 2024-06-25 17:10:06 +02:00
Astrid
0e512962c6 padding same as other tabs
from: ![from](https://astrid.email/u/chrome_3IcAbmCNKW.png)
to: ![to](https://astrid.email/u/chrome_8Cz5rx30wn.png)
2024-06-25 17:09:44 +02:00
Astrid
ac694b855b change gap of proxy buttons
from: ![from](https://astrid.email/u/chrome_ducsHvMI4w.png)
to: ![to](https://astrid.email/u/chrome_L4ncORPQtD.png)
2024-06-25 17:01:32 +02:00
Astrid
490d45e788 server settings css changes
from: ![from](https://astrid.email/u/chrome_REEIhjc2Yp.png)
to: ![to](https://astrid.email/u/chrome_J5XwGaOs84.png)
2024-06-25 17:01:17 +02:00
Astrid
b0863eb5ea remove h4 padding on server proxy settings
from: ![from](https://astrid.email/u/chrome_9wK3HTTy12.png)
to: ![to](https://astrid.email/u/chrome_7m5jXr1aWH.png)
2024-06-25 17:01:17 +02:00
Andras Bacsai
ee199ed038 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-06-25 15:05:53 +02:00
Andras Bacsai
41268fa20b api: able to update application 2024-06-25 15:05:51 +02:00
andrasbacsai
7474896368 Fix styling 2024-06-25 12:30:37 +00:00
Andras Bacsai
54c4296a25 chore: Update Monaco Editor for Docker Compose and Proxy Configuration 2024-06-25 14:29:51 +02:00
Andras Bacsai
116f5afe3c chore: Refactor ServerStatusJob constructor formatting 2024-06-25 14:29:47 +02:00
Andras Bacsai
c015c8f45d service: glances 2024-06-25 14:21:25 +02:00
Andras Bacsai
fe26b3d759 chore: Update version to 4.0.0-beta.307 2024-06-25 14:20:21 +02:00
Andras Bacsai
afee2d8ca8 Merge pull request #2541 from leocabeza/next
feat: add glances service to templates
2024-06-25 14:20:02 +02:00
Andras Bacsai
e9158b7305 Merge pull request #2678 from coollabsio/next
v4.0.0-beta.306
2024-06-25 14:04:44 +02:00
Andras Bacsai
0f5690db85 fix: run container commands on high priority 2024-06-25 13:59:39 +02:00
Andras Bacsai
3ebb35a5cd fix: remove lemon + paddle things 2024-06-25 13:54:58 +02:00
Andras Bacsai
f557cd0933 fix: load js locally 2024-06-25 13:54:44 +02:00
Andras Bacsai
063aa702b1 chore: Add log1x/laravel-webfonts package 2024-06-25 13:44:46 +02:00
Andras Bacsai
4f1070083a chore: Update version to 4.0.0-beta.306 2024-06-25 13:36:02 +02:00
Andras Bacsai
5e625f71c5 feat: local fonts 2024-06-25 13:35:58 +02:00
Andras Bacsai
cc36a0ecd1 Merge pull request #2677 from coollabsio/next
v4.0.0-beta.305
2024-06-25 12:53:11 +02:00
Andras Bacsai
a849c25672 improve: update process 2024-06-25 12:48:56 +02:00
Andras Bacsai
8b95b93c72 refactor: Add is_build_time property to nixpacks_php_fallback_path and nixpacks_php_root_dir 2024-06-25 12:43:16 +02:00
Andras Bacsai
3d9557bc50 Merge branch 'next' of github.com:coollabsio/coolify into next 2024-06-25 10:52:52 +02:00
Andras Bacsai
01f2f56be6 refactor: Improve formatting and readability of source.blade.php 2024-06-25 10:52:50 +02:00
andrasbacsai
e658ed993a Fix styling 2024-06-25 08:52:24 +00:00
Andras Bacsai
58fc897ea5 fix: run user commands on high prio queue 2024-06-25 10:51:32 +02:00
Andras Bacsai
f8c5a35b56 chore: Update version to 4.0.0-beta.305 2024-06-25 10:37:45 +02:00
Andras Bacsai
2c92cc40e1 refactor: Update code to use str() instead of Str::of() for string manipulation 2024-06-25 10:37:10 +02:00
Andras Bacsai
1266810c4d fix: better parsign performance for huge compose files
fix: env parsing
2024-06-25 10:34:56 +02:00
Andras Bacsai
1de8657a56 refactor: Update stack-form.blade.php to include wire:target attribute for submit button 2024-06-25 10:18:21 +02:00
Leonardo Cabeza
408c24c700 Merge remote-tracking branch 'upstream/next' into next 2024-06-24 17:54:12 -05:00
Andras Bacsai
3a1e5f7f0c Merge pull request #2675 from coollabsio/next
v4.0.0-beta.304
2024-06-24 23:49:16 +02:00
Andras Bacsai
8d85976ac0 revert savecomposeconfig 2024-06-24 23:47:55 +02:00
Andras Bacsai
fe7eaa594f Merge pull request #2672 from coollabsio/next
v4.0.0-beta.303
2024-06-24 22:47:53 +02:00
Andras Bacsai
5500a1edb3 refactor: Remove commented out code in Service model's saveComposeConfigs method 2024-06-24 22:46:19 +02:00
Andras Bacsai
eb27e34972 chore: Update version to 4.0.0-beta.303 2024-06-24 22:46:16 +02:00
Andras Bacsai
8335c299b8 Merge pull request #2670 from coollabsio/next
fix fix fix
2024-06-24 21:00:03 +02:00
Andras Bacsai
feadc60b14 refactor: Improve handling of default environment in Service model's saveComposeConfigs method 2024-06-24 20:59:24 +02:00
Andras Bacsai
b59799dc2b refactor: Add default environment to Service model's saveComposeConfigs method 2024-06-24 20:58:54 +02:00
Andras Bacsai
f0b2f6eb00 Merge pull request #2669 from coollabsio/next
v4.0.0-beta.302
2024-06-24 20:56:48 +02:00
Andras Bacsai
c3fb126a0a refactor: Update Service model's saveComposeConfigs method 2024-06-24 20:55:12 +02:00
Andras Bacsai
f6708f6e47 chore: Update version to 4.0.0-beta.302 2024-06-24 20:54:53 +02:00
Andras Bacsai
d48b5a9079 Merge pull request #2664 from coollabsio/next
Refactor default_environment method to handle projects with 0 environ…
2024-06-24 18:42:21 +02:00
Andras Bacsai
99354f0d7d Refactor default_environment method to handle projects with 0 environments 2024-06-24 18:41:44 +02:00
Andras Bacsai
0fa8552aa3 Merge pull request #2663 from coollabsio/next
v4.0.0-beta.301
2024-06-24 18:35:18 +02:00
Andras Bacsai
4869c388b2 Merge pull request #2659 from kubatron117/cs-lang
CS localization
2024-06-24 18:34:54 +02:00
Andras Bacsai
2adade0927 Merge pull request #2661 from ari-party/main
Small CSS change
2024-06-24 18:34:28 +02:00
Andras Bacsai
87b7337d9e fix: projects with 0 envs 2024-06-24 18:33:01 +02:00
Andras Bacsai
351c9c1ad3 chore: Update version to 4.0.0-beta.301 2024-06-24 18:31:15 +02:00
kubatron 117
7759fe2cdf Add czech localization 2024-06-24 17:24:08 +02:00
Astrid
5d2651afc1 Add margin top to button 2024-06-24 17:20:56 +02:00
Leon Adomaitis
53032469e7 added smtp config 2024-06-23 21:43:14 +02:00
Leon Adomaitis
768e27d68c Add metadata 2024-06-23 04:12:03 +02:00
Leon Adomaitis
691ae04ca9 Add gitlab.svg 2024-06-23 04:02:46 +02:00
Leon Adomaitis
6585db1f9c Merge branch 'next' into main 2024-06-23 03:56:50 +02:00
Leon Adomaitis
42aa2d0088 add first outline of the gitlab compose file 2024-06-23 03:52:29 +02:00
Nicolas Bondoux
2f87deb10b Update reactive-resume.yaml 2024-06-16 21:54:53 +02:00
Nicolas Bondoux
65253ca54e Update reactive-resume.yaml 2024-06-16 21:51:00 +02:00
Leonardo Cabeza
7cc4a21383 fix: image logo 2024-06-13 20:46:13 -05:00
Leonardo Cabeza
af464c2af7 add: glances service 2024-06-13 20:39:37 -05:00
Emircan ERKUL
e7e85456ea Drupal svg logo 2024-06-12 06:54:59 +03:00
Emircan ERKUL
440baf6009 Create drupal-with-postgresql.yaml 2024-06-12 06:50:50 +03:00
367 changed files with 20712 additions and 5276 deletions

View File

@@ -1,6 +1,6 @@
name: Bug report
description: 'Create a new bug report.'
title: '[Bug]: '
description: "Create a new bug report."
title: "[Bug]: "
body:
- type: markdown
attributes:
@@ -35,3 +35,12 @@ body:
description: Coolify's version (see top of your screen).
validations:
required: true
- type: checkboxes
attributes:
label: Cloud?
description: "Are you using the cloud version of Coolify?"
options:
- label: 'Yes'
required: false
- label: 'No'
required: false

View File

@@ -4,5 +4,5 @@ contact_links:
url: https://coollabs.io/discord
about: Reach out to us on Discord.
- name: 🙋‍♂️ Feature Requests
url: https://github.com/coollabsio/coolify/discussions/categories/feature-requests-ideas
url: https://github.com/coollabsio/coolify/discussions/categories/new-features
about: All feature requests will be discussed here.

81
.github/workflows/pr-build.yml vendored Normal file
View File

@@ -0,0 +1,81 @@
name: PR Build (v4)
on:
pull_request:
types:
- opened
branches-ignore: ["main", "v3"]
paths-ignore:
- .github/workflows/coolify-helper.yml
- docker/coolify-helper/Dockerfile
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify"
jobs:
amd64:
runs-on: ubuntu-latest
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: Build image and push to registry
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
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: Build image and push to registry
uses: docker/build-push-action@v5
with:
context: .
file: docker/prod/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64
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: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.number }}
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }}

View File

@@ -2,8 +2,6 @@
)
[![Bounty Issues](https://img.shields.io/static/v1?labelColor=grey&color=6366f1&label=Algora&message=%F0%9F%92%8E+Bounty+issues&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties/new)
[![Open Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dopen&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=open)
[![Rewarded Bounties](https://img.shields.io/endpoint?url=https%3A%2F%2Fconsole.algora.io%2Fapi%2Fshields%2Fcoollabsio%2Fbounties%3Fstatus%3Dcompleted&style=for-the-badge)](https://console.algora.io/org/coollabsio/bounties?status=completed)
# About the Project
@@ -49,6 +47,8 @@ Special thanks to our biggest sponsors!
<a href="https://coolify.ad.vin/?ref=coolify.io" target="_blank"><img src="./other/logos/advin.png" alt="advin logo" width="250"/></a>
<a href="https://trieve.ai/?ref=coolify.io" target="_blank"><img src="./other/logos/trieve_bg.png" alt="trieve logo" width="180"/></a>
<a href="https://blacksmith.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/blacksmith.svg" alt="blacksmith logo" width="200"/></a>
<a href="https://latitude.sh/?ref=coolify.io" target="_blank"><img src="./other/logos/latitude.svg" alt="latitude logo" width="200"/></a>
<a href="https://brand.dev/?ref=coolify.io" target="_blank"><img src="./other/logos/branddev.png" alt="branddev logo" width="200"/></a>
## Github Sponsors ($40+)
<a href="https://serpapi.com/?ref=coolify.io"><img width="60px" alt="SerpAPI" src="https://github.com/serpapi.png"/></a>

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Actions\Application;
use App\Models\Application;
use Lorisleiva\Actions\Concerns\AsAction;
class LoadComposeFile
{
use AsAction;
public function handle(Application $application)
{
$application->loadComposeFile();
}
}

View File

@@ -9,7 +9,7 @@ class StopApplication
{
use AsAction;
public function handle(Application $application)
public function handle(Application $application, bool $previewDeployments = false)
{
if ($application->destination->server->isSwarm()) {
instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
@@ -26,18 +26,27 @@ class StopApplication
if (! $server->isFunctional()) {
return 'Server is not functional';
}
$containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
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(
["docker rm -f {$containerName}"],
$server
);
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);
}
}
}
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);
}
}
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Actions\CoolifyTask;
use App\Data\CoolifyTaskArgs;
use App\Enums\ActivityTypes;
use App\Jobs\CoolifyTask;
use Spatie\Activitylog\Models\Activity;
@@ -40,8 +41,18 @@ class PrepareCoolifyTask
public function __invoke(): Activity
{
$job = new CoolifyTask($this->activity, ignore_errors: $this->remoteProcessArgs->ignore_errors, call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish, call_event_data: $this->remoteProcessArgs->call_event_data);
dispatch($job);
$job = new CoolifyTask(
activity: $this->activity,
ignore_errors: $this->remoteProcessArgs->ignore_errors,
call_event_on_finish: $this->remoteProcessArgs->call_event_on_finish,
call_event_data: $this->remoteProcessArgs->call_event_data,
);
if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) {
ray('Dispatching a high priority job');
dispatch($job)->onQueue('high');
} else {
dispatch($job);
}
$this->activity->refresh();
return $this->activity;

View File

@@ -39,7 +39,7 @@ class RunRemoteProcess
public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null)
{
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value) {
if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) {
throw new \RuntimeException('Incompatible Activity to run a remote command.');
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class RestartDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
StopDatabase::run($database);
return StartDatabase::run($database);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -155,11 +154,11 @@ class StartClickhouse
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_USER'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_USER={$this->database->clickhouse_admin_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('CLICKHOUSE_ADMIN_PASSWORD'))->isEmpty()) {
$environment_variables->push("CLICKHOUSE_ADMIN_PASSWORD={$this->database->clickhouse_admin_password}");
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Actions\Database;
use App\Models\StandaloneClickhouse;
use App\Models\StandaloneDragonfly;
use App\Models\StandaloneKeydb;
use App\Models\StandaloneMariadb;
use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Lorisleiva\Actions\Concerns\AsAction;
class StartDatabase
{
use AsAction;
public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
{
$server = $database->destination->server;
if (! $server->isFunctional()) {
return 'Server is not functional';
}
switch ($database->getMorphClass()) {
case 'App\Models\StandalonePostgresql':
$activity = StartPostgresql::run($database);
break;
case 'App\Models\StandaloneRedis':
$activity = StartRedis::run($database);
break;
case 'App\Models\StandaloneMongodb':
$activity = StartMongodb::run($database);
break;
case 'App\Models\StandaloneMysql':
$activity = StartMysql::run($database);
break;
case 'App\Models\StandaloneMariadb':
$activity = StartMariadb::run($database);
break;
case 'App\Models\StandaloneKeydb':
$activity = StartKeydb::run($database);
break;
case 'App\Models\StandaloneDragonfly':
$activity = StartDragonfly::run($database);
break;
case 'App\Models\StandaloneClickhouse':
$activity = StartClickhouse::run($database);
break;
}
if ($database->is_public && $database->public_port) {
StartDatabaseProxy::dispatch($database);
}
return $activity;
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneDragonfly;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -155,7 +154,7 @@ class StartDragonfly
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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

@@ -4,7 +4,6 @@ namespace App\Actions\Database;
use App\Models\StandaloneKeydb;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -163,7 +162,7 @@ class StartKeydb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMariadb;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -157,18 +156,18 @@ class StartMariadb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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::of($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::of($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::of($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

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMongodb;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -174,15 +173,15 @@ class StartMongodb
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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::of($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::of($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

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandaloneMysql;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -157,18 +156,18 @@ class StartMysql
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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::of($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::of($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::of($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

@@ -3,7 +3,6 @@
namespace App\Actions\Database;
use App\Models\StandalonePostgresql;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -179,18 +178,18 @@ class StartPostgresql
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_USER'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_USER'))->isEmpty()) {
$environment_variables->push("POSTGRES_USER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('PGUSER'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('PGUSER'))->isEmpty()) {
$environment_variables->push("PGUSER={$this->database->postgres_user}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_PASSWORD'))->isEmpty()) {
$environment_variables->push("POSTGRES_PASSWORD={$this->database->postgres_password}");
}
if ($environment_variables->filter(fn ($env) => Str::of($env)->contains('POSTGRES_DB'))->isEmpty()) {
if ($environment_variables->filter(fn ($env) => str($env)->contains('POSTGRES_DB'))->isEmpty()) {
$environment_variables->push("POSTGRES_DB={$this->database->postgres_db}");
}

View File

@@ -4,7 +4,6 @@ namespace App\Actions\Database;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
@@ -167,7 +166,7 @@ class StartRedis
$environment_variables->push("$env->key=$env->real_value");
}
if ($environment_variables->filter(fn ($env) => Str::of($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

@@ -22,14 +22,13 @@ class StopDatabase
if (! $server->isFunctional()) {
return 'Server is not functional';
}
instant_remote_process(
["docker rm -f {$database->uuid}"],
$server
);
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);
if ($database->is_public) {
StopDatabaseProxy::run($database);
}
// TODO: make notification for services
// $database->environment->project->team->notify(new StatusChanged($database));
}
}

View File

@@ -27,7 +27,6 @@ class StopDatabaseProxy
$server = data_get($database, 'service.server');
}
instant_remote_process(["docker rm -f {$uuid}-proxy"], $server);
$database->is_public = false;
$database->save();
DatabaseStatusChanged::dispatch();
}

View File

@@ -12,6 +12,7 @@ use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Lorisleiva\Actions\Concerns\AsAction;
class GetContainersStatus
@@ -20,13 +21,16 @@ class GetContainersStatus
public $applications;
public ?Collection $containers;
public ?Collection $containerReplicates;
public $server;
public function handle(Server $server)
public function handle(Server $server, ?Collection $containers = null, ?Collection $containerReplicates = null)
{
// if (isDev()) {
// $server = Server::find(0);
// }
$this->containers = $containers;
$this->containerReplicates = $containerReplicates;
$this->server = $server;
if (! $this->server->isFunctional()) {
return 'Server is not ready.';
@@ -66,322 +70,312 @@ class GetContainersStatus
// }
}
private function sentinel()
{
try {
$containers = $this->server->getContainers();
if ($containers->count() === 0) {
return;
}
$databases = $this->server->databases();
$services = $this->server->services()->get();
$previews = $this->server->previews();
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
$foundServices = [];
// private function sentinel()
// {
// try {
// $this->containers = $this->server->getContainersWithSentinel();
// if ($this->containers->count() === 0) {
// return;
// }
// $databases = $this->server->databases();
// $services = $this->server->services()->get();
// $previews = $this->server->previews();
// $foundApplications = [];
// $foundApplicationPreviews = [];
// $foundDatabases = [];
// $foundServices = [];
foreach ($containers as $container) {
$labels = Arr::undot(data_get($container, 'labels'));
$containerStatus = data_get($container, 'state');
$containerHealth = data_get($container, 'health_status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $this->applications->where('id', $applicationId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
$type = data_get($labels, 'coolify.type');
if ($uuid) {
if ($type === 'service') {
$database_id = data_get($labels, 'coolify.service.subId');
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = $service_db->service->uuid;
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'name') === "$uuid-proxy";
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}
}
} else {
$database = $databases->where('uuid', $uuid)->first();
if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'name') === "$uuid-proxy";
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else {
// Notify user that this container should not be there.
}
}
}
if (data_get($container, 'name') === 'coolify-db') {
$foundDatabases[] = 0;
}
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $services->where('id', $serviceLabelId)->first();
if (! $service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
}
}
}
}
$exitedServices = collect([]);
foreach ($services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) {
continue;
} else {
$exitedServices->push($app);
}
}
foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) {
continue;
} else {
$exitedServices->push($db);
}
}
}
$exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
if ($name) {
if ($fqdn) {
$containerName = "$name, available at $fqdn";
} else {
$containerName = $name;
}
} else {
if ($fqdn) {
$containerName = $fqdn;
} else {
$containerName = null;
}
}
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
// foreach ($this->containers as $container) {
// $labels = Arr::undot(data_get($container, 'labels'));
// $containerStatus = data_get($container, 'state');
// $containerHealth = data_get($container, 'health_status', 'unhealthy');
// $containerStatus = "$containerStatus ($containerHealth)";
// $applicationId = data_get($labels, 'coolify.applicationId');
// if ($applicationId) {
// $pullRequestId = data_get($labels, 'coolify.pullRequestId');
// if ($pullRequestId) {
// if (str($applicationId)->contains('-')) {
// $applicationId = str($applicationId)->before('-');
// }
// $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
// if ($preview) {
// $foundApplicationPreviews[] = $preview->id;
// $statusFromDb = $preview->status;
// if ($statusFromDb !== $containerStatus) {
// $preview->update(['status' => $containerStatus]);
// }
// } else {
// //Notify user that this container should not be there.
// }
// } else {
// $application = $this->applications->where('id', $applicationId)->first();
// if ($application) {
// $foundApplications[] = $application->id;
// $statusFromDb = $application->status;
// if ($statusFromDb !== $containerStatus) {
// $application->update(['status' => $containerStatus]);
// }
// } else {
// //Notify user that this container should not be there.
// }
// }
// } else {
// $uuid = data_get($labels, 'com.docker.compose.service');
// $type = data_get($labels, 'coolify.type');
// if ($uuid) {
// if ($type === 'service') {
// $database_id = data_get($labels, 'coolify.service.subId');
// if ($database_id) {
// $service_db = ServiceDatabase::where('id', $database_id)->first();
// if ($service_db) {
// $uuid = $service_db->service->uuid;
// $isPublic = data_get($service_db, 'is_public');
// if ($isPublic) {
// $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// // TODO: fix this with sentinel
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'name') === "$uuid-proxy";
// }
// })->first();
// if (! $foundTcpProxy) {
// StartDatabaseProxy::run($service_db);
// // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
// }
// }
// }
// }
// } else {
// $database = $databases->where('uuid', $uuid)->first();
// if ($database) {
// $isPublic = data_get($database, 'is_public');
// $foundDatabases[] = $database->id;
// $statusFromDb = $database->status;
// if ($statusFromDb !== $containerStatus) {
// $database->update(['status' => $containerStatus]);
// }
// if ($isPublic) {
// $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
// if ($this->server->isSwarm()) {
// // TODO: fix this with sentinel
// return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
// } else {
// return data_get($value, 'name') === "$uuid-proxy";
// }
// })->first();
// if (! $foundTcpProxy) {
// StartDatabaseProxy::run($database);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
// }
// }
// } else {
// // Notify user that this container should not be there.
// }
// }
// }
// if (data_get($container, 'name') === 'coolify-db') {
// $foundDatabases[] = 0;
// }
// }
// $serviceLabelId = data_get($labels, 'coolify.serviceId');
// if ($serviceLabelId) {
// $subType = data_get($labels, 'coolify.service.subType');
// $subId = data_get($labels, 'coolify.service.subId');
// $service = $services->where('id', $serviceLabelId)->first();
// if (! $service) {
// continue;
// }
// if ($subType === 'application') {
// $service = $service->applications()->where('id', $subId)->first();
// } else {
// $service = $service->databases()->where('id', $subId)->first();
// }
// if ($service) {
// $foundServices[] = "$service->id-$service->name";
// $statusFromDb = $service->status;
// if ($statusFromDb !== $containerStatus) {
// // ray('Updating status: ' . $containerStatus);
// $service->update(['status' => $containerStatus]);
// }
// }
// }
// }
// $exitedServices = collect([]);
// foreach ($services as $service) {
// $apps = $service->applications()->get();
// $dbs = $service->databases()->get();
// foreach ($apps as $app) {
// if (in_array("$app->id-$app->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($app);
// }
// }
// foreach ($dbs as $db) {
// if (in_array("$db->id-$db->name", $foundServices)) {
// continue;
// } else {
// $exitedServices->push($db);
// }
// }
// }
// $exitedServices = $exitedServices->unique('id');
// foreach ($exitedServices as $exitedService) {
// if (str($exitedService->status)->startsWith('exited')) {
// continue;
// }
// $name = data_get($exitedService, 'name');
// $fqdn = data_get($exitedService, 'fqdn');
// if ($name) {
// if ($fqdn) {
// $containerName = "$name, available at $fqdn";
// } else {
// $containerName = $name;
// }
// } else {
// if ($fqdn) {
// $containerName = $fqdn;
// } else {
// $containerName = null;
// }
// }
// $projectUuid = data_get($service, 'environment.project.uuid');
// $serviceUuid = data_get($service, 'uuid');
// $environmentName = data_get($service, 'environment.name');
if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
// if ($projectUuid && $serviceUuid && $environmentName) {
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
// } else {
// $url = null;
// }
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// $exitedService->update(['status' => 'exited']);
// }
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) {
$application = $this->applications->where('id', $applicationId)->first();
if (str($application->status)->startsWith('exited')) {
continue;
}
$application->update(['status' => 'exited']);
// $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
// foreach ($notRunningApplications as $applicationId) {
// $application = $this->applications->where('id', $applicationId)->first();
// if (str($application->status)->startsWith('exited')) {
// continue;
// }
// $application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
// $name = data_get($application, 'name');
// $fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
// $projectUuid = data_get($application, 'environment.project.uuid');
// $applicationUuid = data_get($application, 'uuid');
// $environment = data_get($application, 'environment.name');
if ($projectUuid && $applicationUuid && $environment) {
$url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
} else {
$url = null;
}
// if ($projectUuid && $applicationUuid && $environment) {
// $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $previews->where('id', $previewId)->first();
if (str($preview->status)->startsWith('exited')) {
continue;
}
$preview->update(['status' => 'exited']);
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews);
// foreach ($notRunningApplicationPreviews as $previewId) {
// $preview = $previews->where('id', $previewId)->first();
// if (str($preview->status)->startsWith('exited')) {
// continue;
// }
// $preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
// $name = data_get($preview, 'name');
// $fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
// $containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
// $projectUuid = data_get($preview, 'application.environment.project.uuid');
// $environmentName = data_get($preview, 'application.environment.name');
// $applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
} else {
$url = null;
}
// if ($projectUuid && $applicationUuid && $environmentName) {
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
// } else {
// $url = null;
// }
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
$database = $databases->where('id', $database)->first();
if (str($database->status)->startsWith('exited')) {
continue;
}
$database->update(['status' => 'exited']);
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases);
// foreach ($notRunningDatabases as $database) {
// $database = $databases->where('id', $database)->first();
// if (str($database->status)->startsWith('exited')) {
// continue;
// }
// $database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
// $name = data_get($database, 'name');
// $fqdn = data_get($database, 'fqdn');
$containerName = $name;
// $containerName = $name;
$projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
// $projectUuid = data_get($database, 'environment.project.uuid');
// $environmentName = data_get($database, 'environment.name');
// $databaseUuid = data_get($database, 'uuid');
if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// if ($projectUuid && $databaseUuid && $environmentName) {
// $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
// } else {
// $url = null;
// }
// // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
// }
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
// TODO: fix this with sentinel
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'name') === 'coolify-proxy';
}
})->first();
if (! $foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'state');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} catch (\Exception $e) {
// send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
// // Check if proxy is running
// $this->server->proxyType();
// $foundProxyContainer = $this->containers->filter(function ($value, $key) {
// if ($this->server->isSwarm()) {
// // TODO: fix this with sentinel
// return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
// } else {
// return data_get($value, 'name') === 'coolify-proxy';
// }
// })->first();
// if (! $foundProxyContainer) {
// try {
// $shouldStart = CheckProxy::run($this->server);
// if ($shouldStart) {
// StartProxy::run($this->server, false);
// $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
// }
// } catch (\Throwable $e) {
// ray($e);
// }
// } else {
// $this->server->proxy->status = data_get($foundProxyContainer, 'state');
// $this->server->save();
// $connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
// instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
// }
// } catch (\Exception $e) {
// // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
// ray($e->getMessage());
return handleError($e);
}
}
// return handleError($e);
// }
// }
private function old_way()
{
if ($this->server->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this->server, false);
} else {
// Precheck for containers
$containers = instant_remote_process(['docker container ls -q'], $this->server, false);
if (! $containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server, false);
$containerReplicates = null;
if ($this->containers === null) {
['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers();
}
if (is_null($containers)) {
if (is_null($this->containers)) {
return;
}
$containers = format_docker_command_output_to_json($containers);
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
if ($this->containerReplicates) {
foreach ($this->containerReplicates as $containerReplica) {
$name = data_get($containerReplica, 'Name');
$containers = $containers->map(function ($container) use ($name, $containerReplica) {
$this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) {
if (data_get($container, 'Spec.Name') === $name) {
$replicas = data_get($containerReplica, 'Replicas');
$running = str($replicas)->explode('/')[0];
@@ -407,7 +401,7 @@ class GetContainersStatus
$foundDatabases = [];
$foundServices = [];
foreach ($containers as $container) {
foreach ($this->containers as $container) {
if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name');
@@ -461,7 +455,7 @@ class GetContainersStatus
if ($uuid) {
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
@@ -486,7 +480,7 @@ class GetContainersStatus
$database->update(['status' => $containerStatus]);
}
if ($isPublic) {
$foundTcpProxy = $containers->filter(function ($value, $key) use ($uuid) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
@@ -659,7 +653,7 @@ class GetContainersStatus
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class CheckConfiguration
@@ -22,9 +21,8 @@ class CheckConfiguration
"cat $proxy_path/docker-compose.yml",
];
$proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = Str::of(generate_default_proxy_configuration($server))->trim()->value;
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value;
}
if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration');

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Proxy;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
class SaveConfiguration
@@ -18,7 +17,7 @@ class SaveConfiguration
$proxy_path = $server->proxyPath();
$docker_compose_yml_base64 = base64_encode($proxy_settings);
$server->proxy->last_saved_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->proxy->last_saved_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
return instant_remote_process([

View File

@@ -4,7 +4,6 @@ namespace App\Actions\Proxy;
use App\Events\ProxyStarted;
use App\Models\Server;
use Illuminate\Support\Str;
use Lorisleiva\Actions\Concerns\AsAction;
use Spatie\Activitylog\Models\Activity;
@@ -12,11 +11,11 @@ class StartProxy
{
use AsAction;
public function handle(Server $server, bool $async = true): string|Activity
public function handle(Server $server, bool $async = true, bool $force = false): string|Activity
{
try {
$proxyType = $server->proxyType();
if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) {
if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) {
return 'OK';
}
$commands = collect([]);
@@ -27,7 +26,7 @@ class StartProxy
}
SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = Str::of($docker_compose_yml_base64)->pipe('md5')->value;
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value;
$server->save();
if ($server->isSwarm()) {
$commands = $commands->merge([

View File

@@ -11,6 +11,7 @@ class CleanupDocker
public function handle(Server $server, bool $force = true)
{
// cleanup docker images, containers, and builder caches
if ($force) {
instant_remote_process(['docker image prune -af'], $server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $server, false);

View File

@@ -24,12 +24,7 @@ class InstallLogDrain
}
try {
if ($type === 'none') {
$command = [
"echo 'Stopping old Fluent Bit'",
'docker rm -f coolify-log-drain || true',
];
return instant_remote_process($command, $server);
return 'No log drain is enabled.';
} elseif ($type === 'newrelic') {
if (! $server->settings->is_logdrain_newrelic_enabled) {
throw new \Exception('New Relic log drain is not enabled.');

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Actions\Server;
use App\Enums\ActivityTypes;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class RunCommand
{
use AsAction;
public function handle(Server $server, $command)
{
$activity = remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value);
return $activity;
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class StopLogDrain
{
use AsAction;
public function handle(Server $server)
{
try {
return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -20,12 +20,11 @@ class UpdateCoolify
{
try {
$settings = InstanceSettings::get();
ray('Running InstanceAutoUpdateJob');
$this->server = Server::find(0);
if (! $this->server) {
return;
}
CleanupDocker::run($this->server, false);
CleanupDocker::dispatch($this->server, false)->onQueue('high');
$this->latestVersion = get_latest_version_of_coolify();
$this->currentVersion = config('version');
if (! $manual_update) {

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Actions\Server;
use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction;
class ValidateServer
{
use AsAction;
public ?string $uptime = null;
public ?string $error = null;
public ?string $supported_os_type = null;
public ?string $docker_installed = null;
public ?string $docker_compose_installed = null;
public ?string $docker_version = null;
public function handle(Server $server)
{
$server->update([
'validation_logs' => null,
]);
['uptime' => $this->uptime, 'error' => $error] = $server->validateConnection();
if (! $this->uptime) {
$this->error = 'Server is not reachable. Please validate your configuration and connection.<br>Check this <a target="_blank" class="text-black underline dark:text-white" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help. <br><br><div class="text-error">Error: '.$error.'</div>';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->supported_os_type = $server->validateOS();
if (! $this->supported_os_type) {
$this->error = 'Server OS type is not supported. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_installed = $server->validateDockerEngine();
$this->docker_compose_installed = $server->validateDockerCompose();
if (! $this->docker_installed || ! $this->docker_compose_installed) {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
$this->docker_version = $server->validateDockerEngineVersion();
if ($this->docker_version) {
return 'OK';
} else {
$this->error = 'Docker Engine is not installed. Please install Docker manually before continuing: <a target="_blank" class="text-black underline dark:text-white" href="https://docs.docker.com/engine/install/#server">documentation</a>.';
$server->update([
'validation_logs' => $this->error,
]);
throw new \Exception($this->error);
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Actions\Service;
use App\Models\Service;
use Lorisleiva\Actions\Concerns\AsAction;
class RestartService
{
use AsAction;
public function handle(Service $service)
{
StopService::run($service);
return StartService::run($service);
}
}

View File

@@ -19,20 +19,21 @@ class StopService
ray('Stopping service: '.$service->name);
$applications = $service->applications()->get();
foreach ($applications as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server);
instant_remote_process(command: ["docker stop --time=30 {$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) {
instant_remote_process(["docker rm -f {$db->name}-{$service->uuid}"], $service->server);
instant_remote_process(command: ["docker stop --time=30 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
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 2>/dev/null"], $service->server, false);
instant_remote_process(["docker network rm {$service->uuid} 2>/dev/null"], $service->server, false);
// TODO: make notification for databases
// $service->environment->project->team->notify(new StatusChanged($service));
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) {
echo $e->getMessage();
ray($e->getMessage());
return $e->getMessage();

View File

@@ -18,7 +18,12 @@ class CleanupDatabase extends Command
} else {
echo "Running database cleanup in dry-run mode...\n";
}
$keep_days = 60;
if (isCloud()) {
// Later on we can increase this to 180 days or dynamically set
$keep_days = 60;
} else {
$keep_days = 60;
}
echo "Keep days: $keep_days\n";
// Cleanup failed jobs table
$failed_jobs = DB::table('failed_jobs')->where('failed_at', '<', now()->subDays(1));

View File

@@ -18,7 +18,7 @@ class CleanupUnreachableServers extends Command
if ($servers->count() > 0) {
foreach ($servers as $server) {
echo "Cleanup unreachable server ($server->id) with name $server->name";
send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
// send_internal_notification("Server $server->name is unreachable for 7 days. Cleaning up...");
$server->update([
'ip' => '1.2.3.4',
]);

View File

@@ -0,0 +1,101 @@
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
class CloudCleanupSubscriptions extends Command
{
protected $signature = 'cloud:cleanup-subs';
protected $description = 'Cleanup subcriptions teams';
public function handle()
{
try {
if (! isCloud()) {
$this->error('This command can only be run on cloud');
return;
}
ray()->clearAll();
$this->info('Cleaning up subcriptions teams');
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$teams = Team::all()->filter(function ($team) {
return $team->id !== 0;
})->sortBy('id');
foreach ($teams as $team) {
if ($team) {
$this->info("Checking team {$team->id}");
}
if (! data_get($team, 'subscription')) {
$this->disableServers($team);
continue;
}
// If the team has no subscription id and the invoice is paid, we need to reset the invoice paid status
if (! (data_get($team, 'subscription.stripe_subscription_id'))) {
$this->info("Resetting invoice paid status for team {$team->id} {$team->name}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_subscription_id' => null,
]);
$this->disableServers($team);
continue;
} else {
$subscription = $stripe->subscriptions->retrieve(data_get($team, 'subscription.stripe_subscription_id'), []);
$status = data_get($subscription, 'status');
if ($status === 'active' || $status === 'past_due') {
$team->subscription->update([
'stripe_invoice_paid' => true,
'stripe_trial_already_ended' => false,
]);
continue;
}
$this->info('Subscription status: '.$status);
$this->info('Subscription id: '.data_get($team, 'subscription.stripe_subscription_id'));
$confirm = $this->confirm('Do you want to cancel the subscription?', true);
if (! $confirm) {
$this->info("Skipping team {$team->id} {$team->name}");
} else {
$this->info("Cancelling subscription for team {$team->id} {$team->name}");
$team->subscription->update([
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_subscription_id' => null,
]);
$this->disableServers($team);
}
}
}
} catch (\Exception $e) {
$this->error($e->getMessage());
return;
}
}
private function disableServers(Team $team)
{
foreach ($team->servers as $server) {
if ($server->settings->is_usable === true || $server->settings->is_reachable === true || $server->ip !== '1.2.3.4') {
$this->info("Disabling server {$server->id} {$server->name}");
$server->settings()->update([
'is_usable' => false,
'is_reachable' => false,
]);
$server->update([
'ip' => '1.2.3.4',
]);
}
}
}
}

View File

@@ -9,13 +9,41 @@ use Illuminate\Support\Facades\Process;
class Dev extends Command
{
protected $signature = 'dev:init';
protected $signature = 'dev {--init} {--generate-openapi}';
protected $description = 'Init the app in dev mode';
protected $description = 'Helper commands for development.';
public function handle()
{
if ($this->option('init')) {
$this->init();
return;
}
if ($this->option('generate-openapi')) {
$this->generateOpenApi();
return;
}
}
public function generateOpenApi()
{
// Generate OpenAPI documentation
echo "Generating OpenAPI documentation.\n";
$process = Process::run(['/var/www/html/vendor/bin/openapi', 'app', '-o', 'openapi.yaml']);
$error = $process->errorOutput();
$error = preg_replace('/^.*an object literal,.*$/m', '', $error);
$error = preg_replace('/^\h*\v+/m', '', $error);
echo $error;
echo $process->output();
}
public function init()
{
// Generate APP_KEY if not exists
if (empty(env('APP_KEY'))) {
echo "Generating APP_KEY.\n";
Artisan::call('key:generate');

View File

@@ -81,7 +81,7 @@ class Emails extends Command
}
set_transanctional_email_settings();
$this->mail = new MailMessage();
$this->mail = new MailMessage;
$this->mail->subject('Test Email');
switch ($type) {
case 'updates':
@@ -107,7 +107,7 @@ class Emails extends Command
$confirmed = confirm('Are you sure?');
if ($confirmed) {
foreach ($emails as $email) {
$this->mail = new MailMessage();
$this->mail = new MailMessage;
$this->mail->subject('One-click Services, Docker Compose support');
$unsubscribeUrl = route('unsubscribe.marketing.emails', [
'token' => encrypt($email),
@@ -118,7 +118,7 @@ class Emails extends Command
}
break;
case 'emails-test':
$this->mail = (new Test())->toMail();
$this->mail = (new Test)->toMail();
$this->sendEmail();
break;
case 'database-backup-statuses-daily':
@@ -224,7 +224,7 @@ class Emails extends Command
// $this->sendEmail();
// break;
case 'waitlist-invitation-link':
$this->mail = new MailMessage();
$this->mail = new MailMessage;
$this->mail->view('emails.waitlist-invitation', [
'loginLink' => 'https://coolify.io',
]);
@@ -241,7 +241,7 @@ class Emails extends Command
break;
case 'realusers-before-trial':
$this->mail = new MailMessage();
$this->mail = new MailMessage;
$this->mail->view('emails.before-trial-conversion');
$this->mail->subject('Trial period has been added for all subscription plans.');
$teams = Team::doesntHave('subscription')->where('id', '!=', 0)->get();
@@ -287,7 +287,7 @@ class Emails extends Command
foreach ($admins as $admin) {
$this->info($admin);
}
$this->mail = new MailMessage();
$this->mail = new MailMessage;
$this->mail->view('emails.server-lost-connection', [
'name' => $server->name,
]);

View File

@@ -2,6 +2,8 @@
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;
@@ -11,21 +13,36 @@ use App\Models\ScheduledDatabaseBackup;
use App\Models\Server;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
class Init extends Command
{
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments}';
protected $signature = 'app:init {--full-cleanup} {--cleanup-deployments} {--cleanup-proxy-networks}';
protected $description = 'Cleanup instance related stuffs';
public $servers = null;
public function handle()
{
$this->servers = Server::all();
$this->alive();
get_public_ips();
if (version_compare('4.0.0-beta.312', config('version'), '<=')) {
foreach ($this->servers as $server) {
if ($server->settings->is_metrics_enabled === true) {
$server->settings->update(['is_metrics_enabled' => false]);
}
if ($server->isFunctional()) {
StopSentinel::dispatch($server);
}
}
}
$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";
@@ -33,17 +50,26 @@ class Init extends Command
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 {
$server = Server::find(0)->first();
$server->setupDynamicProxyConfiguration();
$localhost = $this->servers->where('id', 0)->first();
$localhost->setupDynamicProxyConfiguration();
} catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
}
@@ -57,6 +83,13 @@ class Init extends Command
$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;
}
@@ -64,6 +97,79 @@ class Init extends Command
$this->call('cleanup:stucked-resources');
}
private function update_traefik_labels()
{
try {
Server::where('proxy->type', 'TRAEFIK_V2')->update(['proxy->type' => 'TRAEFIK']);
} catch (\Throwable $e) {
echo "Error in updating traefik labels: {$e->getMessage()}\n";
}
}
private function cleanup_unnecessary_dynamic_proxy_configuration()
{
if (isCloud()) {
foreach ($this->servers as $server) {
try {
if (! $server->isFunctional()) {
continue;
}
if ($server->id === 0) {
continue;
}
$file = $server->proxyPath().'/dynamic/coolify.yaml';
return instant_remote_process([
"rm -f $file",
], $server, false);
} catch (\Throwable $e) {
echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n";
}
}
}
}
private function cleanup_unused_network_from_coolify_proxy()
{
foreach ($this->servers as $server) {
if (! $server->isFunctional()) {
continue;
}
if (! $server->isProxyShouldRun()) {
continue;
}
try {
['networks' => $networks, 'allNetworks' => $allNetworks] = collectDockerNetworksByServer($server);
$removeNetworks = $allNetworks->diff($networks);
$commands = collect();
foreach ($removeNetworks as $network) {
$out = instant_remote_process(["docker network inspect -f json $network | jq '.[].Containers | if . == {} then null else . end'"], $server, false);
if (empty($out)) {
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
} else {
$data = collect(json_decode($out, true));
if ($data->count() === 1) {
// If only coolify-proxy itself is connected to that network (it should not be possible, but who knows)
$isCoolifyProxyItself = data_get($data->first(), 'Name') === 'coolify-proxy';
if ($isCoolifyProxyItself) {
$commands->push("docker network disconnect $network coolify-proxy >/dev/null 2>&1 || true");
$commands->push("docker network rm $network >/dev/null 2>&1 || true");
}
}
}
}
if ($commands->isNotEmpty()) {
echo "Cleaning up unused networks from coolify proxy\n";
remote_process(command: $commands, type: ActivityTypes::INLINE->value, server: $server, ignore_errors: false);
}
} catch (\Throwable $e) {
echo "Error in cleaning up unused networks from coolify proxy: {$e->getMessage()}\n";
}
}
}
private function restore_coolify_db_backup()
{
try {
@@ -91,8 +197,7 @@ class Init extends Command
private function cleanup_stucked_helper_containers()
{
$servers = Server::all();
foreach ($servers as $server) {
foreach ($this->servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
@@ -137,7 +242,6 @@ class Init extends Command
private function cleanup_in_progress_application_deployments()
{
// Cleanup any failed deployments
try {
if (isCloud()) {
return;

View File

@@ -82,7 +82,7 @@ class WaitlistInvite extends Command
if (! $already_registered) {
$this->password = Str::password();
User::create([
'name' => Str::of($this->next_patient->email)->before('@'),
'name' => str($this->next_patient->email)->before('@'),
'email' => $this->next_patient->email,
'password' => Hash::make($this->password),
'force_password_reset' => true,
@@ -103,7 +103,7 @@ class WaitlistInvite extends Command
{
$token = Crypt::encryptString("{$this->next_patient->email}@@@$this->password");
$loginLink = route('auth.link', ['token' => $token]);
$mail = new MailMessage();
$mail = new MailMessage;
$mail->view('emails.waitlist-invitation', [
'loginLink' => $loginLink,
]);

View File

@@ -2,16 +2,18 @@
namespace App\Console;
use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CheckForUpdatesJob;
use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\ContainerStatusJob;
use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\PullCoolifyImageJob;
use App\Jobs\PullHelperImageJob;
use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerStatusJob;
use App\Jobs\ServerCheckJob;
use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask;
use App\Models\Server;
@@ -26,25 +28,26 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
$this->all_servers = Server::all();
$settings = InstanceSettings::get();
if (isDev()) {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyMinute();
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->everyTwoHours()->onOneServer();
// Server Jobs
$this->check_scheduled_backups($schedule);
$this->check_resources($schedule);
$this->check_scheduled_backups($schedule);
$this->check_scheduled_tasks($schedule);
$schedule->command('uploads:clear')->everyTwoMinutes();
} else {
// Instance Jobs
$schedule->command('horizon:snapshot')->everyFiveMinutes();
$schedule->command('cleanup:unreachable-servers')->daily();
$schedule->job(new PullCoolifyImageJob)->everyTenMinutes()->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->everyThirtyMinutes()->onOneServer();
$schedule->job(new PullCoolifyImageJob)->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->onOneServer();
$schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer();
// $schedule->job(new CheckResaleLicenseJob)->hourly()->onOneServer();
$this->schedule_updates($schedule);
// Server Jobs
$this->check_scheduled_backups($schedule);
@@ -59,12 +62,26 @@ class Kernel extends ConsoleKernel
private function pull_images($schedule)
{
$settings = InstanceSettings::get();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) {
if ($server->isSentinelEnabled()) {
$schedule->job(new PullSentinelImageJob($server))->everyFiveMinutes()->onOneServer();
$schedule->job(new PullSentinelImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
}
$schedule->job(new PullHelperImageJob($server))->everyFiveMinutes()->onOneServer();
$schedule->job(new PullHelperImageJob($server))->cron($settings->update_check_frequency)->onOneServer();
}
}
private function schedule_updates($schedule)
{
$settings = InstanceSettings::get();
$updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob)->cron($updateCheckFrequency)->onOneServer();
if ($settings->is_auto_update_enabled) {
$autoUpdateFrequency = $settings->auto_update_frequency;
$schedule->job(new UpdateCoolifyJob)->cron($autoUpdateFrequency)->onOneServer();
}
}
@@ -74,19 +91,12 @@ class Kernel extends ConsoleKernel
$servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
} else {
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
$containerServers = $servers->where('settings.is_swarm_worker', false)->where('settings.is_build_server', false);
}
foreach ($containerServers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
}
foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyMinute()->onOneServer();
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
}
}

View File

@@ -5,4 +5,5 @@ namespace App\Enums;
enum ActivityTypes: string
{
case INLINE = 'inline';
case COMMAND = 'command';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Enums;
enum BuildPackTypes: string
{
case NIXPACKS = 'nixpacks';
case STATIC = 'static';
case DOCKERFILE = 'dockerfile';
case DOCKERCOMPOSE = 'dockercompose';
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Enums;
enum NewDatabaseTypes: string
{
case POSTGRESQL = 'postgresql';
case MYSQL = 'mysql';
case MONGODB = 'mongodb';
case REDIS = 'redis';
case MARIADB = 'mariadb';
case KEYDB = 'keydb';
case DRAGONFLY = 'dragonfly';
case CLICKHOUSE = 'clickhouse';
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Enums;
enum NewResourceTypes: string
{
case PUBLIC = 'public';
case PRIVATE_GH_APP = 'private-gh-app';
case PRIVATE_DEPLOY_KEY = 'private-deploy-key';
case DOCKERFILE = 'dockerfile';
case DOCKERCOMPOSE = 'dockercompose';
case DOCKER_IMAGE = 'docker-image';
case SERVICE = 'service';
case POSTGRESQL = 'postgresql';
case MYSQL = 'mysql';
case MONGODB = 'mongodb';
case REDIS = 'redis';
case MARIADB = 'mariadb';
case KEYDB = 'keydb';
case DRAGONFLY = 'dragonfly';
case CLICKHOUSE = 'clickhouse';
}

View File

@@ -5,7 +5,7 @@ namespace App\Enums;
enum ProxyTypes: string
{
case NONE = 'NONE';
case TRAEFIK_V2 = 'TRAEFIK_V2';
case TRAEFIK = 'TRAEFIK';
case NGINX = 'NGINX';
case CADDY = 'CADDY';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums;
enum RedirectTypes: string
{
case BOTH = 'both';
case WWW = 'www';
case NON_WWW = 'non-www';
}

View File

@@ -12,7 +12,7 @@ class DatabaseStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
public ?string $userId = null;
public function __construct($userId = null)
{
@@ -20,15 +20,19 @@ class DatabaseStatusChanged implements ShouldBroadcast
$userId = auth()->user()->id ?? null;
}
if (is_null($userId)) {
throw new \Exception('User id is null');
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): array
public function broadcastOn(): ?array
{
return [
new PrivateChannel("user.{$this->userId}"),
];
if ($this->userId) {
return [
new PrivateChannel("user.{$this->userId}"),
];
}
return null;
}
}

View File

@@ -0,0 +1,32 @@
<?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 FileStorageChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId;
public function __construct($teamId = null)
{
ray($teamId);
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

@@ -12,7 +12,7 @@ class ServiceStatusChanged implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId;
public ?string $userId = null;
public function __construct($userId = null)
{
@@ -20,15 +20,19 @@ class ServiceStatusChanged implements ShouldBroadcast
$userId = auth()->user()->id ?? null;
}
if (is_null($userId)) {
throw new \Exception('User id is null');
return false;
}
$this->userId = $userId;
}
public function broadcastOn(): array
public function broadcastOn(): ?array
{
return [
new PrivateChannel("user.{$this->userId}"),
];
if (! is_null($this->userId)) {
return [
new PrivateChannel("user.{$this->userId}"),
];
}
return null;
}
}

View File

@@ -50,7 +50,7 @@ class Handler extends ExceptionHandler
return response()->json(['message' => $exception->getMessage()], 401);
}
return redirect()->guest($exception->redirectTo() ?? route('login'));
return redirect()->guest($exception->redirectTo($request) ?? route('login'));
}
/**
@@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) {
return;
}
$this->settings = InstanceSettings::get();
$this->settings = \App\Models\InstanceSettings::get();
if ($this->settings->do_not_track) {
return;
}

View File

@@ -1,183 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Application\StopApplication;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Project;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Applications extends Controller
{
public function applications(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = Project::where('team_id', $teamId)->get();
$applications = collect();
$applications->push($projects->pluck('applications')->flatten());
$applications = $applications->flatten();
return response()->json($applications);
}
public function application_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
return response()->json($application);
}
public function update_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
if ($request->collect()->count() == 0) {
return response()->json([
'message' => 'No data provided.',
], 400);
}
$application = Application::where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'success' => false,
'message' => 'Application not found',
], 404);
}
ray($request->collect());
// if ($request->has('domains')) {
// $existingDomains = explode(',', $application->fqdn);
// $newDomains = $request->domains;
// $filteredNewDomains = array_filter($newDomains, function ($domain) use ($existingDomains) {
// return ! in_array($domain, $existingDomains);
// });
// $mergedDomains = array_unique(array_merge($existingDomains, $filteredNewDomains));
// $application->fqdn = implode(',', $mergedDomains);
// $application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application)));
// $application->save();
// }
return response()->json([
'message' => 'Application updated successfully.',
'application' => serialize_api_response($application),
]);
}
public function action_deploy(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$force = $request->query->get('force') ?? false;
$instant_deploy = $request->query->get('instant_deploy') ?? false;
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
is_api: true,
no_questions_asked: $instant_deploy
);
return response()->json(
[
'message' => 'Deployment request queued.',
'deployment_uuid' => $deployment_uuid->toString(),
'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(),
],
200
);
}
public function action_stop(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
$sync = $request->query->get('sync') ?? false;
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
if ($sync) {
StopApplication::run($application);
return response()->json(['message' => 'Stopped the application.'], 200);
} else {
StopApplication::dispatch($application);
return response()->json(['message' => 'Stopping request queued.'], 200);
}
}
public function action_restart(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$application = Application::where('uuid', $uuid)->first();
if (! $application) {
return response()->json(['error' => 'Application not found.'], 404);
}
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
restart_only: true,
is_api: true,
);
return response()->json(
[
'message' => 'Restart request queued.',
'deployment_uuid' => $deployment_uuid->toString(),
'deployment_api_url' => base_url().'/api/v1/deployment/'.$deployment_uuid->toString(),
],
200
);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,234 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartClickhouse;
use App\Actions\Database\StartDragonfly;
use App\Actions\Database\StartKeydb;
use App\Actions\Database\StartMariadb;
use App\Actions\Database\StartMongodb;
use App\Actions\Database\StartMysql;
use App\Actions\Database\StartPostgresql;
use App\Actions\Database\StartRedis;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use Visus\Cuid2\Cuid2;
class Deploy extends Controller
{
public function deployments(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = Server::whereTeamId($teamId)->get();
$deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get([
'id',
'application_id',
'application_name',
'deployment_url',
'pull_request_id',
'server_name',
'server_id',
'status',
])->sortBy('id')->toArray();
return response()->json(serialize_api_response($deployments_per_server), 200);
}
public function deployment_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['error' => 'UUID is required.'], 400);
}
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first()->makeHidden('logs');
if (! $deployment) {
return response()->json(['error' => 'Deployment not found.'], 404);
}
return response()->json(serialize_api_response($deployment), 200);
}
public function deploy(Request $request)
{
$teamId = get_team_id_from_token();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['error' => 'You can only use uuid or tag, not both.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
if (is_null($teamId)) {
return invalid_token();
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['error' => 'You must provide uuid or tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['error' => 'No UUIDs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
$deployments = collect();
$payload = collect();
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
}
}
}
if ($deployments->count() > 0) {
$payload->put('deployments', $deployments->toArray());
return response()->json($payload->toArray(), 200);
}
return response()->json(['error' => 'No resources found.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['error' => 'No TAGs provided.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 400);
}
$message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (! $found_tag) {
// $message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications()->get();
$services = $found_tag->services()->get();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($applications as $resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
}
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
ray($message);
if ($message->count() > 0) {
$payload->put('message', $message->toArray());
if ($deployments->count() > 0) {
$payload->put('details', $deployments->toArray());
}
return response()->json($payload->toArray(), 200);
}
return response()->json(['error' => 'No resources found with this tag.', 'docs' => 'https://coolify.io/docs/api-reference/deploy-webhook'], 404);
}
public function deploy_resource($resource, bool $force = false): array
{
$message = null;
$deployment_uuid = null;
if (gettype($resource) !== 'object') {
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
$type = $resource?->getMorphClass();
if ($type === 'App\Models\Application') {
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
);
$message = "Application {$resource->name} deployment queued.";
} elseif ($type === 'App\Models\StandalonePostgresql') {
StartPostgresql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneRedis') {
StartRedis::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneKeydb') {
StartKeydb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneDragonfly') {
StartDragonfly::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneClickhouse') {
StartClickhouse::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMongodb') {
StartMongodb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMysql') {
StartMysql::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\StandaloneMariadb') {
StartMariadb::run($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
} elseif ($type === 'App\Models\Service') {
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
}
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
}

View File

@@ -0,0 +1,317 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Database\StartDatabase;
use App\Actions\Service\StartService;
use App\Http\Controllers\Controller;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Server;
use App\Models\Tag;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
class DeployController extends Controller
{
private function removeSensitiveData($deployment)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($deployment);
}
$deployment->makeHidden([
'logs',
]);
return serializeApiResponse($deployment);
}
#[OA\Get(
summary: 'List',
description: 'List currently running deployments',
path: '/deployments',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
responses: [
new OA\Response(
response: 200,
description: 'Get all currently running deployments.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/ApplicationDeploymentQueue'),
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function deployments(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = Server::whereTeamId($teamId)->get();
$deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $servers->pluck('id'))->get()->sortBy('id');
$deployments_per_server = $deployments_per_server->map(function ($deployment) {
return $this->removeSensitiveData($deployment);
});
return response()->json($deployments_per_server);
}
#[OA\Get(
summary: 'Get',
description: 'Get deployment by UUID.',
path: '/deployments/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Deployment Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get deployment by UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/ApplicationDeploymentQueue',
)
),
]),
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 deployment_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $uuid)->first();
if (! $deployment) {
return response()->json(['message' => 'Deployment not found.'], 404);
}
return response()->json($this->removeSensitiveData($deployment));
}
#[OA\Get(
summary: 'Deploy',
description: 'Deploy by tag or uuid. `Post` request also accepted.',
path: '/deploy',
security: [
['bearerAuth' => []],
],
tags: ['Deployments'],
parameters: [
new OA\Parameter(name: 'tag', in: 'query', description: 'Tag name(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'uuid', in: 'query', description: 'Resource UUID(s). Comma separated list is also accepted.', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'force', in: 'query', description: 'Force rebuild (without cache)', schema: new OA\Schema(type: 'boolean')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get deployment(s) Uuid\'s',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'deployments' => new OA\Property(
property: 'deployments',
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'message' => ['type' => 'string'],
'resource_uuid' => ['type' => 'string'],
'deployment_uuid' => ['type' => 'string'],
]
),
),
],
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function deploy(Request $request)
{
$teamId = getTeamIdFromToken();
$uuids = $request->query->get('uuid');
$tags = $request->query->get('tag');
$force = $request->query->get('force') ?? false;
if ($uuids && $tags) {
return response()->json(['message' => 'You can only use uuid or tag, not both.'], 400);
}
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($tags) {
return $this->by_tags($tags, $teamId, $force);
} elseif ($uuids) {
return $this->by_uuids($uuids, $teamId, $force);
}
return response()->json(['message' => 'You must provide uuid or tag.'], 400);
}
private function by_uuids(string $uuid, int $teamId, bool $force = false)
{
$uuids = explode(',', $uuid);
$uuids = collect(array_filter($uuids));
if (count($uuids) === 0) {
return response()->json(['message' => 'No UUIDs provided.'], 400);
}
$deployments = collect();
$payload = collect();
foreach ($uuids as $uuid) {
$resource = getResourceByUuid($uuid, $teamId);
if ($resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
} else {
$deployments->push(['message' => $return_message, 'resource_uuid' => $uuid]);
}
}
}
if ($deployments->count() > 0) {
$payload->put('deployments', $deployments->toArray());
return response()->json(serializeApiResponse($payload->toArray()));
}
return response()->json(['message' => 'No resources found.'], 404);
}
public function by_tags(string $tags, int $team_id, bool $force = false)
{
$tags = explode(',', $tags);
$tags = collect(array_filter($tags));
if (count($tags) === 0) {
return response()->json(['message' => 'No TAGs provided.'], 400);
}
$message = collect([]);
$deployments = collect();
$payload = collect();
foreach ($tags as $tag) {
$found_tag = Tag::where(['name' => $tag, 'team_id' => $team_id])->first();
if (! $found_tag) {
// $message->push("Tag {$tag} not found.");
continue;
}
$applications = $found_tag->applications()->get();
$services = $found_tag->services()->get();
if ($applications->count() === 0 && $services->count() === 0) {
$message->push("No resources found for tag {$tag}.");
continue;
}
foreach ($applications as $resource) {
['message' => $return_message, 'deployment_uuid' => $deployment_uuid] = $this->deploy_resource($resource, $force);
if ($deployment_uuid) {
$deployments->push(['resource_uuid' => $resource->uuid, 'deployment_uuid' => $deployment_uuid->toString()]);
}
$message = $message->merge($return_message);
}
foreach ($services as $resource) {
['message' => $return_message] = $this->deploy_resource($resource, $force);
$message = $message->merge($return_message);
}
}
if ($message->count() > 0) {
$payload->put('message', $message->toArray());
if ($deployments->count() > 0) {
$payload->put('details', $deployments->toArray());
}
return response()->json(serializeApiResponse($payload->toArray()));
}
return response()->json(['message' => 'No resources found with this tag.'], 404);
}
public function deploy_resource($resource, bool $force = false): array
{
$message = null;
$deployment_uuid = null;
if (gettype($resource) !== 'object') {
return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid];
}
switch ($resource?->getMorphClass()) {
case 'App\Models\Application':
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $resource,
deployment_uuid: $deployment_uuid,
force_rebuild: $force,
);
$message = "Application {$resource->name} deployment queued.";
break;
case 'App\Models\Service':
StartService::run($resource);
$message = "Service {$resource->name} started. It could take a while, be patient.";
break;
default:
// Database resource
StartDatabase::dispatch($resource);
$resource->update([
'started_at' => now(),
]);
$message = "Database {$resource->name} started.";
break;
}
return ['message' => $message, 'deployment_uuid' => $deployment_uuid];
}
}

View File

@@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class Domains extends Controller
{
public function deleteDomains(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$validator = Validator::make($request->all(), [
'uuid' => 'required|string|exists:applications,uuid',
'domains' => 'required|array',
'domains.*' => 'required|string|distinct',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'message' => 'Validation failed',
'errors' => $validator->errors(),
], 422);
}
$application = Application::where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'success' => false,
'message' => 'Application not found',
], 404);
}
$existingDomains = explode(',', $application->fqdn);
$domainsToDelete = $request->domains;
$updatedDomains = array_diff($existingDomains, $domainsToDelete);
$application->fqdn = implode(',', $updatedDomains);
$application->custom_labels = base64_encode(implode("\n ", generateLabelsApplication($application)));
$application->save();
return response()->json([
'success' => true,
'message' => 'Domains updated successfully',
'application' => $application,
]);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\EnvironmentVariable;
use Illuminate\Http\Request;
class EnvironmentVariablesController extends Controller
{
public function delete_env_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$env = EnvironmentVariable::where('uuid', $request->env_uuid)->first();
if (! $env) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$found_app = $env->resource()->whereRelation('environment.project.team', 'id', $teamId)->first();
if (! $found_app) {
return response()->json([
'message' => 'Environment variable not found.',
], 404);
}
$env->delete();
return response()->json([
'message' => 'Environment variable deleted.',
]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers\Api;
use OpenApi\Attributes as OA;
#[OA\Info(title: 'Coolify', version: '0.1')]
#[OA\Server(url: 'https://app.coolify.io/api/v1', description: 'Coolify Cloud API. Change the host to your own instance if you are self-hosting.')]
#[OA\SecurityScheme(
type: 'http',
scheme: 'bearer',
securityScheme: 'bearerAuth',
description: 'Go to `Keys & Tokens` / `API tokens` and create a new token. Use the token as the bearer token.')]
#[OA\Components(
responses: [
new OA\Response(
response: 400,
description: 'Invalid token.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Invalid token.'),
]
)),
new OA\Response(
response: 401,
description: 'Unauthenticated.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Unauthenticated.'),
]
)),
new OA\Response(
response: 404,
description: 'Resource not found.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'Resource not found.'),
]
)),
],
)]
class OpenApi
{
// This class is used to generate OpenAPI documentation
// for the Coolify API. It is not a controller and does
// not contain any routes. It is used to define the
// OpenAPI metadata and security scheme for the API.
}

View File

@@ -0,0 +1,183 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use OpenApi\Attributes as OA;
class OtherController extends Controller
{
#[OA\Get(
summary: 'Version',
description: 'Get Coolify version.',
path: '/version',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Returns the version of the application',
content: new OA\JsonContent(
type: 'string',
example: 'v4.0.0',
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function version(Request $request)
{
return response(config('version'));
}
#[OA\Get(
summary: 'Enable API',
description: 'Enable API (only with root permissions).',
path: '/enable',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Enable API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'API enabled.'),
]
)),
new OA\Response(
response: 403,
description: 'You are not allowed to enable the API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to enable the API.'),
]
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function enable_api(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
}
$settings = \App\Models\InstanceSettings::get();
$settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200);
}
#[OA\Get(
summary: 'Disable API',
description: 'Disable API (only with root permissions).',
path: '/disable',
security: [
['bearerAuth' => []],
],
responses: [
new OA\Response(
response: 200,
description: 'Disable API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'API disabled.'),
]
)),
new OA\Response(
response: 403,
description: 'You are not allowed to disable the API.',
content: new OA\JsonContent(
type: 'object',
properties: [
new OA\Property(property: 'message', type: 'string', example: 'You are not allowed to disable the API.'),
]
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function disable_api(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
}
$settings = \App\Models\InstanceSettings::get();
$settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200);
}
public function feedback(Request $request)
{
$content = $request->input('content');
$webhook_url = config('coolify.feedback_discord_webhook');
if ($webhook_url) {
Http::post($webhook_url, [
'content' => $content,
]);
}
return response()->json(['message' => 'Feedback sent.'], 200);
}
#[OA\Get(
summary: 'Healthcheck',
description: 'Healthcheck endpoint.',
path: '/healthcheck',
responses: [
new OA\Response(
response: 200,
description: 'Healthcheck endpoint.',
content: new OA\JsonContent(
type: 'string',
example: 'OK',
)),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function healthcheck(Request $request)
{
return 'OK';
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project as ModelsProject;
use Illuminate\Http\Request;
class Project extends Controller
{
public function projects(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$projects = ModelsProject::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json($projects);
}
public function project_by_uuid(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
return response()->json($project);
}
public function environment_details(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$project = ModelsProject::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
$environment = $project->environments()->whereName(request()->environment_name)->first()->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json($environment);
}
}

View File

@@ -0,0 +1,425 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class ProjectController extends Controller
{
#[OA\Get(
summary: 'List',
description: 'list projects.',
path: '/projects',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
responses: [
new OA\Response(
response: 200,
description: 'Get all projects.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Project')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function projects(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$projects = Project::whereTeamId($teamId)->select('id', 'name', 'uuid')->get();
return response()->json(serializeApiResponse($projects),
);
}
#[OA\Get(
summary: 'Get',
description: 'Get project by Uuid.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Project details',
content: new OA\JsonContent(ref: '#/components/schemas/Project')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Project not found.',
),
]
)]
public function project_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$project = Project::whereTeamId($teamId)->whereUuid(request()->uuid)->first()->load(['environments']);
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
return response()->json(
serializeApiResponse($project),
);
}
#[OA\Get(
summary: 'Environment',
description: 'Get environment by name.',
path: '/projects/{uuid}/{environment_name}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Project UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'environment_name', in: 'path', required: true, description: 'Environment name', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Project details',
content: new OA\JsonContent(ref: '#/components/schemas/Environment')),
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 environment_details(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
if (! $request->environment_name) {
return response()->json(['message' => 'Environment name is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
$environment = $project->environments()->whereName($request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$environment = $environment->load(['applications', 'postgresqls', 'redis', 'mongodbs', 'mysqls', 'mariadbs', 'services']);
return response()->json(serializeApiResponse($environment));
}
#[OA\Post(
summary: 'Create',
description: 'Create Project.',
path: '/projects',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
requestBody: new OA\RequestBody(
required: true,
description: 'Project created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Project created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the project.'],
]
)
),
]),
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 create_project(Request $request)
{
$allowedFields = ['name', 'description'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|required',
'description' => 'string|nullable',
]);
$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);
}
$project = Project::create([
'name' => $request->name,
'description' => $request->description,
'team_id' => $teamId,
]);
return response()->json([
'uuid' => $project->uuid,
])->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update Project.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
requestBody: new OA\RequestBody(
required: true,
description: 'Project updated.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The name of the project.'],
'description' => ['type' => 'string', 'description' => 'The description of the project.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Project updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os'],
'name' => ['type' => 'string', 'example' => 'Project Name'],
'description' => ['type' => 'string', 'example' => 'Project Description'],
]
)
),
]),
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 update_project(Request $request)
{
$allowedFields = ['name', 'description'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|nullable',
'description' => 'string|nullable',
]);
$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);
}
$uuid = $request->uuid;
if (! $uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$project->update($request->only($allowedFields));
return response()->json([
'uuid' => $project->uuid,
'name' => $project->name,
'description' => $project->description,
])->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete project by UUID.',
path: '/projects/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Projects'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Project deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Project deleted.'],
]
)
),
]),
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 delete_project(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
if ($project->resource_count() > 0) {
return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400);
}
$project->delete();
return response()->json(['message' => 'Project deleted.']);
}
}

View File

@@ -5,14 +5,42 @@ namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Project;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class Resources extends Controller
class ResourcesController extends Controller
{
#[OA\Get(
summary: 'List',
description: 'Get all resources.',
path: '/resources',
security: [
['bearerAuth' => []],
],
tags: ['Resources'],
responses: [
new OA\Response(
response: 200,
description: 'Get all resources',
content: new OA\JsonContent(
type: 'string',
example: 'Content is very complex. Will be implemented later.',
),
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function resources(Request $request)
{
$teamId = get_team_id_from_token();
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalid_token();
return invalidTokenResponse();
}
$projects = Project::where('team_id', $teamId)->get();
$resources = collect();
@@ -34,6 +62,6 @@ class Resources extends Controller
return $payload;
});
return response()->json($resources);
return response()->json(serializeApiResponse($resources));
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\PrivateKey;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class SecurityController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
$team->makeHidden([
'private_key',
]);
return serializeApiResponse($team);
}
#[OA\Get(
summary: 'List',
description: 'List all private keys.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
responses: [
new OA\Response(
response: 200,
description: 'Get all private keys.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function keys(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$keys = PrivateKey::where('team_id', $teamId)->get();
return response()->json($this->removeSensitiveData($keys));
}
#[OA\Get(
summary: 'Get',
description: 'Get key by UUID.',
path: '/security/keys/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get all private keys.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/PrivateKey')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Private Key not found.',
),
]
)]
public function key_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($key)) {
return response()->json([
'message' => 'Private Key not found.',
], 404);
}
return response()->json($this->removeSensitiveData($key));
}
#[OA\Post(
summary: 'Create',
description: 'Create a new private key.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
requestBody: new OA\RequestBody(
required: true,
content: [
'application/json' => new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['private_key'],
properties: [
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string'],
],
additionalProperties: false,
)
),
]
),
responses: [
new OA\Response(
response: 201,
description: 'The created private key\'s UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function create_key(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|max:255',
'private_key' => 'required|string',
]);
if ($validator->fails()) {
$errors = $validator->errors();
return response()->json([
'message' => 'Validation failed.',
'errors' => $errors,
], 422);
}
if (! $request->name) {
$request->offsetSet('name', generate_random_name());
}
if (! $request->description) {
$request->offsetSet('description', 'Created by Coolify via API');
}
$key = PrivateKey::create([
'team_id' => $teamId,
'name' => $request->name,
'description' => $request->description,
'private_key' => $request->private_key,
]);
return response()->json(serializeApiResponse([
'uuid' => $key->uuid,
]))->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update a private key.',
path: '/security/keys',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
requestBody: new OA\RequestBody(
required: true,
content: [
'application/json' => new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['private_key'],
properties: [
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'private_key' => ['type' => 'string'],
],
additionalProperties: false,
)
),
]
),
responses: [
new OA\Response(
response: 201,
description: 'The updated private key\'s UUID.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function update_key(Request $request)
{
$allowedFields = ['name', 'description', 'private_key'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|max:255',
'private_key' => 'required|string',
]);
$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);
}
$foundKey = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($foundKey)) {
return response()->json([
'message' => 'Private Key not found.',
], 404);
}
$foundKey->update($request->all());
return response()->json(serializeApiResponse([
'uuid' => $foundKey->uuid,
]))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete a private key.',
path: '/security/keys/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Private Keys'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Private Key Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Private Key deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Private Key deleted.'],
]
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
description: 'Private Key not found.',
),
]
)]
public function delete_key(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 422);
}
$key = PrivateKey::where('team_id', $teamId)->where('uuid', $request->uuid)->first();
if (is_null($key)) {
return response()->json(['message' => 'Private Key not found.'], 404);
}
$key->forceDelete();
return response()->json([
'message' => 'Private Key deleted.',
]);
}
}

View File

@@ -1,167 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
class Server extends Controller
{
public function servers(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
return response()->json($servers);
}
public function server_by_uuid(Request $request)
{
$with_resources = $request->query('resources');
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['error' => 'Server not found.'], 404);
}
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
} else {
$server->load(['settings']);
}
return response()->json($server);
}
public function get_domains_by_server(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$uuid = $request->query->get('uuid');
if ($uuid) {
$domains = Application::getDomainsByUuid($uuid);
return response()->json([
'uuid' => $uuid,
'domains' => $domains,
]);
}
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
$settings = InstanceSettings::get();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
$services = $projects->pluck('services')->flatten();
if ($services->count() > 0) {
foreach ($services as $service) {
$service_applications = $service->applications;
if ($service_applications->count() > 0) {
foreach ($service_applications as $application) {
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
return str($fqdn)->replace('http://', '')->replace('https://', '')->replace('/', '');
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
}
}
$domains = $domains->groupBy('ip')->map(function ($domain) {
return $domain->pluck('domain')->flatten();
})->map(function ($domain, $ip) {
return [
'ip' => $ip,
'domains' => $domain,
];
})->values();
return response()->json($domains);
}
}

View File

@@ -0,0 +1,785 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Server\ValidateServer;
use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server as ModelsServer;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
use Stringable;
class ServersController extends Controller
{
private function removeSensitiveDataFromSettings($settings)
{
$token = auth()->user()->currentAccessToken();
if ($token->can('view:sensitive')) {
return serializeApiResponse($settings);
}
$settings = $settings->makeHidden([
'metrics_token',
]);
return serializeApiResponse($settings);
}
private function removeSensitiveData($server)
{
$token = auth()->user()->currentAccessToken();
$server->makeHidden([
'id',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($server);
}
return serializeApiResponse($server);
}
#[OA\Get(
summary: 'List',
description: 'List all servers.',
path: '/servers',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
responses: [
new OA\Response(
response: 200,
description: 'Get all servers.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function servers(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$servers = ModelsServer::whereTeamId($teamId)->select('id', 'name', 'uuid', 'ip', 'user', 'port', 'description')->get()->load(['settings'])->map(function ($server) {
$server['is_reachable'] = $server->settings->is_reachable;
$server['is_usable'] = $server->settings->is_usable;
return $server;
});
$servers = $servers->map(function ($server) {
$settings = $this->removeSensitiveDataFromSettings($server->settings);
$server = $this->removeSensitiveData($server);
data_set($server, 'settings', $settings);
return $server;
});
return response()->json($servers);
}
#[OA\Get(
summary: 'Get',
description: 'Get server by UUID.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get server by UUID',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/Server'
)
),
]),
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 server_by_uuid(Request $request)
{
$with_resources = $request->query('resources');
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
if ($with_resources) {
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
} else {
$server->load(['settings']);
}
$settings = $this->removeSensitiveDataFromSettings($server->settings);
$server = $this->removeSensitiveData($server);
data_set($server, 'settings', $settings);
return response()->json(serializeApiResponse($server));
}
#[OA\Get(
summary: 'Resources',
description: 'Get resources by server.',
path: '/servers/{uuid}/resources',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get resources by server',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'type' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
'status' => ['type' => 'string'],
]
)
)),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function resources_by_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid(request()->uuid)->first();
if (is_null($server)) {
return response()->json(['message' => 'Server not found.'], 404);
}
$server['resources'] = $server->definedResources()->map(function ($resource) {
$payload = [
'id' => $resource->id,
'uuid' => $resource->uuid,
'name' => $resource->name,
'type' => $resource->type(),
'created_at' => $resource->created_at,
'updated_at' => $resource->updated_at,
];
if ($resource->type() === 'service') {
$payload['status'] = $resource->status();
} else {
$payload['status'] = $resource->status;
}
return $payload;
});
$server = $this->removeSensitiveData($server);
ray($server);
return response()->json(serializeApiResponse(data_get($server, 'resources')));
}
#[OA\Get(
summary: 'Domains',
description: 'Get domains by server.',
path: '/servers/{uuid}/domains',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server\'s Uuid', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get domains by server',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'ip' => ['type' => 'string'],
'domains' => ['type' => 'array', 'items' => ['type' => 'string']],
]
)
)),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function domains_by_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->get('uuid');
if ($uuid) {
$domains = Application::getDomainsByUuid($uuid);
return response()->json(serializeApiResponse($domains));
}
$projects = Project::where('team_id', $teamId)->get();
$domains = collect();
$applications = $projects->pluck('applications')->flatten();
$settings = \App\Models\InstanceSettings::get();
if ($applications->count() > 0) {
foreach ($applications as $application) {
$ip = $application->destination->server->ip;
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
$f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
return str(str($f[0])->explode(':')[0]);
})->filter(function (Stringable $fqdn) {
return $fqdn->isNotEmpty();
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
$services = $projects->pluck('services')->flatten();
if ($services->count() > 0) {
foreach ($services as $service) {
$service_applications = $service->applications;
if ($service_applications->count() > 0) {
foreach ($service_applications as $application) {
$fqdn = str($application->fqdn)->explode(',')->map(function ($fqdn) {
$f = str($fqdn)->replace('http://', '')->replace('https://', '')->explode('/');
return str(str($f[0])->explode(':')[0]);
})->filter(function (Stringable $fqdn) {
return $fqdn->isNotEmpty();
});
if ($ip === 'host.docker.internal') {
if ($settings->public_ipv4) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv4,
]);
}
if ($settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $settings->public_ipv6,
]);
}
if (! $settings->public_ipv4 && ! $settings->public_ipv6) {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
} else {
$domains->push([
'domain' => $fqdn,
'ip' => $ip,
]);
}
}
}
}
}
$domains = $domains->groupBy('ip')->map(function ($domain) {
return $domain->pluck('domain')->flatten();
})->map(function ($domain, $ip) {
return [
'ip' => $ip,
'domains' => $domain,
];
})->values();
return response()->json(serializeApiResponse($domains));
}
#[OA\Post(
summary: 'Create',
description: 'Create Server.',
path: '/servers',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
requestBody: new OA\RequestBody(
required: true,
description: 'Server created.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'example' => 'My Server', 'description' => 'The name of the server.'],
'description' => ['type' => 'string', 'example' => 'My Server Description', 'description' => 'The description of the server.'],
'ip' => ['type' => 'string', 'example' => '127.0.0.1', 'description' => 'The IP of the server.'],
'port' => ['type' => 'integer', 'example' => 22, 'description' => 'The port of the server.'],
'user' => ['type' => 'string', 'example' => 'root', 'description' => 'The user of the server.'],
'private_key_uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'example' => false, 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Instant validate.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Server created.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'example' => 'og888os', 'description' => 'The UUID of the server.'],
]
)
),
]),
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 create_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255',
'description' => 'string|nullable',
'ip' => 'string|required',
'port' => 'integer|nullable',
'private_key_uuid' => 'string|required',
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
]);
$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);
}
if (! $request->name) {
$request->offsetSet('name', generate_random_name());
}
if (! $request->user) {
$request->offsetSet('user', 'root');
}
if (is_null($request->port)) {
$request->offsetSet('port', 22);
}
if (is_null($request->is_build_server)) {
$request->offsetSet('is_build_server', false);
}
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
if (! $privateKey) {
return response()->json(['message' => 'Private key not found.'], 404);
}
$allServers = ModelsServer::whereIp($request->ip)->get();
if ($allServers->count() > 0) {
return response()->json(['message' => 'Server with this IP already exists.'], 400);
}
$server = ModelsServer::create([
'name' => $request->name,
'description' => $request->description,
'ip' => $request->ip,
'port' => $request->port,
'user' => $request->user,
'private_key_id' => $privateKey->id,
'team_id' => $teamId,
'proxy' => [
'type' => ProxyTypes::TRAEFIK->value,
'status' => ProxyStatus::EXITED->value,
],
]);
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
if ($request->instant_validate) {
ValidateServer::dispatch($server);
}
return response()->json([
'uuid' => $server->uuid,
])->setStatusCode(201);
}
#[OA\Patch(
summary: 'Update',
description: 'Update Server.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
requestBody: new OA\RequestBody(
required: true,
description: 'Server updated.',
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'name' => ['type' => 'string', 'description' => 'The name of the server.'],
'description' => ['type' => 'string', 'description' => 'The description of the server.'],
'ip' => ['type' => 'string', 'description' => 'The IP of the server.'],
'port' => ['type' => 'integer', 'description' => 'The port of the server.'],
'user' => ['type' => 'string', 'description' => 'The user of the server.'],
'private_key_uuid' => ['type' => 'string', 'description' => 'The UUID of the private key.'],
'is_build_server' => ['type' => 'boolean', 'description' => 'Is build server.'],
'instant_validate' => ['type' => 'boolean', 'description' => 'Instant validate.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Server updated.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Server')
)
),
]),
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 update_server(Request $request)
{
$allowedFields = ['name', 'description', 'ip', 'port', 'user', 'private_key_uuid', 'is_build_server', 'instant_validate'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'name' => 'string|max:255|nullable',
'description' => 'string|nullable',
'ip' => 'string|nullable',
'port' => 'integer|nullable',
'private_key_uuid' => 'string|nullable',
'user' => 'string|nullable',
'is_build_server' => 'boolean|nullable',
'instant_validate' => 'boolean|nullable',
]);
$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);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$server->update($request->only(['name', 'description', 'ip', 'port', 'user']));
if ($request->is_build_server) {
$server->settings()->update([
'is_build_server' => $request->is_build_server,
]);
}
if ($request->instant_validate) {
ValidateServer::dispatch($server);
}
return response()->json(serializeApiResponse($server))->setStatusCode(201);
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete server by UUID.',
path: '/servers/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the server.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Server deleted.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Server deleted.'],
]
)
),
]),
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 delete_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
if ($server->definedResources()->count() > 0) {
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
}
$server->delete();
return response()->json(['message' => 'Server deleted.']);
}
#[OA\Get(
summary: 'Validate',
description: 'Validate server by UUID.',
path: '/servers/{uuid}/validate',
security: [
['bearerAuth' => []],
],
tags: ['Servers'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Server UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 201,
description: 'Server validation started.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Validation started.'],
]
)
),
]),
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 validate_server(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'Uuid is required.'], 422);
}
$server = ModelsServer::whereTeamId($teamId)->whereUuid($request->uuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
ValidateServer::dispatch($server);
return response()->json(['message' => 'Validation started.']);
}
}

View File

@@ -0,0 +1,704 @@
<?php
namespace App\Http\Controllers\Api;
use App\Actions\Service\RestartService;
use App\Actions\Service\StartService;
use App\Actions\Service\StopService;
use App\Http\Controllers\Controller;
use App\Jobs\DeleteResourceJob;
use App\Models\EnvironmentVariable;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class ServicesController extends Controller
{
private function removeSensitiveData($service)
{
$token = auth()->user()->currentAccessToken();
$service->makeHidden([
'id',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($service);
}
$service->makeHidden([
'docker_compose_raw',
'docker_compose',
]);
return serializeApiResponse($service);
}
#[OA\Get(
summary: 'List',
description: 'List all services.',
path: '/services',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
responses: [
new OA\Response(
response: 200,
description: 'Get all services',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Service')
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function services(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$projects = Project::where('team_id', $teamId)->get();
$services = collect();
foreach ($projects as $project) {
$services->push($project->services()->get());
}
foreach ($services as $service) {
$service = $this->removeSensitiveData($service);
}
return response()->json($services->flatten());
}
#[OA\Post(
summary: 'Create',
description: 'Create a one-click service',
path: '/services',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['server_uuid', 'project_uuid', 'environment_name', 'type'],
properties: [
'type' => [
'description' => 'The one-click service type',
'type' => 'string',
'enum' => [
'activepieces',
'appsmith',
'appwrite',
'authentik',
'babybuddy',
'budge',
'changedetection',
'chatwoot',
'classicpress-with-mariadb',
'classicpress-with-mysql',
'classicpress-without-database',
'cloudflared',
'code-server',
'dashboard',
'directus',
'directus-with-postgresql',
'docker-registry',
'docuseal',
'docuseal-with-postgres',
'dokuwiki',
'duplicati',
'emby',
'embystat',
'fider',
'filebrowser',
'firefly',
'formbricks',
'ghost',
'gitea',
'gitea-with-mariadb',
'gitea-with-mysql',
'gitea-with-postgresql',
'glance',
'glances',
'glitchtip',
'grafana',
'grafana-with-postgresql',
'grocy',
'heimdall',
'homepage',
'jellyfin',
'kuzzle',
'listmonk',
'logto',
'mediawiki',
'meilisearch',
'metabase',
'metube',
'minio',
'moodle',
'n8n',
'n8n-with-postgresql',
'next-image-transformation',
'nextcloud',
'nocodb',
'odoo',
'openblocks',
'pairdrop',
'penpot',
'phpmyadmin',
'pocketbase',
'posthog',
'reactive-resume',
'rocketchat',
'shlink',
'slash',
'snapdrop',
'statusnook',
'stirling-pdf',
'supabase',
'syncthing',
'tolgee',
'trigger',
'trigger-with-external-database',
'twenty',
'umami',
'unleash-with-postgresql',
'unleash-without-database',
'uptime-kuma',
'vaultwarden',
'vikunja',
'weblate',
'whoogle',
'wordpress-with-mariadb',
'wordpress-with-mysql',
'wordpress-without-database',
],
],
'name' => ['type' => 'string', 'maxLength' => 255, 'description' => 'Name of the service.'],
'description' => ['type' => 'string', 'nullable' => true, 'description' => 'Description of the service.'],
'project_uuid' => ['type' => 'string', 'description' => 'Project UUID.'],
'environment_name' => ['type' => 'string', 'description' => 'Environment name.'],
'server_uuid' => ['type' => 'string', 'description' => 'Server UUID.'],
'destination_uuid' => ['type' => 'string', 'description' => 'Destination UUID. Required if server has multiple destinations.'],
'instant_deploy' => ['type' => 'boolean', 'default' => false, 'description' => 'Start the service immediately after creation.'],
],
),
),
),
responses: [
new OA\Response(
response: 201,
description: 'Create a service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'uuid' => ['type' => 'string', 'description' => 'Service UUID.'],
'domains' => ['type' => 'array', 'items' => ['type' => 'string'], 'description' => 'Service domains.'],
]
)
),
]
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function create_service(Request $request)
{
$allowedFields = ['type', 'name', 'description', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$validator = customApiValidator($request->all(), [
'type' => 'string|required',
'project_uuid' => 'string|required',
'environment_name' => 'string|required',
'server_uuid' => 'string|required',
'destination_uuid' => 'string',
'name' => 'string|max:255',
'description' => 'string|nullable',
'instant_deploy' => 'boolean',
]);
$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);
}
$serverUuid = $request->server_uuid;
$instantDeploy = $request->instant_deploy ?? false;
if ($request->is_public && ! $request->public_port) {
$request->offsetSet('is_public', false);
}
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) {
return response()->json(['message' => 'Project not found.'], 404);
}
$environment = $project->environments()->where('name', $request->environment_name)->first();
if (! $environment) {
return response()->json(['message' => 'Environment not found.'], 404);
}
$server = Server::whereTeamId($teamId)->whereUuid($serverUuid)->first();
if (! $server) {
return response()->json(['message' => 'Server not found.'], 404);
}
$destinations = $server->destinations();
if ($destinations->count() == 0) {
return response()->json(['message' => 'Server has no destinations.'], 400);
}
if ($destinations->count() > 1 && ! $request->has('destination_uuid')) {
return response()->json(['message' => 'Server has multiple destinations and you do not set destination_uuid.'], 400);
}
$destination = $destinations->first();
$services = get_service_templates();
$serviceKeys = $services->keys();
if ($serviceKeys->contains($request->type)) {
$oneClickServiceName = $request->type;
$oneClickService = data_get($services, "$oneClickServiceName.compose");
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) {
$oneClickDotEnvs = str(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/')->filter(function ($value) {
return ! empty($value);
});
}
if ($oneClickService) {
$service_payload = [
'name' => "$oneClickServiceName-".str()->random(10),
'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id,
'service_type' => $oneClickServiceName,
'server_id' => $server->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
];
if ($oneClickServiceName === 'cloudflared') {
data_set($service_payload, 'connect_to_docker_network', true);
}
$service = Service::create($service_payload);
$service->name = "$oneClickServiceName-".$service->uuid;
$service->save();
if ($oneClickDotEnvs?->count() > 0) {
$oneClickDotEnvs->each(function ($value) use ($service) {
$key = str()->before($value, '=');
$value = str(str()->after($value, '='));
$generatedValue = $value;
if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_');
$generatedValue = generateEnvValue($command->value(), $service);
}
EnvironmentVariable::create([
'key' => $key,
'value' => $generatedValue,
'service_id' => $service->id,
'is_build_time' => false,
'is_preview' => false,
]);
});
}
$service->parse(isNew: true);
if ($instantDeploy) {
StartService::dispatch($service);
}
$domains = $service->applications()->get()->pluck('fqdn')->sort();
$domains = $domains->map(function ($domain) {
return str($domain)->beforeLast(':')->value();
});
return response()->json([
'uuid' => $service->uuid,
'domains' => $domains,
]);
}
return response()->json(['message' => 'Service not found.'], 404);
} else {
return response()->json(['message' => 'Invalid service type.', 'valid_service_types' => $serviceKeys], 400);
}
return response()->json(['message' => 'Invalid service type.'], 400);
}
#[OA\Get(
summary: 'Get',
description: 'Get service by UUID.',
path: '/services/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Get a service by Uuid.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
ref: '#/components/schemas/Service'
)
),
]
),
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 service_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 404);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
$service = $service->load(['applications', 'databases']);
return response()->json($this->removeSensitiveData($service));
}
#[OA\Delete(
summary: 'Delete',
description: 'Delete service by UUID.',
path: '/services/{uuid}',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Delete a service by Uuid',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service deletion request queued.'],
],
)
),
]
),
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 delete_by_uuid(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
if (! $request->uuid) {
return response()->json(['message' => 'UUID is required.'], 404);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
DeleteResourceJob::dispatch($service);
return response()->json([
'message' => 'Service deletion request queued.',
]);
}
#[OA\Get(
summary: 'Start',
description: 'Start service. `Post` request is also accepted.',
path: '/services/{uuid}/start',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Start service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
])
),
]),
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 action_deploy(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('running')) {
return response()->json(['message' => 'Service is already running.'], 400);
}
StartService::dispatch($service);
return response()->json(
[
'message' => 'Service starting request queued.',
],
200
);
}
#[OA\Get(
summary: 'Stop',
description: 'Stop service. `Post` request is also accepted.',
path: '/services/{uuid}/stop',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Stop service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
])
),
]),
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 action_stop(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) {
return response()->json(['message' => 'Service is already stopped.'], 400);
}
StopService::dispatch($service);
return response()->json(
[
'message' => 'Service stopping request queued.',
],
200
);
}
#[OA\Get(
summary: 'Restart',
description: 'Restart service. `Post` request is also accepted.',
path: '/services/{uuid}/restart',
security: [
['bearerAuth' => []],
],
tags: ['Services'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the service.',
required: true,
schema: new OA\Schema(
type: 'string',
format: 'uuid',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'Restart service.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
])
),
]),
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 action_restart(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$uuid = $request->route('uuid');
if (! $uuid) {
return response()->json(['message' => 'UUID is required.'], 400);
}
$service = Service::whereRelation('environment.project.team', 'id', $teamId)->whereUuid($request->uuid)->first();
if (! $service) {
return response()->json(['message' => 'Service not found.'], 404);
}
RestartService::dispatch($service);
return response()->json(
[
'message' => 'Service restarting request queued.',
],
200
);
}
}

View File

@@ -1,74 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class Team extends Controller
{
public function teams(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
return response()->json($teams);
}
public function team_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid'], 404);
}
return response()->json($team);
}
public function members_by_id(Request $request)
{
$id = $request->id;
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['error' => 'Team not found.', 'docs' => 'https://coolify.io/docs/api-reference/get-team-by-teamid-members'], 404);
}
return response()->json($team->members);
}
public function current_team(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team);
}
public function current_team_members(Request $request)
{
$teamId = get_team_id_from_token();
if (is_null($teamId)) {
return invalid_token();
}
$team = auth()->user()->currentTeam();
return response()->json($team->members);
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use OpenApi\Attributes as OA;
class TeamController extends Controller
{
private function removeSensitiveData($team)
{
$token = auth()->user()->currentAccessToken();
$team->makeHidden([
'custom_server_limit',
'pivot',
]);
if ($token->can('view:sensitive')) {
return serializeApiResponse($team);
}
$team->makeHidden([
'smtp_username',
'smtp_password',
'resend_api_key',
'telegram_token',
]);
return serializeApiResponse($team);
}
#[OA\Get(
summary: 'List',
description: 'Get all teams.',
path: '/teams',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/Team')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function teams(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams->sortBy('id');
$teams = $teams->map(function ($team) {
return $this->removeSensitiveData($team);
});
return response()->json(
$teams,
);
}
#[OA\Get(
summary: 'Get',
description: 'Get team by TeamId.',
path: '/teams/{id}',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of teams.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')
),
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 team_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$team = $this->removeSensitiveData($team);
return response()->json(
serializeApiResponse($team),
);
}
#[OA\Get(
summary: 'Members',
description: 'Get members by TeamId.',
path: '/teams/{id}/members',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
parameters: [
new OA\Parameter(name: 'id', in: 'path', required: true, description: 'Team ID', schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(
response: 200,
description: 'List of members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
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 members_by_id(Request $request)
{
$id = $request->id;
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$teams = auth()->user()->teams;
$team = $teams->where('id', $id)->first();
if (is_null($team)) {
return response()->json(['message' => 'Team not found.'], 404);
}
$members = $team->members;
$members->makeHidden([
'pivot',
]);
return response()->json(
serializeApiResponse($members),
);
}
#[OA\Get(
summary: 'Authenticated Team',
description: 'Get currently authenticated team.',
path: '/teams/current',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Current Team.',
content: new OA\JsonContent(ref: '#/components/schemas/Team')),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->currentTeam();
return response()->json(
$this->removeSensitiveData($team),
);
}
#[OA\Get(
summary: 'Authenticated Team Members',
description: 'Get currently authenticated team members.',
path: '/teams/current/members',
security: [
['bearerAuth' => []],
],
tags: ['Teams'],
responses: [
new OA\Response(
response: 200,
description: 'Currently authenticated team members.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(ref: '#/components/schemas/User')
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
]
)]
public function current_team_members(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$team = auth()->user()->currentTeam();
$team->members->makeHidden([
'pivot',
]);
return response()->json(
serializeApiResponse($team->members),
);
}
}

View File

@@ -81,8 +81,8 @@ class Controller extends BaseController
$token = request()->get('token');
if ($token) {
$decrypted = Crypt::decryptString($token);
$email = Str::of($decrypted)->before('@@@');
$password = Str::of($decrypted)->after('@@@');
$email = str($decrypted)->before('@@@');
$password = str($decrypted)->after('@@@');
$user = User::whereEmail($email)->first();
if (! $user) {
return redirect()->route('login');

View File

@@ -21,7 +21,7 @@ class UploadController extends BaseController
$receiver = new FileReceiver('file', $request, HandlerFactory::classFromRequest($request));
if ($receiver->isUploaded() === false) {
throw new UploadMissingFileException();
throw new UploadMissingFileException;
}
$save = $receiver->receive();

View File

@@ -103,7 +103,7 @@ class Bitbucket extends Controller
if ($x_bitbucket_event === 'repo:push') {
if ($application->isDeployable()) {
ray('Deploying '.$application->name.' with branch '.$branch);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -127,7 +127,7 @@ class Bitbucket extends Controller
if ($x_bitbucket_event === 'pullrequest:created') {
if ($application->isPRDeployable()) {
ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {

View File

@@ -123,7 +123,7 @@ class Gitea extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
ray('Deploying '.$application->name.' with branch '.$branch);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -162,7 +162,7 @@ class Gitea extends Controller
if ($x_gitea_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {

View File

@@ -128,7 +128,7 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
ray('Deploying '.$application->name.' with branch '.$branch);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -167,7 +167,7 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {
@@ -340,7 +340,6 @@ class Github extends Controller
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
foreach ($applications as $application) {
$isFunctional = $application->destination->server->isFunctional();
if (! $isFunctional) {
@@ -358,7 +357,7 @@ class Github extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
ray('Deploying '.$application->name.' with branch '.$branch);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -397,7 +396,7 @@ class Github extends Controller
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
ApplicationPreview::create([
@@ -432,8 +431,13 @@ class Github extends Controller
if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$container_name = generateApplicationContainerName($application, $pull_request_id);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
$containers = getCurrentApplicationContainerStatus($application->destination->server, $application->id, $pull_request_id);
if ($containers->isNotEmpty()) {
$containers->each(function ($container) use ($application) {
$container_name = data_get($container, 'Names');
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
});
}
ApplicationPullRequestUpdateJob::dispatchSync(application: $application, preview: $found, status: ProcessStatus::CLOSED);
$found->delete();

View File

@@ -137,7 +137,7 @@ class Gitlab extends Controller
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
if ($is_watch_path_triggered || is_null($application->watch_paths)) {
ray('Deploying '.$application->name.' with branch '.$branch);
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
queue_application_deployment(
application: $application,
deployment_uuid: $deployment_uuid,
@@ -177,7 +177,7 @@ class Gitlab extends Controller
if ($x_gitlab_event === 'merge_request') {
if ($action === 'open' || $action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$deployment_uuid = new Cuid2;
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (! $found) {
if ($application->build_pack === 'dockercompose') {

View File

@@ -54,6 +54,34 @@ class Stripe extends Controller
$type = data_get($event, 'type');
$data = data_get($event, 'data.object');
switch ($type) {
case 'radar.early_fraud_warning.created':
$stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key'));
$id = data_get($data, 'id');
$charge = data_get($data, 'charge');
if ($charge) {
$stripe->refunds->create(['charge' => $charge]);
}
$pi = data_get($data, 'payment_intent');
$piData = $stripe->paymentIntents->retrieve($pi, []);
$customerId = data_get($piData, 'customer');
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
if (! $subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
}
if (! $subscription) {
Sleep::for(5)->seconds();
$subscription = Subscription::where('stripe_customer_id', $customerId)->first();
}
if ($subscription) {
$subscriptionId = data_get($subscription, 'stripe_subscription_id');
$stripe->subscriptions->cancel($subscriptionId, []);
$subscription->update([
'stripe_invoice_paid' => false,
]);
}
send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}");
break;
case 'checkout.session.completed':
$clientReferenceId = data_get($data, 'client_reference_id');
if (is_null($clientReferenceId)) {
@@ -231,7 +259,7 @@ class Stripe extends Controller
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => true,
'stripe_trial_already_ended' => false,
]);
// send_internal_notification('customer.subscription.deleted for customer: '.$customerId);
break;

View File

@@ -67,5 +67,7 @@ class Kernel extends HttpKernel
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'abilities' => \Laravel\Sanctum\Http\Middleware\CheckAbilities::class,
'ability' => \Laravel\Sanctum\Http\Middleware\CheckForAnyAbility::class,
];
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ApiAllowed
{
public function handle(Request $request, Closure $next): Response
{
ray()->clearAll();
if (isCloud()) {
return $next($request);
}
$settings = \App\Models\InstanceSettings::get();
if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
}
if (! isDev()) {
if ($settings->allowed_ips) {
$allowedIps = explode(',', $settings->allowed_ips);
if (! in_array($request->ip(), $allowedIps)) {
return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
}
}
}
return $next($request);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class IgnoreReadOnlyApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
if ($token->can('read-only')) {
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
return $next($request);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class OnlyRootApiToken
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$token = auth()->user()->currentAccessToken();
if ($token->can('*')) {
return $next($request);
}
return response()->json(['message' => 'You are not allowed to perform this action.'], 403);
}
}

View File

@@ -127,7 +127,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private string $dockerfile_location = '/Dockerfile';
private string $docker_compose_location = '/docker-compose.yml';
private string $docker_compose_location = '/docker-compose.yaml';
private ?string $docker_compose_custom_start_command = null;
@@ -157,6 +157,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private ?string $coolify_variables = null;
private bool $preserveRepository = true;
public $tries = 1;
public function __construct(int $application_deployment_queue_id)
@@ -187,6 +189,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->server = $this->mainServer = $this->destination->server;
$this->serverUser = $this->server->user;
$this->is_this_additional_server = $this->application->additional_servers()->wherePivot('server_id', $this->server->id)->count() > 0;
$this->preserveRepository = $this->application->settings->is_preserve_repository_enabled;
$this->basedir = $this->application->generateBaseDir($this->deployment_uuid);
$this->workdir = "{$this->basedir}".rtrim($this->application->base_directory, '/');
@@ -194,6 +197,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
if ($this->application->settings->custom_internal_name && ! $this->application->settings->is_consistent_container_name_enabled) {
$this->container_name = $this->application->settings->custom_internal_name;
}
ray('New container name: ', $this->container_name);
savePrivateKeyToFs($this->server);
@@ -301,14 +307,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]
);
// $this->execute_remote_command(
// [
// "docker image prune -f >/dev/null 2>&1",
// "hidden" => true,
// "ignore_errors" => true,
// ]
// );
ApplicationStatusChanged::dispatch(data_get($this->application, 'environment.project.team.id'));
}
}
@@ -459,7 +457,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build";
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build --pull";
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
@@ -484,40 +482,41 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
// Start compose file
if ($this->application->settings->is_raw_compose_deployment_enabled) {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->workdir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
$this->write_deployment_configurations();
} else {
$this->write_deployment_configurations();
$server_workdir = $this->application->workdir();
$this->docker_compose_location = '/docker-compose.yaml';
$command = "{$this->coolify_variables} docker compose";
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
$command .= " --env-file {$server_workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$server_workdir} -f {$server_workdir}{$this->docker_compose_location} up -d";
$this->execute_remote_command(
['command' => $command, 'hidden' => true],
);
}
} else {
if ($this->docker_compose_custom_start_command) {
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "cd {$this->basedir} && {$this->docker_compose_custom_start_command}"), 'hidden' => true],
);
$this->write_deployment_configurations();
} else {
$command = "{$this->coolify_variables} docker compose";
if ($this->env_filename) {
$command .= " --env-file {$this->workdir}/{$this->env_filename}";
}
$command .= " --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
$command .= " --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up -d";
$this->write_deployment_configurations();
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, $command), 'hidden' => true],
);
$this->write_deployment_configurations();
}
}
@@ -602,26 +601,53 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function write_deployment_configurations()
{
if ($this->preserveRepository) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
if (str($this->configuration_dir)->isNotEmpty()) {
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir",
],
// removing this now as we are using docker cp
// [
// "rm -rf $this->configuration_dir/{*,.*}",
// ],
[
"docker cp {$this->deployment_uuid}:{$this->workdir}/. {$this->configuration_dir}",
],
);
}
if ($this->use_build_server) {
$this->server = $this->build_server;
}
}
if (isset($this->docker_compose_base64)) {
if ($this->use_build_server) {
$this->server = $this->original_server;
}
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$mainDir = $this->configuration_dir;
if ($this->application->settings->is_raw_compose_deployment_enabled) {
$mainDir = $this->application->workdir();
}
if ($this->pull_request_id === 0) {
$composeFileName = "$this->configuration_dir/docker-compose.yml";
$composeFileName = "$mainDir/docker-compose.yaml";
} else {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yml";
$composeFileName = "$mainDir/docker-compose-pr-{$this->pull_request_id}.yaml";
$this->docker_compose_location = "/docker-compose-pr-{$this->pull_request_id}.yaml";
}
$this->execute_remote_command(
[
"mkdir -p $this->configuration_dir",
"mkdir -p $mainDir",
],
[
"echo '{$this->docker_compose_base64}' | base64 -d | tee $composeFileName > /dev/null",
],
[
"echo '{$readme}' > $this->configuration_dir/README.md",
"echo '{$readme}' > $mainDir/README.md",
]
);
if ($this->use_build_server) {
@@ -845,8 +871,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables_preview->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
}
}
// Add HOST if not exists
if ($this->application->environment_variables_preview->where('key', 'HOST')->isEmpty()) {
@@ -889,15 +917,16 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$envs->push($env->key.'='.$real_value);
}
// Add PORT if not exists, use the first port as default
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
if ($this->build_pack !== 'dockercompose') {
if ($this->application->environment_variables->where('key', 'PORT')->isEmpty()) {
$envs->push("PORT={$ports[0]}");
}
}
// Add HOST if not exists
if ($this->application->environment_variables->where('key', 'HOST')->isEmpty()) {
$envs->push('HOST=0.0.0.0');
}
}
if ($envs->isEmpty()) {
$this->env_filename = null;
if ($this->use_build_server) {
@@ -962,16 +991,18 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$nixpacks_php_root_dir = $this->application->environment_variables_preview->where('key', 'NIXPACKS_PHP_ROOT_DIR')->first();
}
if (! $nixpacks_php_fallback_path) {
$nixpacks_php_fallback_path = new EnvironmentVariable();
$nixpacks_php_fallback_path = new EnvironmentVariable;
$nixpacks_php_fallback_path->key = 'NIXPACKS_PHP_FALLBACK_PATH';
$nixpacks_php_fallback_path->value = '/index.php';
$nixpacks_php_fallback_path->is_build_time = false;
$nixpacks_php_fallback_path->application_id = $this->application->id;
$nixpacks_php_fallback_path->save();
}
if (! $nixpacks_php_root_dir) {
$nixpacks_php_root_dir = new EnvironmentVariable();
$nixpacks_php_root_dir = new EnvironmentVariable;
$nixpacks_php_root_dir->key = 'NIXPACKS_PHP_ROOT_DIR';
$nixpacks_php_root_dir->value = '/app/public';
$nixpacks_php_root_dir->is_build_time = false;
$nixpacks_php_root_dir->application_id = $this->application->id;
$nixpacks_php_root_dir->save();
}
@@ -994,7 +1025,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->write_deployment_configurations();
$this->server = $this->original_server;
}
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name) || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
if (count($this->application->ports_mappings_array) > 0 || (bool) $this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty() || $this->pull_request_id !== 0 || str($this->application->custom_docker_run_options)->contains('--ip') || str($this->application->custom_docker_run_options)->contains('--ip6')) {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
if (count($this->application->ports_mappings_array) > 0) {
$this->application_deployment_queue->addLogEntry('Application has ports mapped to the host system, rolling update is not supported.');
@@ -1002,7 +1033,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$this->application_deployment_queue->addLogEntry('Consistent container name feature enabled, rolling update is not supported.');
}
if (isset($this->application->settings->custom_internal_name)) {
if (str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry('Custom internal name is set, rolling update is not supported.');
}
if ($this->pull_request_id !== 0) {
@@ -1076,13 +1107,13 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application_deployment_queue->addLogEntry("Healthcheck logs: {$health_check_logs} | Return code: {$health_check_return_code}");
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'healthy') {
$this->newVersionIsHealthy = true;
$this->application->update(['status' => 'running']);
$this->application_deployment_queue->addLogEntry('New container is healthy.');
break;
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'unhealthy') {
$this->newVersionIsHealthy = false;
$this->query_logs();
break;
@@ -1094,7 +1125,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$sleeptime++;
}
}
if (Str::of($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
if (str($this->saved_outputs->get('health_check'))->replace('"', '')->value() === 'starting') {
$this->query_logs();
}
}
@@ -1242,7 +1273,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
continue;
}
// ray('Deploying to additional destination: ', $server->name);
$deployment_uuid = new Cuid2();
$deployment_uuid = new Cuid2;
queue_application_deployment(
deployment_uuid: $deployment_uuid,
application: $this->application,
@@ -1416,6 +1447,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
$this->nixpacks_plan = json_encode($parsed, JSON_PRETTY_PRINT);
$this->application_deployment_queue->addLogEntry("Final Nixpacks plan: {$this->nixpacks_plan}", hidden: true);
if ($this->nixpacks_type === 'rust') {
// temporary: disable healthcheck for rust because the start phase does not have curl/wget
$this->application->health_check_enabled = false;
$this->application->save();
}
}
}
}
@@ -1518,7 +1554,9 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->application->custom_labels = base64_encode($labels->implode("\n"));
$this->application->save();
} else {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
if (! $this->application->settings->is_container_label_readonly_enabled) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
}
}
if ($this->pull_request_id !== 0) {
$labels = collect(generateLabelsApplication($this->application, $this->preview));
@@ -1535,7 +1573,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile_from_repo', 'ignore_errors' => true,
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$dockerfile = collect(str($this->saved_outputs->get('dockerfile_from_repo'))->trim()->explode("\n"));
$this->application->parseHealthcheckFromDockerfile($dockerfile);
}
$docker_compose = [
@@ -1568,23 +1606,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
],
];
if (isset($this->application->settings->custom_internal_name)) {
$docker_compose['services'][$this->container_name]['networks'][$this->destination->network]['aliases'][] = $this->application->settings->custom_internal_name;
}
// if (str($this->saved_outputs->get('dotenv'))->isNotEmpty()) {
// if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = '.env';
// } else {
// $docker_compose['services'][$this->container_name]['env_file'] = ['.env'];
// }
// }
// if ($this->env_filename) {
// if (data_get($docker_compose, "services.{$this->container_name}.env_file")) {
// $docker_compose['services'][$this->container_name]['env_file'][] = $this->env_filename;
// } else {
// $docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
// }
// }
if (! is_null($this->env_filename)) {
$docker_compose['services'][$this->container_name]['env_file'] = [$this->env_filename];
}
@@ -1636,12 +1657,15 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
],
];
if (data_get($this->application, 'swarm_placement_constraints')) {
$swarm_placement_constraints = Yaml::parse(base64_decode(data_get($this->application, 'swarm_placement_constraints')));
$docker_compose['services'][$this->container_name]['deploy'] = array_merge(
$docker_compose['services'][$this->container_name]['deploy'],
$swarm_placement_constraints
);
}
if (data_get($this->application, 'settings.is_swarm_only_worker_nodes')) {
$docker_compose['services'][$this->container_name]['deploy']['placement'] = [
'constraints' => [
'node.role == worker',
],
];
$docker_compose['services'][$this->container_name]['deploy']['placement']['constraints'][] = 'node.role == worker';
}
if ($this->pull_request_id !== 0) {
$docker_compose['services'][$this->container_name]['deploy']['replicas'] = 1;
@@ -1695,32 +1719,28 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
// if ($this->build_pack === 'dockerfile') {
// $docker_compose['services'][$this->container_name]['build'] = [
// 'context' => $this->workdir,
// 'dockerfile' => $this->workdir . $this->dockerfile_location,
// ];
// }
if ($this->pull_request_id === 0) {
$custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options);
if ((bool) $this->application->settings->is_consistent_container_name_enabled) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
if (! $this->application->settings->custom_internal_name) {
$docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name];
if (count($custom_compose) > 0) {
$ipv4 = data_get($custom_compose, 'ip.0');
$ipv6 = data_get($custom_compose, 'ip6.0');
data_forget($custom_compose, 'ip');
data_forget($custom_compose, 'ip6');
if ($ipv4 || $ipv6) {
data_forget($docker_compose['services'][$this->application->uuid], 'networks');
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
if ($ipv4) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv4_address'] = $ipv4;
}
if ($ipv6) {
$docker_compose['services'][$this->application->uuid]['networks'][$this->destination->network]['ipv6_address'] = $ipv6;
}
$docker_compose['services'][$this->application->uuid] = array_merge_recursive($docker_compose['services'][$this->application->uuid], $custom_compose);
}
} else {
if (count($custom_compose) > 0) {
@@ -1744,7 +1764,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yml > /dev/null"), 'hidden' => true]);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d | tee {$this->workdir}/docker-compose.yaml > /dev/null"), 'hidden' => true]);
}
private function generate_local_persistent_volumes()
@@ -2007,27 +2027,43 @@ 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)
{
try {
$this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
);
} catch (\Exception $error) {
// report error if needed
}
$this->execute_remote_command(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
}
private function stop_running_container(bool $force = false)
{
$this->application_deployment_queue->addLogEntry('Removing old containers.');
if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
$this->graceful_shutdown_container($this->container_name);
} else {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id === 0) {
$containers = $containers->filter(function ($container) {
return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
});
}
$containers->each(function ($container) {
$this->graceful_shutdown_container(data_get($container, 'Names'));
});
}
$containers->each(function ($container) {
$containerName = data_get($container, 'Names');
$this->execute_remote_command(
["docker rm -f $containerName >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
);
});
if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) {
$this->execute_remote_command(
["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
);
}
} else {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('----------------------------------------');
@@ -2038,45 +2074,26 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->application_deployment_queue->update([
'status' => ApplicationDeploymentStatus::FAILED->value,
]);
$this->execute_remote_command(
["docker rm -f $this->container_name >/dev/null 2>&1", 'hidden' => true, 'ignore_errors' => true],
);
$this->graceful_shutdown_container($this->container_name);
}
}
private function build_by_compose_file()
{
$this->application_deployment_queue->addLogEntry('Pulling & building required images.');
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} build"), 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} build"), 'hidden' => true],
);
}
$this->application_deployment_queue->addLogEntry('New images built.');
}
private function start_by_compose_file()
{
if ($this->application->build_pack === 'dockerimage') {
$this->application_deployment_queue->addLogEntry('Pulling latest images from the registry.');
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} pull"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} up --build -d"), 'hidden' => true],
);
} else {
if ($this->use_build_server) {
$this->execute_remote_command(
["{$this->coolify_variables} docker compose --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
["{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->configuration_dir} -f {$this->configuration_dir}{$this->docker_compose_location} up --build -d", 'hidden' => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
[executeInDocker($this->deployment_uuid, "{$this->coolify_variables} docker compose --project-name {$this->application->uuid} --project-directory {$this->workdir} -f {$this->workdir}{$this->docker_compose_location} up --build -d"), 'hidden' => true],
);
}
}
@@ -2107,7 +2124,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), 'hidden' => true, 'save' => 'dockerfile',
]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
$dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
if ($this->pull_request_id === 0) {
foreach ($this->application->build_environment_variables as $env) {
if (data_get($env, 'is_multiline') === true) {
@@ -2232,7 +2249,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
ray($code);
if ($code !== 69420) {
// 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one
if ($this->application->settings->is_consistent_container_name_enabled || isset($this->application->settings->custom_internal_name)) {
if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) {
// do not remove already running container
} else {
$this->application_deployment_queue->addLogEntry('Deployment failed. Removing the new version of your application.', 'stderr');

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Jobs;
use App\Models\InstanceSettings;
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;
use Illuminate\Support\Facades\Http;
class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function handle(): void
{
try {
if (isDev() || isCloud()) {
return;
}
$settings = InstanceSettings::get();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
$versions = $response->json();
$latest_version = data_get($versions, 'coolify.v4.version');
$current_version = config('version');
if (version_compare($latest_version, $current_version, '>')) {
// New version available
$settings->update(['new_version_available' => true]);
} else {
$settings->update(['new_version_available' => false]);
}
}
} catch (\Throwable $e) {
// Consider implementing a notification to administrators
}
}
}

View File

@@ -20,9 +20,9 @@ class CoolifyTask implements ShouldBeEncrypted, ShouldQueue
*/
public function __construct(
public Activity $activity,
public bool $ignore_errors = false,
public $call_event_on_finish = null,
public $call_event_data = null
public bool $ignore_errors,
public $call_event_on_finish,
public $call_event_data,
) {}
/**

View File

@@ -90,6 +90,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{
try {
BackupCreated::dispatch($this->team->id);
// Check if team is exists
if (is_null($this->team)) {
$this->backup->update(['status' => 'failed']);
@@ -98,7 +99,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
$status = Str::of(data_get($this->database, 'status'));
$status = str(data_get($this->database, 'status'));
if (! $status->startsWith('running') && $this->database->id !== 0) {
ray('database not running');
@@ -236,7 +237,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
return;
}
}
$this->backup_dir = backup_dir().'/databases/'.Str::of($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify'];
@@ -332,8 +333,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function backup_standalone_mongodb(string $databaseWithCollections): void
{
try {
ray($this->database->toArray());
$url = $this->database->get_db_url(useInternal: true);
$url = $this->database->internal_db_url;
if ($databaseWithCollections === 'all') {
$commands[] = 'mkdir -p '.$this->backup_dir;
if (str($this->database->image)->startsWith('mongo:4.0')) {
@@ -477,7 +477,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} else {
$network = $this->database->destination->network;
}
$commands[] = "docker run --pull=always -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server);

View File

@@ -28,15 +28,19 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public bool $deleteConfigurations = false) {}
public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations = false,
public bool $deleteVolumes = false) {}
public function handle()
{
try {
$this->resource->forceDelete();
$persistentStorages = collect();
switch ($this->resource->type()) {
case 'application':
StopApplication::run($this->resource);
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource, previewDeployments: true);
break;
case 'standalone-postgresql':
case 'standalone-redis':
@@ -46,6 +50,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource);
break;
case 'service':
@@ -53,6 +58,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
DeleteService::run($this->resource);
break;
}
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
$this->resource?->delete_volumes($persistentStorages);
}
if ($this->deleteConfigurations) {
$this->resource?->delete_configurations();
}
@@ -61,6 +70,7 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
throw $e;
} finally {
$this->resource->forceDelete();
Artisan::queue('cleanup:stucked-resources');
}
}

View File

@@ -12,7 +12,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use RuntimeException;
class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
{
@@ -20,47 +19,43 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
public $timeout = 300;
public ?int $usageBefore = null;
public int|string|null $usageBefore = null;
public function __construct(public Server $server) {}
public function handle(): void
{
try {
$isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) {
$isInprogress = true;
return;
}
});
if ($isInprogress) {
throw new RuntimeException('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
if (! $this->server->isFunctional()) {
return;
}
if ($this->server->settings->is_force_cleanup_enabled) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
return;
}
$this->usageBefore = $this->server->getDiskUsage();
ray('Usage before: '.$this->usageBefore);
if (str($this->usageBefore)->isEmpty() || $this->usageBefore === null || $this->usageBefore === 0) {
Log::info('DockerCleanupJob force cleanup on '.$this->server->name);
CleanupDocker::run(server: $this->server, force: true);
return;
}
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up '.$this->server->name);
CleanupDocker::run($this->server);
CleanupDocker::run(server: $this->server, force: false);
$usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) {
$this->server->team?->notify(new DockerCleanup($this->server, 'Saved '.($this->usageBefore - $usageAfter).'% disk space.'));
// ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
// send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
Log::info('DockerCleanupJob done: Saved '.($this->usageBefore - $usageAfter).'% disk space on '.$this->server->name);
} else {
Log::info('DockerCleanupJob failed to save disk space on '.$this->server->name);
}
} else {
ray('No need to clean up '.$this->server->name);
Log::info('No need to clean up '.$this->server->name);
}
} catch (\Throwable $e) {
// send_internal_notification('DockerCleanupJob failed with: '.$e->getMessage());
ray($e->getMessage());
throw $e;
}

View File

@@ -17,16 +17,13 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 1000;
public function __construct() {}
public function handle(): void
{
try {
if (isDev() || isCloud()) {
return;
}
$settings = InstanceSettings::get();
$server = Server::findOrFail(0);
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) {
@@ -36,7 +33,6 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
$latest_version = get_latest_version_of_coolify();
instant_remote_process(["docker pull -q ghcr.io/coollabsio/coolify:{$latest_version}"], $server, false);
$settings = InstanceSettings::get();
$current_version = config('version');
if (! $settings->is_auto_update_enabled) {
return;
@@ -47,10 +43,6 @@ class PullCoolifyImageJob implements ShouldBeEncrypted, ShouldQueue
if (version_compare($latest_version, $current_version, '<')) {
return;
}
instant_remote_process([
'curl -fsSL https://cdn.coollabs.io/coolify/upgrade.sh -o /data/coolify/source/upgrade.sh',
"bash /data/coolify/source/upgrade.sh $latest_version",
], $server);
} catch (\Throwable $e) {
throw $e;
}

View File

@@ -22,15 +22,16 @@ class PullTemplatesFromCDN implements ShouldBeEncrypted, ShouldQueue
public function handle(): void
{
try {
if (! isDev()) {
ray('PullTemplatesAndVersions service-templates');
$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 {
send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
}
if (isDev() || isCloud()) {
return;
}
ray('PullTemplatesAndVersions service-templates');
$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 {
send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body());
}
} catch (\Throwable $e) {
send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage());

View File

@@ -19,7 +19,7 @@ class SendConfirmationForWaitlistJob implements ShouldBeEncrypted, ShouldQueue
public function handle()
{
try {
$mail = new MailMessage();
$mail = new MailMessage;
$confirmation_url = base_url().'/webhooks/waitlist/confirm?email='.$this->email.'&confirmation_code='.$this->uuid;
$cancel_url = base_url().'/webhooks/waitlist/cancel?email='.$this->email.'&confirmation_code='.$this->uuid;
$mail->view('emails.waitlist-confirmation',

440
app/Jobs/ServerCheckJob.php Normal file
View File

@@ -0,0 +1,440 @@
<?php
namespace App\Jobs;
use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallLogDrain;
use App\Models\ApplicationPreview;
use App\Models\Server;
use App\Models\ServiceDatabase;
use App\Notifications\Container\ContainerRestarted;
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\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $containers;
public $applications;
public $databases;
public $services;
public $previews;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
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()
{
try {
$this->applications = $this->server->applications();
$this->databases = $this->server->databases();
$this->services = $this->server->services()->get();
$this->previews = $this->server->previews();
$up = $this->serverStatus();
if (! $up) {
ray('Server is not reachable.');
return 'Server is not reachable.';
}
if (! $this->server->isFunctional()) {
ray('Server is not ready.');
return 'Server is not ready.';
}
if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) {
['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers();
if (is_null($this->containers)) {
return 'No containers found.';
}
GetContainersStatus::run($this->server, $this->containers, $containerReplicates);
$this->checkLogDrainContainer();
$this->checkSentinel();
}
} catch (\Throwable $e) {
ray($e->getMessage());
return handleError($e);
}
}
private function checkSentinel()
{
if ($this->server->isSentinelEnabled()) {
$sentinelContainerFound = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-sentinel';
})->first();
if ($sentinelContainerFound) {
$status = data_get($sentinelContainerFound, 'State.Status');
if ($status !== 'running') {
PullSentinelImageJob::dispatch($this);
}
}
}
}
private function serverStatus()
{
['uptime' => $uptime] = $this->server->validateConnection();
if ($uptime) {
if ($this->server->unreachable_notification_sent === true) {
$this->server->update(['unreachable_notification_sent' => false]);
}
} else {
// $this->server->team?->notify(new Unreachable($this->server));
foreach ($this->applications as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return false;
}
return true;
}
private function checkLogDrainContainer()
{
$foundLogDrainContainer = $this->containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if ($foundLogDrainContainer) {
$status = data_get($foundLogDrainContainer, 'State.Status');
if ($status !== 'running') {
InstallLogDrain::dispatch($this->server);
}
} else {
InstallLogDrain::dispatch($this->server);
}
}
private function containerStatus()
{
$foundApplications = [];
$foundApplicationPreviews = [];
$foundDatabases = [];
$foundServices = [];
foreach ($this->containers as $container) {
if ($this->server->isSwarm()) {
$labels = data_get($container, 'Spec.Labels');
$uuid = data_get($labels, 'coolify.name');
} else {
$labels = data_get($container, 'Config.Labels');
}
$containerStatus = data_get($container, 'State.Status');
$containerHealth = data_get($container, 'State.Health.Status', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = Arr::undot(format_docker_labels_to_json($labels));
$applicationId = data_get($labels, 'coolify.applicationId');
if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) {
$foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status;
if ($statusFromDb !== $containerStatus) {
$preview->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
} else {
$application = $this->applications->where('id', $applicationId)->first();
if ($application) {
$foundApplications[] = $application->id;
$statusFromDb = $application->status;
if ($statusFromDb !== $containerStatus) {
$application->update(['status' => $containerStatus]);
}
} else {
//Notify user that this container should not be there.
}
}
} else {
$uuid = data_get($labels, 'com.docker.compose.service');
$type = data_get($labels, 'coolify.type');
if ($uuid) {
if ($type === 'service') {
$database_id = data_get($labels, 'coolify.service.subId');
if ($database_id) {
$service_db = ServiceDatabase::where('id', $database_id)->first();
if ($service_db) {
$uuid = data_get($service_db, 'service.uuid');
if ($uuid) {
$isPublic = data_get($service_db, 'is_public');
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($service_db);
// $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server));
}
}
}
}
}
} else {
$database = $this->databases->where('uuid', $uuid)->first();
if ($database) {
$isPublic = data_get($database, 'is_public');
$foundDatabases[] = $database->id;
$statusFromDb = $database->status;
if ($statusFromDb !== $containerStatus) {
$database->update(['status' => $containerStatus]);
}
if ($isPublic) {
$foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid";
} else {
return data_get($value, 'Name') === "/$uuid-proxy";
}
})->first();
if (! $foundTcpProxy) {
StartDatabaseProxy::run($database);
$this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server));
}
}
} else {
// Notify user that this container should not be there.
}
}
}
if (data_get($container, 'Name') === '/coolify-db') {
$foundDatabases[] = 0;
}
}
$serviceLabelId = data_get($labels, 'coolify.serviceId');
if ($serviceLabelId) {
$subType = data_get($labels, 'coolify.service.subType');
$subId = data_get($labels, 'coolify.service.subId');
$service = $this->services->where('id', $serviceLabelId)->first();
if (! $service) {
continue;
}
if ($subType === 'application') {
$service = $service->applications()->where('id', $subId)->first();
} else {
$service = $service->databases()->where('id', $subId)->first();
}
if ($service) {
$foundServices[] = "$service->id-$service->name";
$statusFromDb = $service->status;
if ($statusFromDb !== $containerStatus) {
// ray('Updating status: ' . $containerStatus);
$service->update(['status' => $containerStatus]);
}
}
}
}
$exitedServices = collect([]);
foreach ($this->services as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
if (in_array("$app->id-$app->name", $foundServices)) {
continue;
} else {
$exitedServices->push($app);
}
}
foreach ($dbs as $db) {
if (in_array("$db->id-$db->name", $foundServices)) {
continue;
} else {
$exitedServices->push($db);
}
}
}
$exitedServices = $exitedServices->unique('id');
foreach ($exitedServices as $exitedService) {
if (str($exitedService->status)->startsWith('exited')) {
continue;
}
$name = data_get($exitedService, 'name');
$fqdn = data_get($exitedService, 'fqdn');
if ($name) {
if ($fqdn) {
$containerName = "$name, available at $fqdn";
} else {
$containerName = $name;
}
} else {
if ($fqdn) {
$containerName = $fqdn;
} else {
$containerName = null;
}
}
$projectUuid = data_get($service, 'environment.project.uuid');
$serviceUuid = data_get($service, 'uuid');
$environmentName = data_get($service, 'environment.name');
if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']);
}
$notRunningApplications = $this->applications->pluck('id')->diff($foundApplications);
foreach ($notRunningApplications as $applicationId) {
$application = $this->applications->where('id', $applicationId)->first();
if (str($application->status)->startsWith('exited')) {
continue;
}
$application->update(['status' => 'exited']);
$name = data_get($application, 'name');
$fqdn = data_get($application, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($application, 'environment.project.uuid');
$applicationUuid = data_get($application, 'uuid');
$environment = data_get($application, 'environment.name');
if ($projectUuid && $applicationUuid && $environment) {
$url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews);
foreach ($notRunningApplicationPreviews as $previewId) {
$preview = $this->previews->where('id', $previewId)->first();
if (str($preview->status)->startsWith('exited')) {
continue;
}
$preview->update(['status' => 'exited']);
$name = data_get($preview, 'name');
$fqdn = data_get($preview, 'fqdn');
$containerName = $name ? "$name ($fqdn)" : $fqdn;
$projectUuid = data_get($preview, 'application.environment.project.uuid');
$environmentName = data_get($preview, 'application.environment.name');
$applicationUuid = data_get($preview, 'application.uuid');
if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
$notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases);
foreach ($notRunningDatabases as $database) {
$database = $this->databases->where('id', $database)->first();
if (str($database->status)->startsWith('exited')) {
continue;
}
$database->update(['status' => 'exited']);
$name = data_get($database, 'name');
$fqdn = data_get($database, 'fqdn');
$containerName = $name;
$projectUuid = data_get($database, 'environment.project.uuid');
$environmentName = data_get($database, 'environment.name');
$databaseUuid = data_get($database, 'uuid');
if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid;
} else {
$url = null;
}
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
}
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
if ($this->server->isSwarm()) {
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
} else {
return data_get($value, 'Name') === '/coolify-proxy';
}
})->first();
if (! $foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
@@ -44,57 +43,18 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
}
try {
if ($this->server->isFunctional()) {
$this->cleanup(notify: false);
$this->remove_unnecessary_coolify_yaml();
if ($this->server->isSentinelEnabled()) {
$this->server->checkSentinel();
}
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
// send_internal_notification('ServerStatusJob failed with: '.$e->getMessage());
ray($e->getMessage());
return handleError($e);
}
try {
// $this->check_docker_engine();
} catch (\Throwable $e) {
// Do nothing
}
}
private function check_docker_engine()
{
$version = instant_remote_process([
'docker info',
], $this->server, false);
if (is_null($version)) {
$os = instant_remote_process([
'cat /etc/os-release | grep ^ID=',
], $this->server, false);
$os = str($os)->after('ID=')->trim();
if ($os === 'ubuntu') {
try {
instant_remote_process([
'systemctl start docker',
], $this->server);
} catch (\Throwable $e) {
ray($e->getMessage());
return handleError($e);
}
} else {
try {
instant_remote_process([
'service docker start',
], $this->server);
} catch (\Throwable $e) {
ray($e->getMessage());
return handleError($e);
}
}
}
}
private function remove_unnecessary_coolify_yaml()
@@ -108,28 +68,4 @@ class ServerStatusJob implements ShouldBeEncrypted, ShouldQueue
], $this->server, false);
}
}
public function cleanup(bool $notify = false): void
{
$this->disk_usage = $this->server->getDiskUsage();
if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) {
if ($notify) {
if ($this->server->high_disk_usage_notification_sent) {
ray('high disk usage notification already sent');
return;
} else {
$this->server->high_disk_usage_notification_sent = true;
$this->server->save();
$this->server->team?->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
}
} else {
DockerCleanupJob::dispatchSync($this->server);
$this->cleanup(notify: true);
}
} else {
$this->server->high_disk_usage_notification_sent = false;
$this->server->save();
}
}
}

View File

@@ -21,7 +21,7 @@ class SubscriptionInvoiceFailedJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail = new MailMessage;
$mail->view('emails.subscription-invoice-failed', [
'stripeCustomerPortal' => $session->url,
]);

View File

@@ -23,7 +23,7 @@ class SubscriptionTrialEndedJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail = new MailMessage;
$mail->subject('Action required: You trial in Coolify Cloud ended.');
$mail->view('emails.trial-ended', [
'stripeCustomerPortal' => $session->url,

View File

@@ -23,7 +23,7 @@ class SubscriptionTrialEndsSoonJob implements ShouldBeEncrypted, ShouldQueue
{
try {
$session = getStripeCustomerPortalSession($this->team);
$mail = new MailMessage();
$mail = new MailMessage;
$mail->subject('You trial in Coolify Cloud ends soon.');
$mail->view('emails.trial-ends-soon', [
'stripeCustomerPortal' => $session->url,

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Jobs;
use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use App\Models\Server;
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;
use Illuminate\Support\Facades\Log;
class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 600;
public function handle(): void
{
try {
CheckForUpdatesJob::dispatchSync();
$settings = InstanceSettings::get();
if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.');
return;
}
$server = Server::findOrFail(0);
if (! $server) {
Log::error('Server not found. Cannot proceed with update.');
return;
}
Log::info('Starting Coolify update process...');
UpdateCoolify::run(false); // false means it's not a manual update
$settings->update(['new_version_available' => false]);
Log::info('Coolify update completed successfully.');
} catch (\Throwable $e) {
Log::error('UpdateCoolifyJob failed: '.$e->getMessage());
// Consider implementing a notification to administrators
}
}
}

View File

@@ -38,7 +38,7 @@ class MaintenanceModeDisabledNotification
$class = "App\Http\Controllers\Webhook\\".ucfirst(str($endpoint)->before('::')->value());
$method = str($endpoint)->after('::')->value();
try {
$instance = new $class();
$instance = new $class;
$instance->$method($request);
} catch (\Throwable $th) {
ray($th);

View File

@@ -179,7 +179,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function getProxyType()
{
// Set Default Proxy Type
$this->selectProxy(ProxyTypes::TRAEFIK_V2->value);
$this->selectProxy(ProxyTypes::TRAEFIK->value);
// $proxyTypeSet = $this->createdServer->proxy->type;
// if (!$proxyTypeSet) {
// $this->currentState = 'select-proxy';
@@ -257,7 +257,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->createdServer->settings->is_swarm_manager = $this->isSwarmManager;
$this->createdServer->settings->is_cloudflare_tunnel = $this->isCloudflareTunnel;
$this->createdServer->settings->save();
$this->createdServer->addInitialNetwork();
$this->selectedExistingServer = $this->createdServer->id;
$this->currentState = 'validate-server';
}

View File

@@ -52,7 +52,7 @@ class Docker extends Component
if (request()->query('network_name')) {
$this->network = request()->query('network_name');
} else {
$this->network = new Cuid2(7);
$this->network = new Cuid2;
}
if ($this->servers->count() > 0) {
$this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab();

View File

@@ -2,7 +2,6 @@
namespace App\Livewire;
use App\Models\InstanceSettings;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Http;
@@ -39,7 +38,7 @@ class Help extends Component
$this->rateLimit(3, 30);
$this->validate();
$debug = "Route: {$this->path}";
$mail = new MailMessage();
$mail = new MailMessage;
$mail->view(
'emails.help',
[
@@ -48,7 +47,7 @@ class Help extends Component
]
);
$mail->subject("[HELP]: {$this->subject}");
$settings = InstanceSettings::get();
$settings = \App\Models\InstanceSettings::get();
$type = set_transanctional_email_settings($settings);
if (! $type) {
$url = 'https://app.coolify.io/api/feedback';

View File

@@ -39,7 +39,7 @@ class MonacoEditor extends Component
public function render()
{
if (is_null($this->id)) {
$this->id = new Cuid2(7);
$this->id = new Cuid2;
}
if (is_null($this->name)) {

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