Compare commits

...

140 Commits

Author SHA1 Message Date
Andras Bacsai
7f69eb3c2e Merge pull request #1479 from coollabsio/next
v4.0.0-beta.146 - quick fix before release
2023-11-22 14:27:04 +01:00
Andras Bacsai
6ccbf911b2 Fix condition for pushing to Docker registry 2023-11-22 14:25:55 +01:00
Andras Bacsai
5c77cec68f Merge pull request #1478 from coollabsio/next
v4.0.0-beta.146
2023-11-22 14:22:10 +01:00
Andras Bacsai
25a0489f7f Fix log drain issue in advanced and service application 2023-11-22 14:21:03 +01:00
Andras Bacsai
5e27b88bef Add new console commands for root email change, root password reset, and service deletion 2023-11-22 13:21:25 +01:00
Andras Bacsai
ec98afe707 Merge pull request #1474 from coollabsio/next
v4.0.0-beta.145
2023-11-22 08:45:00 +01:00
Andras Bacsai
ce26127705 wip: new deployment jobs 2023-11-21 22:17:35 +01:00
Andras Bacsai
ef7fc1b260 Refactor code and update destination component 2023-11-21 15:31:46 +01:00
Andras Bacsai
f58e6766e1 Update Docker Engine version check 2023-11-21 13:06:05 +01:00
Andras Bacsai
4a21102983 fix: server adding process 2023-11-21 12:07:06 +01:00
Andras Bacsai
e78b6758d8 feat: add docker engine support install script to rhel based systems 2023-11-21 11:39:19 +01:00
Andras Bacsai
16eb7f4fb4 Add tracing option to Sentry configuration 2023-11-21 09:01:52 +01:00
Andras Bacsai
4974ce6eda Update release version to 4.0.0-beta.145 2023-11-21 08:41:43 +01:00
Andras Bacsai
6cdba17aca Update token retrieval in reset-password.blade.php 2023-11-20 15:16:23 +01:00
Andras Bacsai
30f8e8f232 fix: handle different label formats in services 2023-11-20 15:01:35 +01:00
Andras Bacsai
608f0b7840 Refactor Docker image name generation and push to
registry
2023-11-20 14:23:11 +01:00
Andras Bacsai
d0366c4054 Update Docker Registry link in general.blade.php 2023-11-20 13:58:31 +01:00
Andras Bacsai
f88e3c5b29 feat: push locally built image to docker registry
ui: fixes here and there
2023-11-20 13:49:10 +01:00
Andras Bacsai
e33fec0e1a Refactor checkbox component and update GPU
settings helper links
2023-11-20 11:37:09 +01:00
Andras Bacsai
912b0a263e feat: gpu enabled containers
feat: move advanced settings to different view
2023-11-20 11:35:31 +01:00
Andras Bacsai
8f963adbd4 fix: only report nonruntime errors 2023-11-20 10:32:06 +01:00
Andras Bacsai
8f2c24d7e9 fix: reset password 2023-11-18 17:50:44 +01:00
Andras Bacsai
9f3dbc3cbb Merge pull request #1469 from coollabsio/next
v4.0.0-beta.144
2023-11-17 21:28:18 +01:00
Andras Bacsai
8a9ee84925 Fix log drain container notification bug 2023-11-17 21:24:22 +01:00
Andras Bacsai
689480003a feat: log drainer container check 2023-11-17 21:16:25 +01:00
Andras Bacsai
3b20eee909 feat: enable/disable log drain by service 2023-11-17 20:08:21 +01:00
Andras Bacsai
e8cadc176b Merge pull request #1468 from coollabsio/next
v4.0.0-beta.143
2023-11-17 15:21:29 +01:00
Andras Bacsai
b0c96e64c9 Fix server unreachable notification count 2023-11-17 15:18:08 +01:00
Andras Bacsai
9ce3b43e09 Add Team model and merge servers with own servers 2023-11-17 15:11:29 +01:00
Andras Bacsai
4c2b3df861 Update server runtime and comments 2023-11-17 14:56:39 +01:00
Andras Bacsai
467471f54a Fix server readiness check in ContainerStatusJob and ServerStatusJob 2023-11-17 14:46:04 +01:00
Andras Bacsai
60171093c5 Update version to 4.0.0-beta.143 2023-11-17 14:43:57 +01:00
Andras Bacsai
38f2a2dac7 Merge pull request #1467 from coollabsio/next
v4.0.0-beta.142
2023-11-17 14:32:32 +01:00
Andras Bacsai
307ee52ac0 wtf 2023-11-17 14:29:37 +01:00
Andras Bacsai
b66c9835b7 Fix server status check and add new job 2023-11-17 14:22:05 +01:00
Andras Bacsai
d38d50dca2 Fix server readiness check and update version
number
2023-11-17 14:14:13 +01:00
Andras Bacsai
40023be4ea Merge pull request #1466 from coollabsio/next
Quick fix version
2023-11-17 14:02:00 +01:00
Andras Bacsai
48d7c6e76f Fix config version key 2023-11-17 13:59:45 +01:00
Andras Bacsai
debacfe2f7 Merge pull request #1465 from coollabsio/next
v4.0.0-beta.141
2023-11-17 13:54:20 +01:00
Andras Bacsai
d430813230 Update versions and add server readiness check 2023-11-17 13:53:56 +01:00
Andras Bacsai
e30c37b041 Merge pull request #1464 from coollabsio/next
v4.0.0-beta.140
2023-11-17 13:25:03 +01:00
Andras Bacsai
8c73068cc7 Refactor server filtering logic in Kernel.php 2023-11-17 13:11:46 +01:00
Andras Bacsai
2c4e69ad50 Fix server readiness check in ContainerStatusJob
and ServerStatusJob
2023-11-17 13:04:51 +01:00
Andras Bacsai
5ae08d009e Add skipServer() method to Server model 2023-11-17 12:47:15 +01:00
Andras Bacsai
673b944647 Fix IP address validation in server forms 2023-11-17 12:38:47 +01:00
Andras Bacsai
16281248ac Refactor Dockerfile deployment logic and server
validation
2023-11-17 12:22:45 +01:00
Andras Bacsai
8670b41671 fix: do not allow to enter local ip addresses 2023-11-17 11:56:14 +01:00
Andras Bacsai
9c69044da5 Merge pull request #1463 from coollabsio/next
v4.0.0-beta.139
2023-11-17 11:35:48 +01:00
Andras Bacsai
ebc4ab9af5 Remove unnecessary ray() statement 2023-11-17 11:33:46 +01:00
Andras Bacsai
57738198ad Add fluentd logging configuration for database & services 2023-11-17 11:32:52 +01:00
Andras Bacsai
b8252b85b0 Refactor logging configuration in ApplicationDeploymentJob.php 2023-11-17 11:13:16 +01:00
Andras Bacsai
479c2743bd Update Fluent Bit configuration file 2023-11-17 10:50:02 +01:00
Andras Bacsai
81e6482d7a Remove commented out code and fix indentation 2023-11-17 10:21:42 +01:00
Andras Bacsai
88c5d87084 Add log drain settings for New Relic,Highlight.io, and Axiom 2023-11-17 10:21:19 +01:00
Andras Bacsai
6c7e091e1b feat: log drain (wip) 2023-11-17 00:37:09 +01:00
Andras Bacsai
91e3d33c0b Add cleanup of stucked helper containers on servers 2023-11-16 20:48:25 +01:00
Andras Bacsai
aa00389824 Remove redundant sentence about cloud version in
README.md
2023-11-16 17:43:20 +01:00
Andras Bacsai
b4e54ab3e3 Improve Cloud version features and reduce
maintenance
2023-11-16 17:42:25 +01:00
Andras Bacsai
8f3c5d4bd3 Add donation link and update version numbers 2023-11-16 17:40:49 +01:00
Andras Bacsai
26668c71a1 Merge pull request #1460 from coollabsio/next
v4.0.0-beta.138
2023-11-16 15:28:48 +01:00
Andras Bacsai
bd7637c696 Add healthcheck URL to deployment job and update
version to beta.138
2023-11-16 15:23:07 +01:00
Andras Bacsai
cff54f48a3 Merge pull request #1459 from coollabsio/next
v4.0.0-beta.137
2023-11-16 14:39:48 +01:00
Andras Bacsai
5c0f239f62 Update server readiness check runtime to 1 minute 2023-11-16 14:36:43 +01:00
Andras Bacsai
d56c28c8d9 Remove unused notifications from
ContainerStatusJob
2023-11-16 14:29:23 +01:00
Andras Bacsai
2b666ff121 Refactor server and docker cleanup jobs 2023-11-16 14:29:01 +01:00
Andras Bacsai
fb42c43953 Add isLocalhost method to Server model and
conditionally show Cloudflare Tunnel checkbox in
server form view
2023-11-16 14:28:26 +01:00
Andras Bacsai
81437e6822 Fix high disk usage notification bug in
ServerStatusJob.php and HighDiskUsage.php
2023-11-16 13:49:08 +01:00
Andras Bacsai
2fe429fe92 Comment out logging configuration in
ApplicationDeploymentJob.php
2023-11-16 13:32:07 +01:00
Andras Bacsai
4f0b214042 Add timeout to ApplicationDeploymentJob 2023-11-16 13:27:51 +01:00
Andras Bacsai
c866213f34 fix: when to pull image 2023-11-16 13:22:12 +01:00
Andras Bacsai
7cec6330cf Update server status check and notifications 2023-11-16 11:53:37 +01:00
Andras Bacsai
f5de21a343 Add OTLP exporter and host metrics receiver
configuration to config.yaml.
2023-11-16 11:16:41 +01:00
Andras Bacsai
ecbfc4d790 Add Fluent Bit and New Relic configurations 2023-11-15 15:45:37 +01:00
Andras Bacsai
55ff00e028 Add logging configuration to compose file 2023-11-15 15:19:31 +01:00
Andras Bacsai
a0fc2bbb85 Merge pull request #1457 from coollabsio/next
v4.0.0-beta.136
2023-11-15 10:55:39 +01:00
Andras Bacsai
51a704b22a Remove middleware and uniqueId methods from
DockerCleanupJob
2023-11-15 10:37:55 +01:00
Andras Bacsai
6d49678842 Remove unnecessary echo and add alive message 2023-11-15 10:37:02 +01:00
Andras Bacsai
0459b3a115 Add init-script to prod-ssu Docker container 2023-11-15 10:31:48 +01:00
Andras Bacsai
82592c8222 Add alive request to Init command 2023-11-15 10:26:31 +01:00
Andras Bacsai
25bf8895e2 Add InstanceSettings to Init command 2023-11-15 10:20:48 +01:00
Andras Bacsai
f4f7bdf7d5 Update dependencies and add new feature 2023-11-15 10:18:41 +01:00
Andras Bacsai
c008564aa3 Merge pull request #1456 from coollabsio/next
v4.0.0-beta.135
2023-11-15 09:40:33 +01:00
Andras Bacsai
b825d98b2d Refactor storage connection handling and project
initialization
2023-11-15 09:34:27 +01:00
Andras Bacsai
1f711d9281 Update version and fix webhook generation 2023-11-15 09:15:49 +01:00
Andras Bacsai
1de850f640 Merge pull request #1453 from coollabsio/next
v4.0.0-beta.134
2023-11-14 19:31:24 +01:00
Andras Bacsai
f176247b02 Update application deployment and version numbers 2023-11-14 19:29:59 +01:00
Andras Bacsai
3f3a1283df Merge pull request #1452 from coollabsio/next
v4.0.0-beta.133
2023-11-14 15:07:15 +01:00
Andras Bacsai
087bfcad08 Update server model and version configurations 2023-11-14 15:06:03 +01:00
Andras Bacsai
efd2899ae3 Merge pull request #1450 from coollabsio/next
v4.0.0-beta.132
2023-11-14 14:22:17 +01:00
Andras Bacsai
e4b2195932 Fix manual Git webhook generation 2023-11-14 14:14:21 +01:00
Andras Bacsai
0590ed7b2e Update webhooks configuration and application search. 2023-11-14 14:07:48 +01:00
Andras Bacsai
3a3c9448a4 Add gitWebhook method to Application model and fix
Dockerfile input display
2023-11-14 14:07:42 +01:00
Andras Bacsai
36d65ad5a8 Fix Dockerfile location in deployment job 2023-11-14 14:07:33 +01:00
Andras Bacsai
8db66952e8 Add manual Git webhooks and migration files 2023-11-14 13:26:14 +01:00
Andras Bacsai
45fa88ca4d Add error handling for missing email settings in
EmailChannel.php
2023-11-14 11:04:45 +01:00
Andras Bacsai
84b74f0b57 Update version numbers to 4.0.0-beta.132 2023-11-14 10:59:02 +01:00
Andras Bacsai
423cf62d92 Add support for dynamic docker-compose file name
in ApplicationDeploymentJob.php
2023-11-14 08:52:17 +01:00
Andras Bacsai
c4d9deabef Add debugging statement to report exceptions in
development environment
2023-11-13 21:17:17 +01:00
Andras Bacsai
776b1cb68d Add unauthenticated method to handle
authentication exceptions
2023-11-13 21:16:48 +01:00
Andras Bacsai
fc3025398e Merge pull request #1447 from coollabsio/next
v4.0.0-beta.131
2023-11-13 19:35:43 +01:00
Andras Bacsai
457c16c4dc remove ray 2023-11-13 19:26:11 +01:00
Andras Bacsai
ccf63c67e8 fix: mariadb backups 2023-11-13 19:25:18 +01:00
Andras Bacsai
945157b30c Merge pull request #1446 from coollabsio/next
v4.0.0-beta.130
2023-11-13 17:09:10 +01:00
Andras Bacsai
13798392be fix: generate service fields 2023-11-13 17:06:43 +01:00
Andras Bacsai
0d05b0a3d6 Merge pull request #1445 from coollabsio/next
v4.0.0-beta.129
2023-11-13 16:48:18 +01:00
Andras Bacsai
e0d2f88d99 fix: fqdn for minio 2023-11-13 16:45:54 +01:00
Andras Bacsai
e260bfae02 Merge pull request #1443 from coollabsio/next
v4.0.0-beta.128
2023-11-13 15:49:51 +01:00
Andras Bacsai
5abd4a6d78 Update version and fix MINIO_BROWSER_REDIRECT_URL
and MINIO_SERVER_URL
2023-11-13 15:49:23 +01:00
Andras Bacsai
9dff1e5631 Merge pull request #1442 from coollabsio/next
v4.0.0-beta.127
2023-11-13 15:42:28 +01:00
Andras Bacsai
02332ade1b Fix URLs and remove unnecessary command in
ApplicationDeploymentJob.php
2023-11-13 15:41:49 +01:00
Andras Bacsai
486de58d5b Update database start commands 2023-11-13 15:27:33 +01:00
Andras Bacsai
606aeb2b61 Merge pull request #1441 from coollabsio/next
v4.0.0-beta.126
2023-11-13 15:21:45 +01:00
Andras Bacsai
3fc264560c Update dependencies and fix minor bugs. 2023-11-13 15:19:49 +01:00
Andras Bacsai
3dd9182281 Add sponsorship notification and disable option,
update dependencies
2023-11-13 14:44:54 +01:00
Andras Bacsai
c838ff7198 Update version numbers to 4.0.0-beta.126 2023-11-13 13:38:50 +01:00
Andras Bacsai
ca6db9c1a9 Merge pull request #1440 from coollabsio/next
v4.0.0-beta.125
2023-11-13 13:21:00 +01:00
Andras Bacsai
f27e00e80e Update version.json to include v4.0.0-beta.125 2023-11-13 13:20:28 +01:00
Andras Bacsai
60cf296f31 Update preview application deployment labels and version 2023-11-13 13:20:12 +01:00
Andras Bacsai
ea64e9d5ad Merge pull request #1439 from coollabsio/next
v4.0.0-beta.124
2023-11-13 13:03:46 +01:00
Andras Bacsai
55846c5635 Fix service retrieval and add error handling 2023-11-13 12:59:59 +01:00
Andras Bacsai
7763594e6e Add pull_latest_image function and update
build_image function to use it. Also add check for
dockerfile existence in start_by_compose_file
function.
2023-11-13 12:30:25 +01:00
Andras Bacsai
6b5339c1c1 Remove ray debug statement and refactor random
name generator
2023-11-13 11:44:13 +01:00
Andras Bacsai
f2980738e4 Fix documentation link in service-templates.json 2023-11-13 11:30:20 +01:00
Andras Bacsai
f0e3ad0461 Merge pull request #1432 from AlejandroAkbal/main
fix(fider template): use the correct docs url
2023-11-13 11:29:39 +01:00
Andras Bacsai
187050e098 Merge pull request #1435 from AshikNesin/main
Fix typo in onboarding page
2023-11-13 11:29:02 +01:00
Andras Bacsai
9e7823795d Fix null check for MINIO_BROWSER_REDIRECT_URL and
MINIO_SERVER_URL in generateServiceSpecificFqdns
function
2023-11-13 11:17:49 +01:00
Andras Bacsai
239459dfa8 Remove commented out code for minio service 2023-11-13 11:13:16 +01:00
Andras Bacsai
ce0f560c44 Add service-specific configuration fields and save
them to the database
2023-11-13 11:09:21 +01:00
Andras Bacsai
95baec99dd Fix typo in General.php component 2023-11-13 09:04:19 +01:00
Andras Bacsai
363e8fc0b5 Update code with bug fixes and improvements 2023-11-13 08:46:43 +01:00
Andras Bacsai
e49caba920 Add STRIPE_EXCLUDED_PLANS to services in
docker-compose.prod.yml
2023-11-13 08:46:17 +01:00
Ashik Nesin
30db2b2a09 Update typo in onboarding screen 2023-11-12 19:30:20 +00:00
Andras Bacsai
285666e181 Merge pull request #1434 from coollabsio/next
v4.0.0-beta.123
2023-11-12 19:11:31 +01:00
Andras Bacsai
003934ee1d disable service confs for now 2023-11-12 19:10:54 +01:00
Andras Bacsai
44c7958aa6 make fqdn super long 2023-11-12 19:09:38 +01:00
Alejandro Akbal
35b1a81dfe fix(fider template): use the correct docs url 2023-11-12 12:10:53 +00:00
Andras Bacsai
e40f397cc7 fix: service updates 2023-11-11 21:32:41 +01:00
Andras Bacsai
9fd8cd7e6c Merge pull request #1430 from coollabsio/next
v4.0.0-beta.122
2023-11-11 10:19:28 +01:00
Andras Bacsai
a94b7ee611 fix: container status jobs for old pr deployments 2023-11-11 10:18:40 +01:00
Andras Bacsai
fc68bf50b5 save 2023-11-10 22:04:04 +01:00
139 changed files with 5120 additions and 862 deletions

View File

@@ -0,0 +1,22 @@
<?php
use App\Models\User;
$email = 'test@example.com';
$user = User::whereEmail($email)->first();
$teams = $user->teams;
foreach ($teams as $team) {
$servers = $team->servers;
if ($servers->count() > 0) {
foreach ($servers as $server) {
dump($server);
$server->delete();
}
}
dump($team);
$team->delete();
}
if ($user) {
dump($user);
$user->delete();
}

View File

@@ -10,35 +10,40 @@ No vendor lock-in, which means that all the configuration for your applications/
For more information, take a look at our landing page [here](https://coolify.io). For more information, take a look at our landing page [here](https://coolify.io).
> If you are looking for previous (v3) version, it is [here](https://github.com/coollabsio/coolify/tree/v3). # Donations
To stay completely free, open-source, no feature behind paywall and evolve the project, we need your help. If you like Coolify, please consider donating to help us fund the future development of the project.
https://coolify.io/sponsorships
Thank you so much!
# Cloud # Cloud
If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io If you do not want to self-host Coolify, there is a paid cloud version available: https://app.coolify.io
You can easily attach your own servers, get all the automations, free email notifications, etc.
For more information & pricing, take a look at our landing page [here](https://coolify.io). For more information & pricing, take a look at our landing page [here](https://coolify.io).
# Beta ## Why should I use the Cloud version?
The recommended way to use Coolify is to have one server for Coolify and one (or more) for the resources you are deploying. A server is around 4-5$/month.
The latest version (v4) is still in beta. That does not mean it is unstable. All the features that are available are stable enough be usable in real-life. By subscribing to the cloud version, you get the Coolify server for the same price, but with:
- High-availability
There are hundreds of people using it for managing their client's applications, freelancers, hobbyists, businesses. - Free email notifications
- Better support
- Less maintenance for you
# Installation # Installation
```bash ```bash
curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
``` ```
You can find the installation script source [here](./scripts/install.sh).
You can find the installation script [here](./scripts/install.sh). # Support
## Support
Contact us [here](https://coolify.io/docs/contact). Contact us [here](https://coolify.io/docs/contact).
## Recognitions # Recognitions
<p> <p>
<a href="https://news.ycombinator.com/item?id=26624341"> <a href="https://news.ycombinator.com/item?id=26624341">
@@ -54,11 +59,11 @@ Contact us [here](https://coolify.io/docs/contact).
<a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/634" target="_blank"><img src="https://trendshift.io/api/badge/repositories/634" alt="coollabsio%2Fcoolify | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
## 💰 Financial Contributors # 💰 Financial Contributors
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)] Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/coollabsio/contribute)]
### Organizations ## Organizations
Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)! Special thanks to our biggest sponsors, [CCCareers](https://cccareers.org/) and [Appwrite](https://appwrite.io)!
@@ -78,10 +83,10 @@ Support this project with your organization. Your logo will show up here with a
<a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/8/website"><img src="https://opencollective.com/coollabsio/organization/8/avatar.svg"></a>
<a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a> <a href="https://opencollective.com/coollabsio/organization/9/website"><img src="https://opencollective.com/coollabsio/organization/9/avatar.svg"></a>
### Individuals ## Individuals
<a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a> <a href="https://opencollective.com/coollabsio"><img src="https://opencollective.com/coollabsio/individuals.svg?width=890"></a>
## Star History # Star History
[![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date) [![Star History Chart](https://api.star-history.com/svg?repos=coollabsio/coolify&type=Date)](https://star-history.com/#coollabsio/coolify&Date)

View File

@@ -129,6 +129,7 @@ class StartDatabaseProxy
"echo '{$dockerfile_base64}' | base64 -d > $configuration_dir/Dockerfile", "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} pull",
"docker compose --project-directory {$configuration_dir} up --build -d", "docker compose --project-directory {$configuration_dir} up --build -d",
], $server); ], $server);
} }

View File

@@ -23,7 +23,7 @@ class StartMariadb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -69,6 +69,16 @@ class StartMariadb
] ]
] ]
]; ];
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) { if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
} }
@@ -91,8 +101,10 @@ class StartMariadb
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -25,7 +25,7 @@ class StartMongodb
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -76,6 +76,16 @@ class StartMongodb
] ]
] ]
]; ];
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) { if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
} }
@@ -107,8 +117,10 @@ class StartMongodb
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -23,7 +23,7 @@ class StartMysql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -69,6 +69,16 @@ class StartMysql
] ]
] ]
]; ];
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) { if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
} }
@@ -91,8 +101,10 @@ class StartMysql
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -23,7 +23,7 @@ class StartPostgresql
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
"mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/" "mkdir -p $this->configuration_dir/docker-entrypoint-initdb.d/"
]; ];
@@ -79,6 +79,17 @@ class StartPostgresql
] ]
] ]
]; ];
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
ray('Log Drain Enabled');
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) { if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
} }
@@ -116,8 +127,10 @@ class StartPostgresql
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }

View File

@@ -26,7 +26,7 @@ class StartRedis
$this->configuration_dir = database_configuration_dir() . '/' . $container_name; $this->configuration_dir = database_configuration_dir() . '/' . $container_name;
$this->commands = [ $this->commands = [
"echo '####### Starting {$database->name}.'", "echo 'Starting {$database->name}.'",
"mkdir -p $this->configuration_dir", "mkdir -p $this->configuration_dir",
]; ];
@@ -78,6 +78,16 @@ class StartRedis
] ]
] ]
]; ];
if ($this->database->destination->server->isLogDrainEnabled() && $this->database->isLogDrainEnabled()) {
$docker_compose['services'][$container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if (count($this->database->ports_mappings_array) > 0) { if (count($this->database->ports_mappings_array) > 0) {
$docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array; $docker_compose['services'][$container_name]['ports'] = $this->database->ports_mappings_array;
} }
@@ -101,8 +111,10 @@ class StartRedis
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml"; $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml";
$readme = generate_readme_file($this->database->name, now()); $readme = generate_readme_file($this->database->name, now());
$this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md"; $this->commands[] = "echo '{$readme}' > $this->configuration_dir/README.md";
$this->commands[] = "echo 'Pulling {$database->image} image.'";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml pull";
$this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d"; $this->commands[] = "docker compose -f $this->configuration_dir/docker-compose.yml up -d";
$this->commands[] = "echo '####### {$database->name} started.'"; $this->commands[] = "echo '{$database->name} started.'";
return remote_process($this->commands, $database->destination->server); return remote_process($this->commands, $database->destination->server);
} }
@@ -154,6 +166,5 @@ class StartRedis
$content = $this->database->redis_conf; $content = $this->database->redis_conf;
$content_base64 = base64_encode($content); $content_base64 = base64_encode($content);
$this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}"; $this->commands[] = "echo '{$content_base64}' | base64 -d > $this->configuration_dir/{$filename}";
} }
} }

View File

@@ -11,6 +11,11 @@ class InstallDocker
use AsAction; use AsAction;
public function handle(Server $server) public function handle(Server $server)
{ {
$supported_os_type = $server->validateOS();
if (!$supported_os_type) {
throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
}
ray('Installing Docker on server: ' . $server->name . ' (' . $server->ip . ')' . ' with OS: ' . $supported_os_type);
$dockerVersion = '24.0'; $dockerVersion = '24.0';
$config = base64_encode('{ $config = base64_encode('{
"log-driver": "json-file", "log-driver": "json-file",
@@ -27,36 +32,49 @@ class InstallDocker
'server_id' => $server->id, 'server_id' => $server->id,
]); ]);
} }
$command = collect([]);
if (isDev() && $server->id === 0) { if (isDev() && $server->id === 0) {
$command = [ $command = $command->merge([
"echo '####### Installing Prerequisites...'", "echo 'Installing Prerequisites...'",
"sleep 1", "sleep 1",
"echo '####### Installing/updating Docker Engine...'", "echo 'Installing Docker Engine...'",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"sleep 4", "sleep 4",
"echo '####### Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"ls -l /tmp" "ls -l /tmp"
]; ]);
} else { } else {
$command = [ if ($supported_os_type === 'debian') {
"echo '####### Installing Prerequisites...'", $command = $command->merge([
"command -v jq >/dev/null || apt-get update", "echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || apt install -y jq", "command -v jq >/dev/null || apt-get update",
"echo '####### Installing/updating Docker Engine...'", "command -v jq >/dev/null || apt install -y jq",
]);
} else if ($supported_os_type === 'rhel') {
$command = $command->merge([
"echo 'Installing Prerequisites...'",
"command -v jq >/dev/null || dnf install -y jq",
]);
} else {
throw new \Exception('Unsupported OS');
}
$command = $command->merge([
"echo 'Installing Docker Engine...'",
"curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh", "curl https://releases.rancher.com/install-docker/{$dockerVersion}.sh | sh",
"echo '####### Configuring Docker Engine (merging existing configuration with the required)...'", "echo 'Configuring Docker Engine (merging existing configuration with the required)...'",
"test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json", "test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json \"/etc/docker/daemon.json.original-`date +\"%Y%m%d-%H%M%S\"`\" || echo '{$config}' | base64 -d > /etc/docker/daemon.json",
"echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify", "echo '{$config}' | base64 -d > /etc/docker/daemon.json.coolify",
"cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify", "cat <<< $(jq . /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json.coolify",
"cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json", "cat <<< $(jq -s '.[0] * .[1]' /etc/docker/daemon.json /etc/docker/daemon.json.coolify) > /etc/docker/daemon.json",
"echo '####### Restarting Docker Engine...'", "echo 'Restarting Docker Engine...'",
"systemctl enable docker >/dev/null 2>&1 || true",
"systemctl restart docker", "systemctl restart docker",
"echo '####### Creating default Docker network (coolify)...'", "echo 'Creating default Docker network (coolify)...'",
"docker network create --attachable coolify >/dev/null 2>&1 || true", "docker network create --attachable coolify >/dev/null 2>&1 || true",
"echo '####### Done!'" "echo 'Done!'"
]; ]);
return remote_process($command, $server);
} }
return remote_process($command, $server);
} }
} }

View File

@@ -0,0 +1,195 @@
<?php
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
class InstallLogDrain
{
use AsAction;
public function handle(Server $server)
{
if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic';
} else if ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight';
} else if ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom';
} else {
$type = 'none';
}
try {
if ($type === 'none') {
$command = [
"echo 'Stopping old Fluent Bit'",
"docker rm -f coolify-log-drain || true",
];
return instant_remote_process($command, $server);
} else if ($type === 'newrelic') {
if (!$server->settings->is_logdrain_newrelic_enabled) {
throw new \Exception('New Relic log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Tag container_logs
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[FILTER]
Name grep
Match *
Exclude log 127.0.0.1
[FILTER]
Name modify
Match *
Set server_name {$server->name}
[OUTPUT]
Name nrlogs
Match *
license_key \${LICENSE_KEY}
# https://log-api.eu.newrelic.com/log/v1 - EU
# https://log-api.newrelic.com/log/v1 - US
base_uri \${BASE_URI}
");
} else if ($type === 'highlight') {
if (!$server->settings->is_logdrain_highlight_enabled) {
throw new \Exception('Highlight log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
tag \${HIGHLIGHT_PROJECT_ID}
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[OUTPUT]
Name forward
Match *
Host otel.highlight.io
Port 24224
");
} else if ($type === 'axiom') {
if (!$server->settings->is_logdrain_axiom_enabled) {
throw new \Exception('Axiom log drain is not enabled.');
}
$config = base64_encode("
[SERVICE]
Flush 5
Daemon off
Log_Level debug
Parsers_File parsers.conf
[INPUT]
Name forward
Buffer_Chunk_Size 1M
Buffer_Max_Size 6M
[FILTER]
Name grep
Match *
Exclude log 127.0.0.1
[FILTER]
Name modify
Match *
Set server_name {$server->name}
[OUTPUT]
Name http
Match *
Host api.axiom.co
Port 443
URI /v1/datasets/\${AXIOM_DATASET_NAME}/ingest
# Authorization Bearer should be an API token
Header Authorization Bearer \${AXIOM_API_KEY}
compress gzip
format json
json_date_key _time
json_date_format iso8601
tls On
");
} else {
throw new \Exception('Unknown log drain type.');
}
$parsers = base64_encode("
[PARSER]
Name empty_line_skipper
Format regex
Regex /^(?!\s*$).+/
");
$compose = base64_encode("
services:
coolify-log-drain:
image: cr.fluentbit.io/fluent/fluent-bit:2.0
container_name: coolify-log-drain
command: -c /fluent-bit.conf
env_file:
- .env
volumes:
- ./fluent-bit.conf:/fluent-bit.conf
- ./parsers.conf:/parsers.conf
ports:
- 127.0.0.1:24224:24224
restart: unless-stopped
");
$readme = base64_encode('# New Relic Log Drain
This log drain is based on [Fluent Bit](https://fluentbit.io/) and New Relic Log Forwarder.
Files:
- `fluent-bit.conf` - configuration file for Fluent Bit
- `docker-compose.yml` - docker-compose file to run Fluent Bit
- `.env` - environment variables for Fluent Bit
');
$license_key = $server->settings->logdrain_newrelic_license_key;
$base_uri = $server->settings->logdrain_newrelic_base_uri;
$base_path = config('coolify.base_config_path');
$config_path = $base_path . '/log-drains';
$fluent_bit_config = $config_path . '/fluent-bit.conf';
$parsers_config = $config_path . '/parsers.conf';
$compose_path = $config_path . '/docker-compose.yml';
$readme_path = $config_path . '/README.md';
$command = [
"echo 'Saving configuration'",
"mkdir -p $config_path",
"echo '{$parsers}' | base64 -d > $parsers_config",
"echo '{$config}' | base64 -d > $fluent_bit_config",
"echo '{$compose}' | base64 -d > $compose_path",
"echo '{$readme}' | base64 -d > $readme_path",
"test -f $config_path/.env && rm $config_path/.env",
];
if ($type === 'newrelic') {
$add_envs_command = [
"echo LICENSE_KEY=$license_key >> $config_path/.env",
"echo BASE_URI=$base_uri >> $config_path/.env",
];
} else if ($type === 'highlight') {
$add_envs_command = [
"echo HIGHLIGHT_PROJECT_ID={$server->settings->logdrain_highlight_project_id} >> $config_path/.env",
];
} else if ($type === 'axiom') {
$add_envs_command = [
"echo AXIOM_DATASET_NAME={$server->settings->logdrain_axiom_dataset_name} >> $config_path/.env",
"echo AXIOM_API_KEY={$server->settings->logdrain_axiom_api_key} >> $config_path/.env",
];
}
$restart_command = [
"echo 'Stopping old Fluent Bit'",
"cd $config_path && docker rm -f coolify-log-drain || true",
"echo 'Starting Fluent Bit'",
"cd $config_path && docker compose up -d --remove-orphans",
];
$command = array_merge($command, $add_envs_command, $restart_command);
return instant_remote_process($command, $server);
} catch (\Throwable $e) {
return handleError($e);
}
}
}

View File

@@ -14,13 +14,13 @@ class StartService
$network = $service->destination->network; $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()}.'";
$commands[] = "echo '####### Creating Docker network.'"; $commands[] = "echo 'Creating Docker network.'";
$commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true"; $commands[] = "docker network create --attachable '{$service->uuid}' >/dev/null || true";
$commands[] = "echo '####### Starting service {$service->name} on {$service->server->name}.'"; $commands[] = "echo 'Starting service {$service->name} on {$service->server->name}.'";
$commands[] = "echo '####### Pulling images.'"; $commands[] = "echo 'Pulling images.'";
$commands[] = "docker compose pull"; $commands[] = "docker compose pull";
$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 || true"; $commands[] = "docker network connect $service->uuid coolify-proxy || true";
$compose = data_get($service,'docker_compose',[]); $compose = data_get($service,'docker_compose',[]);

View File

@@ -3,8 +3,11 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\InstanceSettings;
use App\Models\Server;
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@@ -14,6 +17,7 @@ use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage; use Illuminate\Support\Facades\Storage;
class Init extends Command class Init extends Command
@@ -23,15 +27,41 @@ class Init extends Command
public function handle() public function handle()
{ {
ray()->clearAll(); $this->alive();
$cleanup = $this->option('cleanup'); $cleanup = $this->option('cleanup');
if ($cleanup) { if ($cleanup) {
$this->cleanup_stucked_resources(); $this->cleanup_stucked_resources();
$this->cleanup_ssh(); $this->cleanup_ssh();
} }
$this->cleanup_in_progress_application_deployments(); $this->cleanup_in_progress_application_deployments();
$this->cleanup_stucked_helper_containers();
} }
private function cleanup_stucked_helper_containers() {
$servers = Server::all();
foreach ($servers as $server) {
if ($server->isFunctional()) {
CleanupHelperContainersJob::dispatch($server);
}
}
}
private function alive()
{
$id = config('app.id');
$version = config('version');
$settings = InstanceSettings::get();
$do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n";
return;
}
try {
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id&version=$version");
echo "I am alive!\n";
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
}
}
private function cleanup_ssh() private function cleanup_ssh()
{ {
try { try {

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use App\Models\User;
use Illuminate\Console\Command;
class RootChangeEmail extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'root:change-email';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Change Root Email';
/**
* Execute the console command.
*/
public function handle()
{
//
$this->info('You are about to change the root user\'s email.');
$email = $this->ask('Give me a new email for root user');
$this->info('Updating root email...');
try {
User::find(0)->update(['email' => $email]);
$this->info('Root user\'s email updated successfully.');
} catch (\Exception $e) {
$this->error('Failed to update root user\'s email.');
return;
}
}
}

View File

@@ -8,14 +8,14 @@ use Illuminate\Support\Facades\Hash;
use function Laravel\Prompts\password; use function Laravel\Prompts\password;
class UsersResetRoot extends Command class RootResetPassword extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'users:reset-root'; protected $signature = 'root:reset-password';
/** /**
* The console command description. * The console command description.

View File

@@ -12,21 +12,21 @@ use function Laravel\Prompts\confirm;
use function Laravel\Prompts\multiselect; use function Laravel\Prompts\multiselect;
use function Laravel\Prompts\select; use function Laravel\Prompts\select;
class ResourcesDelete extends Command class ServicesDelete extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'resources:delete'; protected $signature = 'services:delete';
/** /**
* The console command description. * The console command description.
* *
* @var string * @var string
*/ */
protected $description = 'Delete a resource from the database'; protected $description = 'Delete a service from the database';
/** /**
* Execute the console command. * Execute the console command.
@@ -34,7 +34,7 @@ class ResourcesDelete extends Command
public function handle() public function handle()
{ {
$resource = select( $resource = select(
'What resource do you want to delete?', 'What service do you want to delete?',
['Application', 'Database', 'Service', 'Server'], ['Application', 'Database', 'Service', 'Server'],
); );
if ($resource === 'Application') { if ($resource === 'Application') {

View File

@@ -2,16 +2,17 @@
namespace App\Console; namespace App\Console;
use App\Jobs\CheckResaleLicenseJob; use App\Jobs\CheckLogDrainContainerJob;
use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupInstanceStuffsJob;
use App\Jobs\DatabaseBackupJob; use App\Jobs\DatabaseBackupJob;
use App\Jobs\DockerCleanupJob;
use App\Jobs\InstanceAutoUpdateJob; use App\Jobs\InstanceAutoUpdateJob;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\PullHelperImageJob; use App\Jobs\PullHelperImageJob;
use App\Jobs\ServerStatusJob;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\Team;
use Illuminate\Console\Scheduling\Schedule; use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel; use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -27,7 +28,6 @@ class Kernel extends ConsoleKernel
// Server Jobs // Server Jobs
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->pull_helper_image($schedule); $this->pull_helper_image($schedule);
} else { } else {
@@ -40,33 +40,31 @@ class Kernel extends ConsoleKernel
$this->instance_auto_update($schedule); $this->instance_auto_update($schedule);
$this->check_scheduled_backups($schedule); $this->check_scheduled_backups($schedule);
$this->check_resources($schedule); $this->check_resources($schedule);
$this->cleanup_servers($schedule);
$this->pull_helper_image($schedule); $this->pull_helper_image($schedule);
} }
} }
private function pull_helper_image($schedule) private function pull_helper_image($schedule)
{ {
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true); $servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer(); $schedule->job(new PullHelperImageJob($server))->everyTenMinutes()->onOneServer();
} }
} }
private function cleanup_servers($schedule)
{
$servers = Server::all()->where('settings.is_usable', true)->where('settings.is_reachable', true);
foreach ($servers as $server) {
$schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->onOneServer();
}
}
private function check_resources($schedule) private function check_resources($schedule)
{ {
if (isCloud()) { if (isCloud()) {
$servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false); $servers = Server::all()->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4');
$own = Team::find(0)->servers;
$servers = $servers->merge($own);
} else { } else {
$servers = Server::all(); $servers = Server::all()->where('ip', '!=', '1.2.3.4');
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerStatusJob($server))->everyTenMinutes()->onOneServer();
$schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer(); $schedule->job(new ContainerStatusJob($server))->everyMinute()->onOneServer();
if ($server->isLogDrainEnabled()) {
$schedule->job(new CheckLogDrainContainerJob($server))->everyMinute()->onOneServer();
}
} }
} }
private function instance_auto_update($schedule) private function instance_auto_update($schedule)

View File

@@ -4,7 +4,9 @@ namespace App\Exceptions;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use RuntimeException;
use Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
use Sentry\State\Scope; use Sentry\State\Scope;
use Throwable; use Throwable;
@@ -40,6 +42,13 @@ class Handler extends ExceptionHandler
]; ];
private InstanceSettings $settings; private InstanceSettings $settings;
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->is('api/*') || $request->expectsJson() || $this->shouldReturnJson($request, $exception)) {
return response()->json(['message' => $exception->getMessage()], 401);
}
return redirect()->guest($exception->redirectTo() ?? route('login'));
}
/** /**
* Register the exception handling callbacks for the application. * Register the exception handling callbacks for the application.
*/ */
@@ -47,6 +56,9 @@ class Handler extends ExceptionHandler
{ {
$this->reportable(function (Throwable $e) { $this->reportable(function (Throwable $e) {
if (isDev()) { if (isDev()) {
// return;
}
if ($e instanceof RuntimeException) {
return; return;
} }
$this->settings = InstanceSettings::get(); $this->settings = InstanceSettings::get();
@@ -65,6 +77,7 @@ class Handler extends ExceptionHandler
); );
} }
); );
ray('reporting to sentry');
Integration::captureUnhandledException($e); Integration::captureUnhandledException($e);
}); });
} }

View File

@@ -10,23 +10,6 @@ class ApplicationController extends Controller
{ {
use AuthorizesRequests, ValidatesRequests; use AuthorizesRequests, ValidatesRequests;
public function configuration()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
return view('project.application.configuration', ['application' => $application]);
}
public function deployments() public function deployments()
{ {
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first(); $project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();

View File

@@ -33,7 +33,7 @@ class Index extends Component
public ?string $remoteServerUser = 'root'; public ?string $remoteServerUser = 'root';
public ?Server $createdServer = null; public ?Server $createdServer = null;
public Collection|array $projects = []; public Collection $projects;
public ?int $selectedExistingProject = null; public ?int $selectedExistingProject = null;
public ?Project $createdProject = null; public ?Project $createdProject = null;
@@ -188,7 +188,6 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function validateServer() public function validateServer()
{ {
try { try {
$customErrorMessage = "Server is not reachable:";
config()->set('coolify.mux_enabled', false); config()->set('coolify.mux_enabled', false);
instant_remote_process(['uptime'], $this->createdServer, true); instant_remote_process(['uptime'], $this->createdServer, true);
@@ -198,7 +197,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
]); ]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->serverReachable = false; $this->serverReachable = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, livewire: $this);
} }
try { try {
@@ -206,7 +205,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$dockerVersion = checkMinimumDockerEngineVersion($dockerVersion); $dockerVersion = checkMinimumDockerEngineVersion($dockerVersion);
if (is_null($dockerVersion)) { if (is_null($dockerVersion)) {
$this->currentState = 'install-docker'; $this->currentState = 'install-docker';
throw new \Exception('Docker version is not supported or not installed.'); throw new \Exception('Docker not found or old version is installed.');
} }
$this->createdServer->settings()->update([ $this->createdServer->settings()->update([
'is_usable' => true, 'is_usable' => true,
@@ -214,14 +213,20 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
$this->getProxyType(); $this->getProxyType();
} catch (\Throwable $e) { } catch (\Throwable $e) {
// $this->dockerInstallationStarted = false; // $this->dockerInstallationStarted = false;
return handleError(error: $e, customErrorMessage: $customErrorMessage, livewire: $this); return handleError(error: $e, livewire: $this);
} }
} }
public function installDocker() public function installDocker()
{ {
$this->dockerInstallationStarted = true; try {
$activity = InstallDocker::run($this->createdServer); $this->dockerInstallationStarted = true;
$this->emit('newMonitorActivity', $activity->id); $activity = InstallDocker::run($this->createdServer);
$this->emit('installDocker');
$this->emit('newMonitorActivity', $activity->id);
} catch (\Throwable $e) {
$this->dockerInstallationStarted = false;
return handleError(error: $e, livewire: $this);
}
} }
public function dockerInstalledOrSkipped() public function dockerInstalledOrSkipped()
{ {

View File

@@ -3,7 +3,6 @@
namespace App\Http\Livewire; namespace App\Http\Livewire;
use App\Models\Project; use App\Models\Project;
use App\Models\S3Storage;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use Livewire\Component;
class Advanced extends Component
{
public Application $application;
protected $rules = [
'application.settings.is_git_submodules_enabled' => 'boolean|required',
'application.settings.is_git_lfs_enabled' => 'boolean|required',
'application.settings.is_preview_deployments_enabled' => 'boolean|required',
'application.settings.is_auto_deploy_enabled' => 'boolean|required',
'application.settings.is_force_https_enabled' => 'boolean|required',
'application.settings.is_log_drain_enabled' => 'boolean|required',
'application.settings.is_gpu_enabled' => 'boolean|required',
'application.settings.gpu_driver' => 'string|required',
'application.settings.gpu_count' => 'string|required',
'application.settings.gpu_device_ids' => 'string|required',
'application.settings.gpu_options' => 'string|required',
];
public function instantSave()
{
if ($this->application->isLogDrainEnabled()) {
if (!$this->application->destination->server->isLogDrainEnabled()) {
$this->application->settings->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on this server.');
return;
}
}
if ($this->application->settings->is_force_https_enabled) {
$this->emit('resetDefaultLabels', false);
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function submit() {
if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) {
$this->emit('error', 'You cannot set both GPU count and GPU device IDs.');
$this->application->settings->gpu_count = null;
$this->application->settings->gpu_device_ids = null;
$this->application->settings->save();
return;
}
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function render()
{
return view('livewire.project.application.advanced');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Livewire\Project\Application;
use App\Models\Application;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Livewire\Component;
class Configuration extends Component
{
public Application $application;
public $servers;
public function mount()
{
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
if (!$project) {
return redirect()->route('dashboard');
}
$environment = $project->load(['environments'])->environments->where('name', request()->route('environment_name'))->first()->load(['applications']);
if (!$environment) {
return redirect()->route('dashboard');
}
$application = $environment->applications->where('uuid', request()->route('application_uuid'))->first();
if (!$application) {
return redirect()->route('dashboard');
}
$this->application = $application;
$mainServer = $application->destination->server;
$servers = Server::ownedByCurrentTeam()->get();
$this->servers = $servers->filter(function ($server) use ($mainServer) {
return $server->id != $mainServer->id;
});
}
public function render()
{
return view('livewire.project.application.configuration');
}
}

View File

@@ -26,14 +26,10 @@ class General extends Component
public bool $isConfigurationChanged = false; public bool $isConfigurationChanged = false;
public bool $is_static; public bool $is_static;
public bool $is_git_submodules_enabled;
public bool $is_git_lfs_enabled;
public bool $is_debug_enabled;
public bool $is_preview_deployments_enabled;
public bool $is_auto_deploy_enabled;
public bool $is_force_https_enabled;
protected $listeners = [
'resetDefaultLabels'
];
protected $rules = [ protected $rules = [
'application.name' => 'required', 'application.name' => 'required',
'application.description' => 'nullable', 'application.description' => 'nullable',
@@ -56,6 +52,7 @@ class General extends Component
'application.dockerfile_location' => 'nullable', 'application.dockerfile_location' => 'nullable',
'application.custom_labels' => 'nullable', 'application.custom_labels' => 'nullable',
'application.dockerfile_target_build' => 'nullable', 'application.dockerfile_target_build' => 'nullable',
'application.settings.is_static' => 'boolean|required',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'application.name' => 'name', 'application.name' => 'name',
@@ -79,6 +76,7 @@ class General extends Component
'application.dockerfile_location' => 'Dockerfile location', 'application.dockerfile_location' => 'Dockerfile location',
'application.custom_labels' => 'Custom labels', 'application.custom_labels' => 'Custom labels',
'application.dockerfile_target_build' => 'Dockerfile target build', 'application.dockerfile_target_build' => 'Dockerfile target build',
'application.settings.is_static' => 'Is static',
]; ];
public function mount() public function mount()
@@ -93,17 +91,13 @@ class General extends Component
} else { } else {
$this->customLabels = str($this->application->custom_labels)->replace(',', "\n"); $this->customLabels = str($this->application->custom_labels)->replace(',', "\n");
} }
if (data_get($this->application, 'settings')) {
$this->is_static = $this->application->settings->is_static;
$this->is_git_submodules_enabled = $this->application->settings->is_git_submodules_enabled;
$this->is_git_lfs_enabled = $this->application->settings->is_git_lfs_enabled;
$this->is_debug_enabled = $this->application->settings->is_debug_enabled;
$this->is_preview_deployments_enabled = $this->application->settings->is_preview_deployments_enabled;
$this->is_auto_deploy_enabled = $this->application->settings->is_auto_deploy_enabled;
$this->is_force_https_enabled = $this->application->settings->is_force_https_enabled;
}
$this->checkLabelUpdates(); $this->checkLabelUpdates();
} }
public function instantSave()
{
$this->application->settings->save();
$this->emit('success', 'Settings saved.');
}
public function updatedApplicationBuildPack() public function updatedApplicationBuildPack()
{ {
if ($this->application->build_pack !== 'nixpacks') { if ($this->application->build_pack !== 'nixpacks') {
@@ -120,32 +114,6 @@ class General extends Component
$this->labelsChanged = false; $this->labelsChanged = false;
} }
} }
public function instantSave()
{
// @TODO: find another way - if possible
$force_https = $this->application->settings->is_force_https_enabled;
$this->application->settings->is_static = $this->is_static;
if ($this->is_static) {
$this->application->ports_exposes = 80;
} else {
$this->application->ports_exposes = 3000;
}
$this->application->settings->is_git_submodules_enabled = $this->is_git_submodules_enabled;
$this->application->settings->is_git_lfs_enabled = $this->is_git_lfs_enabled;
$this->application->settings->is_debug_enabled = $this->is_debug_enabled;
$this->application->settings->is_preview_deployments_enabled = $this->is_preview_deployments_enabled;
$this->application->settings->is_auto_deploy_enabled = $this->is_auto_deploy_enabled;
$this->application->settings->is_force_https_enabled = $this->is_force_https_enabled;
$this->application->settings->save();
$this->application->save();
$this->application->refresh();
$this->emit('success', 'Application settings updated!');
$this->checkLabelUpdates();
$this->isConfigurationChanged = $this->application->isConfigurationChanged();
if ($force_https !== $this->is_force_https_enabled) {
$this->resetDefaultLabels(false);
}
}
public function getWildcardDomain() public function getWildcardDomain()
{ {

View File

@@ -4,6 +4,7 @@ namespace App\Http\Livewire\Project\Application;
use App\Actions\Application\StopApplication; use App\Actions\Application\StopApplication;
use App\Jobs\ContainerStatusJob; use App\Jobs\ContainerStatusJob;
use App\Jobs\ServerStatusJob;
use App\Models\Application; use App\Models\Application;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -28,6 +29,8 @@ class Heading extends Component
$this->application->previews->each(function ($preview) { $this->application->previews->each(function ($preview) {
$preview->refresh(); $preview->refresh();
}); });
} else {
dispatch(new ServerStatusJob($this->application->destination->server));
} }
} }

View File

@@ -38,32 +38,36 @@ class Rollback extends Component
]); ]);
} }
public function loadImages() public function loadImages($showToast = false)
{ {
try { try {
$image = $this->application->uuid; $image = $this->application->docker_registry_image_name ?? $this->application->uuid;
$output = instant_remote_process([ if ($this->application->destination->server->isFunctional()) {
"docker inspect --format='{{.Config.Image}}' {$this->application->uuid}", $output = instant_remote_process([
], $this->application->destination->server, throwError: false); "docker inspect --format='{{.Config.Image}}' {$this->application->uuid}",
$current_tag = Str::of($output)->trim()->explode(":"); ], $this->application->destination->server, throwError: false);
$this->current = data_get($current_tag, 1); $current_tag = Str::of($output)->trim()->explode(":");
$this->current = data_get($current_tag, 1);
$output = instant_remote_process([ $output = instant_remote_process([
"docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'", "docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'",
], $this->application->destination->server); ], $this->application->destination->server);
$this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) { $this->images = Str::of($output)->trim()->explode("\n")->filter(function ($item) use ($image) {
return Str::of($item)->contains($image); return Str::of($item)->contains($image);
})->map(function ($item) { })->map(function ($item) {
$item = Str::of($item)->explode('#'); $item = Str::of($item)->explode('#');
if ($item[1] === $this->current) { if ($item[1] === $this->current) {
// $is_current = true; // $is_current = true;
} }
return [ return [
'tag' => $item[1], 'tag' => $item[1],
'created_at' => $item[2], 'created_at' => $item[2],
'is_current' => $is_current ?? null, 'is_current' => $is_current ?? null,
]; ];
})->toArray(); })->toArray();
}
$showToast && $this->emit('success', 'Images loaded.');
return [];
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -50,6 +51,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -27,6 +27,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -48,7 +49,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -28,6 +28,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -50,6 +51,21 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced()
{
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -34,6 +34,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -57,6 +58,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
try { try {

View File

@@ -25,6 +25,7 @@ class General extends Component
'database.ports_mappings' => 'nullable', 'database.ports_mappings' => 'nullable',
'database.is_public' => 'nullable|boolean', 'database.is_public' => 'nullable|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_log_drain_enabled' => 'nullable|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
'database.name' => 'Name', 'database.name' => 'Name',
@@ -43,6 +44,20 @@ class General extends Component
$this->db_url_public = $this->database->getDbUrl(); $this->db_url_public = $this->database->getDbUrl();
} }
} }
public function instantSaveAdvanced() {
try {
if (!$this->database->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->database->save();
$this->emit('success', 'Database updated successfully.');
$this->emit('success', 'You need to restart the service for the changes to take effect.');
} catch (Exception $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -16,6 +16,7 @@ class Application extends Component
'application.image' => 'required', 'application.image' => 'required',
'application.exclude_from_status' => 'required|boolean', 'application.exclude_from_status' => 'required|boolean',
'application.required_fqdn' => 'required|boolean', 'application.required_fqdn' => 'required|boolean',
'application.is_log_drain_enabled' => 'nullable|boolean',
]; ];
public function render() public function render()
{ {
@@ -25,7 +26,16 @@ class Application extends Component
{ {
$this->submit(); $this->submit();
} }
public function instantSaveAdvanced()
{
if (!$this->application->service->destination->server->isLogDrainEnabled()) {
$this->application->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->application->save();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function delete() public function delete()
{ {
try { try {

View File

@@ -21,18 +21,31 @@ class Database extends Component
'database.exclude_from_status' => 'required|boolean', 'database.exclude_from_status' => 'required|boolean',
'database.public_port' => 'nullable|integer', 'database.public_port' => 'nullable|integer',
'database.is_public' => 'required|boolean', 'database.is_public' => 'required|boolean',
'database.is_log_drain_enabled' => 'required|boolean',
]; ];
public function render() public function render()
{ {
return view('livewire.project.service.database'); return view('livewire.project.service.database');
} }
public function mount() { public function mount()
{
if ($this->database->is_public) { if ($this->database->is_public) {
$this->db_url_public = $this->database->getServiceDatabaseUrl(); $this->db_url_public = $this->database->getServiceDatabaseUrl();
} }
$this->refreshFileStorages(); $this->refreshFileStorages();
} }
public function instantSave() { public function instantSaveAdvanced()
{
if (!$this->database->service->destination->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->emit('error', 'Log drain is not enabled on the server. Please enable it first.');
return;
}
$this->submit();
$this->emit('success', 'You need to restart the service for the changes to take effect.');
}
public function instantSave()
{
if ($this->database->is_public && !$this->database->public_port) { if ($this->database->is_public && !$this->database->public_port) {
$this->emit('error', 'Public port is required.'); $this->emit('error', 'Public port is required.');
$this->database->is_public = false; $this->database->is_public = false;

View File

@@ -7,15 +7,39 @@ use Livewire\Component;
class StackForm extends Component class StackForm extends Component
{ {
public $service; public $service;
public $fields = [];
protected $listeners = ["saveCompose"]; protected $listeners = ["saveCompose"];
protected $rules = [ public $rules = [
'service.docker_compose_raw' => 'required', 'service.docker_compose_raw' => 'required',
'service.docker_compose' => 'required', 'service.docker_compose' => 'required',
'service.name' => 'required', 'service.name' => 'required',
'service.description' => 'nullable', 'service.description' => 'nullable',
]; ];
public $validationAttributes = [];
public function mount()
{
$extraFields = $this->service->extraFields();
foreach ($extraFields as $serviceName => $fields) {
foreach ($fields as $fieldKey => $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules');
$isPassword = data_get($field, 'isPassword');
$this->fields[$key] = [
"serviceName" => $serviceName,
"key" => $key,
"name" => $fieldKey,
"value" => $value,
"isPassword" => $isPassword,
];
$this->rules["fields.$key.value"] = $rules;
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
}
public function saveCompose($raw) public function saveCompose($raw)
{ {
$this->service->docker_compose_raw = $raw; $this->service->docker_compose_raw = $raw;
$this->submit(); $this->submit();
} }
@@ -25,6 +49,7 @@ class StackForm extends Component
try { try {
$this->validate(); $this->validate();
$this->service->save(); $this->service->save();
$this->service->saveExtraFields($this->fields);
$this->service->parse(); $this->service->parse();
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();

View File

@@ -6,5 +6,7 @@ use Livewire\Component;
class Destination extends Component class Destination extends Component
{ {
public $destination; public $resource;
public $servers = [];
public $additionalServers = [];
} }

View File

@@ -8,9 +8,27 @@ class Webhooks extends Component
{ {
public $resource; public $resource;
public ?string $deploywebhook = null; public ?string $deploywebhook = null;
public ?string $githubManualWebhook = null;
public ?string $gitlabManualWebhook = null;
protected $rules = [
'resource.manual_webhook_secret_github' => 'nullable|string',
'resource.manual_webhook_secret_gitlab' => 'nullable|string',
];
public function saveSecret()
{
try {
$this->validate();
$this->resource->save();
$this->emit('success','Secret Saved.');
} catch (\Exception $e) {
return handleError($e, $this);
}
}
public function mount() public function mount()
{ {
$this->deploywebhook = generateDeployWebhook($this->resource); $this->deploywebhook = generateDeployWebhook($this->resource);
$this->githubManualWebhook = generateGitManualWebhook($this->resource, 'github');
$this->gitlabManualWebhook = generateGitManualWebhook($this->resource, 'gitlab');
} }
public function render() public function render()
{ {

View File

@@ -43,9 +43,9 @@ 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 serverRefresh() public function serverRefresh($install = true)
{ {
$this->validateServer(); $this->validateServer($install);
} }
public function instantSave() public function instantSave()
{ {
@@ -77,12 +77,15 @@ class Form extends Component
{ {
try { try {
$uptime = $this->server->validateConnection(); $uptime = $this->server->validateConnection();
if ($uptime) { if (!$uptime) {
$install && $this->emit('success', 'Server is reachable.');
} else {
$install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.'); $install && $this->emit('error', 'Server is not reachable. Please check your connection and configuration.');
return; return;
} }
$supported_os_type = $this->server->validateOS();
if (!$supported_os_type) {
$install && $this->emit('error', 'Server OS type is not supported for automated installation. Please install Docker manually before continuing: <a target="_blank" class="underline" href="https://coolify.io/docs/servers#install-docker-engine-manually">documentation</a>.');
return;
}
$dockerInstalled = $this->server->validateDockerEngine(); $dockerInstalled = $this->server->validateDockerEngine();
if ($dockerInstalled) { if ($dockerInstalled) {
$install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.'); $install && $this->emit('success', 'Docker Engine is installed.<br> Checking version.');
@@ -92,7 +95,7 @@ class Form extends Component
} }
$dockerVersion = $this->server->validateDockerEngineVersion(); $dockerVersion = $this->server->validateDockerEngineVersion();
if ($dockerVersion) { if ($dockerVersion) {
$install && $this->emit('success', 'Docker Engine version is 23+.'); $install && $this->emit('success', 'Docker Engine version is 22+.');
} else { } else {
$install && $this->installDocker(); $install && $this->installDocker();
return; return;

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Http\Livewire\Server;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use Livewire\Component;
class LogDrains extends Component
{
public Server $server;
public $parameters = [];
protected $rules = [
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
];
protected $validationAttributes = [
'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain',
'server.settings.logdrain_newrelic_license_key' => 'New Relic license key',
'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI',
'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain',
'server.settings.logdrain_highlight_project_id' => 'Highlight project ID',
'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain',
'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name',
'server.settings.logdrain_axiom_api_key' => 'Axiom API key',
];
public function mount()
{
$this->parameters = get_route_parameters();
try {
$server = Server::ownedByCurrentTeam(['name', 'description', 'ip', 'port', 'user', 'proxy'])->whereUuid(request()->server_uuid)->first();
if (is_null($server)) {
return redirect()->route('server.all');
}
$this->server = $server;
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function configureLogDrain()
{
try {
InstallLogDrain::run($this->server);
if (!$this->server->isLogDrainEnabled()) {
$this->emit('serverRefresh');
$this->emit('success', 'Log drain service stopped.');
return;
}
$this->emit('serverRefresh');
$this->emit('success', 'Log drain service started successfully.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave(string $type)
{
try {
$ok = $this->submit($type);
if (!$ok) {
return;
}
$this->configureLogDrain();
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit(string $type)
{
try {
$this->resetErrorBag();
if ($type === 'newrelic') {
$this->validate([
'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean',
'server.settings.logdrain_newrelic_license_key' => 'required|string',
'server.settings.logdrain_newrelic_base_uri' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
} else if ($type === 'highlight') {
$this->validate([
'server.settings.is_logdrain_highlight_enabled' => 'required|boolean',
'server.settings.logdrain_highlight_project_id' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_axiom_enabled' => false,
]);
} else if ($type === 'axiom') {
$this->validate([
'server.settings.is_logdrain_axiom_enabled' => 'required|boolean',
'server.settings.logdrain_axiom_dataset_name' => 'required|string',
'server.settings.logdrain_axiom_api_key' => 'required|string',
]);
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
'is_logdrain_highlight_enabled' => false,
]);
}
$this->server->settings->save();
$this->emit('success', 'Settings saved successfully.');
return true;
} catch (\Throwable $e) {
if ($type === 'newrelic') {
$this->server->settings->update([
'is_logdrain_newrelic_enabled' => false,
]);
} else if ($type === 'highlight') {
$this->server->settings->update([
'is_logdrain_highlight_enabled' => false,
]);
} else if ($type === 'axiom') {
$this->server->settings->update([
'is_logdrain_axiom_enabled' => false,
]);
}
handleError($e, $this);
return false;
}
}
public function render()
{
return view('livewire.server.log-drains');
}
}

View File

@@ -25,7 +25,7 @@ class Show extends Component
} }
public function submit() public function submit()
{ {
$this->emit('serverRefresh'); $this->emit('serverRefresh',false);
} }
public function render() public function render()
{ {

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Livewire;
use Livewire\Component;
class Sponsorship extends Component
{
public function disable()
{
auth()->user()->update(['is_notification_sponsorship_enabled' => false]);
}
public function render()
{
return view('livewire.sponsorship');
}
}

View File

@@ -64,21 +64,10 @@ class Create extends Component
} }
$this->storage->team_id = currentTeam()->id; $this->storage->team_id = currentTeam()->id;
$this->storage->testConnection(); $this->storage->testConnection();
$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) {
return handleError($e, $this); return handleError($e, $this);
} }
} }
private function test_s3_connection()
{
try {
$this->storage->testConnection();
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
} }

View File

@@ -32,7 +32,7 @@ class Form extends Component
public function test_s3_connection() public function test_s3_connection()
{ {
try { try {
$this->storage->testConnection(); $this->storage->testConnection(shouldSave: true);
return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.'); return $this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
@@ -53,10 +53,7 @@ class Form extends Component
{ {
$this->validate(); $this->validate();
try { try {
$this->storage->testConnection(); $this->test_s3_connection();
$this->emit('success', 'Connection is working. Tested with "ListObjectsV2" action.');
$this->storage->save();
$this->emit('success', 'Storage settings saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Jobs;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Traits\ExecuteRemoteCommandNew;
use Exception;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Throwable;
class ApplicationDeployDockerImageJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommandNew;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle()
{
ray()->clearAll();
ray('Deploying Docker Image');
try {
$applicationDeploymentQueue = ApplicationDeploymentQueue::find($this->applicationDeploymentQueueId);
$application = Application::find($applicationDeploymentQueue->application_id);
$deploymentUuid = data_get($applicationDeploymentQueue, 'deployment_uuid');
$dockerImage = data_get($application, 'docker_registry_image_name');
$dockerImageTag = data_get($application, 'docker_registry_image_tag');
$productionImageName = str("{$dockerImage}:{$dockerImageTag}");
$destination = $application->destination->getMorphClass()::where('id', $application->destination->id)->first();
$pullRequestId = data_get($applicationDeploymentQueue, 'pull_request_id');
$server = data_get($destination, 'server');
$network = data_get($destination, 'network');
$containerName = generateApplicationContainerName($application, $pullRequestId);
savePrivateKeyToFs($server);
ray("echo 'Starting deployment of {$productionImageName}.'");
$applicationDeploymentQueue->update([
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
]);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: prepareHelperContainer($server, $network, $deploymentUuid)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: generateComposeFile(
deploymentUuid: $deploymentUuid,
server: $server,
network: $network,
application: $application,
containerName: $containerName,
imageName: $productionImageName,
pullRequestId: $pullRequestId
)
);
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: rollingUpdate(application: $application, deploymentUuid: $deploymentUuid)
);
} catch (Throwable $e) {
$this->executeRemoteCommand(
server: $server,
logModel: $applicationDeploymentQueue,
commands: [
"echo 'Oops something is not okay, are you okay? 😢'",
"echo '{$e->getMessage()}'",
"echo -n 'Deployment failed. Removing the new version of your application.'",
executeInDocker($deploymentUuid, "docker rm -f $containerName >/dev/null 2>&1"),
]
);
// $this->next(ApplicationDeploymentStatus::FAILED->value);
throw $e;
}
}
// private function next(string $status)
// {
// // If the deployment is cancelled by the user, don't update the status
// if ($this->application_deployment_queue->status !== ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
// $this->application_deployment_queue->update([
// 'status' => $status,
// ]);
// }
// queue_next_deployment($this->application);
// if ($status === ApplicationDeploymentStatus::FINISHED->value) {
// $this->application->environment->project->team->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
// }
// if ($status === ApplicationDeploymentStatus::FAILED->value) {
// $this->application->environment->project->team->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
// }
// }
}

View File

@@ -0,0 +1,29 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationDeploySimpleDockerfileJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Deploying Simple Dockerfile');
}
}

View File

@@ -24,6 +24,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Throwable; use Throwable;
@@ -33,6 +34,8 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public static int $batch_counter = 0; public static int $batch_counter = 0;
private int $application_deployment_queue_id; private int $application_deployment_queue_id;
@@ -52,7 +55,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private GithubApp|GitlabApp|string $source = 'other'; private GithubApp|GitlabApp|string $source = 'other';
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
private Server $mainServer;
private ?ApplicationPreview $preview = null; private ?ApplicationPreview $preview = null;
private ?string $git_type = null;
private string $container_name; private string $container_name;
private ?string $currently_running_container_name = null; private ?string $currently_running_container_name = null;
@@ -72,6 +77,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private ?string $buildTarget = null; private ?string $buildTarget = null;
private $log_model; private $log_model;
private Collection $saved_outputs; private Collection $saved_outputs;
private ?string $full_healthcheck_url = null;
private string $serverUser = 'root'; private string $serverUser = 'root';
private string $serverUserHomeDir = '/root'; private string $serverUserHomeDir = '/root';
@@ -99,12 +105,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->force_rebuild = $this->application_deployment_queue->force_rebuild; $this->force_rebuild = $this->application_deployment_queue->force_rebuild;
$this->restart_only = $this->application_deployment_queue->restart_only; $this->restart_only = $this->application_deployment_queue->restart_only;
$this->git_type = data_get($this->application_deployment_queue, 'git_type');
$source = data_get($this->application, 'source'); $source = data_get($this->application, 'source');
if ($source) { if ($source) {
$this->source = $source->getMorphClass()::where('id', $this->application->source->id)->first(); $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->mainServer = $this->destination->server;
$this->serverUser = $this->server->user; $this->serverUser = $this->server->user;
$this->basedir = "/artifacts/{$this->deployment_uuid}"; $this->basedir = "/artifacts/{$this->deployment_uuid}";
$this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/'); $this->workdir = "{$this->basedir}" . rtrim($this->application->base_directory, '/');
@@ -119,11 +127,16 @@ 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')) { if (str($this->application->fqdn)->contains(',')) {
$preview_fqdn = getFqdnWithoutPort(data_get($this->preview, 'fqdn')); $url = Url::fromString(str($this->application->fqdn)->explode(',')[0]);
$preview_fqdn = getFqdnWithoutPort(str($this->application->fqdn)->explode(',')[0]);
} else {
$url = Url::fromString($this->application->fqdn);
if (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);
$host = $url->getHost(); $host = $url->getHost();
$schema = $url->getScheme(); $schema = $url->getScheme();
$random = new Cuid2(7); $random = new Cuid2(7);
@@ -169,10 +182,6 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->buildTarget = " --target {$this->application->dockerfile_target_build} "; $this->buildTarget = " --target {$this->application->dockerfile_target_build} ";
} }
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
// Check custom port // Check custom port
preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches); preg_match('/(?<=:)\d+(?=\/)/', $this->application->git_repository, $matches);
if (count($matches) === 1) { if (count($matches) === 1) {
@@ -184,8 +193,14 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->customRepository = $this->application->git_repository; $this->customRepository = $this->application->git_repository;
} }
try { try {
if ($this->restart_only) { if ($this->restart_only && $this->application->build_pack !== 'dockerimage') {
$this->just_restart(); $this->just_restart();
if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server));
}
$this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true);
return;
} else if ($this->application->dockerfile) { } else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
@@ -204,21 +219,27 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
if ($this->server->isProxyShouldRun()) { if ($this->server->isProxyShouldRun()) {
dispatch(new ContainerStatusJob($this->server)); dispatch(new ContainerStatusJob($this->server));
} }
if ($this->application->docker_registry_image_name && $this->application->build_pack !== 'dockerimage') {
$this->push_to_docker_registry();
}
$this->next(ApplicationDeploymentStatus::FINISHED->value); $this->next(ApplicationDeploymentStatus::FINISHED->value);
$this->application->isConfigurationChanged(true); $this->application->isConfigurationChanged(true);
} catch (Exception $e) { } catch (Exception $e) {
ray($e);
$this->fail($e); $this->fail($e);
throw $e; throw $e;
} finally { } finally {
if (isset($this->docker_compose_base64)) { if (isset($this->docker_compose_base64)) {
$readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at); $readme = generate_readme_file($this->application->name, $this->application_deployment_queue->updated_at);
$composeFileName = "$this->configuration_dir/docker-compose.yml";
if ($this->pull_request_id !== 0) {
$composeFileName = "$this->configuration_dir/docker-compose-pr-{$this->pull_request_id}.yml";
}
$this->execute_remote_command( $this->execute_remote_command(
[ [
"mkdir -p $this->configuration_dir" "mkdir -p $this->configuration_dir"
], ],
[ [
"echo '{$this->docker_compose_base64}' | base64 -d > $this->configuration_dir/docker-compose.yml", "echo '{$this->docker_compose_base64}' | base64 -d > $composeFileName",
], ],
[ [
"echo '{$readme}' > $this->configuration_dir/README.md", "echo '{$readme}' > $this->configuration_dir/README.md",
@@ -241,7 +262,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
} }
} }
private function push_to_docker_registry()
{
try {
instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server);
$this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Pushing image to docker registry ({$this->production_image_name}).'"],
[
executeInDocker($this->deployment_uuid, "docker push {$this->production_image_name}"), 'hidden' => true
],
);
if ($this->application->docker_registry_image_tag) {
// Tag image with latest
$this->execute_remote_command(
['echo -n "Tagging and pushing image with latest tag."'],
[
executeInDocker($this->deployment_uuid, "docker tag {$this->production_image_name} {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
[
executeInDocker($this->deployment_uuid, "docker push {$this->application->docker_registry_image_name}:{$this->application->docker_registry_image_tag}"), 'ignore_errors' => true, 'hidden' => true
],
);
}
$this->execute_remote_command([
"echo -n 'Image pushed to docker registry.'"
]);
} catch (Exception $e) {
$this->execute_remote_command(
["echo -n 'Failed to push image to docker registry. Please check debug logs for more information.'"],
);
ray($e);
}
}
// private function deploy_docker_compose() // private function deploy_docker_compose()
// { // {
// $dockercompose_base64 = base64_encode($this->application->dockercompose); // $dockercompose_base64 = base64_encode($this->application->dockercompose);
@@ -281,20 +336,32 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_image_names() private function generate_image_names()
{ {
if ($this->application->dockerfile) { if ($this->application->dockerfile) {
$this->build_image_name = Str::lower("{$this->application->uuid}:build"); if ($this->application->docker_registry_image_name) {
$this->production_image_name = Str::lower("{$this->application->uuid}:latest"); $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:latest");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:build");
$this->production_image_name = Str::lower("{$this->application->uuid}:latest");
}
} else if ($this->application->build_pack === 'dockerimage') { } else if ($this->application->build_pack === 'dockerimage') {
$this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}"); $this->production_image_name = Str::lower("{$this->dockerImage}:{$this->dockerImageTag}");
} else if ($this->pull_request_id !== 0) { } else if ($this->pull_request_id !== 0) {
$this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build"); if ($this->application->docker_registry_image_name) {
$this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}"); $this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build");
} else { $this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}");
$tag = Str::of("{$this->commit}-{$this->application->id}-{$this->pull_request_id}"); } else {
if (strlen($tag) > 128) { $this->build_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}-build");
$tag = $tag->substr(0, 128); $this->production_image_name = Str::lower("{$this->application->uuid}:pr-{$this->pull_request_id}");
}
} else {
$this->dockerImageTag = str($this->commit)->substr(0, 128);
if ($this->application->docker_registry_image_name) {
$this->build_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->docker_registry_image_name}:{$this->dockerImageTag}");
} else {
$this->build_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$this->dockerImageTag}");
} }
$this->build_image_name = Str::lower("{$this->application->uuid}:{$tag}-build");
$this->production_image_name = Str::lower("{$this->application->uuid}:{$tag}");
} }
} }
private function just_restart() private function just_restart()
@@ -308,31 +375,41 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->check_git_if_build_needed(); $this->check_git_if_build_needed();
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
$this->execute_remote_command([ $this->check_image_locally_or_remotely();
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) { if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty()) {
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
return; return;
} }
$this->execute_remote_command([ throw new RuntimeException('Cannot find image anywhere. Please redeploy the application.');
"echo 'Cannot find image {$this->production_image_name} locally. Please redeploy the application.'",
]);
} }
private function save_environment_variables() private function check_image_locally_or_remotely()
{ {
$envs = collect([]); $this->execute_remote_command([
foreach ($this->application->environment_variables as $env) { "docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
$envs->push($env->key . '=' . $env->value); ]);
if (str($this->saved_outputs->get('local_image_found'))->isEmpty() && $this->application->docker_registry_image_name) {
$this->execute_remote_command([
"docker pull {$this->production_image_name} 2>/dev/null", "ignore_errors" => true, "hidden" => true
]);
$this->execute_remote_command([
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found"
]);
} }
$envs_base64 = base64_encode($envs->implode("\n"));
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
],
);
} }
// private function save_environment_variables()
// {
// $envs = collect([]);
// foreach ($this->application->environment_variables as $env) {
// $envs->push($env->key . '=' . $env->value);
// }
// $envs_base64 = base64_encode($envs->implode("\n"));
// $this->execute_remote_command(
// [
// executeInDocker($this->deployment_uuid, "echo '$envs_base64' | base64 -d > $this->workdir/.env")
// ],
// );
// }
private function deploy_simple_dockerfile() private function deploy_simple_dockerfile()
{ {
$dockerfile_base64 = base64_encode($this->application->dockerfile); $dockerfile_base64 = base64_encode($this->application->dockerfile);
@@ -344,7 +421,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->prepare_builder_image(); $this->prepare_builder_image();
$this->execute_remote_command( $this->execute_remote_command(
[ [
executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir/Dockerfile") executeInDocker($this->deployment_uuid, "echo '$dockerfile_base64' | base64 -d > $this->workdir$this->dockerfile_location")
], ],
); );
$this->generate_image_names(); $this->generate_image_names();
@@ -391,7 +468,12 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->generate_build_env_variables(); $this->generate_build_env_variables();
$this->add_build_env_variables_to_dockerfile(); $this->add_build_env_variables_to_dockerfile();
$this->build_image(); $this->build_image();
$this->rolling_update(); // if ($this->application->additional_destinations) {
// $this->push_to_docker_registry();
// $this->deploy_to_additional_destinations();
// } else {
$this->rolling_update();
// }
} }
private function deploy_nixpacks_buildpack() private function deploy_nixpacks_buildpack()
{ {
@@ -405,12 +487,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->set_base_dir(); $this->set_base_dir();
$this->generate_image_names(); $this->generate_image_names();
if (!$this->force_rebuild) { if (!$this->force_rebuild) {
$this->execute_remote_command([ $this->check_image_locally_or_remotely();
"docker images -q {$this->production_image_name} 2>/dev/null", "hidden" => true, "save" => "local_image_found" if (str($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
]);
if (Str::of($this->saved_outputs->get('local_image_found'))->isNotEmpty() && !$this->application->isConfigurationChanged()) {
$this->execute_remote_command([ $this->execute_remote_command([
"echo 'No configuration changed & Docker Image found locally with the same Git Commit SHA {$this->application->uuid}:{$this->commit}. Build step skipped.'", "echo 'No configuration changed & image found ({$this->production_image_name}) with the same Git Commit SHA. Build step skipped.'",
]); ]);
$this->generate_compose_file(); $this->generate_compose_file();
$this->rolling_update(); $this->rolling_update();
@@ -453,12 +533,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
if (count($this->application->ports_mappings_array) > 0) { if (count($this->application->ports_mappings_array) > 0) {
$this->execute_remote_command( $this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"], ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
); );
$this->stop_running_container(force: true); $this->stop_running_container(force: true);
$this->start_by_compose_file(); $this->start_by_compose_file();
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
["echo -n 'Rolling update started.'"], ["echo -n 'Rolling update started.'"],
); );
$this->start_by_compose_file(); $this->start_by_compose_file();
@@ -474,17 +560,21 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
// ray('New container name: ', $this->container_name); // ray('New container name: ', $this->container_name);
if ($this->container_name) { if ($this->container_name) {
$counter = 0; $counter = 1;
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Waiting for healthcheck to pass on the new version of your application.'" "echo 'Waiting for healthcheck to pass on the new container.'"
], ]
); );
while ($counter < $this->application->health_check_retries) { if ($this->full_healthcheck_url) {
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Attempt {$counter} of {$this->application->health_check_retries}'" "echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
], ]
);
}
while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command(
[ [
"docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}", "docker inspect --format='{{json .State.Health.Status}}' {$this->container_name}",
"hidden" => true, "hidden" => true,
@@ -494,17 +584,17 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
); );
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'New version healthcheck status: {$this->saved_outputs->get('health_check')}'" "echo 'Attempt {$counter} of {$this->application->health_check_retries} | Healthcheck status: {$this->saved_outputs->get('health_check')}'"
], ],
); );
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->application->update(['status' => 'running']);
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo 'Rolling update completed.'" "echo 'New container is healthy.'"
], ],
); );
$this->application->update(['status' => 'running']);
break; break;
} }
$counter++; $counter++;
@@ -541,12 +631,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function prepare_builder_image() private function prepare_builder_image()
{ {
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
// Get user home directory
$this->serverUserHomeDir = instant_remote_process(["echo \$HOME"], $this->server);
$this->dockerConfigFileExists = instant_remote_process(["test -f {$this->serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $this->server);
if ($this->dockerConfigFileExists === 'OK') { if ($this->dockerConfigFileExists === 'OK') {
$runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}"; $runCommand = "docker run -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v {$this->serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} else { } else {
$runCommand = "docker run -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 -d --network {$this->destination->network} -v /:/host --name {$this->deployment_uuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}";
} }
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n 'Preparing container with helper image: $helperImage.'", "echo -n 'Preparing container with helper image: $helperImage.'",
@@ -560,7 +653,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
], ],
); );
} }
private function deploy_to_additional_destinations()
{
$destination_ids = collect(str($this->application->additional_destinations)->explode(','));
foreach ($destination_ids as $destination_id) {
$destination = StandaloneDocker::find($destination_id);
$server = $destination->server;
if ($server->team_id !== $this->mainServer->team_id) {
$this->execute_remote_command(
[
"echo -n 'Skipping deployment to {$server->name}. Not in the same team?!'",
],
);
continue;
}
$this->server = $server;
$this->execute_remote_command(
[
"echo -n 'Deploying to {$this->server->name}.'",
],
);
$this->prepare_builder_image();
$this->generate_image_names();
$this->rolling_update();
}
}
private function set_base_dir() private function set_base_dir()
{ {
$this->execute_remote_command( $this->execute_remote_command(
@@ -609,6 +726,9 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
{ {
$importCommands = $this->generate_git_import_commands(); $importCommands = $this->generate_git_import_commands();
$this->execute_remote_command( $this->execute_remote_command(
[
"echo '\n----------------------------------------'",
],
[ [
"echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '" "echo -n 'Importing {$this->customRepository}:{$this->application->git_branch} (commit sha {$this->application->git_commit_sha}) to {$this->basedir}. '"
], ],
@@ -647,7 +767,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name"; $this->branch = "pull/{$this->pull_request_id}/head:$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")); $commands->push(executeInDocker($this->deployment_uuid, "cd {$this->basedir} && git fetch origin $this->branch && git checkout $pr_branch_name"));
} }
return $commands->implode(' && '); return $commands->implode(' && ');
} }
@@ -656,17 +776,31 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$this->fullRepoUrl = $this->customRepository; $this->fullRepoUrl = $this->customRepository;
$private_key = data_get($this->application, 'private_key.private_key'); $private_key = data_get($this->application, 'private_key.private_key');
if (is_null($private_key)) { if (is_null($private_key)) {
throw new Exception('Private key not found. Please add a private key to the application and try again.'); throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
} }
$private_key = base64_encode($private_key); $private_key = base64_encode($private_key);
$git_clone_command = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}"; $git_clone_command_base = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$git_clone_command} {$this->customRepository} {$this->basedir}";
$git_clone_command = $this->set_git_import_settings($git_clone_command); $git_clone_command = $this->set_git_import_settings($git_clone_command_base);
$commands = collect([ $commands = collect([
executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"), executeInDocker($this->deployment_uuid, "mkdir -p /root/.ssh"),
executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"), executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d > /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"), executeInDocker($this->deployment_uuid, "chmod 600 /root/.ssh/id_rsa"),
executeInDocker($this->deployment_uuid, $git_clone_command)
]); ]);
if ($this->pull_request_id !== 0) {
ray($this->git_type);
if ($this->git_type === 'gitlab') {
$this->branch = "merge-requests/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
if ($this->git_type === 'github') {
$this->branch = "pull/{$this->pull_request_id}/head:$pr_branch_name";
$commands->push(executeInDocker($this->deployment_uuid, "echo 'Checking out $this->branch'"));
$git_clone_command = "{$git_clone_command} && cd {$this->basedir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $this->branch && git checkout $pr_branch_name";
}
}
$commands->push(executeInDocker($this->deployment_uuid, $git_clone_command));
return $commands->implode(' && '); return $commands->implode(' && ');
} }
if ($this->application->deploymentType() === 'other') { if ($this->application->deploymentType() === 'other') {
@@ -701,16 +835,10 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private function generate_nixpacks_confs() private function generate_nixpacks_confs()
{ {
$this->execute_remote_command(
[
"echo -n 'Generating nixpacks configuration.'",
]
);
$nixpacks_command = $this->nixpacks_build_cmd(); $nixpacks_command = $this->nixpacks_build_cmd();
$this->execute_remote_command( $this->execute_remote_command(
[ [
"echo -n Running: $nixpacks_command", "echo -n 'Generating nixpacks configuration with: $nixpacks_command'",
], ],
[executeInDocker($this->deployment_uuid, $nixpacks_command)], [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")],
@@ -770,21 +898,22 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$labels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
} }
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
$newLabels = collect(generateLabelsApplication($this->application, $this->preview)); $labels = collect(generateLabelsApplication($this->application, $this->preview));
$newHostLabel = $newLabels->filter(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->reject(function ($label) {
return str($label)->contains('Host');
});
$labels = $labels->map(function ($label) { // $newHostLabel = $newLabels->filter(function ($label) {
$pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/'; // return str($label)->contains('Host');
$replacement = "$1-pr-{$this->pull_request_id}-$2-$3"; // });
$newLabel = preg_replace($pattern, $replacement, $label); // $labels = $labels->reject(function ($label) {
return $newLabel; // return str($label)->contains('Host');
}); // });
$labels = $labels->merge($newHostLabel); // ray($labels,$newLabels);
// $labels = $labels->map(function ($label) {
// $pattern = '/([a-zA-Z0-9]+)-(\d+)-(http|https)/';
// $replacement = "$1-pr-{$this->pull_request_id}-$2-$3";
// $newLabel = preg_replace($pattern, $replacement, $label);
// return $newLabel;
// });
// $labels = $labels->merge($newHostLabel);
} }
$labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray(); $labels = $labels->merge(defaultLabels($this->application->id, $this->application->uuid, $this->pull_request_id))->toArray();
$docker_compose = [ $docker_compose = [
@@ -827,6 +956,36 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->server->isLogDrainEnabled() && $this->application->isLogDrainEnabled()) {
$docker_compose['services'][$this->container_name]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($this->application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($this->application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($this->application, 'settings.gpu_options', [])
]
];
if (data_get($this->application, 'settings.gpu_count')) {
$count = data_get($this->application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($this->application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$this->container_name]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($this->application, 'settings.gpu_device_ids');
}
}
if ($this->application->isHealthcheckDisabled()) { if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
} }
@@ -924,17 +1083,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$health_check_port = $this->application->health_check_port; $health_check_port = $this->application->health_check_port;
} }
if ($this->application->health_check_path) { if ($this->application->health_check_path) {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path}";
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}{$this->application->health_check_path} > /dev/null"
]; ];
} else { } else {
$this->full_healthcheck_url = "{$this->application->health_check_method}: {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/";
$generated_healthchecks_commands = [ $generated_healthchecks_commands = [
"curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/" "curl -s -X {$this->application->health_check_method} -f {$this->application->health_check_scheme}://{$this->application->health_check_host}:{$health_check_port}/"
]; ];
} }
return implode(' ', $generated_healthchecks_commands); return implode(' ', $generated_healthchecks_commands);
} }
private function pull_latest_image($image)
{
$this->execute_remote_command(
["echo -n 'Pulling latest image ($image) from the registry.'"],
[
executeInDocker($this->deployment_uuid, "docker pull {$image}"), "hidden" => true
]
);
}
private function build_image() private function build_image()
{ {
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
@@ -942,12 +1112,18 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
"echo -n 'Static deployment. Copying static assets to the image.'", "echo -n 'Static deployment. Copying static assets to the image.'",
]); ]);
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command(
"echo -n 'Building docker image for your application. To check the current progress, click on Show Debug Logs.'", [
]); "echo -n 'Building docker image started.'",
],
["echo -n 'To check the current progress, click on Show Debug Logs.'"]
);
} }
if ($this->application->settings->is_static || $this->application->build_pack === 'static') { if ($this->application->settings->is_static || $this->application->build_pack === 'static') {
if ($this->application->static_image) {
$this->pull_latest_image($this->application->static_image);
}
if ($this->application->build_pack === 'static') { if ($this->application->build_pack === 'static') {
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
WORKDIR /usr/share/nginx/html/ WORKDIR /usr/share/nginx/html/
@@ -974,7 +1150,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
}"); }");
} else { } else {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}/{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->build_image_name {$this->workdir}"), "hidden" => true
]); ]);
$dockerfile = base64_encode("FROM {$this->application->static_image} $dockerfile = base64_encode("FROM {$this->application->static_image}
@@ -1012,16 +1188,25 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
] ]
); );
} else { } else {
$this->execute_remote_command([ // Pure Dockerfile based deployment
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true if ($this->application->dockerfile) {
]); $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build --pull $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
} else {
$this->execute_remote_command([
executeInDocker($this->deployment_uuid, "docker build $this->buildTarget $this->addHosts --network host -f {$this->workdir}{$this->dockerfile_location} {$this->build_args} --progress plain -t $this->production_image_name {$this->workdir}"), "hidden" => true
]);
}
} }
$this->execute_remote_command([
"echo -n 'Building docker image completed.'",
]);
} }
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)
{ {
$this->execute_remote_command(["echo -n 'Removing old version of your application.'"]); $this->execute_remote_command(["echo -n 'Removing old container.'"]);
if ($this->newVersionIsHealthy || $force) { if ($this->newVersionIsHealthy || $force) {
$containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id); $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
if ($this->pull_request_id !== 0) { if ($this->pull_request_id !== 0) {
@@ -1039,9 +1224,14 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
[executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $containerName >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
}); });
$this->execute_remote_command(
[
"echo 'Rolling update completed.'"
],
);
} else { } else {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'New version is not healthy, rolling back to the old version.'"], ["echo -n 'New container is not healthy, rolling back to the old container.'"],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true], [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true],
); );
} }
@@ -1049,10 +1239,17 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file() private function start_by_compose_file()
{ {
$this->execute_remote_command( if ($this->application->build_pack === 'dockerimage') {
["echo -n 'Starting application (could take a while).'"], $this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], ["echo -n 'Pulling latest images from the registry.'"],
); [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
} else {
$this->execute_remote_command(
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
);
}
} }
private function generate_build_env_variables() private function generate_build_env_variables()
@@ -1074,7 +1271,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function add_build_env_variables_to_dockerfile() private function add_build_env_variables_to_dockerfile()
{ {
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile' executeInDocker($this->deployment_uuid, "cat {$this->workdir}{$this->dockerfile_location}"), "hidden" => true, "save" => 'dockerfile'
]); ]);
$dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); $dockerfile = collect(Str::of($this->saved_outputs->get('dockerfile'))->trim()->explode("\n"));
@@ -1083,7 +1280,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
} }
$dockerfile_base64 = base64_encode($dockerfile->implode("\n")); $dockerfile_base64 = base64_encode($dockerfile->implode("\n"));
$this->execute_remote_command([ $this->execute_remote_command([
executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}/{$this->dockerfile_location}"), executeInDocker($this->deployment_uuid, "echo '{$dockerfile_base64}' | base64 -d > {$this->workdir}{$this->dockerfile_location}"),
"hidden" => true "hidden" => true
]); ]);
} }
@@ -1108,9 +1305,9 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
public function failed(Throwable $exception): void public function failed(Throwable $exception): void
{ {
$this->execute_remote_command( $this->execute_remote_command(
["echo 'Oops something is not okay, are you okay? 😢'"], ["echo 'Oops something is not okay, are you okay? 😢'", 'type' => 'err'],
["echo '{$exception->getMessage()}'"], ["echo '{$exception->getMessage()}'", 'type' => 'err'],
["echo -n 'Deployment failed. Removing the new version of your application.'"], ["echo -n 'Deployment failed. Removing the new version of your application.'", 'type' => 'err'],
[executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true] [executeInDocker($this->deployment_uuid, "docker rm -f $this->container_name >/dev/null 2>&1"), "hidden" => true, "ignore_errors" => true]
); );

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Jobs;
use App\Traits\ExecuteRemoteCommand;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ApplicationRestartJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, ExecuteRemoteCommand;
public $timeout = 3600;
public $tries = 1;
public string $applicationDeploymentQueueId;
public function __construct(string $applicationDeploymentQueueId)
{
$this->applicationDeploymentQueueId = $applicationDeploymentQueueId;
}
public function handle() {
ray('Restarting application');
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace App\Jobs;
use App\Actions\Server\InstallLogDrain;
use App\Models\Server;
use App\Notifications\Container\ContainerRestarted;
use App\Notifications\Container\ContainerStopped;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Sleep;
class CheckLogDrainContainerJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function healthcheck()
{
$status = instant_remote_process(["docker inspect --format='{{json .State.Status}}' coolify-log-drain"], $this->server, false);
if (str($status)->contains('running')) {
return true;
} else {
return false;
}
}
public function handle(): void
{
// ray("checking log drain statuses for {$this->server->id}");
try {
if (!$this->server->isServerReady()) {
return;
};
$containers = instant_remote_process(["docker container ls -q"], $this->server);
if (!$containers) {
return;
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this->server);
$containers = format_docker_command_output_to_json($containers);
$foundLogDrainContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-log-drain';
})->first();
if (!$foundLogDrainContainer || !$this->healthcheck()) {
ray('Log drain container not found or unhealthy. Restarting...');
InstallLogDrain::run($this->server);
Sleep::for(10)->seconds();
if ($this->healthcheck()) {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
return;
}
if (!$this->server->log_drain_notification_sent) {
ray('Log drain container still unhealthy. Sending notification...');
$this->server->team->notify(new ContainerStopped('Coolify Log Drainer', $this->server, null));
$this->server->update(['log_drain_notification_sent' => true]);
}
} else {
if ($this->server->log_drain_notification_sent) {
$this->server->team->notify(new ContainerRestarted('Coolify Log Drainer', $this->server));
$this->server->update(['log_drain_notification_sent' => false]);
}
}
} catch (\Throwable $e) {
send_internal_notification("CheckLogDrainContainerJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage());
handleError($e);
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class CleanupHelperContainersJob implements ShouldQueue, ShouldBeUnique, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public Server $server)
{
}
public function handle(): void
{
try {
ray('Cleaning up helper containers on ' . $this->server->name);
$containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
$containers = format_docker_command_output_to_json($containers);
if ($containers->count() > 0) {
foreach ($containers as $container) {
$containerId = data_get($container,'ID');
ray('Removing container ' . $containerId);
instant_remote_process(['docker container rm -f ' . $containerId], $this->server, false);
}
}
} catch (\Throwable $e) {
send_internal_notification('CleanupHelperContainersJob failed with error: ' . $e->getMessage());
ray($e->getMessage());
}
}
}

View File

@@ -8,8 +8,6 @@ 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 Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -39,78 +37,11 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function handle(): void public function handle(): void
{ {
// ray("checking server status for {$this->server->id}"); // ray("checking container statuses for {$this->server->id}");
try { try {
// ray()->clearAll(); if (!$this->server->isServerReady()) {
$serverUptimeCheckNumber = $this->server->unreachable_count;
$serverUptimeCheckNumberMax = 3;
// ray('checking # ' . $serverUptimeCheckNumber);
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->server->unreachable_email_sent === false) {
ray('Server unreachable, sending notification...');
$this->server->team->notify(new Unreachable($this->server));
$this->server->update(['unreachable_email_sent' => true]);
}
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => 0,
]);
// Update all applications, databases and services to exited
foreach ($this->server->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->server->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->server->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
return; return;
} };
$result = $this->server->validateConnection();
if ($result) {
$this->server->settings()->update([
'is_reachable' => true,
]);
$this->server->update([
'unreachable_count' => 0,
]);
} else {
$serverUptimeCheckNumber++;
$this->server->settings()->update([
'is_reachable' => false,
]);
$this->server->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
return;
}
if (data_get($this->server, 'unreachable_email_sent') === true) {
ray('Server is reachable again, sending notification...');
$this->server->team->notify(new Revived($this->server));
$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;
@@ -121,29 +52,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
$databases = $this->server->databases(); $databases = $this->server->databases();
$services = $this->server->services()->get(); $services = $this->server->services()->get();
$previews = $this->server->previews(); $previews = $this->server->previews();
$this->server->proxyType();
/// Check if proxy is running
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
$foundApplications = []; $foundApplications = [];
$foundApplicationPreviews = []; $foundApplicationPreviews = [];
$foundDatabases = []; $foundDatabases = [];
@@ -159,6 +68,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($applicationId) { if ($applicationId) {
$pullRequestId = data_get($labels, 'coolify.pullRequestId'); $pullRequestId = data_get($labels, 'coolify.pullRequestId');
if ($pullRequestId) { if ($pullRequestId) {
if (str($applicationId)->contains('-')) {
$applicationId = str($applicationId)->before('-');
}
$preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
if ($preview) { if ($preview) {
$foundApplicationPreviews[] = $preview->id; $foundApplicationPreviews[] = $preview->id;
@@ -252,6 +164,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $serviceUuid && $environmentName) { if ($projectUuid && $serviceUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/service/" . $serviceUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
$exitedService->update(['status' => 'exited']); $exitedService->update(['status' => 'exited']);
@@ -276,6 +190,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $applicationUuid && $environment) { if ($projectUuid && $applicationUuid && $environment) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environment . "/application/" . $applicationUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
@@ -299,6 +215,8 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $applicationUuid && $environmentName) { if ($projectUuid && $applicationUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/application/" . $applicationUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
@@ -322,11 +240,37 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
if ($projectUuid && $databaseUuid && $environmentName) { if ($projectUuid && $databaseUuid && $environmentName) {
$url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid; $url = base_url() . '/project/' . $projectUuid . "/" . $environmentName . "/database/" . $databaseUuid;
} else {
$url = null;
} }
$this->server->team->notify(new ContainerStopped($containerName, $this->server, $url)); $this->server->team->notify(new ContainerStopped($containerName, $this->server, $url));
} }
// Check if proxy is running
$this->server->proxyType();
$foundProxyContainer = $containers->filter(function ($value, $key) {
return data_get($value, 'Name') === '/coolify-proxy';
})->first();
if (!$foundProxyContainer) {
try {
$shouldStart = CheckProxy::run($this->server);
if ($shouldStart) {
StartProxy::run($this->server, false);
$this->server->team->notify(new ContainerRestarted('coolify-proxy', $this->server));
} else {
ray('Proxy could not be started.');
}
} catch (\Throwable $e) {
ray($e);
}
} else {
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
$this->server->save();
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
send_internal_notification('ContainerStatusJob failed with: ' . $e->getMessage()); send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage());
ray($e->getMessage()); ray($e->getMessage());
handleError($e); handleError($e);
} }

View File

@@ -141,21 +141,20 @@ class DatabaseBackupJob implements ShouldQueue, ShouldBeEncrypted
} else if ($databaseType === 'standalone-mariadb') { } else if ($databaseType === 'standalone-mariadb') {
$this->container_name = "{$this->database->name}-$serviceUuid"; $this->container_name = "{$this->database->name}-$serviceUuid";
$this->directory_name = $serviceName . '-' . $this->container_name; $this->directory_name = $serviceName . '-' . $this->container_name;
$commands[] = "docker exec $this->container_name env | grep MARIADB_"; $commands[] = "docker exec $this->container_name env";
$envs = instant_remote_process($commands, $this->server); $envs = instant_remote_process($commands, $this->server);
$envs = str($envs)->explode("\n"); $envs = str($envs)->explode("\n");
$rootPassword = $envs->filter(function ($env) { $rootPassword = $envs->filter(function ($env) {
return str($env)->startsWith('MARIADB_ROOT_PASSWORD='); return str($env)->startsWith('MARIADB_ROOT_PASSWORD=');
})->first(); })->first();
if ($rootPassword) { if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value(); $this->database->mariadb_root_password = str($rootPassword)->after('MARIADB_ROOT_PASSWORD=')->value();
} else { } else {
$rootPassword = $envs->filter(function ($env) { $rootPassword = $envs->filter(function ($env) {
return str($env)->startsWith('MYSQL_ROOT_PASSWORD='); return str($env)->startsWith('MYSQL_ROOT_PASSWORD=');
})->first(); })->first();
if ($rootPassword) { if ($rootPassword) {
$this->database->mysql_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value(); $this->database->mariadb_root_password = str($rootPassword)->after('MYSQL_ROOT_PASSWORD=')->value();
} }
} }
@@ -435,7 +434,7 @@ 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(); $this->s3->testConnection(shouldSave: true);
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$network = $this->database->service->destination->network; $network = $this->database->service->destination->network;
} else { } else {

View File

@@ -3,6 +3,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\Server; use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Exception; use Exception;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -18,45 +19,35 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 300; public $timeout = 300;
public ?string $dockerRootFilesystem = null;
public ?int $usageBefore = null; public ?int $usageBefore = null;
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->uuid))];
}
public function uniqueId(): string
{
return $this->server->uuid;
}
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
} }
public function handle(): void public function handle(): void
{ {
$isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) {
$isInprogress = true;
return;
}
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
try { try {
$isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) {
$isInprogress = true;
return;
}
});
if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
}
if (!$this->server->isFunctional()) { if (!$this->server->isFunctional()) {
return; return;
} }
$this->dockerRootFilesystem = "/"; $this->usageBefore = $this->server->getDiskUsage();
$this->usageBefore = $this->getFilesystemUsage(); ray('Usage before: ' . $this->usageBefore);
if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) { if ($this->usageBefore >= $this->server->settings->cleanup_after_percentage) {
ray('Cleaning up ' . $this->server->name); ray('Cleaning up ' . $this->server->name);
instant_remote_process(['docker image prune -af'], $this->server); instant_remote_process(['docker image prune -af'], $this->server, false);
instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server); instant_remote_process(['docker container prune -f --filter "label=coolify.managed=true"'], $this->server, false);
instant_remote_process(['docker builder prune -af'], $this->server); instant_remote_process(['docker builder prune -af'], $this->server, false);
$usageAfter = $this->getFilesystemUsage(); $usageAfter = $this->server->getDiskUsage();
if ($usageAfter < $this->usageBefore) { if ($usageAfter < $this->usageBefore) {
ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); ray('Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name); send_internal_notification('DockerCleanupJob done: Saved ' . ($this->usageBefore - $usageAfter) . '% disk space on ' . $this->server->name);
@@ -74,9 +65,4 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
throw $e; throw $e;
} }
} }
private function getFilesystemUsage()
{
return instant_remote_process(["df '{$this->dockerRootFilesystem}'| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this->server, false);
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use App\Notifications\Server\HighDiskUsage;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
class ServerStatusJob implements ShouldQueue, ShouldBeEncrypted
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public ?int $disk_usage = null;
public function __construct(public Server $server)
{
}
public function middleware(): array
{
return [(new WithoutOverlapping($this->server->id))->dontRelease()];
}
public function uniqueId(): int
{
return $this->server->id;
}
public function handle(): void
{
ray("checking server status for {$this->server->id}");
try {
if ($this->server->isServerReady()) {
$this->cleanup(notify: false);
}
} catch (\Throwable $e) {
send_internal_notification('ServerStatusJob failed with: ' . $e->getMessage());
ray($e->getMessage());
handleError($e);
}
}
public function cleanup(bool $notify = false): void
{
$this->disk_usage = $this->server->getDiskUsage();
if ($this->disk_usage >= $this->server->settings->cleanup_after_percentage) {
if ($notify) {
if ($this->server->high_disk_usage_notification_sent) {
ray('high disk usage notification already sent');
return;
} else {
$this->server->high_disk_usage_notification_sent = true;
$this->server->save();
$this->server->team->notify(new HighDiskUsage($this->server, $this->disk_usage, $this->server->settings->cleanup_after_percentage));
}
} else {
DockerCleanupJob::dispatchSync($this->server);
$this->cleanup(notify: true);
}
} else {
$this->server->high_disk_usage_notification_sent = false;
$this->server->save();
}
}
}

View File

@@ -85,6 +85,18 @@ class Application extends BaseModel
); );
} }
public function gitWebhook(): Attribute
{
return Attribute::make(
get: function () {
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}/settings/hooks";
}
return $this->git_repository;
}
);
}
public function gitCommits(): Attribute public function gitCommits(): Attribute
{ {
return Attribute::make( return Attribute::make(
@@ -213,7 +225,8 @@ class Application extends BaseModel
return $this->morphTo(); return $this->morphTo();
} }
public function isDeploymentInprogress() { public function isDeploymentInprogress()
{
$deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count(); $deployments = ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'in_progress')->count();
if ($deployments > 0) { if ($deployments > 0) {
return true; return true;
@@ -288,6 +301,10 @@ class Application extends BaseModel
} }
return false; return false;
} }
public function isLogDrainEnabled()
{
return data_get($this, 'settings.is_log_drain_enabled', false);
}
public function isConfigurationChanged($save = false) public function isConfigurationChanged($save = false)
{ {
$newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels; $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->port_exposes . $this->port_mappings . $this->base_directory . $this->publish_directory . $this->health_check_path . $this->health_check_port . $this->health_check_host . $this->health_check_method . $this->health_check_return_code . $this->health_check_scheme . $this->health_check_response_text . $this->health_check_interval . $this->health_check_timeout . $this->health_check_retries . $this->health_check_start_period . $this->health_check_enabled . $this->limits_memory . $this->limits_swap . $this->limits_swappiness . $this->limits_reservation . $this->limits_cpus . $this->limits_cpuset . $this->limits_cpu_shares . $this->dockerfile . $this->dockerfile_location . $this->custom_labels;
@@ -315,4 +332,14 @@ class Application extends BaseModel
return true; return true;
} }
} }
public function isMultipleServerDeployment()
{
if (isDev()) {
return true;
}
if (data_get($this, 'additional_destinations') && data_get($this, 'docker_registry_image_name')) {
return true;
}
return false;
}
} }

View File

@@ -36,14 +36,13 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}"; return "{$this->endpoint}/{$this->bucket}";
} }
public function testConnection() public function testConnection(bool $shouldSave = false)
{ {
try { try {
set_s3_target($this); set_s3_target($this);
Storage::disk('custom-s3')->files(); Storage::disk('custom-s3')->files();
$this->unusable_email_sent = false; $this->unusable_email_sent = false;
$this->is_usable = true; $this->is_usable = true;
return;
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->is_usable = false; $this->is_usable = false;
if ($this->unusable_email_sent === false && is_transactional_emails_active()) { if ($this->unusable_email_sent === false && is_transactional_emails_active()) {
@@ -65,7 +64,9 @@ class S3Storage extends BaseModel
throw $e; throw $e;
} finally { } finally {
$this->save(); if ($shouldSave) {
$this->save();
}
} }
} }
} }

View File

@@ -2,10 +2,15 @@
namespace App\Models; namespace App\Models;
use App\Actions\Server\InstallLogDrain;
use App\Actions\Server\InstallNewRelic;
use App\Enums\ProxyStatus; use App\Enums\ProxyStatus;
use App\Enums\ProxyTypes; use App\Enums\ProxyTypes;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Sleep;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@@ -56,6 +61,8 @@ class Server extends BaseModel
public $casts = [ public $casts = [
'proxy' => SchemalessAttributes::class, 'proxy' => SchemalessAttributes::class,
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
]; ];
protected $schemalessAttributes = [ protected $schemalessAttributes = [
'proxy', 'proxy',
@@ -109,11 +116,84 @@ class Server extends BaseModel
return $this->proxy->modelScope(); return $this->proxy->modelScope();
} }
public function isLocalhost()
{
return $this->ip === 'host.docker.internal' || $this->id === 0;
}
public function skipServer()
{
if ($this->ip === '1.2.3.4') {
ray('skipping 1.2.3.4');
return true;
}
return false;
}
public function isServerReady()
{
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 3;
$currentTime = now()->timestamp;
$runtime = 30;
$isReady = false;
// Run for 30 seconds max and check every 5 seconds for 3 times
while ($currentTime + $runtime > now()->timestamp) {
if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) {
if ($this->unreachable_notification_sent === false) {
ray('Server unreachable, sending notification...');
$this->team->notify(new Unreachable($this));
$this->update(['unreachable_notification_sent' => true]);
}
$this->settings()->update([
'is_reachable' => false,
]);
$this->update([
'unreachable_count' => 0,
]);
foreach ($this->applications() as $application) {
$application->update(['status' => 'exited']);
}
foreach ($this->databases() as $database) {
$database->update(['status' => 'exited']);
}
foreach ($this->services() as $service) {
$apps = $service->applications()->get();
$dbs = $service->databases()->get();
foreach ($apps as $app) {
$app->update(['status' => 'exited']);
}
foreach ($dbs as $db) {
$db->update(['status' => 'exited']);
}
}
$isReady = false;
break;
}
$result = $this->validateConnection();
// ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
Sleep::for(5)->seconds();
return;
}
$isReady = true;
break;
}
return $isReady;
}
public function getDiskUsage()
{
return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false);
}
public function hasDefinedResources() public function hasDefinedResources()
{ {
$applications = $this->applications()->count() === 0; $applications = $this->applications()->count() > 0;
$databases = $this->databases()->count() === 0; $databases = $this->databases()->count() > 0;
$services = $this->services()->count() === 0; $services = $this->services()->count() > 0;
if ($applications || $databases || $services) { if ($applications || $databases || $services) {
return true; return true;
} }
@@ -148,7 +228,7 @@ class Server extends BaseModel
if (isDev()) { if (isDev()) {
return '127.0.0.1'; return '127.0.0.1';
} }
if ($this->ip === 'host.docker.internal') { if ($this->isLocalhost()) {
return base_ip(); return base_ip();
} }
return $this->ip; return $this->ip;
@@ -220,16 +300,57 @@ 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() public function isLogDrainEnabled()
{ {
$uptime = instant_remote_process(['uptime'], $this, false); return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
if (!$uptime) { }
$this->settings->is_reachable = false; public function validateOS()
$this->settings->save(); {
$os_release = instant_remote_process(['cat /etc/os-release'], $this);
$datas = collect(explode("\n", $os_release));
$collectedData = collect([]);
foreach ($datas as $data) {
$item = Str::of($data)->trim();
$collectedData->put($item->before('=')->value(), $item->after('=')->lower()->replace('"', '')->value());
}
$ID = data_get($collectedData, 'ID');
$ID_LIKE = data_get($collectedData, 'ID_LIKE');
$VERSION_ID = data_get($collectedData, 'VERSION_ID');
// ray($ID, $ID_LIKE, $VERSION_ID);
if (collect(SUPPORTED_OS)->contains($ID_LIKE)) {
ray('supported');
return str($ID_LIKE)->explode(' ')->first();
} else {
ray('not supported');
return false; return false;
} }
$this->settings->is_reachable = true; }
$this->settings->save(); public function validateConnection()
{
if ($this->skipServer()) {
return false;
}
$uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings()->update([
'is_reachable' => false,
]);
return false;
} else {
$this->settings()->update([
'is_reachable' => true,
]);
$this->update([
'unreachable_count' => 0,
]);
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
return true; return true;
} }
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)
@@ -239,7 +360,7 @@ class Server extends BaseModel
$this->settings->is_usable = false; $this->settings->is_usable = false;
$this->settings->save(); $this->settings->save();
if ($throwError) { if ($throwError) {
throw new \Exception('Server is not usable.'); throw new \Exception('Server is not usable. Docker Engine is not installed.');
} }
return false; return false;
} }
@@ -257,6 +378,7 @@ class Server extends BaseModel
$this->settings->save(); $this->settings->save();
return false; return false;
} }
$this->settings->is_reachable = true;
$this->settings->is_usable = true; $this->settings->is_usable = true;
$this->settings->save(); $this->settings->save();
return true; return true;

View File

@@ -45,7 +45,261 @@ class Service extends BaseModel
{ {
return 'service'; return 'service';
} }
public function extraFields()
{
$fields = collect([]);
$applications = $this->applications()->get();
foreach ($applications as $application) {
$image = str($application->image)->before(':')->value();
switch ($image) {
case str($image)->contains('minio'):
$data = collect([]);
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
if (is_null($admin_user)) {
$admin_user = $this->environment_variables()->where('key', 'MINIO_ROOT_USER')->first();
}
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
if (is_null($admin_password)) {
$admin_password = $this->environment_variables()->where('key', 'MINIO_ROOT_PASSWORD')->first();
}
if ($console_url) {
$data = $data->merge([
'Console URL' => [
'key' => data_get($console_url, 'key'),
'value' => data_get($console_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($s3_api_url) {
$data = $data->merge([
'S3 API URL' => [
'key' => data_get($s3_api_url, 'key'),
'value' => data_get($s3_api_url, 'value'),
'rules' => 'required|url',
],
]);
}
if ($admin_user) {
$data = $data->merge([
'Admin User' => [
'key' => data_get($admin_user, 'key'),
'value' => data_get($admin_user, 'value'),
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('MinIO', $data->toArray());
break;
case str($image)->contains('weblate'):
$data = collect([]);
$admin_email = $this->environment_variables()->where('key', 'WEBLATE_ADMIN_EMAIL')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_WEBLATE')->first();
if ($admin_email) {
$data = $data->merge([
'Admin Email' => [
'key' => data_get($admin_email, 'key'),
'value' => data_get($admin_email, 'value'),
'rules' => 'required|email',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Admin Password' => [
'key' => data_get($admin_password, 'key'),
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Weblate', $data);
}
}
$databases = $this->databases()->get();
foreach ($databases as $database) {
$image = str($database->image)->before(':')->value();
switch ($image) {
case str($image)->contains('postgres'):
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$data = collect([]);
if ($postgres_user) {
$data = $data->merge([
'User' => [
'key' => data_get($postgres_user, 'key'),
'value' => data_get($postgres_user, 'value'),
'rules' => 'required',
],
]);
}
if ($postgres_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($postgres_password, 'key'),
'value' => data_get($postgres_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($postgres_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($postgres_db_name, 'key'),
'value' => data_get($postgres_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('PostgreSQL', $data->toArray());
break;
case str($image)->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
$dbNameVariables = ['MYSQL_DATABASE'];
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$data = collect([]);
if ($mysql_user) {
$data = $data->merge([
'User' => [
'key' => data_get($mysql_user, 'key'),
'value' => data_get($mysql_user, 'value'),
'rules' => 'required',
],
]);
}
if ($mysql_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($mysql_password, 'key'),
'value' => data_get($mysql_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mysql_root_password) {
$data = $data->merge([
'Root Password' => [
'key' => data_get($mysql_root_password, 'key'),
'value' => data_get($mysql_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mysql_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($mysql_db_name, 'key'),
'value' => data_get($mysql_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('MySQL', $data->toArray());
break;
case str($image)->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$data = collect([]);
if ($mariadb_user) {
$data = $data->merge([
'User' => [
'key' => data_get($mariadb_user, 'key'),
'value' => data_get($mariadb_user, 'value'),
'rules' => 'required',
],
]);
}
if ($mariadb_password) {
$data = $data->merge([
'Password' => [
'key' => data_get($mariadb_password, 'key'),
'value' => data_get($mariadb_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mariadb_root_password) {
$data = $data->merge([
'Root Password' => [
'key' => data_get($mariadb_root_password, 'key'),
'value' => data_get($mariadb_root_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
if ($mariadb_db_name) {
$data = $data->merge([
'Database Name' => [
'key' => data_get($mariadb_db_name, 'key'),
'value' => data_get($mariadb_db_name, 'value'),
'rules' => 'required',
],
]);
}
$fields->put('MariaDB', $data->toArray());
break;
}
}
return $fields;
}
public function saveExtraFields($fields)
{
foreach ($fields as $field) {
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$found = $this->environment_variables()->where('key', $key)->first();
if ($found) {
$found->value = $value;
$found->save();
} else {
$this->environment_variables()->create([
'key' => $key,
'value' => $value,
'is_build_time' => false,
'service_id' => $this->id,
'is_preview' => false,
]);
}
}
}
public function documentation() public function documentation()
{ {
$services = getServiceTemplates(); $services = getServiceTemplates();
@@ -146,6 +400,19 @@ class Service extends BaseModel
$serviceNetworks = collect(data_get($service, 'networks', [])); $serviceNetworks = collect(data_get($service, 'networks', []));
$serviceVariables = collect(data_get($service, 'environment', [])); $serviceVariables = collect(data_get($service, 'environment', []));
$serviceLabels = collect(data_get($service, 'labels', [])); $serviceLabels = collect(data_get($service, 'labels', []));
if ($serviceLabels->count() > 0) {
$removedLabels = collect([]);
$serviceLabels = $serviceLabels->filter(function ($serviceLabel, $serviceLabelName) use ($removedLabels) {
if (!str($serviceLabel)->contains('=')) {
$removedLabels->put($serviceLabelName, $serviceLabel);
return false;
}
return $serviceLabel;
});
foreach($removedLabels as $removedLabelName =>$removedLabel) {
$serviceLabels->push("$removedLabelName=$removedLabel");
}
}
$containerName = "$serviceName-{$this->uuid}"; $containerName = "$serviceName-{$this->uuid}";
@@ -257,7 +524,7 @@ class Service extends BaseModel
} }
} }
$networks = collect(); $networks = collect();
foreach ($serviceNetworks as $key =>$serviceNetwork) { foreach ($serviceNetworks as $key => $serviceNetwork) {
if (gettype($serviceNetwork) === 'string') { if (gettype($serviceNetwork) === 'string') {
// networks: // networks:
// - appwrite // - appwrite
@@ -268,7 +535,7 @@ class Service extends BaseModel
// ipv4_address: 192.168.203.254 // ipv4_address: 192.168.203.254
// $networks->put($serviceNetwork, null); // $networks->put($serviceNetwork, null);
ray($key); ray($key);
$networks->put($key,$serviceNetwork); $networks->put($key, $serviceNetwork);
} }
} }
foreach ($definedNetwork as $key => $network) { foreach ($definedNetwork as $key => $network) {
@@ -395,6 +662,7 @@ class Service extends BaseModel
$key = Str::of($variableName); $key = Str::of($variableName);
$value = Str::of($variable); $value = Str::of($variable);
} }
// TODO: here is the problem
if ($key->startsWith('SERVICE_FQDN')) { if ($key->startsWith('SERVICE_FQDN')) {
if ($isNew || $savedService->fqdn === null) { if ($isNew || $savedService->fqdn === null) {
$name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower(); $name = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower();
@@ -452,15 +720,31 @@ class Service extends BaseModel
'service_id' => $this->id, 'service_id' => $this->id,
])->first(); ])->first();
if ($value->startsWith('SERVICE_')) { if ($value->startsWith('SERVICE_')) {
$command = $value->after('SERVICE_')->beforeLast('_'); // Count _ in $value
$forService = $value->afterLast('_'); $count = substr_count($value->value(), '_');
$generatedValue = null; if ($count === 2) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
$forService = $value->afterLast('_');
$generatedValue = null;
$port = null;
}
if ($count === 3) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
$forService = $value->after('SERVICE_')->after('_')->before('_');
$generatedValue = null;
$port = $value->afterLast('_');
}
if ($command->value() === 'FQDN' || $command->value() === 'URL') { if ($command->value() === 'FQDN' || $command->value() === 'URL') {
if (Str::lower($forService) === $serviceName) { if (Str::lower($forService) === $serviceName) {
$fqdn = generateFqdn($this->server, $containerName); $fqdn = generateFqdn($this->server, $containerName);
} else { } else {
$fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid); $fqdn = generateFqdn($this->server, Str::lower($forService) . '-' . $this->uuid);
} }
if ($port) {
$fqdn = "$fqdn:$port";
}
if ($foundEnv) { if ($foundEnv) {
$fqdn = data_get($foundEnv, 'value'); $fqdn = data_get($foundEnv, 'value');
} else { } else {
@@ -476,7 +760,7 @@ class Service extends BaseModel
]); ]);
} }
if (!$isDatabase) { if (!$isDatabase) {
if ($command->value() === 'FQDN') { if ($command->value() === 'FQDN' && is_null($savedService->fqdn)) {
$savedService->fqdn = $fqdn; $savedService->fqdn = $fqdn;
$savedService->save(); $savedService->save();
} }
@@ -547,7 +831,11 @@ class Service extends BaseModel
} }
// Add labels to the service // Add labels to the service
$fqdns = collect(data_get($savedService, 'fqdns')); if ($savedService->serviceType()) {
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
} else {
$fqdns = collect(data_get($savedService, 'fqdns'));
}
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id); $defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
$serviceLabels = $serviceLabels->merge($defaultLabels); $serviceLabels = $serviceLabels->merge($defaultLabels);
if (!$isDatabase && $fqdns->count() > 0) { if (!$isDatabase && $fqdns->count() > 0) {
@@ -555,6 +843,16 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
if ($this->server->isLogDrainEnabled() && $savedService->isLogDrainEnabled()) {
data_set($service, 'logging', [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "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);

View File

@@ -18,10 +18,24 @@ class ServiceApplication extends BaseModel
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type() public function type()
{ {
return 'service'; return 'service';
} }
public function serviceType()
{
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
return str($this->image)->before(':')->value() === $service;
})->first());
if ($found->isNotEmpty()) {
return $found;
}
return null;
}
public function service() public function service()
{ {
return $this->belongsTo(Service::class); return $this->belongsTo(Service::class);

View File

@@ -16,10 +16,18 @@ class ServiceDatabase extends BaseModel
$service->fileStorages()->delete(); $service->fileStorages()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type() public function type()
{ {
return 'service'; return 'service';
} }
public function serviceType()
{
return null;
}
public function databaseType() public function databaseType()
{ {
$image = str($this->image)->before(':'); $image = str($this->image)->before(':');
@@ -28,11 +36,11 @@ class ServiceDatabase extends BaseModel
} }
return "standalone-$image"; return "standalone-$image";
} }
public function getServiceDatabaseUrl() { public function getServiceDatabaseUrl()
// $type = $this->databaseType(); {
$port = $this->public_port; $port = $this->public_port;
$realIp = $this->service->server->ip; $realIp = $this->service->server->ip;
if ($realIp === 'host.docker.internal' || isDev()) { if ($this->service->server->isLocalhost() || isDev()) {
$realIp = base_ip(); $realIp = base_ip();
} }
$url = "{$realIp}:{$port}"; $url = "{$realIp}:{$port}";

View File

@@ -41,6 +41,10 @@ class StandaloneMariadb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function type(): string public function type(): string
{ {
return 'standalone-mariadb'; return 'standalone-mariadb';

View File

@@ -44,7 +44,10 @@ class StandaloneMongodb extends BaseModel
$database->environment_variables()->delete(); $database->environment_variables()->delete();
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function mongoInitdbRootPassword(): Attribute public function mongoInitdbRootPassword(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -46,6 +46,11 @@ class StandaloneMysql extends BaseModel
return 'standalone-mysql'; return 'standalone-mysql';
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -42,6 +42,11 @@ class StandalonePostgresql extends BaseModel
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -37,6 +37,11 @@ class StandaloneRedis extends BaseModel
}); });
} }
public function isLogDrainEnabled()
{
return data_get($this, 'is_log_drain_enabled', false);
}
public function portsMappings(): Attribute public function portsMappings(): Attribute
{ {
return Attribute::make( return Attribute::make(

View File

@@ -52,7 +52,6 @@ class User extends Authenticatable implements SendsEmail
} }
public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null) public function createToken(string $name, array $abilities = ['*'], DateTimeInterface $expiresAt = null)
{ {
ray('asd');
$plainTextToken = sprintf( $plainTextToken = sprintf(
'%s%s%s', '%s%s%s',
config('sanctum.token_prefix', ''), config('sanctum.token_prefix', ''),

View File

@@ -29,6 +29,10 @@ class EmailChannel
->html((string)$mailMessage->render()) ->html((string)$mailMessage->render())
); );
} catch (Exception $e) { } catch (Exception $e) {
$error = $e->getMessage();
if ($error === 'No email settings found.') {
throw $e;
}
ray($e->getMessage()); ray($e->getMessage());
$message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:";
if (isset($recepients)) { if (isset($recepients)) {

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("Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"); $mail->subject("Coolify: A service ({$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 = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"; $message = "Coolify: A service ({$this->name}) has been restarted automatically on {$this->server->name}";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = "Coolify: Container ({$this->name}) has been restarted automatically on {$this->server->name}"; $message = "Coolify: A service ({$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("Coolify: Container ({$this->name}) has been stopped on {$this->server->name}"); $mail->subject("Coolify: A service ({$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 = "Coolify: Container ({$this->name}) has been stopped on {$this->server->name}"; $message = "Coolify: A service ({$this->name}) has been stopped on {$this->server->name}";
return $message; return $message;
} }
public function toTelegram(): array public function toTelegram(): array
{ {
$message = "Coolify: Container ($this->name} has been stopped on {$this->server->name}"; $message = "Coolify: A service ($this->name} has been stopped on {$this->server->name}";
$payload = [ $payload = [
"message" => $message, "message" => $message,
]; ];

View File

@@ -0,0 +1,65 @@
<?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 HighDiskUsage extends Notification implements ShouldQueue
{
use Queueable;
public $tries = 1;
public function __construct(public Server $server, public int $disk_usage, public int $cleanup_after_percentage)
{
}
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}) high disk usage detected!");
$mail->view('emails.high-disk-usage', [
'name' => $this->server->name,
'disk_usage' => $this->disk_usage,
'threshold' => $this->cleanup_after_percentage,
]);
return $mail;
}
public function toDiscord(): string
{
$message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/automated-cleanup.";
return $message;
}
public function toTelegram(): array
{
return [
"message" => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->cleanup_after_percentage}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/automated-cleanup."
];
}
}

View File

@@ -18,7 +18,7 @@ class Revived extends Notification implements ShouldQueue
public $tries = 1; public $tries = 1;
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
if ($this->server->unreachable_email_sent === false) { if ($this->server->unreachable_notification_sent === false) {
return; return;
} }
} }

View File

@@ -43,7 +43,7 @@ class Unreachable extends Notification implements ShouldQueue
public function toMail(): MailMessage public function toMail(): MailMessage
{ {
$mail = new MailMessage(); $mail = new MailMessage();
$mail->subject("Coolify: 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 3 times");
$mail->view('emails.server-lost-connection', [ $mail->view('emails.server-lost-connection', [
'name' => $this->server->name, 'name' => $this->server->name,
]); ]);
@@ -52,13 +52,13 @@ class Unreachable extends Notification implements ShouldQueue
public function toDiscord(): string public function toDiscord(): string
{ {
$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."; $message = "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 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" => "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." "message" => "Coolify: Server '{$this->server->name}' is unreachable after trying to connect to it 3 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

@@ -12,7 +12,6 @@ use Illuminate\Support\Str;
trait ExecuteRemoteCommand trait ExecuteRemoteCommand
{ {
public ?string $save = null; public ?string $save = null;
public function execute_remote_command(...$commands) public function execute_remote_command(...$commands)
{ {
static::$batch_counter++; static::$batch_counter++;
@@ -32,16 +31,20 @@ trait ExecuteRemoteCommand
throw new \RuntimeException('Command is not set'); throw new \RuntimeException('Command is not set');
} }
$hidden = data_get($single_command, 'hidden', false); $hidden = data_get($single_command, 'hidden', false);
$customType = data_get($single_command, 'type');
$ignore_errors = data_get($single_command, 'ignore_errors', false); $ignore_errors = data_get($single_command, 'ignore_errors', false);
$this->save = data_get($single_command, 'save'); $this->save = data_get($single_command, 'save');
$remote_command = generateSshCommand($this->server, $command); $remote_command = generateSshCommand($this->server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden) { $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType) {
$output = Str::of($output)->trim(); $output = Str::of($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$new_log_entry = [ $new_log_entry = [
'command' => $command, 'command' => remove_iip($command),
'output' => $output, 'output' => remove_iip($output),
'type' => $type === 'err' ? 'stderr' : 'stdout', 'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'), 'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden, 'hidden' => $hidden,
'batch' => static::$batch_counter, 'batch' => static::$batch_counter,

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Traits;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
trait ExecuteRemoteCommandNew
{
public static $batch_counter = 0;
public function executeRemoteCommand(Server $server, $logModel, $commands)
{
static::$batch_counter++;
if ($commands instanceof Collection) {
$commandsText = $commands;
} else {
$commandsText = collect($commands);
}
$commandsText->each(function ($singleCommand) use ($server, $logModel) {
$command = data_get($singleCommand, 'command') ?? $singleCommand[0] ?? null;
if ($command === null) {
throw new \RuntimeException('Command is not set');
}
$hidden = data_get($singleCommand, 'hidden', false);
$customType = data_get($singleCommand, 'type');
$ignoreErrors = data_get($singleCommand, 'ignore_errors', false);
$save = data_get($singleCommand, 'save');
$remote_command = generateSshCommand($server, $command);
$process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $logModel, $save) {
$output = str($output)->trim();
if ($output->startsWith('╔')) {
$output = "\n" . $output;
}
$newLogEntry = [
'command' => remove_iip($command),
'output' => remove_iip($output),
'type' => $customType ?? $type === 'err' ? 'stderr' : 'stdout',
'timestamp' => Carbon::now('UTC'),
'hidden' => $hidden,
'batch' => static::$batch_counter,
];
if (!$logModel->logs) {
$newLogEntry['order'] = 1;
} else {
$previousLogs = json_decode($logModel->logs, associative: true, flags: JSON_THROW_ON_ERROR);
$newLogEntry['order'] = count($previousLogs) + 1;
}
$previousLogs[] = $newLogEntry;
$logModel->logs = json_encode($previousLogs, flags: JSON_THROW_ON_ERROR);
$logModel->save();
if ($save) {
$this->remoteCommandOutputs[$save] = str($output)->trim();
}
});
$logModel->update([
'current_process_id' => $process->id(),
]);
$processResult = $process->wait();
if ($processResult->exitCode() !== 0) {
if (!$ignoreErrors) {
$status = ApplicationDeploymentStatus::FAILED->value;
$logModel->status = $status;
$logModel->save();
throw new \RuntimeException($processResult->errorOutput());
}
}
});
}
}

View File

@@ -38,7 +38,7 @@ class Textarea extends Component
if (is_null($this->id)) $this->id = new Cuid2(7); if (is_null($this->id)) $this->id = new Cuid2(7);
if (is_null($this->name)) $this->name = $this->id; if (is_null($this->name)) $this->name = $this->id;
$this->label = Str::title($this->label); // $this->label = Str::title($this->label);
return view('components.forms.textarea'); return view('components.forms.textarea');
} }
} }

View File

@@ -16,22 +16,28 @@ class Links extends Component
{ {
$this->links = collect([]); $this->links = collect([]);
$service->applications()->get()->map(function ($application) { $service->applications()->get()->map(function ($application) {
if ($application->fqdn) { $type = $application->serviceType();
$fqdns = collect(Str::of($application->fqdn)->explode(',')); if ($type) {
$fqdns->map(function ($fqdn) { $links = generateServiceSpecificFqdns($application, false);
$this->links->push(getFqdnWithoutPort($fqdn)); $this->links = $this->links->merge($links);
}); } else {
} if ($application->fqdn) {
if ($application->ports) { $fqdns = collect(Str::of($application->fqdn)->explode(','));
$portsCollection = collect(Str::of($application->ports)->explode(',')); $fqdns->map(function ($fqdn) {
$portsCollection->map(function ($port) { $this->links->push(getFqdnWithoutPort($fqdn));
if (Str::of($port)->contains(':')) { });
$hostPort = Str::of($port)->before(':'); }
} else { if ($application->ports) {
$hostPort = $port; $portsCollection = collect(Str::of($application->ports)->explode(','));
} $portsCollection->map(function ($port) {
$this->links->push(base_url(withPort:false) . ":{$hostPort}"); if (Str::of($port)->contains(':')) {
}); $hostPort = Str::of($port)->before(':');
} else {
$hostPort = $port;
}
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
});
}
} }
}); });
} }

View File

@@ -1,10 +1,17 @@
<?php <?php
use App\Jobs\ApplicationDeployDockerImageJob;
use App\Jobs\ApplicationDeploymentJob; use App\Jobs\ApplicationDeploymentJob;
use App\Jobs\ApplicationDeploySimpleDockerfileJob;
use App\Jobs\ApplicationRestartJob;
use App\Jobs\MultipleApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview;
use App\Models\Server;
use Symfony\Component\Yaml\Yaml;
function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false) function queue_application_deployment(int $application_id, string $deployment_uuid, int | null $pull_request_id = 0, string $commit = 'HEAD', bool $force_rebuild = false, bool $is_webhook = false, bool $restart_only = false, ?string $git_type = null)
{ {
$deployment = ApplicationDeploymentQueue::create([ $deployment = ApplicationDeploymentQueue::create([
'application_id' => $application_id, 'application_id' => $application_id,
@@ -14,6 +21,7 @@ function queue_application_deployment(int $application_id, string $deployment_uu
'is_webhook' => $is_webhook, 'is_webhook' => $is_webhook,
'restart_only' => $restart_only, 'restart_only' => $restart_only,
'commit' => $commit, 'commit' => $commit,
'git_type' => $git_type
]); ]);
$queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at'); $queued_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'queued')->get()->sortByDesc('created_at');
$running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at'); $running_deployments = ApplicationDeploymentQueue::where('application_id', $application_id)->where('status', 'in_progress')->get()->sortByDesc('created_at');
@@ -28,17 +36,305 @@ function queue_application_deployment(int $application_id, string $deployment_uu
if ($running_deployments->count() > 0) { if ($running_deployments->count() > 0) {
return; return;
} }
// New deployment
// dispatchDeploymentJob($deployment->id);
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $deployment->id, application_deployment_queue_id: $deployment->id,
))->onConnection('long-running')->onQueue('long-running'); ))->onConnection('long-running')->onQueue('long-running');
} }
function queue_next_deployment(Application $application) function queue_next_deployment(Application $application)
{ {
$next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first(); $next_found = ApplicationDeploymentQueue::where('application_id', $application->id)->where('status', 'queued')->first();
if ($next_found) { if ($next_found) {
// New deployment
// dispatchDeploymentJob($next_found->id);
dispatch(new ApplicationDeploymentJob( dispatch(new ApplicationDeploymentJob(
application_deployment_queue_id: $next_found->id, application_deployment_queue_id: $next_found->id,
))->onConnection('long-running')->onQueue('long-running'); ))->onConnection('long-running')->onQueue('long-running');
}
}
function dispatchDeploymentJob($id)
{
$applicationQueue = ApplicationDeploymentQueue::find($id);
$application = Application::find($applicationQueue->application_id);
$isRestartOnly = data_get($applicationQueue, 'restart_only');
$isSimpleDockerFile = data_get($application, 'dockerfile');
$isDockerImage = data_get($application, 'build_pack') === 'dockerimage';
if ($isRestartOnly) {
ApplicationRestartJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isSimpleDockerFile) {
ApplicationDeploySimpleDockerfileJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else if ($isDockerImage) {
ApplicationDeployDockerImageJob::dispatch(applicationDeploymentQueueId: $id)->onConnection('long-running')->onQueue('long-running');
} else {
throw new Exception('Unknown build pack');
}
}
// Deployment things
function generateHostIpMapping(Server $server, string $network)
{
// Generate custom host<->ip hostnames
$allContainers = instant_remote_process(["docker network inspect {$network} -f '{{json .Containers}}' "], $server);
$allContainers = format_docker_command_output_to_json($allContainers);
$ips = collect([]);
if (count($allContainers) > 0) {
$allContainers = $allContainers[0];
foreach ($allContainers as $container) {
$containerName = data_get($container, 'Name');
if ($containerName === 'coolify-proxy') {
continue;
}
$containerIp = data_get($container, 'IPv4Address');
if ($containerName && $containerIp) {
$containerIp = str($containerIp)->before('/');
$ips->put($containerName, $containerIp->value());
}
}
}
return $ips->map(function ($ip, $name) {
return "--add-host $name:$ip";
})->implode(' ');
}
function generateBaseDir(string $deplyomentUuid)
{
return "/artifacts/$deplyomentUuid";
}
function generateWorkdir(string $deplyomentUuid, Application $application)
{
return generateBaseDir($deplyomentUuid) . rtrim($application->base_directory, '/');
}
function prepareHelperContainer(Server $server, string $network, string $deploymentUuid)
{
$basedir = generateBaseDir($deploymentUuid);
$helperImage = config('coolify.helper_image');
$serverUserHomeDir = instant_remote_process(["echo \$HOME"], $server);
$dockerConfigFileExists = instant_remote_process(["test -f {$serverUserHomeDir}/.docker/config.json && echo 'OK' || echo 'NOK'"], $server);
$commands = collect([]);
if ($dockerConfigFileExists === 'OK') {
$commands->push([
"command" => "docker run -d --network $network -v /:/host --name $deploymentUuid --rm -v {$serverUserHomeDir}/.docker/config.json:/root/.docker/config.json:ro -v /var/run/docker.sock:/var/run/docker.sock $helperImage",
"hidden" => true,
]);
} else {
$commands->push([
"command" => "docker run -d --network {$network} -v /:/host --name {$deploymentUuid} --rm -v /var/run/docker.sock:/var/run/docker.sock {$helperImage}",
"hidden" => true,
]);
}
$commands->push([
"command" => executeInDocker($deploymentUuid, "mkdir -p {$basedir}"),
"hidden" => true,
]);
return $commands;
}
function generateComposeFile(string $deploymentUuid, Server $server, string $network, Application $application, string $containerName, string $imageName, ?ApplicationPreview $preview = null, int $pullRequestId = 0)
{
$ports = $application->settings->is_static ? [80] : $application->ports_exposes_array;
$workDir = generateWorkdir($deploymentUuid, $application);
$persistent_storages = generateLocalPersistentVolumes($application, $pullRequestId);
$volume_names = generateLocalPersistentVolumesOnlyVolumeNames($application, $pullRequestId);
$environment_variables = generateEnvironmentVariables($application, $ports, $pullRequestId);
if (data_get($application, 'custom_labels')) {
$labels = collect(str($application->custom_labels)->explode(','));
$labels = $labels->filter(function ($value, $key) {
return !str($value)->startsWith('coolify.');
});
$application->custom_labels = $labels->implode(',');
$application->save();
} else {
$labels = collect(generateLabelsApplication($application, $preview));
}
if ($pullRequestId !== 0) {
$labels = collect(generateLabelsApplication($application, $preview));
}
$labels = $labels->merge(defaultLabels($application->id, $application->uuid, 0))->toArray();
$docker_compose = [
'version' => '3.8',
'services' => [
$containerName => [
'image' => $imageName,
'container_name' => $containerName,
'restart' => RESTART_MODE,
'environment' => $environment_variables,
'labels' => $labels,
'expose' => $ports,
'networks' => [
$network,
],
'mem_limit' => $application->limits_memory,
'memswap_limit' => $application->limits_memory_swap,
'mem_swappiness' => $application->limits_memory_swappiness,
'mem_reservation' => $application->limits_memory_reservation,
'cpus' => (int) $application->limits_cpus,
'cpuset' => $application->limits_cpuset,
'cpu_shares' => $application->limits_cpu_shares,
]
],
'networks' => [
$network => [
'external' => true,
'name' => $network,
'attachable' => true
]
]
];
if ($server->isLogDrainEnabled() && $application->isLogDrainEnabled()) {
$docker_compose['services'][$containerName]['logging'] = [
'driver' => 'fluentd',
'options' => [
'fluentd-address' => "tcp://127.0.0.1:24224",
'fluentd-async' => "true",
'fluentd-sub-second-precision' => "true",
]
];
}
if ($application->settings->is_gpu_enabled) {
ray('asd');
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'] = [
[
'driver' => data_get($application, 'settings.gpu_driver', 'nvidia'),
'capabilities' => ['gpu'],
'options' => data_get($application, 'settings.gpu_options', [])
]
];
if (data_get($application, 'settings.gpu_count')) {
$count = data_get($application, 'settings.gpu_count');
if ($count === 'all') {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = $count;
} else {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['count'] = (int) $count;
}
} else if (data_get($application, 'settings.gpu_device_ids')) {
$docker_compose['services'][$containerName]['deploy']['resources']['reservations']['devices'][0]['ids'] = data_get($application, 'settings.gpu_device_ids');
}
}
if ($application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $containerName . '.healthcheck');
}
if (count($application->ports_mappings_array) > 0 && $pullRequestId === 0) {
$docker_compose['services'][$containerName]['ports'] = $application->ports_mappings_array;
}
if (count($persistent_storages) > 0) {
$docker_compose['services'][$containerName]['volumes'] = $persistent_storages;
}
if (count($volume_names) > 0) {
$docker_compose['volumes'] = $volume_names;
}
$docker_compose = Yaml::dump($docker_compose, 10);
$docker_compose_base64 = base64_encode($docker_compose);
$commands = collect([]);
$commands->push([
"command" => executeInDocker($deploymentUuid, "echo '{$docker_compose_base64}' | base64 -d > {$workDir}/docker-compose.yml"),
"hidden" => true,
]);
return $commands;
}
function generateLocalPersistentVolumes(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes = [];
foreach ($application->persistentStorages as $persistentStorage) {
$volume_name = $persistentStorage->host_path ?? $persistentStorage->name;
if ($pullRequestId !== 0) {
$volume_name = $volume_name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes[] = $volume_name . ':' . $persistentStorage->mount_path;
}
return $local_persistent_volumes;
}
function generateLocalPersistentVolumesOnlyVolumeNames(Application $application, int $pullRequestId = 0)
{
$local_persistent_volumes_names = [];
foreach ($application->persistentStorages as $persistentStorage) {
if ($persistentStorage->host_path) {
continue;
}
$name = $persistentStorage->name;
if ($pullRequestId !== 0) {
$name = $name . '-pr-' . $pullRequestId;
}
$local_persistent_volumes_names[$name] = [
'name' => $name,
'external' => false,
];
}
return $local_persistent_volumes_names;
}
function generateEnvironmentVariables(Application $application, $ports, int $pullRequestId = 0)
{
$environment_variables = collect();
// ray('Generate Environment Variables')->green();
if ($pullRequestId === 0) {
// ray($this->application->runtime_environment_variables)->green();
foreach ($application->runtime_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables as $env) {
$environment_variables->push("$env->key=$env->value");
}
} else {
// ray($this->application->runtime_environment_variables_preview)->green();
foreach ($application->runtime_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
foreach ($application->nixpacks_environment_variables_preview as $env) {
$environment_variables->push("$env->key=$env->value");
}
}
// Add PORT if not exists, use the first port as default
if ($environment_variables->filter(fn ($env) => str($env)->contains('PORT'))->isEmpty()) {
$environment_variables->push("PORT={$ports[0]}");
}
return $environment_variables->all();
}
function rollingUpdate(Application $application, string $deploymentUuid)
{
$commands = collect([]);
$workDir = generateWorkdir($deploymentUuid, $application);
if (count($application->ports_mappings_array) > 0) {
// $this->execute_remote_command(
// [
// "echo '\n----------------------------------------'",
// ],
// ["echo -n 'Application has ports mapped to the host system, rolling update is not supported.'"],
// );
// $this->stop_running_container(force: true);
// $this->start_by_compose_file();
} else {
$commands->push(
[
"command" => "echo '\n----------------------------------------'"
],
[
"command" => "echo -n 'Rolling update started.'"
]
);
if ($application->build_pack === 'dockerimage') {
$commands->push(
["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} pull"), "hidden" => true],
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
} else {
$commands->push(
[executeInDocker($deploymentUuid, "docker compose --project-directory {$workDir} up --build -d"), "hidden" => true],
);
}
return $commands;
} }
} }

View File

@@ -1,5 +1,6 @@
<?php <?php
const REDACTED = '<REDACTED>';
const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb']; const DATABASE_TYPES = ['postgresql', 'redis', 'mongodb', 'mysql', 'mariadb'];
const VALID_CRON_STRINGS = [ const VALID_CRON_STRINGS = [
'every_minute' => '* * * * *', 'every_minute' => '* * * * *',
@@ -23,3 +24,11 @@ const DATABASE_DOCKER_IMAGES = [
'influxdb', 'influxdb',
'clickhouse/clickhouse-server' 'clickhouse/clickhouse-server'
]; ];
const SPECIFIC_SERVICES = [
'quay.io/minio/minio',
];
const SUPPORTED_OS = [
'debian',
'rhel centos fedora'
];

View File

@@ -10,7 +10,6 @@ use Visus\Cuid2\Cuid2;
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null): Collection
{ {
ray($id, $pullRequestId);
$containers = collect([]); $containers = collect([]);
$containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server); $containers = instant_remote_process(["docker ps -a --filter='label=coolify.applicationId={$id}' --format '{{json .}}' "], $server);
$containers = format_docker_command_output_to_json($containers); $containers = format_docker_command_output_to_json($containers);
@@ -26,7 +25,6 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul
return null; return null;
}); });
$containers = $containers->filter(); $containers = $containers->filter();
ray($containers);
return $containers; return $containers;
} }
@@ -144,6 +142,42 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
} }
return $labels; return $labels;
} }
function generateServiceSpecificFqdns($service, $forTraefik = false)
{
$variables = collect($service->service->environment_variables);
$type = $service->serviceType();
$payload = collect([]);
switch ($type) {
case $type->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload;
}
if (is_null($MINIO_BROWSER_REDIRECT_URL?->value)) {
$MINIO_BROWSER_REDIRECT_URL?->update([
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
]);
}
if (is_null($MINIO_SERVER_URL?->value)) {
$MINIO_SERVER_URL?->update([
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
]);
}
if ($forTraefik) {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
$MINIO_SERVER_URL->value . ':9000',
]);
} else {
$payload = collect([
$MINIO_BROWSER_REDIRECT_URL->value,
$MINIO_SERVER_URL->value,
]);
}
}
return $payload;
}
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null) function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
{ {
$labels = collect([]); $labels = collect([]);

View File

@@ -50,8 +50,11 @@ function generate_github_jwt_token(GithubApp $source)
return $issuedToken; return $issuedToken;
} }
function githubApi(GithubApp|GitlabApp $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true) function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', array|null $data = null, bool $throwError = true)
{ {
if (is_null($source)) {
throw new \Exception('Not implemented yet.');
}
if ($source->getMorphClass() == 'App\Models\GithubApp') { if ($source->getMorphClass() == 'App\Models\GithubApp') {
if ($source->is_public) { if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint); $response = Http::github($source->api_url)->$method($endpoint);

View File

@@ -170,10 +170,13 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
$i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u'); $i['timestamp'] = Carbon::parse($i['timestamp'])->format('Y-M-d H:i:s.u');
return $i; return $i;
}); });
return $formatted; return $formatted;
} }
function remove_iip($text)
{
$text = preg_replace('/x-access-token:.*?(?=@)/', "x-access-token:" . REDACTED, $text);
return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
}
function refresh_server_connection(?PrivateKey $private_key = null) function refresh_server_connection(?PrivateKey $private_key = null)
{ {
if (is_null($private_key)) { if (is_null($private_key)) {
@@ -191,7 +194,7 @@ function refresh_server_connection(?PrivateKey $private_key = null)
// if (!$uptime) { // if (!$uptime) {
// $server->settings->is_reachable = false; // $server->settings->is_reachable = false;
// $server->team->notify(new Unreachable($server)); // $server->team->notify(new Unreachable($server));
// $server->unreachable_email_sent = true; // $server->unreachable_notification_sent = true;
// $server->save(); // $server->save();
// return [ // return [
// "uptime" => null, // "uptime" => null,
@@ -213,9 +216,9 @@ function refresh_server_connection(?PrivateKey $private_key = null)
// $server->settings->is_usable = false; // $server->settings->is_usable = false;
// } else { // } else {
// $server->settings->is_usable = true; // $server->settings->is_usable = true;
// if (data_get($server, 'unreachable_email_sent') === true) { // if (data_get($server, 'unreachable_notification_sent') === true) {
// $server->team->notify(new Revived($server)); // $server->team->notify(new Revived($server));
// $server->unreachable_email_sent = false; // $server->unreachable_notification_sent = false;
// $server->save(); // $server->save();
// } // }
// } // }

View File

@@ -85,7 +85,6 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase $oneS
} else { } else {
$fileLocation = $path; $fileLocation = $path;
} }
ray($path,$fileLocation);
// Exists and is a file // 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 // Exists and is a directory
@@ -135,19 +134,21 @@ function updateCompose($resource)
$image = data_get($resource, 'image'); $image = data_get($resource, 'image');
data_set($dockerCompose, "services.{$name}.image", $image); data_set($dockerCompose, "services.{$name}.image", $image);
// Update FQDN if (!str($resource->fqdn)->contains(',')) {
$variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper(); // Update FQDN
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $variableName = "SERVICE_FQDN_" . Str::of($resource->name)->upper();
if ($generatedEnv) { $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$generatedEnv->value = $resource->fqdn; if ($generatedEnv) {
$generatedEnv->save(); $generatedEnv->value = $resource->fqdn;
} $generatedEnv->save();
$variableName = "SERVICE_URL_" . Str::of($resource->name)->upper(); }
$generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first(); $variableName = "SERVICE_URL_" . Str::of($resource->name)->upper();
if ($generatedEnv) { $generatedEnv = EnvironmentVariable::where('service_id', $resource->service_id)->where('key', $variableName)->first();
$url = Str::of($resource->fqdn)->after('://'); if ($generatedEnv) {
$generatedEnv->value = $url; $url = Str::of($resource->fqdn)->after('://');
$generatedEnv->save(); $generatedEnv->value = $url;
$generatedEnv->save();
}
} }
$dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2); $dockerComposeRaw = Yaml::dump($dockerCompose, 10, 2);

View File

@@ -27,7 +27,6 @@ use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Support\Stringable; use Illuminate\Support\Stringable;
use Nubs\RandomNameGenerator\All;
use Poliander\Cron\CronExpression; use Poliander\Cron\CronExpression;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
use phpseclib3\Crypt\RSA; use phpseclib3\Crypt\RSA;
@@ -94,8 +93,13 @@ function refreshSession(?Team $team = null): void
} }
function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null)
{ {
ray('handleError'); if ($error instanceof TooManyRequestsException) {
ray($error); if (isset($livewire)) {
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if ($error instanceof Throwable) { if ($error instanceof Throwable) {
$message = $error->getMessage(); $message = $error->getMessage();
} else { } else {
@@ -104,55 +108,12 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n
if ($customErrorMessage) { if ($customErrorMessage) {
$message = $customErrorMessage . ' ' . $message; $message = $customErrorMessage . ' ' . $message;
} }
if ($error instanceof TooManyRequestsException) {
if (isset($livewire)) {
return $livewire->emit('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.");
}
return "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds.";
}
if (isset($livewire)) { if (isset($livewire)) {
return $livewire->emit('error', $message); return $livewire->emit('error', $message);
} }
throw new Exception($message);
throw new RuntimeException($message);
} }
function general_error_handler(Throwable $err, Livewire\Component $that = null, $isJson = false, $customErrorMessage = null): mixed
{
try {
ray($err);
ray('ERROR OCCURRED: ' . $err->getMessage());
if ($err instanceof QueryException) {
if ($err->errorInfo[0] === '23505') {
throw new Exception($customErrorMessage ?? 'Duplicate entry found.', '23505');
} else if (count($err->errorInfo) === 4) {
throw new Exception($customErrorMessage ?? $err->errorInfo[3]);
} else {
throw new Exception($customErrorMessage ?? $err->errorInfo[2]);
}
} elseif ($err instanceof TooManyRequestsException) {
throw new Exception($customErrorMessage ?? "Too many requests. Please try again in {$err->secondsUntilAvailable} seconds.");
} else {
if ($err->getMessage() === 'This action is unauthorized.') {
return redirect()->route('dashboard')->with('error', $customErrorMessage ?? $err->getMessage());
}
throw new Exception($customErrorMessage ?? $err->getMessage());
}
} catch (\Throwable $e) {
if ($that) {
return $that->emit('error', $customErrorMessage ?? $e->getMessage());
} elseif ($isJson) {
return response()->json([
'code' => $e->getCode(),
'error' => $e->getMessage(),
]);
} else {
ray($customErrorMessage);
ray($e);
return $customErrorMessage ?? $e->getMessage();
}
}
}
function get_route_parameters(): array function get_route_parameters(): array
{ {
return Route::current()->parameters(); return Route::current()->parameters();
@@ -173,7 +134,11 @@ function get_latest_version_of_coolify(): string
function generate_random_name(?string $cuid = null): string function generate_random_name(?string $cuid = null): string
{ {
$generator = All::create(); $generator = new \Nubs\RandomNameGenerator\All(
[
new \Nubs\RandomNameGenerator\Alliteration(),
]
);
if (is_null($cuid)) { if (is_null($cuid)) {
$cuid = new Cuid2(7); $cuid = new Cuid2(7);
} }
@@ -444,20 +409,25 @@ 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();
$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')); try {
if ($services->failed()) { $response = Http::retry(3, 50)->get(config('constants.services.official'));
throw new \Exception($services->body()); if ($response->failed()) {
return collect([]);
}
$services = $response->json();
$services = collect($services)->sortKeys();
} catch (\Throwable $e) {
$services = collect([]);
} }
$services = collect($services->json())->sortKeys();
} }
// $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;
// });
return $services; return $services;
} }
@@ -493,7 +463,8 @@ function queryResourcesByUuid(string $uuid)
return $resource; return $resource;
} }
function generateDeployWebhook($resource) { function generateDeployWebhook($resource)
{
$baseUrl = base_url(); $baseUrl = base_url();
$api = Url::fromString($baseUrl) . '/api/v1'; $api = Url::fromString($baseUrl) . '/api/v1';
$endpoint = '/deploy'; $endpoint = '/deploy';
@@ -501,6 +472,18 @@ function generateDeployWebhook($resource) {
$url = $api . $endpoint . "?uuid=$uuid&force=false"; $url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url; return $url;
} }
function removeAnsiColors($text) { function generateGitManualWebhook($resource, $type) {
if ($resource->source_id !== 0 && !is_null($resource->source_id)) {
return null;
}
if ($resource->getMorphClass() === 'App\Models\Application') {
$baseUrl = base_url();
$api = Url::fromString($baseUrl) . "/webhooks/source/$type/events/manual";
return $api;
}
return null;
}
function removeAnsiColors($text)
{
return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text);
} }

View File

@@ -3,11 +3,11 @@
return [ return [
// @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/
'dsn' => 'https://c35fe90ee56e18b220bb55e8217d4839@o1082494.ingest.sentry.io/4505347448045568', 'dsn' => 'https://396748153b19c469f5ceff50f1664323@o1082494.ingest.sentry.io/4505347448045568',
// 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.121', 'release' => '4.0.0-beta.146',
// 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'),
@@ -76,6 +76,7 @@ return [
'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false), 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', false),
// @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#traces-sample-rate
'enable_tracing' => env('SENTRY_ENABLE_TRACING', false),
'traces_sample_rate' => 0.2, 'traces_sample_rate' => 0.2,
'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'), 'profiles_sample_rate' => env('SENTRY_PROFILES_SAMPLE_RATE') === null ? null : (float)env('SENTRY_PROFILES_SAMPLE_RATE'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.121'; return '4.0.0-beta.146';

View File

@@ -0,0 +1,40 @@
<?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->longText('fqdn')->nullable()->change();
});
Schema::table('application_previews', function (Blueprint $table) {
$table->longText('fqdn')->nullable()->change();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->longText('fqdn')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
Schema::table('application_previews', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
Schema::table('service_applications', function (Blueprint $table) {
$table->string('fqdn')->nullable()->change();
});
}
};

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('users', function (Blueprint $table) {
$table->boolean('is_notification_sponsorship_enabled')->default(true);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('is_notification_sponsorship_enabled');
});
}
};

View File

@@ -0,0 +1,30 @@
<?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->string('manual_webhook_secret_github')->nullable();
$table->string('manual_webhook_secret_gitlab')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('manual_webhook_secret_github');
$table->dropColumn('manual_webhook_secret_gitlab');
});
}
};

View File

@@ -0,0 +1,34 @@
<?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('application_previews', function (Blueprint $table) {
$table->string('git_type')->nullable();
});
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->string('git_type')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_previews', function (Blueprint $table) {
$table->dropColumn('git_type');
});
Schema::table('application_deployment_queues', function (Blueprint $table) {
$table->dropColumn('git_type');
});
}
};

View File

@@ -0,0 +1,30 @@
<?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('servers', function (Blueprint $table) {
$table->boolean('high_disk_usage_notification_sent')->default(false);
$table->renameColumn('unreachable_email_sent', 'unreachable_notification_sent');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('high_disk_usage_notification_sent');
$table->renameColumn('unreachable_notification_sent', 'unreachable_email_sent');
});
}
};

View File

@@ -0,0 +1,47 @@
<?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('server_settings', function (Blueprint $table) {
$table->boolean('is_logdrain_newrelic_enabled')->default(false);
$table->string('logdrain_newrelic_license_key')->nullable();
$table->string('logdrain_newrelic_base_uri')->nullable();
$table->boolean('is_logdrain_highlight_enabled')->default(false);
$table->string('logdrain_highlight_project_id')->nullable();
$table->boolean('is_logdrain_axiom_enabled')->default(false);
$table->string('logdrain_axiom_dataset_name')->nullable();
$table->string('logdrain_axiom_api_key')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('is_logdrain_newrelic_enabled');
$table->dropColumn('logdrain_newrelic_license_key');
$table->dropColumn('logdrain_newrelic_base_uri');
$table->dropColumn('is_logdrain_highlight_enabled');
$table->dropColumn('logdrain_highlight_project_id');
$table->dropColumn('is_logdrain_axiom_enabled');
$table->dropColumn('logdrain_axiom_dataset_name');
$table->dropColumn('logdrain_axiom_api_key');
});
}
};

View File

@@ -0,0 +1,76 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_applications', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('service_databases', function (Blueprint $table) {
$table->boolean('is_log_drain_enabled')->default(false);
});
Schema::table('servers', function (Blueprint $table) {
$table->boolean('log_drain_notification_sent')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_redis', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mysqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mariadbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_postgresqls', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('standalone_mongodbs', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_applications', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('service_databases', function (Blueprint $table) {
$table->dropColumn('is_log_drain_enabled');
});
Schema::table('servers', function (Blueprint $table) {
$table->dropColumn('log_drain_notification_sent');
});
}
};

View File

@@ -0,0 +1,36 @@
<?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('application_settings', function (Blueprint $table) {
$table->boolean('is_gpu_enabled')->default(false);
$table->string('gpu_driver')->default('nvidia');
$table->string('gpu_count')->nullable();
$table->string('gpu_device_ids')->nullable();
$table->longText('gpu_options')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('application_settings', function (Blueprint $table) {
$table->dropColumn('is_gpu_enabled');
$table->dropColumn('gpu_driver');
$table->dropColumn('gpu_count');
$table->dropColumn('gpu_device_ids');
$table->dropColumn('gpu_options');
});
}
};

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->string('additional_destinations')->nullable()->after('destination');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('additional_destinations');
});
}
};

View File

@@ -44,6 +44,7 @@ services:
- STRIPE_PRICE_ID_PRO_YEARLY - STRIPE_PRICE_ID_PRO_YEARLY
- STRIPE_PRICE_ID_ULTIMATE_MONTHLY - STRIPE_PRICE_ID_ULTIMATE_MONTHLY
- STRIPE_PRICE_ID_ULTIMATE_YEARLY - STRIPE_PRICE_ID_ULTIMATE_YEARLY
- STRIPE_EXCLUDED_PLANS
- PADDLE_VENDOR_ID - PADDLE_VENDOR_ID
- PADDLE_WEBHOOK_SECRET - PADDLE_WEBHOOK_SECRET
- PADDLE_VENDOR_AUTH_CODE - PADDLE_VENDOR_AUTH_CODE

309
package-lock.json generated
View File

@@ -6,19 +6,19 @@
"": { "": {
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1", "alpinejs": "3.13.2",
"daisyui": "3.9.2", "daisyui": "4.3.1",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.4.0", "@vitejs/plugin-vue": "4.5.0",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"axios": "1.5.1", "axios": "1.6.2",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.5",
"vite": "4.4.11", "vite": "4.5.0",
"vue": "3.3.4" "vue": "3.3.8"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@@ -33,9 +33,9 @@
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.22.4", "version": "7.23.3",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.4.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.3.tgz",
"integrity": "sha512-VLLsx06XkEYqBtE5YGPwfSGwfrjnyPP5oiGty3S8pQLFDFLaS8VwWSIxkTXpcvr5zeYLE6+MBNl2npl/YnfofA==", "integrity": "sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==",
"dev": true, "dev": true,
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -503,90 +503,90 @@
} }
}, },
"node_modules/@vitejs/plugin-vue": { "node_modules/@vitejs/plugin-vue": {
"version": "4.4.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.5.0.tgz",
"integrity": "sha512-xdguqb+VUwiRpSg+nsc2HtbAUSGak25DXYvpQQi4RVU1Xq1uworyoH/md9Rfd8zMmPR/pSghr309QNcftUVseg==", "integrity": "sha512-a2WSpP8X8HTEww/U00bU4mX1QpLINNuz/2KMNpLsdu3BzOpak3AGI1CJYBTXcc4SPhaD0eNRUp7IyQK405L5dQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^14.18.0 || >=16.0.0" "node": "^14.18.0 || >=16.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"vite": "^4.0.0", "vite": "^4.0.0 || ^5.0.0",
"vue": "^3.2.25" "vue": "^3.2.25"
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.3.8.tgz",
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==", "integrity": "sha512-hN/NNBUECw8SusQvDSqqcVv6gWq8L6iAktUR0UF3vGu2OhzRqcOiAno0FmBJWwxhYEXRlQJT5XnoKsVq1WZx4g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.21.3", "@babel/parser": "^7.23.0",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-core/node_modules/@vue/shared": { "node_modules/@vue/compiler-core/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.3.8.tgz",
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==", "integrity": "sha512-+PPtv+p/nWDd0AvJu3w8HS0RIm/C6VGBIRe24b9hSyNWOAPEUosFZ5diwawwP8ip5sJ8n0Pe87TNNNHnvjs0FQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/compiler-dom/node_modules/@vue/shared": { "node_modules/@vue/compiler-dom/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.3.8.tgz",
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==", "integrity": "sha512-WMzbUrlTjfYF8joyT84HfwwXo+8WPALuPxhy+BZ6R4Aafls+jDBnSz8PDz60uFhuqFbl3HxRfxvDzrUf3THwpA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.20.15", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.8",
"@vue/compiler-dom": "3.3.4", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-ssr": "3.3.4", "@vue/compiler-ssr": "3.3.8",
"@vue/reactivity-transform": "3.3.4", "@vue/reactivity-transform": "3.3.8",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.0", "magic-string": "^0.30.5",
"postcss": "^8.1.10", "postcss": "^8.4.31",
"source-map-js": "^1.0.2" "source-map-js": "^1.0.2"
} }
}, },
"node_modules/@vue/compiler-sfc/node_modules/@vue/shared": { "node_modules/@vue/compiler-sfc/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.3.8.tgz",
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==", "integrity": "sha512-hXCqQL/15kMVDBuoBYpUnSYT8doDNwsjvm3jTefnXr+ytn294ySnT8NlsFHmTgKNjwpuFy7XVV8yTeLtNl/P6w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.3.4", "@vue/compiler-dom": "3.3.8",
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/compiler-ssr/node_modules/@vue/shared": { "node_modules/@vue/compiler-ssr/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
@@ -598,83 +598,83 @@
} }
}, },
"node_modules/@vue/reactivity-transform": { "node_modules/@vue/reactivity-transform": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.3.8.tgz",
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==", "integrity": "sha512-49CvBzmZNtcHua0XJ7GdGifM8GOXoUMOX4dD40Y5DxI3R8OUhMlvf2nvgUAcPxaXiV5MQQ1Nwy09ADpnLQUqRw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.20.15", "@babel/parser": "^7.23.0",
"@vue/compiler-core": "3.3.4", "@vue/compiler-core": "3.3.8",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.8",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.0" "magic-string": "^0.30.5"
} }
}, },
"node_modules/@vue/reactivity-transform/node_modules/@vue/shared": { "node_modules/@vue/reactivity-transform/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.3.8.tgz",
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==", "integrity": "sha512-qurzOlb6q26KWQ/8IShHkMDOuJkQnQcTIp1sdP4I9MbCf9FJeGVRXJFr2mF+6bXh/3Zjr9TDgURXrsCr9bfjUw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/reactivity": "3.3.4", "@vue/reactivity": "3.3.8",
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/reactivity": { "node_modules/@vue/runtime-core/node_modules/@vue/reactivity": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.3.8.tgz",
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==", "integrity": "sha512-ctLWitmFBu6mtddPyOKpHg8+5ahouoTCRtmAHZAXmolDtuZXfjL2T3OJ6DL6ezBPQB1SmMnpzjiWjCiMYmpIuw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
} }
}, },
"node_modules/@vue/runtime-core/node_modules/@vue/shared": { "node_modules/@vue/runtime-core/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.3.8.tgz",
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==", "integrity": "sha512-Noy5yM5UIf9UeFoowBVgghyGGPIDPy1Qlqt0yVsUdAVbqI8eeMSsTqBtauaEoT2UFXUk5S64aWVNJN4MJ2vRdA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/runtime-core": "3.3.4", "@vue/runtime-core": "3.3.8",
"@vue/shared": "3.3.4", "@vue/shared": "3.3.8",
"csstype": "^3.1.1" "csstype": "^3.1.2"
} }
}, },
"node_modules/@vue/runtime-dom/node_modules/@vue/shared": { "node_modules/@vue/runtime-dom/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.3.8.tgz",
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==", "integrity": "sha512-zVCUw7RFskvPuNlPn/8xISbrf0zTWsTSdYTsUTN1ERGGZGVnRxM2QZ3x1OR32+vwkkCm0IW6HmJ49IsPm7ilLg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.3.4", "@vue/compiler-ssr": "3.3.8",
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
}, },
"peerDependencies": { "peerDependencies": {
"vue": "3.3.4" "vue": "3.3.8"
} }
}, },
"node_modules/@vue/server-renderer/node_modules/@vue/shared": { "node_modules/@vue/server-renderer/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
@@ -683,9 +683,9 @@
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==" "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA=="
}, },
"node_modules/alpinejs": { "node_modules/alpinejs": {
"version": "3.13.1", "version": "3.13.2",
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.1.tgz", "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.13.2.tgz",
"integrity": "sha512-/LZ7mumW02V7AV5xTTftJFHS0I3KOXLl7tHm4xpxXAV+HJ/zjTT0n8MU7RZ6UoGPhmO/i+KEhQojaH/0RsH5tg==", "integrity": "sha512-WzojeeN082kLZznGI1HAuP8yFJSWqJ1fGdz2mUjj45H4y0XwToE7fFqtI3mCPRR+BpcSbxT/NL+FyPnYAWSltw==",
"dependencies": { "dependencies": {
"@vue/reactivity": "~3.1.1" "@vue/reactivity": "~3.1.1"
} }
@@ -756,9 +756,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.5.1", "version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.0", "follow-redirects": "^1.15.0",
@@ -896,11 +896,6 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/colord": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -952,16 +947,23 @@
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==",
"dev": true "dev": true
}, },
"node_modules/culori": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/culori/-/culori-3.2.0.tgz",
"integrity": "sha512-HIEbTSP7vs1mPq/2P9In6QyFE0Tkpevh0k9a+FkjhD+cwsYm9WRSbn4uMdW9O0yXlNYC3ppxL3gWWPOcvEl57w==",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
"node_modules/daisyui": { "node_modules/daisyui": {
"version": "3.9.2", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-3.9.2.tgz", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.3.1.tgz",
"integrity": "sha512-yJZ1QjHUaL+r9BkquTdzNHb7KIgAJVFh0zbOXql2Wu0r7zx5qZNLxclhjN0WLoIpY+o2h/8lqXg7ijj8oTigOw==", "integrity": "sha512-dCi91VD+57lkoBd10CjdW4wPOeOPYvvzQbxti6xmyQbDMbCeCXwNq2KdoU798I4OsCcD5B+n7yVG7HAgYW+cvw==",
"dependencies": { "dependencies": {
"colord": "^2.9",
"css-selector-tokenizer": "^0.8", "css-selector-tokenizer": "^0.8",
"postcss": "^8", "culori": "^3",
"postcss-js": "^4", "picocolors": "^1",
"tailwindcss": "^3.1" "postcss-js": "^4"
}, },
"engines": { "engines": {
"node": ">=16.9.0" "node": ">=16.9.0"
@@ -1049,9 +1051,9 @@
"dev": true "dev": true
}, },
"node_modules/fast-glob": { "node_modules/fast-glob": {
"version": "3.2.12", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3", "@nodelib/fs.walk": "^1.2.3",
@@ -1151,9 +1153,9 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
}, },
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.2", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"os": [ "os": [
@@ -1281,9 +1283,9 @@
} }
}, },
"node_modules/jiti": { "node_modules/jiti": {
"version": "1.18.2", "version": "1.21.0",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz",
"integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==",
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -1333,12 +1335,12 @@
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
}, },
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.0", "version": "0.30.5",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz",
"integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", "integrity": "sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.13" "@jridgewell/sourcemap-codec": "^1.4.15"
}, },
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -1709,9 +1711,9 @@
} }
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "3.29.3", "version": "3.29.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.3.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz",
"integrity": "sha512-T7du6Hum8jOkSWetjRgbwpM6Sy0nECYrYRSmZjayFcOddtKJWU4d17AC3HNUk7HRuqy4p+G7aEZclSHytqUmEg==", "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==",
"dev": true, "dev": true,
"bin": { "bin": {
"rollup": "dist/bin/rollup" "rollup": "dist/bin/rollup"
@@ -1806,19 +1808,19 @@
} }
}, },
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "3.3.3", "version": "3.3.5",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz",
"integrity": "sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==", "integrity": "sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==",
"dependencies": { "dependencies": {
"@alloc/quick-lru": "^5.2.0", "@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2", "arg": "^5.0.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"didyoumean": "^1.2.2", "didyoumean": "^1.2.2",
"dlv": "^1.1.3", "dlv": "^1.1.3",
"fast-glob": "^3.2.12", "fast-glob": "^3.3.0",
"glob-parent": "^6.0.2", "glob-parent": "^6.0.2",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
"jiti": "^1.18.2", "jiti": "^1.19.1",
"lilconfig": "^2.1.0", "lilconfig": "^2.1.0",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
@@ -1920,9 +1922,9 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "4.4.11", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-4.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz",
"integrity": "sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==", "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"esbuild": "^0.18.10", "esbuild": "^0.18.10",
@@ -1975,35 +1977,40 @@
} }
}, },
"node_modules/vite-plugin-full-reload": { "node_modules/vite-plugin-full-reload": {
"version": "1.0.5", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.0.5.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.1.0.tgz",
"integrity": "sha512-kVZFDFWr0DxiHn6MuDVTQf7gnWIdETGlZh0hvTiMXzRN80vgF4PKbONSq8U1d0WtHsKaFODTQgJeakLacoPZEQ==", "integrity": "sha512-3cObNDzX6DdfhD9E7kf6w2mNunFpD7drxyNgHLw+XwIYAgb+Xt16SEXo0Up4VH+TMf3n+DSVJZtW2POBGcBYAA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
},
"peerDependencies": {
"vite": "^2 || ^3 || ^4"
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.3.4.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.8.tgz",
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==", "integrity": "sha512-5VSX/3DabBikOXMsxzlW8JyfeLKlG9mzqnWgLQLty88vdZL7ZJgrdgBOmrArwxiLtmS+lNNpPcBYqrhE6TQW5w==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.3.4", "@vue/compiler-dom": "3.3.8",
"@vue/compiler-sfc": "3.3.4", "@vue/compiler-sfc": "3.3.8",
"@vue/runtime-dom": "3.3.4", "@vue/runtime-dom": "3.3.8",
"@vue/server-renderer": "3.3.4", "@vue/server-renderer": "3.3.8",
"@vue/shared": "3.3.4" "@vue/shared": "3.3.8"
},
"peerDependencies": {
"typescript": "*"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
} }
}, },
"node_modules/vue/node_modules/@vue/shared": { "node_modules/vue/node_modules/@vue/shared": {
"version": "3.3.4", "version": "3.3.8",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.4.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.3.8.tgz",
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==", "integrity": "sha512-8PGwybFwM4x8pcfgqEQFy70NaQxASvOC5DJwLQfpArw1UDfUXrJkdxD3BhVTMS+0Lef/TU7YO0Jvr0jJY8T+mw==",
"dev": true "dev": true
}, },
"node_modules/wrappy": { "node_modules/wrappy": {

View File

@@ -6,19 +6,19 @@
"build": "vite build" "build": "vite build"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "4.4.0", "@vitejs/plugin-vue": "4.5.0",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"axios": "1.5.1", "axios": "1.6.2",
"laravel-vite-plugin": "0.8.1", "laravel-vite-plugin": "0.8.1",
"postcss": "8.4.31", "postcss": "8.4.31",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.5",
"vite": "4.4.11", "vite": "4.5.0",
"vue": "3.3.4" "vue": "3.3.8"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/typography": "0.5.10", "@tailwindcss/typography": "0.5.10",
"alpinejs": "3.13.1", "alpinejs": "3.13.2",
"daisyui": "3.9.2", "daisyui": "4.3.1",
"tailwindcss-scrollbar": "0.1.0" "tailwindcss-scrollbar": "0.1.0"
} }
} }

View File

@@ -53,10 +53,10 @@ a {
@apply text-white; @apply text-white;
} }
.box { .box {
@apply flex p-2 transition-colors cursor-pointer min-h-16 bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors cursor-pointer min-h-[4rem] bg-coolgray-100 hover:bg-coollabs-100 hover:text-white hover:no-underline min-w-[24rem];
} }
.box-without-bg { .box-without-bg {
@apply flex p-2 transition-colors min-h-16 hover:text-white hover:no-underline min-w-[24rem]; @apply flex p-2 transition-colors h-16 min-h-full hover:text-white hover:no-underline min-h-[4rem];
} }
.description { .description {
@apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white; @apply pt-2 text-xs font-bold text-neutral-500 group-hover:text-white;
@@ -124,3 +124,6 @@ tr td:first-child {
.fullscreen { .fullscreen {
@apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ; @apply fixed top-0 left-0 w-full h-full z-[9999] bg-coolgray-100 overflow-y-auto scrollbar pb-4 ;
} }
input.input-sm {
@apply pr-10;
}

View File

@@ -1,4 +1,4 @@
<div class="form-control min-w-fit"> <div class="px-2 form-control min-w-fit hover:bg-coolgray-100">
<label class="flex gap-4 px-0 cursor-pointer label"> <label class="flex gap-4 px-0 cursor-pointer label">
<span class="flex gap-2 label-text min-w-fit"> <span class="flex gap-2 label-text min-w-fit">
@if ($label) @if ($label)
@@ -7,20 +7,7 @@
{{ $id }} {{ $id }}
@endif @endif
@if ($helper) @if ($helper)
<div class="group w-fit"> <x-helper :helper="$helper" />
<div class="cursor-pointer text-warning">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
class="w-4 h-4 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="absolute hidden text-xs group-hover:block border-coolgray-400 bg-coolgray-500">
<div class="p-4 card-body">
{!! $helper !!}
</div>
</div>
</div>
@endif @endif
</span> </span>
<input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }} <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}

View File

@@ -13,7 +13,7 @@
<div class="relative" x-data> <div class="relative" x-data>
@if ($allowToPeak) @if ($allowToPeak)
<div x-on:click="changePasswordFieldType" <div x-on:click="changePasswordFieldType"
class="absolute inset-y-0 left-0 flex items-center pl-2 cursor-pointer hover:text-white"> class="absolute inset-y-0 right-0 flex items-center pr-2 cursor-pointer hover:text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5" <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6" viewBox="0 0 24 24" stroke-width="1.5"
stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none" /> <path stroke="none" d="M0 0h24v24H0z" fill="none" />

View File

@@ -32,6 +32,12 @@
]) }}"> ]) }}">
<button>Destinations</button> <button>Destinations</button>
</a> </a>
<a class="{{ request()->routeIs('server.log-drains') ? 'text-white' : '' }}"
href="{{ route('server.log-drains', [
'server_uuid' => data_get($parameters, 'server_uuid'),
]) }}">
<button>Log Drains</button>
</a>
<div class="flex-1"></div> <div class="flex-1"></div>
<livewire:server.proxy.deploy :server="$server" /> <livewire:server.proxy.deploy :server="$server" />
</nav> </nav>

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