Compare commits

...

155 Commits

Author SHA1 Message Date
Andras Bacsai
7c96b6207a Merge pull request #1299 from coollabsio/next
v4.0.0-beta.72
2023-10-10 14:06:11 +02:00
Andras Bacsai
0be8ffbdc9 feat: add dockerfile location 2023-10-10 14:02:43 +02:00
Andras Bacsai
24fa56762e fix: database backups 2023-10-10 13:10:43 +02:00
Andras Bacsai
84d8e35411 fix: tcp proxy for dbs 2023-10-10 11:42:35 +02:00
Andras Bacsai
3d3ccc435c ui: fix 2023-10-10 11:29:33 +02:00
Andras Bacsai
be3b01472e ui: fix 2023-10-10 11:28:57 +02:00
Andras Bacsai
de6f5b1105 fix: goto 2023-10-10 11:27:39 +02:00
Andras Bacsai
14d9c06dcd feat: able to deploy docker images 2023-10-10 11:16:38 +02:00
Andras Bacsai
8abfaa1967 fix: no env goto envs from dashboard 2023-10-10 10:57:56 +02:00
Andras Bacsai
46f7ae9588 ui: updated dashboard 2023-10-10 10:56:11 +02:00
Andras Bacsai
f2c32b9aeb fixes 2023-10-09 20:37:42 +02:00
Andras Bacsai
3dab1eb92e fix: server saving 2023-10-09 20:12:03 +02:00
Andras Bacsai
9c22e01716 wip: dockerimage 2023-10-09 15:49:48 +02:00
Andras Bacsai
d1c47a4062 version++ 2023-10-09 15:22:51 +02:00
Andras Bacsai
6c3f97d9ae Merge pull request #1297 from coollabsio/next
v4.0.0-beta.71
2023-10-09 15:19:35 +02:00
Andras Bacsai
ebd8e2ce40 fix: check connection 2023-10-09 15:08:28 +02:00
Andras Bacsai
b650f3f754 fix: contact docs 2023-10-09 14:52:24 +02:00
Andras Bacsai
f33ba40478 fix help 2023-10-09 14:48:51 +02:00
Andras Bacsai
5cea9c4603 isInstanceAdmin() 2023-10-09 14:38:44 +02:00
Andras Bacsai
d32832fabc update 2023-10-09 14:32:30 +02:00
Andras Bacsai
165f0a3d4a feat: add email verification for cloud 2023-10-09 14:20:55 +02:00
Andras Bacsai
f14995200b fix: do not reset unreachable count 2023-10-09 12:41:21 +02:00
Andras Bacsai
12bb2ecc4a update 2023-10-09 12:13:30 +02:00
Andras Bacsai
933ec5741d fix: server unreachable count 2023-10-09 12:07:42 +02:00
Andras Bacsai
8004a40139 updates 2023-10-09 11:49:38 +02:00
Andras Bacsai
a6209fbe5c package updates 2023-10-09 11:45:23 +02:00
Andras Bacsai
b47c327b55 Merge pull request #1296 from coollabsio/next
fix: small
2023-10-09 11:24:06 +02:00
Andras Bacsai
25434a7acd fix: small 2023-10-09 11:23:51 +02:00
Andras Bacsai
0de042dbac Merge pull request #1295 from coollabsio/next
v4.0.0-beta.70
2023-10-09 11:23:32 +02:00
Andras Bacsai
eb9e2203b0 fix: fqdn could be null 2023-10-09 11:10:04 +02:00
Andras Bacsai
dcaa7a6ad7 fix: server validation process 2023-10-09 11:00:18 +02:00
Andras Bacsai
5b584a6c6d Merge pull request #1292 from vaporii/main
corrected 'orAGnize' to 'orGAnize'
2023-10-09 09:24:16 +02:00
vaporii
c40ea6f1da corrected 'orAGnize' to 'orGAnize' 2023-10-08 15:01:09 -05:00
Andras Bacsai
61a7b9ac94 fix: able to set base dir for Dockerfile build pack 2023-10-07 12:54:19 +02:00
Andras Bacsai
9e81416fef fix: better unreachable/revived server statuses 2023-10-07 00:51:01 +02:00
Andras Bacsai
c58706e3e4 Merge pull request #1291 from adiologydev/next
fix(create): flex wrap on server & network selection
2023-10-06 20:55:15 +02:00
Andras Bacsai
45bca8649b fix: public repository names 2023-10-06 20:48:03 +02:00
Andras Bacsai
b095b88281 fix: contribution guide 2023-10-06 20:45:35 +02:00
Aditya Tripathi
cb41584137 fix(create): flex wrap on server & network selection 2023-10-06 22:31:20 +05:30
Andras Bacsai
6659153804 version++
fix: unreachable servers
2023-10-06 15:32:46 +02:00
Andras Bacsai
44c429a224 Merge pull request #1290 from coollabsio/next
v4.0.0-beta.69
2023-10-06 14:54:14 +02:00
Andras Bacsai
fe9c501c1d ui 2023-10-06 14:51:50 +02:00
Andras Bacsai
4e94b4a0c1 fix: private repository 2023-10-06 14:50:49 +02:00
Andras Bacsai
2f4d7c0e43 feat: deploy private repo with ssh key 2023-10-06 14:39:30 +02:00
Andras Bacsai
e443fc394a fix: select branch on other git 2023-10-06 13:52:15 +02:00
Andras Bacsai
5b56c50f03 feat: init version of any git deployment 2023-10-06 13:46:42 +02:00
Andras Bacsai
3adeb2f73f fix: set smtp notifications on by default 2023-10-06 12:32:38 +02:00
Andras Bacsai
9eaa13a08a update 2023-10-06 11:17:35 +02:00
Andras Bacsai
df5a4a9667 fix: contact details in emails
fix: pricing plans
2023-10-06 11:16:29 +02:00
Andras Bacsai
26048339d6 Merge pull request #1289 from coollabsio/next
v4.0.0-beta.68
2023-10-06 10:58:12 +02:00
Andras Bacsai
d85af3fefc fix: ui for self-hosted email settings 2023-10-06 10:57:35 +02:00
Andras Bacsai
575338609b fix 2023-10-06 10:47:48 +02:00
Andras Bacsai
d32e43ef37 fix: test emails only available for user owned smtp/resend 2023-10-06 10:42:32 +02:00
Andras Bacsai
a96ef1bfab use instance settigns by default 2023-10-06 10:30:32 +02:00
Andras Bacsai
1bfedf69f2 Merge pull request #1288 from coollabsio/next
v4.0.0-beta.67
2023-10-06 10:24:13 +02:00
Andras Bacsai
208fe7d87b revert 2023-10-06 10:11:04 +02:00
Andras Bacsai
a6d58b5d72 fix: decrease max horizon processes to get lower memory usage 2023-10-06 10:10:47 +02:00
Andras Bacsai
277b4276e6 small fixes 2023-10-06 10:08:50 +02:00
Andras Bacsai
f6adc9285a fix: services - do not remove unnecessary things for now 2023-10-06 10:08:46 +02:00
Andras Bacsai
f03bbe0e95 feat: basedir / monorepo initial support 2023-10-06 10:07:25 +02:00
Andras Bacsai
535375193c cloud: add shared email option to everyone 2023-10-05 14:46:39 +02:00
Andras Bacsai
d79c063fd6 ui: notifications 2023-10-05 14:44:17 +02:00
Andras Bacsai
35f45492e3 fix: email notifications subscription fixed 2023-10-05 14:37:16 +02:00
Andras Bacsai
ab8a7893d9 composer update 2023-10-05 13:57:59 +02:00
Andras Bacsai
76b8d048d4 fix: PR deployments use the first fqdn as base 2023-10-05 13:35:16 +02:00
Andras Bacsai
0e583334e7 version++ 2023-10-05 11:39:02 +02:00
Andras Bacsai
0ad8ca224f Merge pull request #1287 from coollabsio/next
v4.0.0-beta.66
2023-10-05 11:28:50 +02:00
Andras Bacsai
050e56f69a fix: traefik labelling in case of several http and https domain added 2023-10-05 11:27:50 +02:00
Andras Bacsai
6099ac11d9 version++ 2023-10-05 11:26:45 +02:00
Andras Bacsai
adac728a60 Merge pull request #1286 from coollabsio/next
v4.0.0-beta.65
2023-10-05 11:00:56 +02:00
Andras Bacsai
e2e64e36a0 feat: disable service, required version 2023-10-05 10:58:08 +02:00
Andras Bacsai
762af66cbf add deprecated templates in dev 2023-10-05 10:48:26 +02:00
Andras Bacsai
b08f525bd4 fix: move dev data to volumes to prevent permission issues 2023-10-05 08:54:03 +02:00
Andras Bacsai
5ae16b195c remove ray 2023-10-05 08:50:01 +02:00
Andras Bacsai
91db1953ff fix: delete event to deleting 2023-10-05 08:46:26 +02:00
Andras Bacsai
1c8f92d3b7 fix: remove SERVICE_ from deployable compose 2023-10-05 08:40:17 +02:00
Andras Bacsai
4075572dbc fix: visible version number 2023-10-05 08:32:20 +02:00
Andras Bacsai
2971e360d7 revert last commits 2023-10-04 18:06:25 +02:00
Andras Bacsai
32bb2780f2 rename composes 2023-10-04 15:43:29 +02:00
Andras Bacsai
af69575b29 fix: remove SERVICE_ stuff from raw compose
feat: multiport predefined compose
fix: use predefined name as prefix for fqdn
2023-10-04 15:40:08 +02:00
Andras Bacsai
d4a7d0d25f fix: traefik labels for multiport deployments 2023-10-04 15:39:23 +02:00
Andras Bacsai
45f9def0f6 fix: dev compose files 2023-10-04 14:40:33 +02:00
Andras Bacsai
5a90eed7ef fix: compose parser updated 2023-10-04 14:40:26 +02:00
Andras Bacsai
38e1f17edf feat: multiselect removable resources 2023-10-04 14:40:04 +02:00
Andras Bacsai
1651845e20 version++ 2023-10-04 11:54:56 +02:00
Andras Bacsai
38e96548b5 Merge pull request #1285 from coollabsio/next
v4.0.0-beta.64
2023-10-04 11:33:21 +02:00
Andras Bacsai
47e4126dca fix: compose magic 2023-10-04 11:32:27 +02:00
Andras Bacsai
e0b175ab07 version++ 2023-10-04 11:05:40 +02:00
Andras Bacsai
93ec785f4f Merge pull request #1284 from coollabsio/next
v4.0.0-beta.63
2023-10-04 11:01:05 +02:00
Andras Bacsai
e849addab8 dev: switch back to /data (volume errors) 2023-10-04 10:57:44 +02:00
Andras Bacsai
a5e6975dac fix: ui 2023-10-04 10:34:44 +02:00
Andras Bacsai
4ac8e1cc67 fix: services file/dir read from server
ui: fix storages layout
2023-10-04 09:58:39 +02:00
Andras Bacsai
1a5e3a7836 sync volume script 2023-10-03 15:48:07 +02:00
Andras Bacsai
4498d1ed4b fix: service logs visible if the whole service stack is not running 2023-10-03 15:08:23 +02:00
Andras Bacsai
c8b974820b Merge pull request #1282 from coollabsio/next
fix: volume names
2023-10-03 13:26:02 +02:00
Andras Bacsai
527373e297 fix: volume names 2023-10-03 13:25:41 +02:00
Andras Bacsai
a84be8dc33 Merge pull request #1281 from coollabsio/next
v4.0.0-beta.61
2023-10-03 13:14:39 +02:00
Andras Bacsai
bd856f7f67 fix: volume names in services 2023-10-03 13:14:11 +02:00
Andras Bacsai
8ff216e5fb revert 2023-10-03 12:20:09 +02:00
Andras Bacsai
5255311a2e hmm 2023-10-03 12:14:58 +02:00
Andras Bacsai
774a245e84 Merge pull request #1280 from coollabsio/next
v4.0.0-beta.60
2023-10-03 12:12:17 +02:00
Andras Bacsai
194675c838 update commands 2023-10-03 12:08:57 +02:00
Andras Bacsai
75862ca8de rename resource delete 2023-10-03 11:59:30 +02:00
Andras Bacsai
5580a4e704 feat: delete resource command 2023-10-03 11:56:56 +02:00
Andras Bacsai
51e601a303 fix: only use _ in volume names for services 2023-10-03 11:22:35 +02:00
Andras Bacsai
734e9fd68d more explanation 2023-10-03 11:01:01 +02:00
Andras Bacsai
09fc950ae8 fix: add _data to vite ignore 2023-10-03 11:00:52 +02:00
Andras Bacsai
cf6caa279d fix: ui 2023-10-03 09:02:36 +02:00
Andras Bacsai
68c976ab70 fix: show all storages in one place for services 2023-10-03 08:48:07 +02:00
Andras Bacsai
1560ab2a50 fix: UI 2023-10-03 08:22:03 +02:00
Andras Bacsai
e3a6458506 version++ 2023-10-03 08:16:19 +02:00
Andras Bacsai
1768b9374f fix: move /data to ./_data in dev 2023-10-03 08:14:49 +02:00
Andras Bacsai
51d0a30a6c Merge pull request #1279 from coollabsio/next
v4.0.0-beta.59
2023-10-02 18:03:07 +02:00
Andras Bacsai
9701c65297 fix: predefined content for files 2023-10-02 18:02:32 +02:00
Andras Bacsai
58e3bb2571 version++ 2023-10-02 18:01:24 +02:00
Andras Bacsai
31cbd1602d Merge pull request #1278 from coollabsio/next
v4.0.0-beta.58
2023-10-02 17:21:07 +02:00
Andras Bacsai
dd5723d596 service: uptime kume hc updated 2023-10-02 17:17:49 +02:00
Andras Bacsai
620f26a6f1 fix: add destination to new services 2023-10-02 17:12:50 +02:00
Andras Bacsai
540717e809 feat: attach Coolify defined networks to services 2023-10-02 16:57:55 +02:00
Andras Bacsai
d446cd4103 feat: reset root password 2023-10-02 16:38:05 +02:00
Andras Bacsai
7d1a76570c wip 2023-10-02 15:51:06 +02:00
Andras Bacsai
e18766ec21 fix: if waitlist is disabled, redirect to register 2023-10-02 15:09:57 +02:00
Andras Bacsai
3d0354cf7e add contribution guide 2023-10-02 14:58:29 +02:00
Andras Bacsai
af5b9fced1 Merge pull request #1277 from coollabsio/next
v4.0.0-beta.57
2023-10-02 14:19:24 +02:00
Andras Bacsai
ab5202515e fix: service status 2023-10-02 14:12:19 +02:00
Andras Bacsai
f863db7ea5 only 100 2023-10-02 14:01:54 +02:00
Andras Bacsai
3adc0bdd6e fix: only show last 1000 lines 2023-10-02 14:01:16 +02:00
Andras Bacsai
97027875bf feat: container logs 2023-10-02 13:38:16 +02:00
Andras Bacsai
fd9c13009f fix: always pull helper image in dev 2023-10-02 09:37:55 +02:00
Andras Bacsai
46a72fac47 Merge pull request #1276 from alexjustesen/patch-1
wee little missing spacing
2023-10-02 09:32:33 +02:00
Andras Bacsai
5d6ee04991 fix: new volumes for services should have - instead of _ 2023-10-02 09:18:32 +02:00
Andras Bacsai
d523becb29 fix: add uuid to volume names 2023-10-02 09:08:41 +02:00
Andras Bacsai
41672f75d0 fix: only parse expose in dockerfiles if ports_exposes is empty 2023-10-02 09:08:33 +02:00
Alex Justesen
acd8541e68 wee little spacing
noticed some missing spacing during the onboarding process.
2023-10-01 19:16:37 -04:00
Andras Bacsai
ed6af777a4 fix: show real volume names 2023-10-01 20:46:49 +02:00
Andras Bacsai
05f162f4e8 version++ 2023-10-01 20:29:35 +02:00
Andras Bacsai
5e0adc3777 Merge pull request #1275 from coollabsio/next
v4.0.0-beta.56
2023-10-01 18:16:24 +02:00
Andras Bacsai
7d06fc4403 fix: do not show subscription cancelled noti 2023-10-01 18:14:24 +02:00
Andras Bacsai
0e1bcceb8e feat: able to disable container healthchecks
fix: dockerfile based deployments have hc off by default
2023-10-01 18:14:13 +02:00
Andras Bacsai
a922f2fedf version++ 2023-10-01 18:13:34 +02:00
Andras Bacsai
1e0226c8ed Merge pull request #1274 from coollabsio/next
v4.0.0-beta.55
2023-10-01 17:40:31 +02:00
Andras Bacsai
5c45908087 fix: if app settings is not saved to db 2023-10-01 17:39:57 +02:00
Andras Bacsai
aefdc76805 fix: dockerfile expose is not overwritten 2023-10-01 17:27:12 +02:00
Andras Bacsai
b3c8c881b7 Revert "fix: services should have destination as well"
This reverts commit 9ab5a1f7bd.
2023-10-01 15:31:25 +02:00
Andras Bacsai
9ab5a1f7bd fix: services should have destination as well 2023-10-01 13:59:22 +02:00
Andras Bacsai
23968e7886 version++ 2023-10-01 13:28:33 +02:00
Andras Bacsai
390d24b6d7 Merge pull request #1273 from coollabsio/next
v4.0.0-beta.54
2023-10-01 12:36:10 +02:00
Andras Bacsai
e4296345b3 fix: public repo branch selection
fix: commit sha selection in source tabs
2023-10-01 12:29:50 +02:00
Andras Bacsai
bcffbe418b fix: preview deployments name, status etc 2023-10-01 12:02:44 +02:00
Andras Bacsai
bfbee4e78f version+ 2023-10-01 11:33:15 +02:00
Andras Bacsai
2bdb44dac3 Merge pull request #1272 from coollabsio/next
v4.0.0-beta.52
2023-09-30 20:55:24 +02:00
Andras Bacsai
4daa1b8c16 fix: coolify db backup 2023-09-30 20:54:05 +02:00
Andras Bacsai
febc399568 fix: not found base_branch in git webhooks 2023-09-30 20:47:07 +02:00
Andras Bacsai
9b9d4f9941 Merge pull request #1271 from coollabsio/next
v4.0.0-beta.52
2023-09-30 20:27:12 +02:00
Andras Bacsai
f49b87870c fix: backups are now working again 2023-09-30 20:26:42 +02:00
195 changed files with 3748 additions and 1107 deletions

View File

@@ -1,11 +1,3 @@
############################################################################################################
# Development Environment
# User and group id for the user that will run the application inside the container
# Run in your terminal: `id -u` and `id -g` and that's the results
USERID=
GROUPID=
############################################################################################################
APP_NAME=Coolify-localhost APP_NAME=Coolify-localhost
APP_ID=development APP_ID=development
APP_ENV=local APP_ENV=local
@@ -13,6 +5,7 @@ APP_KEY=
APP_DEBUG=true APP_DEBUG=true
APP_URL=http://localhost APP_URL=http://localhost
APP_PORT=8000 APP_PORT=8000
MUX_ENABLED=false
DUSK_DRIVER_URL=http://selenium:4444 DUSK_DRIVER_URL=http://selenium:4444

29
CONTRIBUTION.md Normal file
View File

@@ -0,0 +1,29 @@
# Contributing
> "First, thanks for considering to contribute to my project.
It really means a lot!" - [@andrasbacsai](https://github.com/andrasbacsai)
You can ask for guidance anytime on our
[Discord server](https://coollabs.io/discord) in the `#contribution` channel.
## 1) Setup your development environment
- You need to have Docker Engine (or equivalent) [installed](https://docs.docker.com/engine/install/) on your system.
- For better DX, install [Spin](https://serversideup.net/open-source/spin/).
## 2) Set your environment variables
- Copy [.env.development.example](./.env.development.example) to .env.
## 3) Start & setup Coolify
- Run `spin up` - You can notice that errors will be thrown. Don't worry.
- If you see weird permission errors, especially on Mac, run `sudo spin up` instead.
- Run `./scripts/run setup:dev` - This will generate a secret key for you, delete any existing database layouts, migrate database to the new layout, and seed your database.
## 4) Start development
You can login your Coolify instance at `localhost:8000` with `test@example.com` and `password`.
Your horizon (Laravel scheduler): `localhost:8000/horizon` - Only reachable if you logged in with root user.

View File

@@ -36,7 +36,7 @@ You can find the installation script [here](./scripts/install.sh).
## Support ## Support
Contact us [here](https://docs.coollabs.io/contact). Contact us [here](https://coolify.io/contact).
## Recognitions ## Recognitions

View File

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

View File

@@ -2,12 +2,14 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
class InstallDocker class InstallDocker
{ {
public function __invoke(Server $server) use AsAction;
public function handle(Server $server)
{ {
$dockerVersion = '24.0'; $dockerVersion = '24.0';
$config = base64_encode('{ $config = base64_encode('{

View File

@@ -4,12 +4,14 @@ namespace App\Actions\Service;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Service; use App\Models\Service;
use Symfony\Component\Yaml\Yaml;
class StartService class StartService
{ {
use AsAction; use AsAction;
public function handle(Service $service) public function handle(Service $service)
{ {
$network = $service->destination->network;
$service->saveComposeConfigs(); $service->saveComposeConfigs();
$commands[] = "cd " . $service->workdir(); $commands[] = "cd " . $service->workdir();
$commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'"; $commands[] = "echo '####### Saved configuration files to {$service->workdir()}.'";
@@ -21,6 +23,11 @@ class StartService
$commands[] = "echo '####### Starting containers.'"; $commands[] = "echo '####### Starting containers.'";
$commands[] = "docker compose up -d --remove-orphans --force-recreate"; $commands[] = "docker compose up -d --remove-orphans --force-recreate";
$commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true"; $commands[] = "docker network connect $service->uuid coolify-proxy 2>/dev/null || true";
$compose = data_get($service,'docker_compose',[]);
$serviceNames = data_get(Yaml::parse($compose),'services',[]);
foreach($serviceNames as $serviceName => $serviceConfig){
$commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} 2>/dev/null || true";
}
$activity = remote_process($commands, $service->server); $activity = remote_process($commands, $service->server);
return $activity; return $activity;
} }

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Console\Commands;
use App\Models\Server;
use Illuminate\Console\Command;
class Cloud extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cloud:unused-servers';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Get Unused Servers from Cloud';
/**
* Execute the console command.
*/
public function handle()
{
Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended',true)->each(function($server){
$this->info($server->name);
});
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Console\Commands;
use App\Models\Application;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Illuminate\Console\Command;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select;
class ResourcesDelete extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'resources:delete';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete a resource from the database';
/**
* Execute the console command.
*/
public function handle()
{
$resource = select(
'What resource do you want to delete?',
['Application', 'Database', 'Service'],
);
if ($resource === 'Application') {
$this->deleteApplication();
} elseif ($resource === 'Database') {
$this->deleteDatabase();
} elseif ($resource === 'Service') {
$this->deleteService();
}
}
private function deleteApplication()
{
$applications = Application::all();
if ($applications->count() === 0) {
$this->error('There are no applications to delete.');
return;
}
$applicationsToDelete = multiselect(
'What application do you want to delete?',
$applications->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($applicationsToDelete as $application) {
$toDelete = $applications->where('name', $application)->first();
$toDelete->delete();
}
}
private function deleteDatabase()
{
$databases = StandalonePostgresql::all();
if ($databases->count() === 0) {
$this->error('There are no databases to delete.');
return;
}
$databasesToDelete = multiselect(
'What database do you want to delete?',
$databases->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($databasesToDelete as $database) {
$toDelete = $databases->where('name', $database)->first();
$toDelete->delete();
}
}
private function deleteService()
{
$services = Service::all();
if ($services->count() === 0) {
$this->error('There are no services to delete.');
return;
}
$servicesToDelete = multiselect(
'What service do you want to delete?',
$services->pluck('name')->toArray(),
);
$confirmed = confirm("Are you sure you want to delete all selected resources?");
if (!$confirmed) {
return;
}
foreach ($servicesToDelete as $service) {
$toDelete = $services->where('name', $service)->first();
$toDelete->delete();
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password;
class UsersResetRoot extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'users:reset-root';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset Root Password';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to reset the root password.');
$password = password('Give me a new password for root user: ');
$passwordAgain = password('Again');
if ($password != $passwordAgain) {
$this->error('Passwords do not match.');
return;
}
$this->info('Updating root password...');
try {
User::find(0)->update(['password' => Hash::make($password)]);
$this->info('Root password updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root password.');
return;
}
}
}

View File

@@ -48,7 +48,11 @@ class Kernel extends ConsoleKernel
} }
private function check_resources($schedule) private function check_resources($schedule)
{ {
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true); if (isCloud()) {
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false);
} else {
$servers = Server::all();
}
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
} }

View File

@@ -6,7 +6,7 @@ use App\Models\EnvironmentVariable;
use App\Models\Project; use App\Models\Project;
use App\Models\Server; use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use Illuminate\Support\Facades\Cache; use App\Models\StandaloneDocker;
use Illuminate\Support\Str; use Illuminate\Support\Str;
class ProjectController extends Controller class ProjectController extends Controller
@@ -69,17 +69,19 @@ class ProjectController extends Controller
if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) { if ($type->startsWith('one-click-service-') && !is_null( (int)$server_id)) {
$oneClickServiceName = $type->after('one-click-service-')->value(); $oneClickServiceName = $type->after('one-click-service-')->value();
$oneClickService = data_get($services, "$oneClickServiceName.compose"); $oneClickService = data_get($services, "$oneClickServiceName.compose");
ray($oneClickServiceName);
$oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null); $oneClickDotEnvs = data_get($services, "$oneClickServiceName.envs", null);
if ($oneClickDotEnvs) { if ($oneClickDotEnvs) {
$oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/'); $oneClickDotEnvs = Str::of(base64_decode($oneClickDotEnvs))->split('/\r\n|\r|\n/');
} }
if ($oneClickService) { if ($oneClickService) {
$destination = StandaloneDocker::whereUuid($destination_uuid)->first();
$service = Service::create([ $service = Service::create([
'name' => "$oneClickServiceName-" . Str::random(10), 'name' => "$oneClickServiceName-" . Str::random(10),
'docker_compose_raw' => base64_decode($oneClickService), 'docker_compose_raw' => base64_decode($oneClickService),
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'server_id' => (int) $server_id, 'server_id' => (int) $server_id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
]); ]);
$service->name = "$oneClickServiceName-" . $service->uuid; $service->name = "$oneClickServiceName-" . $service->uuid;
$service->save(); $service->save();
@@ -90,6 +92,7 @@ class ProjectController extends Controller
$generatedValue = $value; $generatedValue = $value;
if ($value->contains('SERVICE_')) { if ($value->contains('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_'); $command = $value->after('SERVICE_')->beforeLast('_');
// TODO: make it shared with Service.php
switch ($command->value()) { switch ($command->value()) {
case 'PASSWORD': case 'PASSWORD':
$generatedValue = Str::password(symbols: false); $generatedValue = Str::password(symbols: false);

View File

@@ -1,32 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\PrivateKey;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
class ServerController extends Controller
{
use AuthorizesRequests, ValidatesRequests;
public function new_server()
{
$privateKeys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
return view('server.create', [
'limit_reached' => false,
'private_keys' => $privateKeys,
]);
}
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$limit_reached = $servers >= $serverLimit;
return view('server.create', [
'limit_reached' => $limit_reached,
'private_keys' => $privateKeys,
]);
}
}

View File

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

View File

@@ -76,7 +76,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
Team::find(currentTeam()->id)->update([ Team::find(currentTeam()->id)->update([
'show_boarding' => false 'show_boarding' => false
]); ]);
ray(currentTeam());
refreshSession(); refreshSession();
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
@@ -221,7 +220,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function installDocker() public function installDocker()
{ {
$this->dockerInstallationStarted = true; $this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->createdServer); $activity = InstallDocker::run($this->createdServer);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
public function dockerInstalledOrSkipped() public function dockerInstalledOrSkipped()

View File

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

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Http\Livewire\Dev;
use Livewire\Component;
class Compose extends Component
{
public string $compose = '';
public string $base64 = '';
public $services;
public function mount() {
$this->services = getServiceTemplates();
}
public function setService(string $selected) {
$this->base64 = data_get($this->services, $selected . '.compose');
if ($this->base64) {
$this->compose = base64_decode($this->base64);
}
}
public function updatedCompose($value) {
$this->base64 = base64_encode($value);
}
public function render()
{
return view('livewire.dev.compose');
}
}

View File

@@ -17,10 +17,10 @@ class General extends Component
public Application $application; public Application $application;
public Collection $services; public Collection $services;
public string $name; public string $name;
public string|null $fqdn; public ?string $fqdn = null;
public string $git_repository; public string $git_repository;
public string $git_branch; public string $git_branch;
public string|null $git_commit_sha; public ?string $git_commit_sha = null;
public string $build_pack; public string $build_pack;
public bool $is_static; public bool $is_static;
@@ -49,6 +49,9 @@ class General extends Component
'application.ports_exposes' => 'required', 'application.ports_exposes' => 'required',
'application.ports_mappings' => 'nullable', 'application.ports_mappings' => 'nullable',
'application.dockerfile' => 'nullable', 'application.dockerfile' => 'nullable',
'application.docker_registry_image_name' => 'nullable',
'application.docker_registry_image_tag' => 'nullable',
'application.dockerfile_location' => 'nullable',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -67,6 +70,9 @@ class General extends Component
'application.ports_exposes' => 'Ports exposes', 'application.ports_exposes' => 'Ports exposes',
'application.ports_mappings' => 'Ports mappings', 'application.ports_mappings' => 'Ports mappings',
'application.dockerfile' => 'Dockerfile', 'application.dockerfile' => 'Dockerfile',
'application.docker_registry_image_name' => 'Docker registry image name',
'application.docker_registry_image_tag' => 'Docker registry image tag',
'application.dockerfile_location' => 'Dockerfile location',
]; ];
@@ -104,6 +110,7 @@ class General extends Component
} }
public function mount() public function mount()
{ {
if (data_get($this->application,'settings')) {
$this->is_static = $this->application->settings->is_static; $this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled; $this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled; $this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
@@ -112,6 +119,7 @@ class General extends Component
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled; $this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled; $this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
} }
}
public function submit() public function submit()
{ {
@@ -125,7 +133,7 @@ class General extends Component
} }
if (data_get($this->application, 'dockerfile')) { if (data_get($this->application, 'dockerfile')) {
$port = get_port_from_dockerfile($this->application->dockerfile); $port = get_port_from_dockerfile($this->application->dockerfile);
if ($port) { if ($port && !$this->application->ports_exposes) {
$this->application->ports_exposes = $port; $this->application->ports_exposes = $port;
} }
} }

View File

@@ -21,12 +21,14 @@ class Heading extends Component
public function check_status() public function check_status()
{ {
if ($this->application->destination->server->isFunctional()) {
dispatch(new ContainerStatusJob($this->application->destination->server)); dispatch(new ContainerStatusJob($this->application->destination->server));
$this->application->refresh(); $this->application->refresh();
$this->application->previews->each(function ($preview) { $this->application->previews->each(function ($preview) {
$preview->refresh(); $preview->refresh();
}); });
} }
}
public function force_deploy_without_cache() public function force_deploy_without_cache()
{ {

View File

@@ -29,7 +29,8 @@ class Form extends Component
public function generate_real_url() public function generate_real_url()
{ {
if (data_get($this->application, 'fqdn')) { if (data_get($this->application, 'fqdn')) {
$url = Url::fromString($this->application->fqdn); $firstFqdn = Str::of($this->application->fqdn)->before(',');
$url = Url::fromString($firstFqdn);
$host = $url->getHost(); $host = $url->getHost();
$this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host); $this->preview_url_template = Str::of($this->application->preview_url_template)->replace('{{domain}}', $host);
} }

View File

@@ -25,7 +25,7 @@ class Previews extends Component
public function load_prs() public function load_prs()
{ {
try { try {
['rate_limit_remaining' => $rate_limit_remaining, 'data' => $data] = git_api(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/pulls"); ['rate_limit_remaining' => $rate_limit_remaining, 'data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/pulls");
$this->rate_limit_remaining = $rate_limit_remaining; $this->rate_limit_remaining = $rate_limit_remaining;
$this->pull_requests = $data->sortBy('number')->values(); $this->pull_requests = $data->sortBy('number')->values();
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -72,8 +72,7 @@ class Previews extends Component
public function stop(int $pull_request_id) public function stop(int $pull_request_id)
{ {
try { try {
$container_name = generateApplicationContainerName($this->application); $container_name = generateApplicationContainerName($this->application, $pull_request_id);
ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false); instant_remote_process(["docker rm -f $container_name"], $this->application->destination->server, throwError: false);
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete(); ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->delete();

View File

@@ -8,6 +8,7 @@ class BackupEdit extends Component
{ {
public $backup; public $backup;
public $s3s; public $s3s;
public ?string $status = null;
public array $parameters; public array $parameters;
protected $rules = [ protected $rules = [

View File

@@ -10,7 +10,7 @@ class CreateScheduledBackup extends Component
public $database; public $database;
public $frequency; public $frequency;
public bool $enabled = true; public bool $enabled = true;
public bool $save_s3 = true; public bool $save_s3 = false;
public $s3_storage_id; public $s3_storage_id;
public $s3s; public $s3s;

View File

@@ -50,8 +50,9 @@ class General extends Component
$this->getDbUrl(); $this->getDbUrl();
} }
public function getDbUrl() { public function getDbUrl() {
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->ip}:{$this->database->public_port}/{$this->database->postgres_db}"; $this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->destination->server->getIp}:{$this->database->public_port}/{$this->database->postgres_db}";
} else { } else {
$this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}"; $this->db_url = "postgres://{$this->database->postgres_user}:{$this->database->postgres_password}@{$this->database->uuid}:5432/{$this->database->postgres_db}";
} }

View File

@@ -32,8 +32,6 @@ class DockerCompose extends Component
- type: volume - type: volume
source: mydata source: mydata
target: /data target: /data
volume:
nocopy: true
- type: bind - type: bind
source: ./var/lib/ghost/data source: ./var/lib/ghost/data
target: /data target: /data

View File

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

View File

@@ -11,6 +11,7 @@ use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Illuminate\Support\Str;
class GithubPrivateRepositoryDeployKey extends Component class GithubPrivateRepositoryDeployKey extends Component
{ {
@@ -29,7 +30,7 @@ class GithubPrivateRepositoryDeployKey extends Component
public string $repository_url; public string $repository_url;
public string $branch; public string $branch;
protected $rules = [ protected $rules = [
'repository_url' => 'required|url', 'repository_url' => 'required',
'branch' => 'required|string', 'branch' => 'required|string',
'port' => 'required|numeric', 'port' => 'required|numeric',
'is_static' => 'required|boolean', 'is_static' => 'required|boolean',
@@ -43,8 +44,8 @@ class GithubPrivateRepositoryDeployKey extends Component
'publish_directory' => 'Publish directory', 'publish_directory' => 'Publish directory',
]; ];
private object $repository_url_parsed; private object $repository_url_parsed;
private GithubApp|GitlabApp|null $git_source = null; private GithubApp|GitlabApp|string $git_source = 'other';
private string $git_host; private ?string $git_host = null;
private string $git_repository; private string $git_repository;
public function mount() public function mount()
@@ -92,6 +93,21 @@ class GithubPrivateRepositoryDeployKey extends Component
$project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $project = Project::where('uuid', $this->parameters['project_uuid'])->first();
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->branch,
'git_full_url' => $this->git_repository,
'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
'private_key_id' => $this->private_key_id,
];
} else {
$application_init = [ $application_init = [
'name' => generate_random_name(), 'name' => generate_random_name(),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
@@ -107,6 +123,8 @@ class GithubPrivateRepositoryDeployKey extends Component
'source_id' => $this->git_source->id, 'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass() 'source_type' => $this->git_source->getMorphClass()
]; ];
}
$application = Application::create($application_init); $application = Application::create($application_init);
$application->settings->is_static = $this->is_static; $application->settings->is_static = $this->is_static;
$application->settings->save(); $application->settings->save();
@@ -134,10 +152,13 @@ class GithubPrivateRepositoryDeployKey extends Component
if ($this->git_host == 'github.com') { if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first(); $this->git_source = GithubApp::where('name', 'Public GitHub')->first();
} elseif ($this->git_host == 'gitlab.com') { return;
$this->git_source = GitlabApp::where('name', 'Public GitLab')->first(); }
} elseif ($this->git_host == 'bitbucket.org') { if (Str::of($this->repository_url)->startsWith('http')) {
// Not supported yet $this->git_host = $this->repository_url_parsed->getHost();
} $this->git_repository = $this->repository_url_parsed->getSegment(1) . '/' . $this->repository_url_parsed->getSegment(2);
$this->git_repository = Str::finish("git@$this->git_host:$this->git_repository", '.git');
}
$this->git_source = 'other';
} }
} }

View File

@@ -26,6 +26,11 @@ class PublicGitRepository extends Component
public string $git_branch = 'main'; public string $git_branch = 'main';
public int $rate_limit_remaining = 0; public int $rate_limit_remaining = 0;
public $rate_limit_reset = 0; public $rate_limit_reset = 0;
private object $repository_url_parsed;
public GithubApp|GitlabApp|string $git_source = 'other';
public string $git_host;
public string $git_repository;
protected $rules = [ protected $rules = [
'repository_url' => 'required|url', 'repository_url' => 'required|url',
'port' => 'required|numeric', 'port' => 'required|numeric',
@@ -38,10 +43,6 @@ class PublicGitRepository extends Component
'is_static' => 'static', 'is_static' => 'static',
'publish_directory' => 'publish directory', 'publish_directory' => 'publish directory',
]; ];
private object $repository_url_parsed;
private GithubApp|GitlabApp|null $git_source = null;
private string $git_host;
private string $git_repository;
public function mount() public function mount()
{ {
@@ -64,7 +65,10 @@ class PublicGitRepository extends Component
} }
$this->emit('success', 'Application settings updated!'); $this->emit('success', 'Application settings updated!');
} }
public function load_any_git()
{
$this->branch_found = true;
}
public function load_branch() public function load_branch()
{ {
try { try {
@@ -76,6 +80,7 @@ class PublicGitRepository extends Component
$this->get_branch(); $this->get_branch();
$this->selected_branch = $this->git_branch; $this->selected_branch = $this->git_branch;
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage());
if (!$this->branch_found && $this->git_branch == 'main') { if (!$this->branch_found && $this->git_branch == 'main') {
try { try {
$this->git_branch = 'master'; $this->git_branch = 'master';
@@ -98,22 +103,24 @@ class PublicGitRepository extends Component
if ($this->git_host == 'github.com') { if ($this->git_host == 'github.com') {
$this->git_source = GithubApp::where('name', 'Public GitHub')->first(); $this->git_source = GithubApp::where('name', 'Public GitHub')->first();
} elseif ($this->git_host == 'gitlab.com') { return;
$this->git_source = GitlabApp::where('name', 'Public GitLab')->first();
} elseif ($this->git_host == 'bitbucket.org') {
// Not supported yet
}
if (is_null($this->git_source)) {
throw new \Exception('Git source not found. What?!');
} }
$this->git_repository = $this->repository_url;
$this->git_source = 'other';
} }
private function get_branch() private function get_branch()
{ {
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = git_api(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); if ($this->git_source === 'other') {
$this->branch_found = true;
return;
}
if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') {
['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}");
$this->rate_limit_reset = Carbon::parse((int)$this->rate_limit_reset)->format('Y-M-d H:i:s'); $this->rate_limit_reset = Carbon::parse((int)$this->rate_limit_reset)->format('Y-M-d H:i:s');
$this->branch_found = true; $this->branch_found = true;
} }
}
public function submit() public function submit()
{ {
@@ -123,9 +130,6 @@ class PublicGitRepository extends Component
$project_uuid = $this->parameters['project_uuid']; $project_uuid = $this->parameters['project_uuid'];
$environment_name = $this->parameters['environment_name']; $environment_name = $this->parameters['environment_name'];
$this->get_git_source();
$this->git_branch = $this->selected_branch ?? $this->git_branch;
$destination = StandaloneDocker::where('uuid', $destination_uuid)->first(); $destination = StandaloneDocker::where('uuid', $destination_uuid)->first();
if (!$destination) { if (!$destination) {
$destination = SwarmDocker::where('uuid', $destination_uuid)->first(); $destination = SwarmDocker::where('uuid', $destination_uuid)->first();
@@ -138,6 +142,19 @@ class PublicGitRepository extends Component
$project = Project::where('uuid', $project_uuid)->first(); $project = Project::where('uuid', $project_uuid)->first();
$environment = $project->load(['environments'])->environments->where('name', $environment_name)->first(); $environment = $project->load(['environments'])->environments->where('name', $environment_name)->first();
if ($this->git_source === 'other') {
$application_init = [
'name' => generate_random_name(),
'git_repository' => $this->git_repository,
'git_branch' => $this->git_branch,
'build_pack' => 'nixpacks',
'ports_exposes' => $this->port,
'publish_directory' => $this->publish_directory,
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination_class,
];
} else {
$application_init = [ $application_init = [
'name' => generate_application_name($this->git_repository, $this->git_branch), 'name' => generate_application_name($this->git_repository, $this->git_branch),
'git_repository' => $this->git_repository, 'git_repository' => $this->git_repository,
@@ -151,6 +168,8 @@ class PublicGitRepository extends Component
'source_id' => $this->git_source->id, 'source_id' => $this->git_source->id,
'source_type' => $this->git_source->getMorphClass() 'source_type' => $this->git_source->getMorphClass()
]; ];
}
$application = Application::create($application_init); $application = Application::create($application_init);
@@ -159,7 +178,6 @@ class PublicGitRepository extends Component
$fqdn = generateFqdn($destination->server, $application->uuid); $fqdn = generateFqdn($destination->server, $application->uuid);
$application->fqdn = $fqdn; $application->fqdn = $fqdn;
$application->name = generate_application_name($this->git_repository, $this->git_branch, $application->uuid);
$application->save(); $application->save();
return redirect()->route('project.application.configuration', [ return redirect()->route('project.application.configuration', [

View File

@@ -45,6 +45,9 @@ CMD ["nginx", "-g", "daemon off;"]
$environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first();
$port = get_port_from_dockerfile($this->dockerfile); $port = get_port_from_dockerfile($this->dockerfile);
if (!$port) {
$port = 80;
}
$application = Application::create([ $application = Application::create([
'name' => 'dockerfile-' . new Cuid2(7), 'name' => 'dockerfile-' . new Cuid2(7),
'repository_project_id' => 0, 'repository_project_id' => 0,
@@ -56,6 +59,7 @@ CMD ["nginx", "-g", "daemon off;"]
'environment_id' => $environment->id, 'environment_id' => $environment->id,
'destination_id' => $destination->id, 'destination_id' => $destination->id,
'destination_type' => $destination_class, 'destination_type' => $destination_class,
'health_check_enabled' => false,
'source_id' => 0, 'source_id' => 0,
'source_type' => GithubApp::class 'source_type' => GithubApp::class
]); ]);

View File

@@ -29,7 +29,8 @@ class Index extends Component
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
$this->query = request()->query(); $this->query = request()->query();
$this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail(); $this->service = Service::whereUuid($this->parameters['service_uuid'])->firstOrFail();
$this->refreshStack(); $this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
} }
public function saveCompose($raw) public function saveCompose($raw)
{ {

View File

@@ -21,7 +21,6 @@ class Navbar extends Component
} }
public function serviceStatusUpdated() public function serviceStatusUpdated()
{ {
ray('serviceStatusUpdated');
$this->check_status(); $this->check_status();
} }
public function check_status() public function check_status()

View File

@@ -33,9 +33,6 @@ class Show extends Component
$this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first(); $this->serviceDatabase = $this->service->databases()->whereName($this->parameters['service_name'])->first();
$this->serviceDatabase->getFilesFromServer(); $this->serviceDatabase->getFilesFromServer();
} }
if (is_null($service)) {
throw new \Exception("Service not found.");
}
} catch(\Throwable $e) { } catch(\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -0,0 +1,35 @@
<?php
namespace App\Http\Livewire\Project\Service;
use App\Models\LocalPersistentVolume;
use Livewire\Component;
class Storage extends Component
{
protected $listeners = ['addNewVolume'];
public $resource;
public function render()
{
return view('livewire.project.service.storage');
}
public function addNewVolume($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
$this->emit('refreshStorages');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
}

View File

@@ -31,8 +31,10 @@ class Danger extends Component
$destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first(); $destination = $this->resource->destination->getMorphClass()::where('id', $this->resource->destination->id)->first();
$server = $destination->server; $server = $destination->server;
} }
if ($this->resource->destination->server->isFunctional()) {
instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server); instant_remote_process(["docker rm -f {$this->resource->uuid}"], $server);
} }
}
$this->resource->delete(); $this->resource->delete();
return redirect()->route('project.resources', [ return redirect()->route('project.resources', [
'project_uuid' => $this->parameters['project_uuid'], 'project_uuid' => $this->parameters['project_uuid'],

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use App\Models\Server;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class GetLogs extends Component
{
public string $outputs = '';
public string $errors = '';
public Server $server;
public ?string $container = null;
public ?bool $streamLogs = false;
public int $numberOfLines = 100;
public function doSomethingWithThisChunkOfOutput($output)
{
$this->outputs .= $output;
}
public function instantSave()
{
}
public function getLogs($refresh = false)
{
if ($this->container) {
$sshCommand = generateSshCommand($this->server, "docker logs -n {$this->numberOfLines} -t {$this->container}");
if ($refresh) {
$this->outputs = '';
}
Process::run($sshCommand, function (string $type, string $output) {
$this->doSomethingWithThisChunkOfOutput($output);
});
}
}
public function render()
{
return view('livewire.project.shared.get-logs');
}
}

View File

@@ -9,6 +9,7 @@ class HealthChecks extends Component
public $resource; public $resource;
protected $rules = [ protected $rules = [
'resource.health_check_enabled' => 'boolean',
'resource.health_check_path' => 'string', 'resource.health_check_path' => 'string',
'resource.health_check_port' => 'nullable|string', 'resource.health_check_port' => 'nullable|string',
'resource.health_check_host' => 'string', 'resource.health_check_host' => 'string',
@@ -22,12 +23,19 @@ class HealthChecks extends Component
'resource.health_check_start_period' => 'integer', 'resource.health_check_start_period' => 'integer',
]; ];
public function instantSave()
{
$this->resource->save();
$this->emit('success', 'Health check updated.');
}
public function submit() public function submit()
{ {
try { try {
$this->validate(); $this->validate();
$this->resource->save(); $this->resource->save();
$this->emit('saved'); $this->emit('success', 'Health check updated.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Http\Livewire\Project\Shared;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandalonePostgresql;
use Livewire\Component;
class Logs extends Component
{
public ?string $type = null;
public Application|StandalonePostgresql|Service $resource;
public Server $server;
public ?string $container = null;
public $parameters;
public $query;
public $status;
public function mount()
{
$this->parameters = get_route_parameters();
$this->query = request()->query();
if (data_get($this->parameters, 'application_uuid')) {
$this->type = 'application';
$this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id);
if ($containers->count() > 0) {
$this->container = data_get($containers[0], 'Names');
}
} else if (data_get($this->parameters, 'database_uuid')) {
$this->type = 'database';
$this->resource = StandalonePostgresql::where('uuid', $this->parameters['database_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->destination->server;
$this->container = $this->resource->uuid;
} else if (data_get($this->parameters, 'service_uuid')) {
$this->type = 'service';
$this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail();
$this->status = $this->resource->status;
$this->server = $this->resource->server;
$this->container = data_get($this->parameters, 'service_name') . '-' . $this->resource->uuid;
}
}
public function render()
{
return view('livewire.project.shared.logs');
}
}

View File

@@ -6,6 +6,7 @@ use Livewire\Component;
class Add extends Component class Add extends Component
{ {
public $uuid;
public $parameters; public $parameters;
public string $name; public string $name;
public string $mount_path; public string $mount_path;
@@ -31,8 +32,9 @@ class Add extends Component
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
$this->emitUp('submit', [ $name = $this->uuid . '-' . $this->name;
'name' => $this->name, $this->emit('addNewVolume', [
'name' => $name,
'mount_path' => $this->mount_path, 'mount_path' => $this->mount_path,
'host_path' => $this->host_path, 'host_path' => $this->host_path,
]); ]);

View File

@@ -8,28 +8,10 @@ use Livewire\Component;
class All extends Component class All extends Component
{ {
public $resource; public $resource;
protected $listeners = ['refreshStorages', 'submit']; protected $listeners = ['refreshStorages'];
public function refreshStorages() public function refreshStorages()
{ {
$this->resource->refresh(); $this->resource->refresh();
} }
public function submit($data)
{
try {
LocalPersistentVolume::create([
'name' => $data['name'],
'mount_path' => $data['mount_path'],
'host_path' => $data['host_path'],
'resource_id' => $this->resource->id,
'resource_type' => $this->resource->getMorphClass(),
]);
$this->resource->refresh();
$this->emit('success', 'Storage added successfully');
$this->emit('clearAddStorage');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -11,7 +11,7 @@ class Show extends Component
public LocalPersistentVolume $storage; public LocalPersistentVolume $storage;
public bool $isReadOnly = false; public bool $isReadOnly = false;
public ?string $modalId = null; public ?string $modalId = null;
public ?string $realName = null; public bool $isFirst = true;
protected $rules = [ protected $rules = [
'storage.name' => 'required|string', 'storage.name' => 'required|string',
@@ -26,11 +26,6 @@ class Show extends Component
public function mount() public function mount()
{ {
if ($this->storage->resource_type === 'App\Models\ServiceApplication' || $this->storage->resource_type === 'App\Models\ServiceDatabase') {
$this->realName = "{$this->storage->service->service->uuid}_{$this->storage->name}";
} else {
$this->realName = $this->storage->name;
}
$this->modalId = new Cuid2(7); $this->modalId = new Cuid2(7);
} }

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Http\Livewire\Server;
use App\Models\PrivateKey;
use Livewire\Component;
class Create extends Component
{
public $private_keys = [];
public bool $limit_reached = false;
public function mount()
{
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
if (!isCloud()) {
$this->limit_reached = false;
return;
}
$team = currentTeam();
$servers = $team->servers->count();
['serverLimit' => $serverLimit] = $team->limits;
$this->limit_reached = $servers >= $serverLimit;
}
public function render()
{
return view('livewire.server.create');
}
}

View File

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

View File

@@ -11,11 +11,12 @@ class Form extends Component
{ {
use AuthorizesRequests; use AuthorizesRequests;
public Server $server; public Server $server;
public $uptime; public bool $isValidConnection = false;
public $dockerVersion; public bool $isValidDocker = false;
public string|null $wildcard_domain = null; public ?string $wildcard_domain = null;
public int $cleanup_after_percentage; public int $cleanup_after_percentage;
public bool $dockerInstallationStarted = false; public bool $dockerInstallationStarted = false;
protected $listeners = ['serverRefresh'];
protected $rules = [ protected $rules = [
'server.name' => 'required|min:6', 'server.name' => 'required|min:6',
@@ -44,37 +45,49 @@ class Form extends Component
$this->wildcard_domain = $this->server->settings->wildcard_domain; $this->wildcard_domain = $this->server->settings->wildcard_domain;
$this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage; $this->cleanup_after_percentage = $this->server->settings->cleanup_after_percentage;
} }
public function instantSave() { public function serverRefresh() {
$this->validateServer();
}
public function instantSave()
{
refresh_server_connection($this->server->privateKey); refresh_server_connection($this->server->privateKey);
$this->validateServer(); $this->validateServer();
$this->server->settings->save(); $this->server->settings->save();
} }
public function installDocker() public function installDocker()
{ {
$this->emit('installDocker');
$this->dockerInstallationStarted = true; $this->dockerInstallationStarted = true;
$activity = resolve(InstallDocker::class)($this->server); $activity = InstallDocker::run($this->server);
$this->emit('newMonitorActivity', $activity->id); $this->emit('newMonitorActivity', $activity->id);
} }
public function validateServer() public function validateServer($install = true)
{ {
try { try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true); $uptime = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->uptime = $uptime; $install && $this->emit('success', 'Server is reachable.');
$this->emit('success', 'Server is reachable.');
} else { } else {
$this->emit('error', 'Server is not reachable.'); $install &&$this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.');
return; return;
} }
if ($dockerVersion) { $dockerInstalled = $this->server->validateDockerEngine();
$this->dockerVersion = $dockerVersion; if ($dockerInstalled) {
$this->emit('success', 'Docker Engine 23+ is installed!'); $install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
} else { } else {
$this->emit('error', 'No Docker Engine or older than 23 version installed.'); $install && $this->installDocker();
return;
}
$dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.');
} else {
$install && $this->installDocker();
return;
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this, customErrorMessage: "Server is not reachable: "); return handleError($e, $this);
} finally { } finally {
$this->emit('proxyStatusUpdated'); $this->emit('proxyStatusUpdated');
} }

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Livewire\Server\PrivateKey;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
class Show extends Component
{
public ?Server $server = null;
public $privateKeys = [];
public $parameters = [];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) {
return redirect()->route('server.all');
}
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.server.private-key.show');
}
}

View File

@@ -11,7 +11,7 @@ class Deploy extends Component
public Server $server; public Server $server;
public bool $traefikDashboardAvailable = false; public bool $traefikDashboardAvailable = false;
public ?string $currentRoute = null; public ?string $currentRoute = null;
protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable']; protected $listeners = ['proxyStatusUpdated', 'traefikDashboardAvailable', 'serverRefresh' => 'proxyStatusUpdated'];
public function mount() { public function mount() {
$this->currentRoute = request()->route()->getName(); $this->currentRoute = request()->route()->getName();

View File

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

View File

@@ -26,7 +26,9 @@ class Status extends Component
} }
public function getProxyStatusWithNoti() public function getProxyStatusWithNoti()
{ {
if ($this->server->isFunctional()) {
$this->emit('success', 'Refreshed proxy status.'); $this->emit('success', 'Refreshed proxy status.');
$this->getProxyStatus(); $this->getProxyStatus();
} }
} }
}

View File

@@ -10,8 +10,10 @@ class Show extends Component
{ {
use AuthorizesRequests; use AuthorizesRequests;
public ?Server $server = null; public ?Server $server = null;
public $parameters = [];
public function mount() public function mount()
{ {
$this->parameters = get_route_parameters();
try { try {
$this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first(); $this->server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($this->server)) { if (is_null($this->server)) {
@@ -21,6 +23,10 @@ class Show extends Component
return handleError($e, $this); return handleError($e, $this);
} }
} }
public function submit()
{
$this->emit('serverRefresh');
}
public function render() public function render()
{ {
return view('livewire.server.show'); return view('livewire.server.show');

View File

@@ -35,31 +35,13 @@ class ShowPrivateKey extends Component
public function checkConnection() public function checkConnection()
{ {
try { try {
['uptime' => $uptime, 'dockerVersion' => $dockerVersion] = validateServer($this->server, true); $uptime = $this->server->validateConnection();
if ($uptime) { if ($uptime) {
$this->server->settings->update([ $this->emit('success', 'Server is reachable.');
'is_reachable' => true
]);
$this->emit('success', 'Server is reachable with this private key.');
} else { } else {
$this->server->settings->update([ $this->emit('error', 'Server is not reachable. Please check your connection and private key configuration.');
'is_reachable' => false,
'is_usable' => false
]);
$this->emit('error', 'Server is not reachable with this private key.');
return; return;
} }
if ($dockerVersion) {
$this->server->settings->update([
'is_usable' => true
]);
$this->emit('success', 'Server is usable for Coolify.');
} else {
$this->server->settings->update([
'is_usable' => false
]);
$this->emit('error', 'Old (lower than 23) or no Docker version detected. Install Docker Engine on the General tab.');
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

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

View File

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

View File

@@ -5,10 +5,11 @@ namespace App\Http\Livewire;
use App\Actions\Server\UpdateCoolify; use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use Livewire\Component; use Livewire\Component;
use Masmerise\Toaster\Toaster; use DanHarrin\LivewireRateLimiting\WithRateLimiting;
class Upgrade extends Component class Upgrade extends Component
{ {
use WithRateLimiting;
public bool $showProgress = false; public bool $showProgress = false;
public bool $isUpgradeAvailable = false; public bool $isUpgradeAvailable = false;
public string $latestVersion = ''; public string $latestVersion = '';
@@ -31,6 +32,7 @@ class Upgrade extends Component
public function upgrade() public function upgrade()
{ {
try { try {
$this->rateLimit(1, 30);
if ($this->showProgress) { if ($this->showProgress) {
return; return;
} }

View File

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

View File

@@ -23,6 +23,9 @@ class Index extends Component
} }
public function mount() public function mount()
{ {
if (config('coolify.waitlist') == false) {
return redirect()->route('register');
}
$this->waitingInLine = Waitlist::whereVerified(true)->count(); $this->waitingInLine = Waitlist::whereVerified(true)->count();
$this->users = User::count(); $this->users = User::count();
if (isDev()) { if (isDev()) {

View File

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

View File

@@ -45,16 +45,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private string $commit; private string $commit;
private bool $force_rebuild; private bool $force_rebuild;
private GithubApp|GitlabApp $source; private ?string $dockerImage = null;
private ?string $dockerImageTag = null;
private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
private ApplicationPreview|null $preview = null; private ApplicationPreview|null $preview = null;
private string $container_name; private string $container_name;
private string|null $currently_running_container_name = null; private string|null $currently_running_container_name = null;
private string $basedir;
private string $workdir; private string $workdir;
private ?string $build_pack = null;
private string $configuration_dir; private string $configuration_dir;
private string $build_workdir;
private string $build_image_name; private string $build_image_name;
private string $production_image_name; private string $production_image_name;
private bool $is_debug_enabled; private bool $is_debug_enabled;
@@ -62,6 +66,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private $env_args; private $env_args;
private $docker_compose; private $docker_compose;
private $docker_compose_base64; private $docker_compose_base64;
private string $dockerfile_location = '/Dockerfile';
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
@@ -73,6 +78,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id); $this->application_deployment_queue = ApplicationDeploymentQueue::find($application_deployment_queue_id);
$this->log_model = $this->application_deployment_queue; $this->log_model = $this->application_deployment_queue;
$this->application = Application::find($this->application_deployment_queue->application_id); $this->application = Application::find($this->application_deployment_queue->application_id);
$this->build_pack = data_get($this->application, 'build_pack');
$this->application_deployment_queue_id = $application_deployment_queue_id; $this->application_deployment_queue_id = $application_deployment_queue_id;
$this->deployment_uuid = $this->application_deployment_queue->deployment_uuid; $this->deployment_uuid = $this->application_deployment_queue->deployment_uuid;
@@ -80,16 +86,20 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->commit = $this->application_deployment_queue->commit; $this->commit = $this->application_deployment_queue->commit;
$this->force_rebuild = $this->application_deployment_queue->force_rebuild; $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->source = $this->application->source->getMorphClass()::where('id', $this->application->source->id)->first(); $source = data_get($this->application, 'source');
if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first();
}
$this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first(); $this->destination = $this->application->destination->getMorphClass()::where('id', $this->application->destination->id)->first();
$this->server = $this->destination->server; $this->server = $this->destination->server;
$this->workdir = "/artifacts/{$this->deployment_uuid}"; $this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
$this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}"; $this->configuration_dir = application_configuration_dir() . "/{$this->application->uuid}";
$this->build_workdir = "{$this->workdir}" . rtrim($this->application->base_directory, '/');
$this->is_debug_enabled = $this->application->settings->is_debug_enabled; $this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->container_name = generateApplicationContainerName($this->application); ray($this->basedir, $this->workdir);
$this->container_name = generateApplicationContainerName($this->application, $this->pull_request_id);
savePrivateKeyToFs($this->server); savePrivateKeyToFs($this->server);
$this->saved_outputs = collect(); $this->saved_outputs = collect();
@@ -97,7 +107,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id); $this->preview = ApplicationPreview::findPreviewByApplicationAndPullId($this->application->id, $this->pull_request_id);
if ($this->application->fqdn) { if ($this->application->fqdn) {
if (data_get($this->preview, 'fqdn')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn')); $preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn'));
}
$template = $this->application->preview_url_template; $template = $this->application->preview_url_template;
$url = Url::fromString($this->application->fqdn); $url = Url::fromString($this->application->fqdn);
$host = $url->getHost(); $host = $url->getHost();
@@ -129,6 +141,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
try { try {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') {
$this->deploy_dockerimage();
} else if ($this->application->build_pack === 'dockerfile') {
$this->deploy_dockerfile();
} else { } else {
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->deploy_pull_request(); $this->deploy_pull_request();
@@ -167,6 +183,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
} }
} }
private function deploy_docker_compose() private function deploy_docker_compose()
{ {
$dockercompose_base64 = base64_encode($this->application->dockercompose); $dockercompose_base64 = base64_encode($this->application->dockercompose);
@@ -231,7 +248,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
$this->build_image_name = Str::lower("{$this->application->git_repository}:build"); $this->build_image_name = Str::lower("{$this->application->git_repository}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->production_image_name = Str::lower("{$this->application->uuid}:latest");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->generate_compose_file(); $this->generate_compose_file();
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
@@ -239,6 +256,50 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->rolling_update(); $this->rolling_update();
} }
private function deploy_dockerimage()
{
$this->dockerImage = $this->application->docker_registry_image_name;
$this->dockerImageTag = $this->application->docker_registry_image_tag;
ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'");
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag}.'"
],
);
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
$this->prepare_builder_image();
$this->generate_compose_file();
$this->rolling_update();
}
private function deploy_dockerfile()
{
if (data_get($this->application, 'dockerfile_location')) {
$this->dockerfile_location = $this->application->dockerfile_location;
}
$this->execute_remote_command(
[
"echo 'Starting deployment of {$this->application->git_repository}:{$this->application->git_branch}.'"
],
);
$this->prepare_builder_image();
$this->clone_repository();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128);
}
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
// ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->cleanup_git();
$this->generate_compose_file();
$this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile();
// $this->build_image();
$this->rolling_update();
}
private function deploy() private function deploy()
{ {
$this->execute_remote_command( $this->execute_remote_command(
@@ -248,7 +309,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->clone_repository(); $this->clone_repository();
$this->set_base_dir();
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); $tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}");
if (strlen($tag) > 128) { if (strlen($tag) > 128) {
$tag = $tag->substr(0, 128); $tag = $tag->substr(0, 128);
@@ -256,7 +317,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build"); $this->build_image_name = Str::lower("{$this->application->git_repository}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}"); $this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->execute_remote_command([ $this->execute_remote_command([
@@ -301,7 +362,11 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
private function health_check() private function health_check()
{ {
ray('New container name: ', $this->container_name); if ($this->application->isHealthcheckDisabled()) {
$this->newVersionIsHealthy = true;
return;
}
// ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$counter = 0; $counter = 0;
$this->execute_remote_command( $this->execute_remote_command(
@@ -329,9 +394,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) { if (Str::of($this->saved_outputs->get('health_check'))->contains('healthy')) {
$this->newVersionIsHealthy = true; $this->newVersionIsHealthy = true;
$this->execute_remote_command( $this->execute_remote_command(
[
"echo 'New version of your application is healthy.'"
],
[ [
"echo 'Rolling update completed.'" "echo 'Rolling update completed.'"
], ],
@@ -348,12 +410,13 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green(); // ray('Build Image Name: ' . $this->build_image_name . ' & Production Image Name: ' . $this->production_image_name)->green();
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'", "echo 'Starting pull request (#{$this->pull_request_id}) deployment of {$this->application->git_repository}:{$this->application->git_branch}.'",
]); ]);
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->clone_repository(); $this->clone_repository();
$this->set_base_dir();
$this->cleanup_git(); $this->cleanup_git();
if ($this->application->build_pack === 'nixpacks') { if ($this->application->build_pack === 'nixpacks') {
$this->generate_nixpacks_confs(); $this->generate_nixpacks_confs();
@@ -373,9 +436,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image() private function prepare_builder_image()
{ {
$pull = "--pull=always"; $pull = "--pull=always";
if (isDev()) {
$pull = "--pull=never";
}
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run {$pull} -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
@@ -388,24 +448,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"hidden" => true, "hidden" => true,
], ],
[ [
"command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}") "command" => executeInDocker($this->deployment_uuid, "mkdir -p {$this->basedir}")
], ],
); );
} }
private function set_base_dir()
{
$this->execute_remote_command(
[
"echo -n 'Setting base directory to {$this->workdir}.'"
],
);
}
private function clone_repository() private function clone_repository()
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} to {$this->workdir}. '" "echo -n 'Importing {$this->application->git_repository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
], ],
[ [
$this->importing_git_repository() $this->importing_git_repository(), "hidden" => true
], ],
[ [
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git rev-parse HEAD"), executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git rev-parse HEAD"),
"hidden" => true, "hidden" => true,
"save" => "git_commit_sha" "save" => "git_commit_sha"
], ],
@@ -429,23 +496,23 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->source->getMorphClass() == 'App\Models\GithubApp') { if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) { if ($this->source->is_public) {
$git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->workdir}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command)); $commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
} else { } else {
$github_access_token = generate_github_installation_token($this->source); $github_access_token = generate_github_installation_token($this->source);
$commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->workdir}")); $commands->push(executeInDocker($this->deployment_uuid, "git clone -q -b {$this->application->git_branch} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$this->application->git_repository}.git {$this->basedir}"));
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$commands->push(executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name")); $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin pull/{$this->pull_request_id}/head:$pr_branch_name && git checkout $pr_branch_name"));
} }
return $commands->implode(' && '); return $commands->implode(' && ');
} }
} }
if ($this->application->deploymentType() === 'deploy_key') { if ($this->application->deploymentType() === 'deploy_key') {
$private_key = base64_encode($this->application->private_key->private_key); $private_key = base64_encode($this->application->private_key->private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->workdir}"; $git_clone_command = "GIT_SSH_COMMAND=\"ssh -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->application->git_full_url} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands = collect([ $commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
@@ -455,18 +522,25 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
]); ]);
return $commands->implode(' && '); return $commands->implode(' && ');
} }
if ($this->application->deploymentType() === 'other') {
$git_clone_command = "{$git_clone_command} {$this->application->git_repository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command);
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
ray($commands);
return $commands->implode(' && ');
}
} }
private function set_git_import_settings($git_clone_command) private function set_git_import_settings($git_clone_command)
{ {
if ($this->application->git_commit_sha !== 'HEAD') { if ($this->application->git_commit_sha !== 'HEAD') {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1"; $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git -c advice.detachedHead=false checkout {$this->application->git_commit_sha} >/dev/null 2>&1";
} }
if ($this->application->settings->is_git_submodules_enabled) { if ($this->application->settings->is_git_submodules_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git submodule update --init --recursive"; $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git submodule update --init --recursive";
} }
if ($this->application->settings->is_git_lfs_enabled) { if ($this->application->settings->is_git_lfs_enabled) {
$git_clone_command = "{$git_clone_command} && cd {$this->workdir} && git lfs pull"; $git_clone_command = "{$git_clone_command} && cd {$this->basedir} && git lfs pull";
} }
return $git_clone_command; return $git_clone_command;
} }
@@ -474,17 +548,24 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function cleanup_git() private function cleanup_git()
{ {
$this->execute_remote_command( $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "rm -fr {$this->workdir}/.git")], [executeInDocker($this->deployment_uuid, "rm -fr {$this->basedir}/.git")],
); );
} }
private function generate_nixpacks_confs() private function generate_nixpacks_confs()
{ {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Generating nixpacks configuration.'", "echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command(
[
"echo -n Running: $nixpacks_command",
], ],
[$this->nixpacks_build_cmd()], [executeInDocker($this->deployment_uuid, $nixpacks_command)],
[executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")], [executeInDocker($this->deployment_uuid, "cp {$this->workdir}/.nixpacks/Dockerfile {$this->workdir}/Dockerfile")],
[executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")] [executeInDocker($this->deployment_uuid, "rm -f {$this->workdir}/.nixpacks/Dockerfile")]
); );
@@ -493,7 +574,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function nixpacks_build_cmd() private function nixpacks_build_cmd()
{ {
$this->generate_env_variables(); $this->generate_env_variables();
$nixpacks_command = "nixpacks build -o {$this->workdir} {$this->env_args} --no-error-without-start"; $nixpacks_command = "nixpacks build --no-cache -o {$this->workdir} {$this->env_args} --no-error-without-start";
if ($this->application->build_command) { if ($this->application->build_command) {
$nixpacks_command .= " --build-cmd \"{$this->application->build_command}\""; $nixpacks_command .= " --build-cmd \"{$this->application->build_command}\"";
} }
@@ -504,7 +585,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$nixpacks_command .= " --install-cmd \"{$this->application->install_command}\""; $nixpacks_command .= " --install-cmd \"{$this->application->install_command}\"";
} }
$nixpacks_command .= " {$this->workdir}"; $nixpacks_command .= " {$this->workdir}";
return executeInDocker($this->deployment_uuid, $nixpacks_command); return $nixpacks_command;
} }
private function generate_env_variables() private function generate_env_variables()
@@ -571,6 +652,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
}
if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) { if (count($this->application->ports_mappings_array) > 0 && $this->pull_request_id === 0) {
$docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array; $docker_compose['services'][$this->container_name]['ports'] = $this->application->ports_mappings_array;
} }
@@ -580,6 +664,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if (count($volume_names) > 0) { if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names; $docker_compose['volumes'] = $volume_names;
} }
if ($this->build_pack === 'dockerfile') {
$docker_compose['services'][$this->container_name]['build'] = [
'context' => $this->workdir,
'dockerfile' => $this->workdir . $this->dockerfile_location,
];
}
$this->docker_compose = Yaml::dump($docker_compose, 10); $this->docker_compose = Yaml::dump($docker_compose, 10);
$this->docker_compose_base64 = base64_encode($this->docker_compose); $this->docker_compose_base64 = base64_encode($this->docker_compose);
$this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]); $this->execute_remote_command([executeInDocker($this->deployment_uuid, "echo '{$this->docker_compose_base64}' | base64 -d > {$this->workdir}/docker-compose.yml"), "hidden" => true]);
@@ -622,14 +712,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_environment_variables($ports) private function generate_environment_variables($ports)
{ {
$environment_variables = collect(); $environment_variables = collect();
ray('Generate Environment Variables')->green(); // ray('Generate Environment Variables')->green();
if ($this->pull_request_id === 0) { if ($this->pull_request_id === 0) {
ray($this->application->runtime_environment_variables)->green(); // ray($this->application->runtime_environment_variables)->green();
foreach ($this->application->runtime_environment_variables as $env) { foreach ($this->application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value"); $environment_variables->push("$env->key=$env->value");
} }
} else { } else {
ray($this->application->runtime_environment_variables_preview)->green(); // ray($this->application->runtime_environment_variables_preview)->green();
foreach ($this->application->runtime_environment_variables_preview as $env) { foreach ($this->application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value"); $environment_variables->push("$env->key=$env->value");
} }
@@ -643,7 +733,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_healthcheck_commands() private function generate_healthcheck_commands()
{ {
if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile') { if ($this->application->dockerfile || $this->application->build_pack === 'dockerfile' || $this->application->build_pack === 'dockerimage') {
// TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl. // TODO: disabled HC because there are several ways to hc a simple docker image, hard to figure out a good way. Like some docker images (pocketbase) does not have curl.
return 'exit 0'; return 'exit 0';
} }
@@ -667,7 +757,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function build_image() private function build_image()
{ {
$this->execute_remote_command([ $this->execute_remote_command([
"echo -n 'Building docker image for your application.'", "echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'",
]); ]);
if ($this->application->settings->is_static) { if ($this->application->settings->is_static) {
@@ -736,7 +826,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up -d >/dev/null"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d >/dev/null"), "hidden" => true],
); );
} }

View File

@@ -66,7 +66,7 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
private function update_comment() private function update_comment()
{ {
['data' => $data] = git_api(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'patch', data: [ ['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/comments/{$this->preview->pull_request_issue_comment_id}", method: 'patch', data: [
'body' => $this->body, 'body' => $this->body,
], throwError: false); ], throwError: false);
if (data_get($data, 'message') === 'Not Found') { if (data_get($data, 'message') === 'Not Found') {
@@ -77,7 +77,7 @@ class ApplicationPullRequestUpdateJob implements ShouldQueue, ShouldBeEncrypted
private function create_comment() private function create_comment()
{ {
['data' => $data] = git_api(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->pull_request_id}/comments", method: 'post', data: [ ['data' => $data] = githubApi(source: $this->application->source, endpoint: "/repos/{$this->application->git_repository}/issues/{$this->pull_request_id}/comments", method: 'post', data: [
'body' => $this->body, 'body' => $this->body,
]); ]);
$this->preview->pull_request_issue_comment_id = $data['id']; $this->preview->pull_request_issue_comment_id = $data['id'];

View File

@@ -7,6 +7,7 @@ use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Container\ContainerRestarted; use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped; use App\Notifications\Container\ContainerStopped;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable; use App\Notifications\Server\Unreachable;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -40,33 +41,60 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
return $this->server->uuid; return $this->server->uuid;
} }
public function handle()
private function checkServerConnection()
{
$uptime = instant_remote_process(['uptime'], $this->server, false);
if (!is_null($uptime)) {
return true;
}
}
public function handle(): void
{ {
try { try {
// ray("checking server status for {$this->server->name}");
// ray()->clearAll(); // ray()->clearAll();
$serverUptimeCheckNumber = 0; $serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3; $serverUptimeCheckNumberMax = 3;
while (true) {
// ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
$this->server->settings()->update(['is_reachable' => false]); if ($this->server->unreachable_email_sent === false) {
$this->server->team->notify(new Unreachable($this->server)); ray('Server unreachable, sending notification...');
// $this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]);
}
$this->server->settings()->update([
'is_reachable' => false,
]);
return; return;
} }
$result = $this->checkServerConnection(); $result = $this->server->validateConnection();
if ($result) { if ($result) {
break; $this->server->settings()->update([
} 'is_reachable' => true,
]);
$this->server->update([
'unreachable_count' => 0,
]);
} else {
$serverUptimeCheckNumber++; $serverUptimeCheckNumber++;
sleep(5); $this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
return;
} }
if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...');
// $this->server->team->notify(new Revived($this->server));
$this->server->update(['unreachable_email_sent' => false]);
}
if (
data_get($this->server, 'settings.is_reachable') === false ||
data_get($this->server, 'settings.is_usable') === false
) {
$this->server->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
// $this->server->validateDockerEngine(true);
$containers = instant_remote_process(["docker container ls -q"], $this->server); $containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) { if (!$containers) {
return; return;
@@ -108,9 +136,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$labelId = data_get($labels, 'coolify.applicationId'); $labelId = data_get($labels, 'coolify.applicationId');
if ($labelId) { if ($labelId) {
if (str_contains($labelId, '-pr-')) { if (str_contains($labelId, '-pr-')) {
$previewId = (int) Str::after($labelId, '-pr-'); $pullRequestId = data_get($labels, 'coolify.pullRequestId');
$applicationId = (int) Str::before($labelId, '-pr-'); $applicationId = (int) Str::before($labelId, '-pr-');
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $previewId)->first(); $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) { if ($preview) {
$foundApplicationPreviews[] = $preview->id; $foundApplicationPreviews[] = $preview->id;
$statusFromDb = $preview->status; $statusFromDb = $preview->status;
@@ -266,7 +294,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage()); send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
throw $e; return handleError($e);
} }
} }
} }

View File

@@ -31,7 +31,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public ?string $container_name = null; public ?string $container_name = null;
public ?ScheduledDatabaseBackupExecution $backup_log = null; public ?ScheduledDatabaseBackupExecution $backup_log = null;
public string $backup_status; public string $backup_status = 'failed';
public ?string $backup_location = null; public ?string $backup_location = null;
public string $backup_dir; public string $backup_dir;
public string $backup_file; public string $backup_file;
@@ -61,7 +61,8 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
try { try {
if (data_get($this->database, 'status') !== 'running') { $status = Str::of(data_get($this->database, 'status'));
if (!$status->startsWith('running') && $this->database->id !== 0) {
ray('database not running'); ray('database not running');
return; return;
} }
@@ -73,7 +74,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$ip = Str::slug($this->server->ip); $ip = Str::slug($this->server->ip);
$this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip"; $this->backup_dir = backup_dir() . "/coolify" . "/coolify-db-$ip";
} }
$this->backup_file = "/pg_dump-" . Carbon::now()->timestamp . ".dump"; $this->backup_file = "/pg-backup-customformat-" . Carbon::now()->timestamp . ".backup";
$this->backup_location = $this->backup_dir . $this->backup_file; $this->backup_location = $this->backup_dir . $this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
@@ -89,11 +90,17 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
$this->upload_to_s3(); $this->upload_to_s3();
} }
$this->save_backup_logs(); $this->save_backup_logs();
// TODO: Notify user $this->team->notify(new BackupSuccess($this->backup, $this->database));
$this->backup_status = 'success';
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage()); $this->backup_status = 'failed';
send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: ' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
throw $e; throw $e;
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
} }
} }
@@ -103,28 +110,15 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray($this->backup_dir); ray($this->backup_dir);
$commands[] = "mkdir -p " . $this->backup_dir; $commands[] = "mkdir -p " . $this->backup_dir;
$commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location"; $commands[] = "docker exec $this->container_name pg_dump -Fc -U {$this->database->postgres_user} > $this->backup_location";
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
$this->backup_output = null; $this->backup_output = null;
} }
ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location); ray('Backup done for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location);
$this->backup_status = 'success';
$this->team->notify(new BackupSuccess($this->backup, $this->database));
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->backup_status = 'failed';
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage()); ray('Backup failed for ' . $this->container_name . ' at ' . $this->server->name . ':' . $this->backup_location . '\n\nError:' . $e->getMessage());
$this->team->notify(new BackupFailed($this->backup, $this->database, $this->backup_output));
} finally {
$this->backup_log->update([
'status' => $this->backup_status,
]);
} }
} }
@@ -166,8 +160,13 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
// $region = $this->s3->region; // $region = $this->s3->region;
$bucket = $this->s3->bucket; $bucket = $this->s3->bucket;
$endpoint = $this->s3->endpoint; $endpoint = $this->s3->endpoint;
$this->s3->testConnection();
if (isDev()) {
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v coolify_coolify-data-dev:/data/coolify:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
} else {
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper >/dev/null 2>&1";
}
$commands[] = "docker run --pull=always -d --network {$this->database->destination->network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro ghcr.io/coollabsio/coolify-helper";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc 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}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);
@@ -175,7 +174,7 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir); ray('Uploaded to S3. ' . $this->backup_location . ' to s3://' . $bucket . $this->backup_dir);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
ray($e->getMessage()); throw $e;
} finally { } finally {
$command = "docker rm -f backup-of-{$this->backup->uuid}"; $command = "docker rm -f backup-of-{$this->backup->uuid}";
instant_remote_process([$command], $this->server); instant_remote_process([$command], $this->server);

View File

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\Activitylog\Models\Activity; use Spatie\Activitylog\Models\Activity;
use Illuminate\Support\Str;
class Application extends BaseModel class Application extends BaseModel
{ {
@@ -12,16 +13,37 @@ class Application extends BaseModel
protected static function booted() protected static function booted()
{ {
static::saving(function ($application) {
if ($application->fqdn == '') {
$application->fqdn = null;
}
$application->forceFill([
'fqdn' => $application->fqdn,
'install_command' => Str::of($application->install_command)->trim(),
'build_command' => Str::of($application->build_command)->trim(),
'start_command' => Str::of($application->start_command)->trim(),
'base_directory' => Str::of($application->base_directory)->trim(),
'publish_directory' => Str::of($application->publish_directory)->trim(),
]);
});
static::created(function ($application) { static::created(function ($application) {
ApplicationSetting::create([ ApplicationSetting::create([
'application_id' => $application->id, 'application_id' => $application->id,
]); ]);
}); });
static::deleting(function ($application) { static::deleting(function ($application) {
// Stop Container
if ($application->destination->server->isFunctional()) {
instant_remote_process(
["docker rm -f {$application->uuid}"],
$application->destination->server,
false
);
}
$application->settings()->delete(); $application->settings()->delete();
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server); instant_remote_process(["docker volume rm -f $storage->name"], $application->destination->server, false);
} }
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
$application->environment_variables()->delete(); $application->environment_variables()->delete();
@@ -38,6 +60,10 @@ class Application extends BaseModel
{ {
return $this->morphMany(LocalPersistentVolume::class, 'resource'); return $this->morphMany(LocalPersistentVolume::class, 'resource');
} }
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function type() public function type()
{ {
@@ -58,6 +84,7 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}"; return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
} }
return $this->git_repository;
} }
); );
@@ -70,10 +97,25 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) { if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}"; return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
} }
return $this->git_repository;
}
);
}
public function dockerfileLocation(): Attribute
{
return Attribute::make(
set: function ($value) {
if (is_null($value) || $value === '') {
return '/Dockerfile';
} else {
if ($value !== '/') {
return Str::start(Str::replaceEnd('/', '', $value), '/');
}
return Str::start($value, '/');
}
} }
); );
} }
public function baseDirectory(): Attribute public function baseDirectory(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -214,6 +256,8 @@ class Application extends BaseModel
return 'deploy_key'; return 'deploy_key';
} else if (data_get($this, 'source')) { } else if (data_get($this, 'source')) {
return 'source'; return 'source';
} else {
return 'other';
} }
throw new \Exception('No deployment type found'); throw new \Exception('No deployment type found');
} }
@@ -229,6 +273,16 @@ class Application extends BaseModel
if ($this->dockerfile) { if ($this->dockerfile) {
return false; return false;
} }
if ($this->build_pack === 'dockerimage') {
return false;
}
return true; return true;
} }
public function isHealthcheckDisabled(): bool
{
if (data_get($this, 'health_check_enabled') === false) {
return true;
}
return false;
}
} }

View File

@@ -31,23 +31,18 @@ class LocalFileVolume extends BaseModel
} }
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
$isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server);
ray($isFile);
if ($isFile == 'OK' && $fileVolume->is_directory) { if ($isFile == 'OK' && $fileVolume->is_directory) {
throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory."); throw new \Exception("File $path is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.");
} else if ($isDir == 'OK' && !$fileVolume->is_directory) { } else if ($isDir == 'OK' && !$fileVolume->is_directory) {
throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory."); throw new \Exception("File $path is a directory on the server, but you are trying to mark it as a file. Please delete the directory on the server or mark it as directory.");
} }
if (($isFile == 'NOK' && !$fileVolume->is_directory) || $isFile == 'OK') { if (!$fileVolume->is_directory && $isDir == 'NOK') {
$rootDir = Str::of($path)->dirname();
$commands->push("mkdir -p $rootDir > /dev/null 2>&1 || true");
$commands->push("touch $path > /dev/null 2>&1 || true");
if ($content) {
$content = base64_encode($content); $content = base64_encode($content);
$commands->push("echo '$content' | base64 -d > $path"); $commands->push("echo '$content' | base64 -d > $path");
}
} else if ($isDir == 'NOK' && $fileVolume->is_directory) { } else if ($isDir == 'NOK' && $fileVolume->is_directory) {
$commands->push("mkdir -p $path > /dev/null 2>&1 || true"); $commands->push("mkdir -p $path > /dev/null 2>&1 || true");
} }
ray($commands->toArray());
return instant_remote_process($commands, $server); return instant_remote_process($commands, $server);
} }
} }

View File

@@ -22,7 +22,7 @@ class Project extends BaseModel
'project_id' => $project->id, 'project_id' => $project->id,
]); ]);
}); });
static::deleted(function ($project) { static::deleting(function ($project) {
$project->environments()->delete(); $project->environments()->delete();
$project->settings()->delete(); $project->settings()->delete();
}); });

View File

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

View File

@@ -5,8 +5,10 @@ namespace App\Models;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str;
class Server extends BaseModel class Server extends BaseModel
{ {
@@ -14,6 +16,17 @@ class Server extends BaseModel
protected static function booted() protected static function booted()
{ {
static::saving(function ($server) {
$payload = [];
if ($server->user) {
$payload['user'] = Str::of($server->user)->trim();
}
if ($server->ip) {
$payload['ip'] = Str::of($server->ip)->trim();
}
$server->forceFill($payload);
});
static::created(function ($server) { static::created(function ($server) {
ServerSetting::create([ ServerSetting::create([
'server_id' => $server->id, 'server_id' => $server->id,
@@ -120,7 +133,20 @@ class Server extends BaseModel
{ {
return $this->hasMany(Service::class); return $this->hasMany(Service::class);
} }
public function getIp(): Attribute
{
return Attribute::make(
get: function () {
if (isDev()) {
return '127.0.0.1';
}
if ($this->ip === 'host.docker.internal') {
return base_ip();
}
return $this->ip;
}
);
}
public function previews() public function previews()
{ {
return $this->destinations()->map(function ($standaloneDocker) { return $this->destinations()->map(function ($standaloneDocker) {
@@ -185,4 +211,48 @@ class Server extends BaseModel
{ {
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable;
} }
public function validateConnection()
{
$uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings->is_reachable = false;
$this->settings->save();
return false;
}
$this->settings->is_reachable = true;
$this->settings->save();
return true;
}
public function validateDockerEngine($throwError = false)
{
$dockerBinary = instant_remote_process(["command -v docker"], $this, false);
if (is_null($dockerBinary)) {
$this->settings->is_usable = false;
$this->settings->save();
if ($throwError) {
throw new \Exception('Server is not usable.');
}
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
$this->validateCoolifyNetwork();
return true;
}
public function validateDockerEngineVersion()
{
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $this, false);
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) {
$this->settings->is_usable = false;
$this->settings->save();
return false;
}
$this->settings->is_usable = true;
$this->settings->save();
return true;
}
public function validateCoolifyNetwork() {
return instant_remote_process(["docker network create coolify --attachable >/dev/null 2>&1 || true"], $this, false);
}
} }

View File

@@ -8,7 +8,6 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url;
class Service extends BaseModel class Service extends BaseModel
{ {
@@ -17,9 +16,10 @@ class Service extends BaseModel
protected static function booted() protected static function booted()
{ {
static::deleted(function ($service) { static::deleting(function ($service) {
$storagesToDelete = collect([]); $storagesToDelete = collect([]);
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
instant_remote_process(["docker rm -f {$application->name}-{$service->uuid}"], $service->server, false);
$storages = $application->persistentStorages()->get(); $storages = $application->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -27,6 +27,7 @@ class Service extends BaseModel
$application->persistentStorages()->delete(); $application->persistentStorages()->delete();
} }
foreach ($service->databases()->get() as $database) { foreach ($service->databases()->get() as $database) {
instant_remote_process(["docker rm -f {$database->name}-{$service->uuid}"], $service->server, false);
$storages = $database->persistentStorages()->get(); $storages = $database->persistentStorages()->get();
foreach ($storages as $storage) { foreach ($storages as $storage) {
$storagesToDelete->push($storage); $storagesToDelete->push($storage);
@@ -63,6 +64,10 @@ class Service extends BaseModel
{ {
return $this->hasMany(ServiceDatabase::class); return $this->hasMany(ServiceDatabase::class);
} }
public function destination()
{
return $this->morphTo();
}
public function environment() public function environment()
{ {
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);
@@ -112,7 +117,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection public function parse(bool $isNew = false): Collection
{ {
// ray()->clearAll(); ray()->clearAll();
if ($this->docker_compose_raw) { if ($this->docker_compose_raw) {
try { try {
$yaml = Yaml::parse($this->docker_compose_raw); $yaml = Yaml::parse($this->docker_compose_raw);
@@ -124,9 +129,16 @@ class Service extends BaseModel
$topLevelNetworks = collect(data_get($yaml, 'networks', [])); $topLevelNetworks = collect(data_get($yaml, 'networks', []));
$dockerComposeVersion = data_get($yaml, 'version') ?? '3.8'; $dockerComposeVersion = data_get($yaml, 'version') ?? '3.8';
$services = data_get($yaml, 'services'); $services = data_get($yaml, 'services');
$definedNetwork = $this->uuid;
$generatedServiceFQDNS = collect([]); $generatedServiceFQDNS = collect([]);
if (is_null($this->destination)) {
$destination = $this->server->destinations()->first();
if ($destination) {
$this->destination()->associate($destination);
$this->save();
}
}
$definedNetwork = collect([$this->uuid]);
$services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) { $services = collect($services)->map(function ($service, $serviceName) use ($topLevelVolumes, $topLevelNetworks, $definedNetwork, $isNew, $generatedServiceFQDNS) {
$serviceVolumes = collect(data_get($service, 'volumes', [])); $serviceVolumes = collect(data_get($service, 'volumes', []));
@@ -237,18 +249,24 @@ class Service extends BaseModel
return $value == $definedNetwork; return $value == $definedNetwork;
}); });
if (!$definedNetworkExists) { if (!$definedNetworkExists) {
$topLevelNetworks->put($definedNetwork, [ foreach ($definedNetwork as $network) {
'name' => $definedNetwork, $topLevelNetworks->put($network, [
'name' => $network,
'external' => true 'external' => true
]); ]);
} }
}
$networks = $serviceNetworks->toArray(); $networks = $serviceNetworks->toArray();
$networks = array_merge($networks, [$definedNetwork]); foreach ($definedNetwork as $key => $network) {
$networks = array_merge($networks, [
$network
]);
}
data_set($service, 'networks', $networks); data_set($service, 'networks', $networks);
// Collect/create/update volumes // Collect/create/update volumes
if ($serviceVolumes->count() > 0) { if ($serviceVolumes->count() > 0) {
foreach ($serviceVolumes as $volume) { $serviceVolumes = $serviceVolumes->map(function ($volume) use ($savedService, $topLevelVolumes) {
$type = null; $type = null;
$source = null; $source = null;
$target = null; $target = null;
@@ -270,16 +288,19 @@ class Service extends BaseModel
$isDirectory = (bool) data_get($volume, 'isDirectory', false); $isDirectory = (bool) data_get($volume, 'isDirectory', false);
$foundConfig = $savedService->fileStorages()->whereMountPath($target)->first(); $foundConfig = $savedService->fileStorages()->whereMountPath($target)->first();
if ($foundConfig) { if ($foundConfig) {
$content = data_get($foundConfig, 'content'); $contentNotNull = data_get($foundConfig, 'content');
if ($contentNotNull) {
$content = $contentNotNull;
}
$isDirectory = (bool) data_get($foundConfig, 'is_directory'); $isDirectory = (bool) data_get($foundConfig, 'is_directory');
} }
} }
if ($type->value() === 'bind') { if ($type->value() === 'bind') {
if ($source->value() === "/var/run/docker.sock") { if ($source->value() === "/var/run/docker.sock") {
continue; return $volume;
} }
if ($source->value() === '/tmp' || $source->value() === '/tmp/') { if ($source->value() === '/tmp' || $source->value() === '/tmp/') {
continue; return $volume;
} }
LocalFileVolume::updateOrCreate( LocalFileVolume::updateOrCreate(
[ [
@@ -297,7 +318,19 @@ class Service extends BaseModel
] ]
); );
} else if ($type->value() === 'volume') { } else if ($type->value() === 'volume') {
$topLevelVolumes->put($source->value(), null); $slugWithoutUuid = Str::slug($source, '-');
$name = "{$savedService->service->uuid}_{$slugWithoutUuid}";
if (is_string($volume)) {
$source = Str::of($volume)->before(':');
$target = Str::of($volume)->after(':')->beforeLast(':');
$source = $name;
$volume = "$source:$target";
} else if (is_array($volume)) {
data_set($volume, 'source', $name);
}
$topLevelVolumes->put($name, [
'name' => $name,
]);
LocalPersistentVolume::updateOrCreate( LocalPersistentVolume::updateOrCreate(
[ [
'mount_path' => $target, 'mount_path' => $target,
@@ -305,15 +338,17 @@ class Service extends BaseModel
'resource_type' => get_class($savedService) 'resource_type' => get_class($savedService)
], ],
[ [
'name' => Str::slug($source, '-'), 'name' => $name,
'mount_path' => $target, 'mount_path' => $target,
'resource_id' => $savedService->id, 'resource_id' => $savedService->id,
'resource_type' => get_class($savedService) 'resource_type' => get_class($savedService)
] ]
); );
} }
$savedService->getFilesFromServer(); $savedService->getFilesFromServer(isInit: true);
} return $volume;
});
data_set($service, 'volumes', $serviceVolumes->toArray());
} }
// Add env_file with at least .env to the service // Add env_file with at least .env to the service
@@ -349,9 +384,23 @@ class Service extends BaseModel
$value = Str::of($variable); $value = Str::of($variable);
} }
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if (is_null(data_get($savedService, 'fqdn'))) { if ($isNew) {
$fqdn = generateFqdn($this->server, $containerName); $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
if (substr_count($key->value(), '_') === 2 && $key->contains("=")) { $fqdn = generateFqdn($this->server, "{$name->value()}-{$this->uuid}");
if (substr_count($key->value(), '_') === 3) {
// SERVICE_FQDN_UMAMI_1000
$port = $key->afterLast('_');
} else {
// SERVICE_FQDN_UMAMI
$port = null;
}
if ($port) {
$fqdn = "$fqdn:$port";
}
if (substr_count($key->value(), '_') >= 2) {
if (is_null($value)) {
$value = Str::of('/');
}
$path = $value->value(); $path = $value->value();
if ($generatedServiceFQDNS->count() > 0) { if ($generatedServiceFQDNS->count() > 0) {
$alreadyGenerated = $generatedServiceFQDNS->has($key->value()); $alreadyGenerated = $generatedServiceFQDNS->has($key->value());
@@ -365,11 +414,22 @@ class Service extends BaseModel
} }
$fqdn = "$fqdn$path"; $fqdn = "$fqdn$path";
} }
if (!$isDatabase) { if (!$isDatabase) {
if ($savedService->fqdn) {
$fqdn = $savedService->fqdn . ',' . $fqdn;
} else {
$fqdn = $fqdn;
}
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
} }
// data_forget($service, "environment.$variableName");
// $yaml = data_forget($yaml, "services.$serviceName.environment.$variableName");
// if (count(data_get($yaml, 'services.' . $serviceName . '.environment')) === 0) {
// $yaml = data_forget($yaml, "services.$serviceName.environment");
// }
continue; continue;
} }
if ($value?->startsWith('$')) { if ($value?->startsWith('$')) {
@@ -384,10 +444,17 @@ class Service extends BaseModel
$forService = $value->afterLast('_'); $forService = $value->afterLast('_');
$generatedValue = null; $generatedValue = null;
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName); $fqdn = generateFqdn($this->server, $containerName);
} else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
}
if ($foundEnv) { if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value'); $fqdn = data_get($foundEnv, 'value');
} else { } else {
if ($command->value() === 'URL') {
$fqdn = Str::of($fqdn)->after('://')->value();
}
EnvironmentVariable::create([ EnvironmentVariable::create([
'key' => $key, 'key' => $key,
'value' => $fqdn, 'value' => $fqdn,
@@ -396,11 +463,12 @@ class Service extends BaseModel
'is_preview' => false, 'is_preview' => false,
]); ]);
} }
if (!$isDatabase) { if (!$isDatabase) {
if ($command->value() === 'FQDN') {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
}
} else { } else {
switch ($command) { switch ($command) {
case 'PASSWORD': case 'PASSWORD':
@@ -472,25 +540,27 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
if ($fqdns) { if ($fqdns) {
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, $containerName, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($fqdns, true));
} }
} }
data_set($service, 'labels', $serviceLabels->toArray()); data_set($service, 'labels', $serviceLabels->toArray());
data_forget($service, 'is_database'); data_forget($service, 'is_database');
data_set($service, 'restart', RESTART_MODE); data_set($service, 'restart', RESTART_MODE);
data_set($service, 'container_name', $containerName); data_set($service, 'container_name', $containerName);
data_forget($service, 'volumes.*.content'); data_forget($service, 'volumes.*.content');
data_forget($service, 'volumes.*.isDirectory'); data_forget($service, 'volumes.*.isDirectory');
// Remove unnecessary variables from service.environment // Remove unnecessary variables from service.environment
$withoutServiceEnvs = collect([]); // $withoutServiceEnvs = collect([]);
collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) { // collect(data_get($service, 'environment'))->each(function ($value, $key) use ($withoutServiceEnvs) {
if (!Str::of($key)->startsWith('$SERVICE_')) { // ray($key, $value);
$withoutServiceEnvs->put($key, $value); // if (!Str::of($key)->startsWith('$SERVICE_') && !Str::of($value)->startsWith('SERVICE_')) {
} // $k = Str::of($value)->before("=");
}); // $v = Str::of($value)->after("=");
data_set($service, 'environment', $withoutServiceEnvs->toArray()); // $withoutServiceEnvs->put($k->value(), $v->value());
// }
// });
// ray($withoutServiceEnvs);
// data_set($service, 'environment', $withoutServiceEnvs->toArray());
return $service; return $service;
}); });
$finalServices = [ $finalServices = [

View File

@@ -36,8 +36,8 @@ class ServiceApplication extends BaseModel
); );
} }
public function getFilesFromServer() public function getFilesFromServer(bool $isInit = false)
{ {
getFilesystemVolumesFromServer($this); getFilesystemVolumesFromServer($this, $isInit);
} }
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Symfony\Component\Yaml\Yaml;
class ServiceDatabase extends BaseModel class ServiceDatabase extends BaseModel
{ {
@@ -26,8 +25,8 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(LocalFileVolume::class, 'resource'); return $this->morphMany(LocalFileVolume::class, 'resource');
} }
public function getFilesFromServer() public function getFilesFromServer(bool $isInit = false)
{ {
getFilesystemVolumesFromServer($this); getFilesystemVolumesFromServer($this, $isInit);
} }
} }

View File

@@ -28,10 +28,21 @@ class StandalonePostgresql extends BaseModel
'is_readonly' => true 'is_readonly' => true
]); ]);
}); });
static::deleted(function ($database) { static::deleting(function ($database) {
// Stop Container
instant_remote_process(
["docker rm -f {$database->uuid}"],
$database->destination->server,
false
);
// Stop TCP Proxy
if ($database->is_public) {
instant_remote_process(["docker rm -f {$database->uuid}-proxy"], $database->destination->server, false);
}
$database->scheduledBackups()->delete(); $database->scheduledBackups()->delete();
$database->persistentStorages()->delete(); $database->persistentStorages()->delete();
$database->environment_variables()->delete(); $database->environment_variables()->delete();
// Remove Volume
instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false); instant_remote_process(['docker volume rm postgres-data-' . $database->uuid], $database->destination->server, false);
}); });
} }
@@ -65,6 +76,11 @@ class StandalonePostgresql extends BaseModel
return $this->belongsTo(Environment::class); return $this->belongsTo(Environment::class);
} }
public function fileStorages()
{
return $this->morphMany(LocalFileVolume::class, 'resource');
}
public function destination() public function destination()
{ {
return $this->morphTo(); return $this->morphTo();

View File

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

View File

@@ -6,8 +6,12 @@ use App\Notifications\Channels\SendsEmail;
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
@@ -54,6 +58,23 @@ class User extends Authenticatable implements SendsEmail
return $this->email; return $this->email;
} }
public function sendVerificationEmail()
{
$mail = new MailMessage();
$url = Url::temporarySignedRoute(
'verify.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $this->getKey(),
'hash' => sha1($this->getEmailForVerification()),
]
);
$mail->view('emails.email-verification', [
'url' => $url,
]);
$mail->subject('Coolify Cloud: Verify your email.');
send_user_an_email($mail, $this->email);
}
public function sendPasswordResetNotification($token): void public function sendPasswordResetNotification($token): void
{ {
$this->notify(new TransactionalEmailsResetPassword($token)); $this->notify(new TransactionalEmailsResetPassword($token));

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ class StatusChanged extends Notification implements ShouldQueue
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$fqdn = $this->fqdn; $fqdn = $this->fqdn;
$mail->subject(" {$this->application_name} has been stopped"); $mail->subject("Coolify: {$this->application_name} has been stopped");
$mail->view('emails.application-status-changes', [ $mail->view('emails.application-status-changes', [
'name' => $this->application_name, 'name' => $this->application_name,
'fqdn' => $fqdn, 'fqdn' => $fqdn,
@@ -56,7 +56,7 @@ class StatusChanged extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = ' ' . $this->application_name . ' has been stopped. $message = 'Coolify: ' . $this->application_name . ' has been stopped.
'; ';
$message .= '[Open Application in Coolify](' . $this->application_url . ')'; $message .= '[Open Application in Coolify](' . $this->application_url . ')';
@@ -64,7 +64,7 @@ class StatusChanged extends Notification implements ShouldQueue
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = ' ' . $this->application_name . ' has been stopped.'; $message = 'Coolify: ' . $this->application_name . ' has been stopped.';
return [ return [
"message" => $message, "message" => $message,
"buttons" => [ "buttons" => [

View File

@@ -15,7 +15,6 @@ class EmailChannel
try { try {
$this->bootConfigs($notifiable); $this->bootConfigs($notifiable);
$recepients = $notifiable->getRecepients($notification); $recepients = $notifiable->getRecepients($notification);
ray($recepients);
if (count($recepients) === 0) { if (count($recepients) === 0) {
throw new Exception('No email recipients found'); throw new Exception('No email recipients found');
} }

View File

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

View File

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

View File

@@ -30,7 +30,7 @@ class BackupFailed extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject(" [ACTION REQUIRED] Backup FAILED for {$this->database->name}"); $mail->subject("Coolify: [ACTION REQUIRED] Backup FAILED for {$this->database->name}");
$mail->view('emails.backup-failed', [ $mail->view('emails.backup-failed', [
'name' => $this->name, 'name' => $this->name,
'frequency' => $this->frequency, 'frequency' => $this->frequency,
@@ -41,11 +41,11 @@ class BackupFailed extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
return " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; return "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = " Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}"; $message = "Coolify: Database backup for {$this->name} with frequency of {$this->frequency} was FAILED.\n\nReason: {$this->output}";
return [ return [
"message" => $message, "message" => $message,
]; ];

View File

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

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Notifications\Server;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class Revived extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server)
{
if ($this->server->unreachable_email_sent === false) {
return;
}
}
public function via(object $notifiable): array
{
$channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled ) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
}
public function toMail(): MailMessage
{
$mail = new MailMessage();
$mail->subject("Coolify: Server ({$this->server->name}) revived.");
$mail->view('emails.server-revived', [
'name' => $this->server->name,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"
];
}
}

View File

@@ -3,6 +3,9 @@
namespace App\Notifications\Server; namespace App\Notifications\Server;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Channels\DiscordChannel;
use App\Notifications\Channels\EmailChannel;
use App\Notifications\Channels\TelegramChannel;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
@@ -20,13 +23,27 @@ class Unreachable extends Notification implements ShouldQueue
public function via(object $notifiable): array public function via(object $notifiable): array
{ {
return setNotificationChannels($notifiable, 'status_changes'); $channels = [];
$isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
if ($isDiscordEnabled) {
$channels[] = DiscordChannel::class;
}
if ($isEmailEnabled ) {
$channels[] = EmailChannel::class;
}
if ($isTelegramEnabled) {
$channels[] = TelegramChannel::class;
}
return $channels;
} }
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject(" Server ({$this->server->name}) is unreachable after trying to connect to it 5 times"); $mail->subject("Coolify: Server ({$this->server->name}) is unreachable after trying to connect to it 5 times");
$mail->view('emails.server-lost-connection', [ $mail->view('emails.server-lost-connection', [
'name' => $this->server->name, 'name' => $this->server->name,
]); ]);
@@ -35,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$message = " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue."; $message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations.";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
return [ return [
"message" => " Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: You have to validate your server again after you fix the issue." "message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 5 times. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server. If your server is back online, we will automatically turn on all automations & integrations."
]; ];
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<?php <?php
use App\Enums\ProxyTypes;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id): Collection function getCurrentApplicationContainerStatus(Server $server, int $id): Collection
{ {
@@ -104,16 +104,16 @@ function getContainerStatus(Server $server, string $container_id, bool $all_data
return data_get($container[0], 'State.Status', 'exited'); return data_get($container[0], 'State.Status', 'exited');
} }
function generateApplicationContainerName(Application $application) function generateApplicationContainerName(Application $application, $pull_request_id = 0)
{ {
$now = now()->format('Hisu'); $now = now()->format('Hisu');
if ($application->pull_request_id !== 0 && $application->pull_request_id !== null) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
return $application->uuid . '-pr-' . $application->pull_request_id; return $application->uuid . '-pr-' . $pull_request_id;
} else { } else {
return $application->uuid . '-' . $now; return $application->uuid . '-' . $now;
} }
} }
function get_port_from_dockerfile($dockerfile): int function get_port_from_dockerfile($dockerfile): int|null
{ {
$dockerfile_array = explode("\n", $dockerfile); $dockerfile_array = explode("\n", $dockerfile);
$found_exposed_port = null; $found_exposed_port = null;
@@ -127,7 +127,7 @@ function get_port_from_dockerfile($dockerfile): int
if ($found_exposed_port) { if ($found_exposed_port) {
return (int)$found_exposed_port->value(); return (int)$found_exposed_port->value();
} }
return 80; return null;
} }
function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application', $subType = null, $subId = null) function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'application', $subType = null, $subId = null)
@@ -147,20 +147,20 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; return $labels;
} }
function fqdnLabelsForTraefik(Collection $domains, $container_name, $is_force_https_enabled) function fqdnLabelsForTraefik(Collection $domains, bool $is_force_https_enabled)
{ {
$labels = collect([]); $labels = collect([]);
$labels->push('traefik.enable=true'); $labels->push('traefik.enable=true');
foreach ($domains as $domain) { foreach ($domains as $domain) {
$uuid = (string)new Cuid2(7);
$url = Url::fromString($domain); $url = Url::fromString($domain);
$host = $url->getHost(); $host = $url->getHost();
$path = $url->getPath(); $path = $url->getPath();
$schema = $url->getScheme(); $schema = $url->getScheme();
$port = $url->getPort(); $port = $url->getPort();
$slug = Str::slug($host . $path);
$http_label = "{$container_name}-{$slug}-http"; $http_label = "{$uuid}-http";
$https_label = "{$container_name}-{$slug}-https"; $https_label = "{$uuid}-https";
if ($schema === 'https') { if ($schema === 'https') {
// Set labels for https // Set labels for https
@@ -207,10 +207,10 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
{ {
$pull_request_id = data_get($preview, 'pull_request_id', 0); $pull_request_id = data_get($preview, 'pull_request_id', 0);
$container_name = generateApplicationContainerName($application); $container_name = generateApplicationContainerName($application, $pull_request_id);
$appId = $application->id; $appId = $application->id;
if ($pull_request_id !== 0) { if ($pull_request_id !== 0 && $pull_request_id !== null) {
$appId = $appId . '-pr-' . $application->pull_request_id; $appId = $appId . '-pr-' . $pull_request_id;
} }
$labels = collect([]); $labels = collect([]);
$labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id)); $labels = $labels->merge(defaultLabels($appId, $container_name, $pull_request_id));
@@ -221,7 +221,7 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview
$domains = Str::of(data_get($application, 'fqdn'))->explode(','); $domains = Str::of(data_get($application, 'fqdn'))->explode(',');
} }
// Add Traefik labels no matter which proxy is selected // Add Traefik labels no matter which proxy is selected
$labels = $labels->merge(fqdnLabelsForTraefik($domains, $container_name, $application->settings->is_force_https_enabled)); $labels = $labels->merge(fqdnLabelsForTraefik($domains, $application->settings->is_force_https_enabled));
} }
return $labels->all(); return $labels->all();
} }

View File

@@ -50,7 +50,7 @@ function generate_github_jwt_token(GithubApp $source)
return $issuedToken; return $issuedToken;
} }
function git_api(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
{ {
if ($source->getMorphClass() == 'App\Models\GithubApp') { if ($source->getMorphClass() == 'App\Models\GithubApp') {
if ($source->is_public) { if ($source->is_public) {

View File

@@ -204,17 +204,23 @@ stream {
proxy_pass $database->uuid:5432; proxy_pass $database->uuid:5432;
} }
} }
EOF;
$dockerfile = <<< EOF
FROM nginx:stable-alpine
COPY nginx.conf /etc/nginx/nginx.conf
EOF; EOF;
$docker_compose = [ $docker_compose = [
'version' => '3.8', 'version' => '3.8',
'services' => [ 'services' => [
$containerName => [ $containerName => [
'build' => [
'context' => $configuration_dir,
'dockerfile' => 'Dockerfile',
],
'image' => "nginx:stable-alpine", 'image' => "nginx:stable-alpine",
'container_name' => $containerName, 'container_name' => $containerName,
'restart' => RESTART_MODE, 'restart' => RESTART_MODE,
'volumes' => [
"$configuration_dir/nginx.conf:/etc/nginx/nginx.conf:ro",
],
'ports' => [ 'ports' => [
"$database->public_port:$database->public_port", "$database->public_port:$database->public_port",
], ],
@@ -243,13 +249,13 @@ EOF;
]; ];
$dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2)); $dockercompose_base64 = base64_encode(Yaml::dump($docker_compose, 4, 2));
$nginxconf_base64 = base64_encode($nginxconf); $nginxconf_base64 = base64_encode($nginxconf);
$dockerfile_base64 = base64_encode($dockerfile);
instant_remote_process([ instant_remote_process([
"mkdir -p $configuration_dir", "mkdir -p $configuration_dir",
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile",
"echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf", "echo '{$nginxconf_base64}' | base64 -d > $configuration_dir/nginx.conf",
"echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml", "echo '{$dockercompose_base64}' | base64 -d > $configuration_dir/docker-compose.yaml",
"docker compose --project-directory {$configuration_dir} up -d >/dev/null", "docker compose --project-directory {$configuration_dir} up --build -d >/dev/null",
], $database->destination->server); ], $database->destination->server);
} }
function stopPostgresProxy(StandalonePostgresql $database) function stopPostgresProxy(StandalonePostgresql $database)

View File

@@ -7,6 +7,8 @@ use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey; use App\Models\PrivateKey;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -122,7 +124,8 @@ function instant_remote_process(Collection|array $command, Server $server, $thro
} }
return $output; return $output;
} }
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null) { function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
{
$ignoredErrors = collect([ $ignoredErrors = collect([
'Permission denied (publickey', 'Permission denied (publickey',
'Could not resolve hostname', 'Could not resolve hostname',
@@ -177,45 +180,55 @@ function refresh_server_connection(PrivateKey $private_key)
} }
} }
function validateServer(Server $server, bool $throwError = false) // function validateServer(Server $server, bool $throwError = false)
{ // {
try { // try {
$uptime = instant_remote_process(['uptime'], $server, $throwError); // $uptime = instant_remote_process(['uptime'], $server, $throwError);
if (!$uptime) { // if (!$uptime) {
$server->settings->is_reachable = false; // $server->settings->is_reachable = false;
return [ // $server->team->notify(new Unreachable($server));
"uptime" => null, // $server->unreachable_email_sent = true;
"dockerVersion" => null, // $server->save();
]; // return [
} // "uptime" => null,
$server->settings->is_reachable = true; // "dockerVersion" => null,
instant_remote_process(["docker ps"], $server, $throwError); // ];
$dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError); // }
if (!$dockerVersion) { // $server->settings->is_reachable = true;
$dockerVersion = null; // instant_remote_process(["docker ps"], $server, $throwError);
return [ // $dockerVersion = instant_remote_process(["docker version|head -2|grep -i version| awk '{print $2}'"], $server, $throwError);
"uptime" => $uptime, // if (!$dockerVersion) {
"dockerVersion" => null, // $dockerVersion = null;
]; // return [
} // "uptime" => $uptime,
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); // "dockerVersion" => null,
if (is_null($dockerVersion)) { // ];
$server->settings->is_usable = false; // }
} else { // $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
$server->settings->is_usable = true; // if (is_null($dockerVersion)) {
} // $server->settings->is_usable = false;
return [ // } else {
"uptime" => $uptime, // $server->settings->is_usable = true;
"dockerVersion" => $dockerVersion, // if (data_get($server, 'unreachable_email_sent') === true) {
]; // $server->team->notify(new Revived($server));
} catch (\Throwable $e) { // $server->unreachable_email_sent = false;
$server->settings->is_reachable = false; // $server->save();
$server->settings->is_usable = false; // }
throw $e; // }
} finally { // return [
if (data_get($server, 'settings')) $server->settings->save(); // "uptime" => $uptime,
} // "dockerVersion" => $dockerVersion,
} // ];
// } catch (\Throwable $e) {
// $server->settings->is_reachable = false;
// $server->settings->is_usable = false;
// throw $e;
// } finally {
// if (data_get($server, 'settings')) {
// $server->settings->save();
// }
// }
// }
function checkRequiredCommands(Server $server) function checkRequiredCommands(Server $server)
{ {

View File

@@ -64,7 +64,7 @@ function serviceStatus(Service $service)
} }
return 'exited'; return 'exited';
} }
function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService) function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneService, bool $isInit = false)
{ {
// TODO: make this async // TODO: make this async
try { try {
@@ -85,24 +85,39 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
} else { } else {
$fileLocation = $path; $fileLocation = $path;
} }
ray($path,$fileLocation);
// Exists and is a file
$isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server); $isFile = instant_remote_process(["test -f $fileLocation && echo OK || echo NOK"], $server);
// Exists and is a directory
$isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server);
if ($isFile == 'OK' && !$fileVolume->is_directory) {
if ($isFile == 'OK') {
// If its a file & exists
$filesystemContent = instant_remote_process(["cat $fileLocation"], $server); $filesystemContent = instant_remote_process(["cat $fileLocation"], $server);
if (base64_encode($filesystemContent) != base64_encode($content)) {
$fileVolume->content = $filesystemContent; $fileVolume->content = $filesystemContent;
$fileVolume->is_directory = false;
$fileVolume->save(); $fileVolume->save();
} } else if ($isDir == 'OK') {
} else { // If its a directory & exists
if ($isDir == 'OK') {
$fileVolume->content = null; $fileVolume->content = null;
$fileVolume->is_directory = true; $fileVolume->is_directory = true;
$fileVolume->save(); $fileVolume->save();
} else { } else if ($isFile == 'NOK' && $isDir == 'NOK' && !$fileVolume->is_directory && $isInit && $content) {
$fileVolume->content = null; // Does not exists (no dir or file), not flagged as directory, is init, has content
$fileVolume->content = $content;
$fileVolume->is_directory = false; $fileVolume->is_directory = false;
$fileVolume->save(); $fileVolume->save();
} $content = base64_encode($content);
$dir = Str::of($fileLocation)->dirname();
instant_remote_process([
"mkdir -p $dir",
"echo '$content' | base64 -d > $fileLocation"
], $server);
} else if ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) {
$fileVolume->content = null;
$fileVolume->is_directory = true;
$fileVolume->save();
instant_remote_process(["mkdir -p $fileLocation"], $server);
} }
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -122,13 +137,18 @@ function updateCompose($resource)
// Update FQDN // Update FQDN
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper(); $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
ray($variableName);
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) { if ($generatedEnv) {
$generatedEnv->value = $resource->fqdn; $generatedEnv->value = $resource->fqdn;
$generatedEnv->save(); $generatedEnv->save();
} }
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
if ($generatedEnv) {
$url = Str::of($resource->fqdn)->after('://');
$generatedEnv->value = $url;
$generatedEnv->save();
}
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);
$resource->service->docker_compose_raw = $dockerComposeRaw; $resource->service->docker_compose_raw = $dockerComposeRaw;

View File

@@ -14,6 +14,7 @@ use Illuminate\Mail\Message;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
@@ -309,6 +310,7 @@ function send_internal_notification(string $message): void
$baseUrl = config('app.name'); $baseUrl = config('app.name');
$team = Team::find(0); $team = Team::find(0);
$team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message)); $team->notify(new GeneralNotification("👀 {$baseUrl}: " . $message));
ray("👀 {$baseUrl}: " . $message);
} catch (\Throwable $e) { } catch (\Throwable $e) {
ray($e->getMessage()); ray($e->getMessage());
} }
@@ -342,6 +344,15 @@ function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null
); );
} }
} }
function isTestEmailEnabled($notifiable)
{
if (data_get($notifiable, 'use_instance_email_settings') && isInstanceAdmin()) {
return true;
} else if (data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') && auth()->user()->isAdminFromSession()) {
return true;
}
return false;
}
function isEmailEnabled($notifiable) function isEmailEnabled($notifiable)
{ {
return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings'); return data_get($notifiable, 'smtp_enabled') || data_get($notifiable, 'resend_enabled') || data_get($notifiable, 'use_instance_email_settings');
@@ -352,13 +363,14 @@ function setNotificationChannels($notifiable, $event)
$isEmailEnabled = isEmailEnabled($notifiable); $isEmailEnabled = isEmailEnabled($notifiable);
$isDiscordEnabled = data_get($notifiable, 'discord_enabled'); $isDiscordEnabled = data_get($notifiable, 'discord_enabled');
$isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); $isTelegramEnabled = data_get($notifiable, 'telegram_enabled');
$isSubscribedToEmailEvent = data_get($notifiable, "smtp_notifications_$event");
$isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event"); $isSubscribedToDiscordEvent = data_get($notifiable, "discord_notifications_$event");
$isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event"); $isSubscribedToTelegramEvent = data_get($notifiable, "telegram_notifications_$event");
if ($isDiscordEnabled && $isSubscribedToDiscordEvent) { if ($isDiscordEnabled && $isSubscribedToDiscordEvent) {
$channels[] = DiscordChannel::class; $channels[] = DiscordChannel::class;
} }
if ($isEmailEnabled) { if ($isEmailEnabled && $isSubscribedToEmailEvent) {
$channels[] = EmailChannel::class; $channels[] = EmailChannel::class;
} }
if ($isTelegramEnabled && $isSubscribedToTelegramEvent) { if ($isTelegramEnabled && $isSubscribedToTelegramEvent) {
@@ -425,6 +437,16 @@ function getServiceTemplates()
if (isDev()) { if (isDev()) {
$services = File::get(base_path('templates/service-templates.json')); $services = File::get(base_path('templates/service-templates.json'));
$services = collect(json_decode($services))->sortKeys(); $services = collect(json_decode($services))->sortKeys();
$deprecated = File::get(base_path('templates/deprecated.json'));
$deprecated = collect(json_decode($deprecated))->sortKeys();
$services = $services->merge($deprecated);
$version = config('version');
$services = $services->map(function ($service) use ($version) {
if (version_compare($version, data_get($service, 'minVersion', '0.0.0'), '<')) {
$service->disabled = true;
}
return $service;
});
} else { } else {
$services = Http::get(config('constants.services.official')); $services = Http::get(config('constants.services.official'));
if ($services->failed()) { if ($services->failed()) {

View File

@@ -122,14 +122,14 @@ function allowedPathsForUnsubscribedAccounts()
return [ return [
'subscription', 'subscription',
'login', 'login',
'register', 'logout',
'waitlist', 'waitlist',
'force-password-reset', 'force-password-reset',
'logout',
'livewire/message/force-password-reset', 'livewire/message/force-password-reset',
'livewire/message/check-license', 'livewire/message/check-license',
'livewire/message/switch-team', 'livewire/message/switch-team',
'livewire/message/subscription.pricing-plans' 'livewire/message/subscription.pricing-plans',
'livewire/message/help'
]; ];
} }
function allowedPathsForBoardingAccounts() function allowedPathsForBoardingAccounts()
@@ -141,3 +141,11 @@ function allowedPathsForBoardingAccounts()
'livewire/message/activity-monitor' 'livewire/message/activity-monitor'
]; ];
} }
function allowedPathsForInvalidAccounts() {
return [
'logout',
'verify',
'livewire/message/verify-email',
'livewire/message/help'
];
}

391
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,9 +31,9 @@ return [
'ultimate' => 25, 'ultimate' => 25,
], ],
'email' => [ 'email' => [
'zero' => false, 'zero' => true,
'self-hosted' => true, 'self-hosted' => true,
'basic' => false, 'basic' => true,
'pro' => true, 'pro' => true,
'ultimate' => true, 'ultimate' => true,
], ],

View File

@@ -1,6 +1,7 @@
<?php <?php
return [ return [
'docs' => 'https://coolify.io/contact',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),
'waitlist' => env('WAITLIST', false), 'waitlist' => env('WAITLIST', false),
'license_url' => 'https://licenses.coollabs.io', 'license_url' => 'https://licenses.coollabs.io',

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.51', 'release' => '4.0.0-beta.72',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.51'; return '4.0.0-beta.72';

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->boolean('health_check_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('health_check_enabled');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('services', function (Blueprint $table) {
$table->nullableMorphs('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('services', function (Blueprint $table) {
$table->dropMorphs('destination');
});
}
};

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->boolean('use_instance_email_settings')->default(true)->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('teams', function (Blueprint $table) {
$table->boolean('use_instance_email_settings')->default(false)->change();
});
}
};

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