Compare commits

...

66 Commits

Author SHA1 Message Date
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
52 changed files with 1535 additions and 294 deletions

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

@@ -69,6 +69,16 @@ class StartMariadb
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) {
$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;
} }

View File

@@ -76,6 +76,16 @@ class StartMongodb
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) {
$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;
} }

View File

@@ -69,6 +69,16 @@ class StartMysql
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) {
$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;
} }

View File

@@ -79,6 +79,16 @@ class StartPostgresql
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) {
$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;
} }

View File

@@ -78,6 +78,16 @@ class StartRedis
] ]
] ]
]; ];
if ($this->database->destination->server->isDrainLogActivated()) {
$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;
} }

View File

@@ -0,0 +1,185 @@
<?php
namespace App\Actions\Server;
use Lorisleiva\Actions\Concerns\AsAction;
use App\Models\Server;
class InstallLogDrain
{
use AsAction;
public function handle(Server $server, string $type)
{
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
");
$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

@@ -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,40 @@ 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');
$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 {
echo "I am alive!\n";
Http::get("https://get.coollabs.io/coolify/v4/alive?appId=$id");
} catch (\Throwable $e) {
echo "Error in alive: {$e->getMessage()}\n";
}
}
private function cleanup_ssh() private function cleanup_ssh()
{ {
try { try {

View File

@@ -2,13 +2,12 @@
namespace App\Console; namespace App\Console;
use App\Jobs\CheckResaleLicenseJob;
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;
@@ -27,7 +26,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,32 +38,25 @@ 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');
} 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();
} }
} }

View File

@@ -4,6 +4,7 @@ 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 Sentry\Laravel\Integration; use Sentry\Laravel\Integration;
use Sentry\State\Scope; use Sentry\State\Scope;
@@ -40,6 +41,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 +55,7 @@ class Handler extends ExceptionHandler
{ {
$this->reportable(function (Throwable $e) { $this->reportable(function (Throwable $e) {
if (isDev()) { if (isDev()) {
ray($e);
return; return;
} }
$this->settings = InstanceSettings::get(); $this->settings = InstanceSettings::get();

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;

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

@@ -0,0 +1,138 @@
<?php
namespace App\Http\Livewire\Server;
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 {
if ($this->server->settings->is_logdrain_newrelic_enabled) {
$this->server->logDrain('newrelic');
} else if ($this->server->settings->is_logdrain_highlight_enabled) {
$this->server->logDrain('highlight');
} else if ($this->server->settings->is_logdrain_axiom_enabled) {
$this->server->logDrain('axiom');
} else {
$this->server->logDrain('none');
$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

@@ -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

@@ -33,6 +33,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;
@@ -53,6 +55,7 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
private StandaloneDocker|SwarmDocker $destination; private StandaloneDocker|SwarmDocker $destination;
private Server $server; private Server $server;
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 +75,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,6 +103,8 @@ 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();
@@ -189,7 +195,7 @@ 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();
} else if ($this->application->dockerfile) { } else if ($this->application->dockerfile) {
$this->deploy_simple_dockerfile(); $this->deploy_simple_dockerfile();
@@ -218,12 +224,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
} 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",
@@ -349,7 +359,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();
@@ -483,8 +493,15 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
$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 version of your application.'"
], ]
); );
if ($this->full_healthcheck_url) {
$this->execute_remote_command(
[
"echo 'Healthcheck URL (inside the container): {$this->full_healthcheck_url}'"
]
);
}
while ($counter < $this->application->health_check_retries) { while ($counter < $this->application->health_check_retries) {
$this->execute_remote_command( $this->execute_remote_command(
[ [
@@ -652,7 +669,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(' && ');
} }
@@ -664,14 +681,28 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
throw new Exception('Private key not found. Please add a private key to the application and try again.'); throw new Exception('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') {
@@ -833,6 +864,16 @@ class ApplicationDeploymentJob implements ShouldQueue, ShouldBeEncrypted
] ]
] ]
]; ];
if ($this->server->isDrainLogActivated()) {
$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->isHealthcheckDisabled()) { if ($this->application->isHealthcheckDisabled()) {
data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck'); data_forget($docker_compose, 'services.' . $this->container_name . '.healthcheck');
} }
@@ -930,10 +971,12 @@ 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}/"
]; ];
@@ -979,6 +1022,10 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
listen [::]:80; listen [::]:80;
server_name localhost; server_name localhost;
// real_ip_header X-Forwarded-For;
// proxy_set_header X-Real-IP \$remote_addr;
// proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
location / { location / {
root /usr/share/nginx/html; root /usr/share/nginx/html;
index index.html; index index.html;
@@ -992,7 +1039,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}
@@ -1031,9 +1078,15 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
); );
} else { } else {
// Pure Dockerfile based deployment // Pure Dockerfile based deployment
if ($this->application->dockerfile) {
$this->execute_remote_command([ $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 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
]);
}
} }
} }
@@ -1068,22 +1121,20 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
private function start_by_compose_file() private function start_by_compose_file()
{ {
if ( if ($this->application->build_pack === 'dockerimage') {
!$this->application->dockerfile &&
(
$this->application->build_pack === 'dockerimage' ||
$this->application->build_pack === 'dockerfile')
) {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Pulling latest images from the registry.'"], ["echo -n 'Pulling latest images from the registry.'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir}"), "hidden" => true], [executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} pull"), "hidden" => true],
["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true],
); );
} } else {
$this->execute_remote_command( $this->execute_remote_command(
["echo -n 'Starting application (could take a while).'"], ["echo -n 'Starting application (could take a while).'"],
[executeInDocker($this->deployment_uuid, "docker compose --project-directory {$this->workdir} up --build -d"), "hidden" => true], [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()
{ {
@@ -1104,7 +1155,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"));
@@ -1113,7 +1164,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
]); ]);
} }

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;
@@ -25,7 +23,7 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
public function __construct(public Server $server) public function __construct(public Server $server)
{ {
$this->handle(); // $this->handle();
} }
public function middleware(): array public function middleware(): array
{ {
@@ -41,76 +39,9 @@ class ContainerStatusJob implements ShouldQueue, ShouldBeEncrypted
{ {
// ray("checking server status for {$this->server->id}"); // ray("checking server status for {$this->server->id}");
try { try {
// ray()->clearAll(); if (!$this->server->checkServerRediness()) {
$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;

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,23 +19,14 @@ 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
{ {
try {
$isInprogress = false; $isInprogress = false;
$this->server->applications()->each(function ($application) use (&$isInprogress) { $this->server->applications()->each(function ($application) use (&$isInprogress) {
if ($application->isDeploymentInprogress()) { if ($application->isDeploymentInprogress()) {
@@ -45,18 +37,17 @@ class DockerCleanupJob implements ShouldQueue, ShouldBeEncrypted
if ($isInprogress) { if ($isInprogress) {
throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...'); throw new Exception('DockerCleanupJob: ApplicationDeploymentQueue is not empty, skipping...');
} }
try {
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);
}
} }

View File

@@ -0,0 +1,69 @@
<?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->checkServerRediness()) {
return;
}
$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(

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 {
if ($shouldSave) {
$this->save(); $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,82 @@ 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 checkServerRediness()
{
if ($this->skipServer()) {
return false;
}
$serverUptimeCheckNumber = $this->unreachable_count;
$serverUptimeCheckNumberMax = 5;
$currentTime = now()->timestamp;
$runtime5Minutes = 1 * 60;
// Run for 1 minutes max and check every 5 seconds
while ($currentTime + $runtime5Minutes > 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']);
}
}
return false;
}
$result = $this->validateConnection();
ray('validateConnection: ' . $result);
if (!$result) {
$serverUptimeCheckNumber++;
$this->update([
'unreachable_count' => $serverUptimeCheckNumber,
]);
Sleep::for(5)->seconds();
return;
}
break;
}
}
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 +226,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;
@@ -216,20 +294,49 @@ class Server extends BaseModel
// } // }
return true; return true;
} }
public function logDrain($type)
{
InstallLogDrain::run($this, $type);
}
public function isFunctional() public function isFunctional()
{ {
return $this->settings->is_reachable && $this->settings->is_usable; return $this->settings->is_reachable && $this->settings->is_usable;
} }
public function isDrainLogActivated()
{
return $this->settings->is_logdrain_newrelic_enabled || $this->settings->is_logdrain_highlight_enabled || $this->settings->is_logdrain_axiom_enabled;
}
public function validateConnection() public function validateConnection()
{ {
$uptime = instant_remote_process(['uptime'], $this, false); if ($this->skipServer()) {
if (!$uptime) {
$this->settings->is_reachable = false;
$this->settings->save();
return false; return false;
} }
$this->settings->is_reachable = true;
$this->settings->save(); $uptime = instant_remote_process(['uptime'], $this, false);
if (!$uptime) {
$this->settings()->update([
'is_reachable' => false,
'is_usable' => false
]);
return false;
}
if (data_get($this, 'unreachable_notification_sent') === true) {
$this->team->notify(new Revived($this));
$this->update(['unreachable_notification_sent' => false]);
}
if (
data_get($this, 'settings.is_reachable') === false ||
data_get($this, 'settings.is_usable') === false
) {
$this->settings()->update([
'is_reachable' => true,
'is_usable' => true
]);
}
$this->update([
'unreachable_count' => 0,
]);
return true; return true;
} }
public function validateDockerEngine($throwError = false) public function validateDockerEngine($throwError = false)

View File

@@ -111,24 +111,36 @@ class Service extends BaseModel
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); $postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); $postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('PostgreSQL', [ $data = collect([]);
if ($postgres_user) {
$data = $data->merge([
'User' => [ 'User' => [
'key' => data_get($postgres_user, 'key'), 'key' => data_get($postgres_user, 'key'),
'value' => data_get($postgres_user, 'value'), 'value' => data_get($postgres_user, 'value'),
'rules' => 'required', 'rules' => 'required',
], ],
]);
}
if ($postgres_password) {
$data = $data->merge([
'Password' => [ 'Password' => [
'key' => data_get($postgres_password, 'key'), 'key' => data_get($postgres_password, 'key'),
'value' => data_get($postgres_password, 'value'), 'value' => data_get($postgres_password, 'value'),
'rules' => 'required', 'rules' => 'required',
'isPassword' => true, 'isPassword' => true,
], ],
]);
}
if ($postgres_db_name) {
$data = $data->merge([
'Database Name' => [ 'Database Name' => [
'key' => data_get($postgres_db_name, 'key'), 'key' => data_get($postgres_db_name, 'key'),
'value' => data_get($postgres_db_name, 'value'), 'value' => data_get($postgres_db_name, 'value'),
'rules' => 'required', 'rules' => 'required',
], ],
]); ]);
}
$fields->put('PostgreSQL', $data->toArray());
break; break;
case str($image)->contains('mysql'): case str($image)->contains('mysql'):
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS']; $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
@@ -139,30 +151,46 @@ class Service extends BaseModel
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first(); $mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); $mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MySQL', [ $data = collect([]);
if ($mysql_user) {
$data = $data->merge([
'User' => [ 'User' => [
'key' => data_get($mysql_user, 'key'), 'key' => data_get($mysql_user, 'key'),
'value' => data_get($mysql_user, 'value'), 'value' => data_get($mysql_user, 'value'),
'rules' => 'required', 'rules' => 'required',
], ],
]);
}
if ($mysql_password) {
$data = $data->merge([
'Password' => [ 'Password' => [
'key' => data_get($mysql_password, 'key'), 'key' => data_get($mysql_password, 'key'),
'value' => data_get($mysql_password, 'value'), 'value' => data_get($mysql_password, 'value'),
'rules' => 'required', 'rules' => 'required',
'isPassword' => true, 'isPassword' => true,
], ],
]);
}
if ($mysql_root_password) {
$data = $data->merge([
'Root Password' => [ 'Root Password' => [
'key' => data_get($mysql_root_password, 'key'), 'key' => data_get($mysql_root_password, 'key'),
'value' => data_get($mysql_root_password, 'value'), 'value' => data_get($mysql_root_password, 'value'),
'rules' => 'required', 'rules' => 'required',
'isPassword' => true, 'isPassword' => true,
], ],
]);
}
if ($mysql_db_name) {
$data = $data->merge([
'Database Name' => [ 'Database Name' => [
'key' => data_get($mysql_db_name, 'key'), 'key' => data_get($mysql_db_name, 'key'),
'value' => data_get($mysql_db_name, 'value'), 'value' => data_get($mysql_db_name, 'value'),
'rules' => 'required', 'rules' => 'required',
], ],
]); ]);
}
$fields->put('MySQL', $data->toArray());
break; break;
case str($image)->contains('mariadb'): case str($image)->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER']; $userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
@@ -173,31 +201,47 @@ class Service extends BaseModel
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); $mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first(); $mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first(); $mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
$fields->put('MariaDB', [ $data = collect([]);
if ($mariadb_user) {
$data = $data->merge([
'User' => [ 'User' => [
'key' => data_get($mariadb_user, 'key'), 'key' => data_get($mariadb_user, 'key'),
'value' => data_get($mariadb_user, 'value'), 'value' => data_get($mariadb_user, 'value'),
'rules' => 'required', 'rules' => 'required',
], ],
]);
}
if ($mariadb_password) {
$data = $data->merge([
'Password' => [ 'Password' => [
'key' => data_get($mariadb_password, 'key'), 'key' => data_get($mariadb_password, 'key'),
'value' => data_get($mariadb_password, 'value'), 'value' => data_get($mariadb_password, 'value'),
'rules' => 'required', 'rules' => 'required',
'isPassword' => true, 'isPassword' => true,
], ],
]);
}
if ($mariadb_root_password) {
$data = $data->merge([
'Root Password' => [ 'Root Password' => [
'key' => data_get($mariadb_root_password, 'key'), 'key' => data_get($mariadb_root_password, 'key'),
'value' => data_get($mariadb_root_password, 'value'), 'value' => data_get($mariadb_root_password, 'value'),
'rules' => 'required', 'rules' => 'required',
'isPassword' => true, 'isPassword' => true,
], ],
]);
}
if ($mariadb_db_name) {
$data = $data->merge([
'Database Name' => [ 'Database Name' => [
'key' => data_get($mariadb_db_name, 'key'), 'key' => data_get($mariadb_db_name, 'key'),
'value' => data_get($mariadb_db_name, 'value'), 'value' => data_get($mariadb_db_name, 'value'),
'rules' => data_get($mariadb_db_name, 'value') && 'required', 'rules' => 'required',
], ],
]); ]);
}
$fields->put('MariaDB', $data->toArray());
break; break;
} }
} }
@@ -753,6 +797,16 @@ class Service extends BaseModel
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true)); $serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
} }
} }
if ($this->server->isDrainLogActivated()) {
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

@@ -36,7 +36,7 @@ class ServiceDatabase extends BaseModel
{ {
$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

@@ -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

@@ -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

@@ -4,7 +4,7 @@ use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
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 +14,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');

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;
} }
@@ -153,7 +151,6 @@ function generateServiceSpecificFqdns($service, $forTraefik = false)
case $type->contains('minio'): case $type->contains('minio'):
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first(); $MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first(); $MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
return $payload;
if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) { if (is_null($MINIO_BROWSER_REDIRECT_URL) || is_null($MINIO_SERVER_URL)) {
return $payload; return $payload;
} }

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

@@ -191,7 +191,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 +213,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

@@ -510,6 +510,17 @@ function generateDeployWebhook($resource)
$url = $api . $endpoint . "?uuid=$uuid&force=false"; $url = $api . $endpoint . "?uuid=$uuid&force=false";
return $url; return $url;
} }
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) 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

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

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.128'; return '4.0.0-beta.140';

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');
});
}
};

24
package-lock.json generated
View File

@@ -16,7 +16,7 @@
"axios": "1.5.1", "axios": "1.5.1",
"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.4.11",
"vue": "3.3.4" "vue": "3.3.4"
} }
@@ -1051,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",
@@ -1283,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"
} }
@@ -1808,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",

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>

View File

@@ -0,0 +1,9 @@
<x-emails.layout>
Your server ({{ $name }}) has high disk usage ({{ $disk_usage }}% used). Threshold is {{ $threshold }}%.
Please cleanup your disk to prevent data-loss. Here are some [tips](https://coolify.io/docs/automated-cleanup).
(You can change the threshold in the Server Settings menu.)
</x-emails.layout>

View File

@@ -69,10 +69,12 @@
<div class="flex flex-col gap-2 xl:flex-row"> <div class="flex flex-col gap-2 xl:flex-row">
<x-forms.input placeholder="/" id="application.base_directory" label="Base Directory" <x-forms.input placeholder="/" id="application.base_directory" label="Base Directory"
helper="Directory to use as root. Useful for monorepos." /> helper="Directory to use as root. Useful for monorepos." />
@if ($application->build_pack === 'dockerfile') @if ($application->build_pack === 'dockerfile' && !$application->dockerfile)
<x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location" <x-forms.input placeholder="/Dockerfile" id="application.dockerfile_location"
label="Dockerfile Location" label="Dockerfile Location"
helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" /> helper="It is calculated together with the Base Directory: {{ Str::start($application->base_directory . $application->dockerfile_location, '/') }}" />
@endif
@if ($application->build_pack === 'dockerfile')
<x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target" <x-forms.input id="application.dockerfile_target_build" label="Docker Build Stage Target"
helper="Useful if you have multi-staged dockerfile." /> helper="Useful if you have multi-staged dockerfile." />
@endif @endif

View File

@@ -1,10 +1,41 @@
<div> <div class="flex flex-col gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h2>Webhooks</h2> <h2>Webhooks</h2>
<x-helper <x-helper
helper="For more details goto our <a class='text-white underline' href='https://coolify.io/docs/api-endpoints' target='_blank'>docs</a>." /> helper="For more details goto our <a class='text-white underline' href='https://coolify.io/docs/api-endpoints' target='_blank'>docs</a>." />
</div> </div>
<div> <div>
<x-forms.input readonly label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input> <x-forms.input readonly
helper="See details in our <a target='_blank' class='text-white underline' href='https://coolify.io/docs/api-authentication'>documentation</a>."
label="Deploy Webhook (auth required)" id="deploywebhook"></x-forms.input>
</div>
<div>
<h3>Manual Git Webhooks</h3>
@if ($githubManualWebhook && $gitlabManualWebhook)
<form wire:submit.prevent='saveSecret' class="flex flex-col gap-2">
<div class="flex items-end gap-2">
<x-forms.input helper="Content Type in GitHub configuration could be json or form-urlencoded."
readonly label="GitHub" id="githubManualWebhook"></x-forms.input>
<x-forms.input type="password"
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitHub."
label="GitHub Webhook Secret" id="resource.manual_webhook_secret_github"></x-forms.input>
</div>
<a target="_blank" class="flex hover:no-underline" href="{{ $resource?->gitWebhook }}">
<x-forms.button>Webhook Configuration on GitHub
<x-external-link />
</x-forms.button>
</a>
<div class="flex gap-2">
<x-forms.input readonly label="GitLab" id="gitlabManualWebhook"></x-forms.input>
<x-forms.input type="password"
helper="Need to set a secret to be able to use this webhook. It should match with the secret in GitLab."
label="GitLab Webhook Secret" id="resource.manual_webhook_secret_gitlab"></x-forms.input>
</div>
<x-forms.button type="submit">Save</x-forms.button>
</form>
@else
You are using an official Git App. You do not need manual webhooks.
@endif
</div> </div>
</div> </div>

View File

@@ -49,11 +49,13 @@
<x-forms.input type="number" id="server.port" label="Port" required /> <x-forms.input type="number" id="server.port" label="Port" required />
</div> </div>
</div> </div>
@if (!$server->isLocalhost())
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave <x-forms.checkbox instantSave
helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>" helper="If you are using Cloudflare Tunnels, enable this. It will proxy all ssh requests to your server through Cloudflare.<span class='text-warning'>Coolify does not install/setup Cloudflare (cloudflared) on your server.</span>"
id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" /> id="server.settings.is_cloudflare_tunnel" label="Cloudflare Tunnel" />
</div> </div>
@endif
</div> </div>
@if ($server->isFunctional()) @if ($server->isFunctional())

View File

@@ -0,0 +1,66 @@
<div>
<x-server.navbar :server="$server" :parameters="$parameters" />
<h2>Log Drains</h2>
<div class="pb-4">Sends resource logs to external services.</div>
<div class="flex flex-col gap-4 pt-4">
<div class="p-4 border border-coolgray-500">
<form wire:submit.prevent='submit("newrelic")' class="flex flex-col">
<h3>New Relic</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("newrelic")'
id="server.settings.is_logdrain_newrelic_enabled" label="Enabled" />
</div>
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_newrelic_license_key"
label="License Key" />
<x-forms.input required id="server.settings.logdrain_newrelic_base_uri"
placeholder="https://log-api.eu.newrelic.com/log/v1" label="Endpoint (EU / US)" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
{{-- <h3>Highlight.io</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("highlight")'
id="server.settings.is_logdrain_highlight_enabled" label="Enabled" />
</div>
<form wire:submit.prevent='submit("highlight")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_highlight_project_id"
label="Project Id" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form> --}}
<h3>Axiom</h3>
<div class="w-32">
<x-forms.checkbox instantSave='instantSave("axiom")' id="server.settings.is_logdrain_axiom_enabled"
label="Enabled" />
</div>
<form wire:submit.prevent='submit("axiom")' class="flex flex-col">
<div class="flex flex-col gap-4">
<div class="flex flex-col w-full gap-2 xl:flex-row">
<x-forms.input type="password" required id="server.settings.logdrain_axiom_api_key"
label="API Key" />
<x-forms.input required id="server.settings.logdrain_axiom_dataset_name" label="Dataset Name" />
</div>
</div>
<div class="flex justify-end gap-4 pt-6">
<x-forms.button type="submit">
Save
</x-forms.button>
</div>
</form>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<div> <div>
<h1>Create a new S3 Storage</h1> <h1>Create a new S3 Storage</h1>
<div class="subtitle ">S3 Storage used to save backups / files</div> <div class="subtitle">S3 Storage used to save backups / files</div>
<form class="flex flex-col gap-2" wire:submit.prevent='submit'> <form class="flex flex-col gap-2" wire:submit.prevent='submit'>
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input label="Name" id="name" /> <x-forms.input label="Name" id="name" />

View File

@@ -16,6 +16,7 @@ use App\Http\Livewire\Security\ApiTokens;
use App\Http\Livewire\Server\All; use App\Http\Livewire\Server\All;
use App\Http\Livewire\Server\Create; use App\Http\Livewire\Server\Create;
use App\Http\Livewire\Server\Destination\Show as DestinationShow; use App\Http\Livewire\Server\Destination\Show as DestinationShow;
use App\Http\Livewire\Server\LogDrains;
use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow; use App\Http\Livewire\Server\PrivateKey\Show as PrivateKeyShow;
use App\Http\Livewire\Server\Proxy\Show as ProxyShow; use App\Http\Livewire\Server\Proxy\Show as ProxyShow;
use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs; use App\Http\Livewire\Server\Proxy\Logs as ProxyLogs;
@@ -130,6 +131,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('/server/{server_uuid}/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); Route::get('/server/{server_uuid}/proxy/logs', ProxyLogs::class)->name('server.proxy.logs');
Route::get('/server/{server_uuid}/private-key', PrivateKeyShow::class)->name('server.private-key'); Route::get('/server/{server_uuid}/private-key', PrivateKeyShow::class)->name('server.private-key');
Route::get('/server/{server_uuid}/destinations', DestinationShow::class)->name('server.destinations'); Route::get('/server/{server_uuid}/destinations', DestinationShow::class)->name('server.destinations');
Route::get('/server/{server_uuid}/log-drains', LogDrains::class)->name('server.log-drains');
}); });

View File

@@ -63,6 +63,244 @@ Route::get('/source/github/install', function () {
return handleError($e); return handleError($e);
} }
}); });
Route::post('/source/gitlab/events/manual', function () {
try {
$payload = request()->collect();
$headers = request()->headers->all();
ray($payload, $headers);
$x_gitlab_token = data_get($headers, 'x-gitlab-token.0');
$x_gitlab_event = data_get($payload, 'object_kind');
if ($x_gitlab_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'project.path_with_namespace');
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/');
}
if (!$branch) {
return response('Nothing to do. No branch found in the request.');
}
ray('Manual Webhook GitLab Push Event with branch: ' . $branch);
}
if ($x_gitlab_event === 'merge_request') {
$action = data_get($payload, 'object_attributes.action');
ray($action);
$branch = data_get($payload, 'object_attributes.source_branch');
$base_branch = data_get($payload, 'object_attributes.target_branch');
$full_name = data_get($payload, 'project.path_with_namespace');
$pull_request_id = data_get($payload, 'object_attributes.iid');
$pull_request_html_url = data_get($payload, 'object_attributes.url');
if (!$branch) {
return response('Nothing to do. No branch found in the request.');
}
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
if ($x_gitlab_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
}
}
if ($x_gitlab_event === 'merge_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
foreach ($applications as $application) {
$webhook_secret = data_get($application, 'manual_webhook_secret_gitlab');
if ($webhook_secret !== $x_gitlab_token) {
ray('Invalid signature');
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_gitlab_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application_id: $application->id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
} else {
ray('Deployments disabled for ' . $application->name);
}
}
if ($x_gitlab_event === 'merge_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened' || $action === 'reopen' || $action === 'update') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'gitlab',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application_id: $application->id,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'gitlab'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.');
} else {
ray('Preview deployments disabled for ' . $application->name);
return response('Nothing to do. Preview Deployments disabled.');
}
}
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
return response('Preview Deployment closed.');
}
return response('Nothing to do. No Preview Deployment found');
}
}
}
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
}
});
Route::post('/source/github/events/manual', function () {
try {
$x_github_event = Str::lower(request()->header('X-GitHub-Event'));
$x_hub_signature_256 = Str::after(request()->header('X-Hub-Signature-256'), 'sha256=');
$content_type = request()->header('Content-Type');
$payload = request()->collect();
if ($x_github_event === 'ping') {
// Just pong
return response('pong');
}
if ($content_type !== 'application/json') {
$payload = json_decode(data_get($payload, 'payload'), true);
}
if ($x_github_event === 'push') {
$branch = data_get($payload, 'ref');
$full_name = data_get($payload, 'repository.full_name');
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
$branch = Str::after($branch, 'refs/heads/');
}
ray('Manual Webhook GitHub Push Event with branch: ' . $branch);
}
if ($x_github_event === 'pull_request') {
$action = data_get($payload, 'action');
$full_name = data_get($payload, 'repository.full_name');
$pull_request_id = data_get($payload, 'number');
$pull_request_html_url = data_get($payload, 'pull_request.html_url');
$branch = data_get($payload, 'pull_request.head.ref');
$base_branch = data_get($payload, 'pull_request.base.ref');
ray('Webhook GitHub Pull Request Event with branch: ' . $branch . ' and base branch: ' . $base_branch . ' and pull request id: ' . $pull_request_id);
}
if (!$branch) {
return response('Nothing to do. No branch found in the request.');
}
$applications = Application::where('git_repository', 'like', "%$full_name%");
if ($x_github_event === 'push') {
$applications = $applications->where('git_branch', $branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with deploy key set, branch is '$branch' and Git Repository name has $full_name.");
}
}
if ($x_github_event === 'pull_request') {
$applications = $applications->where('git_branch', $base_branch)->get();
if ($applications->isEmpty()) {
return response("Nothing to do. No applications found with branch '$base_branch'.");
}
}
ray($applications);
foreach ($applications as $application) {
ray($application);
$webhook_secret = data_get($application, 'manual_webhook_secret_github');
ray($webhook_secret);
$hmac = hash_hmac('sha256', request()->getContent(), $webhook_secret);
ray($hmac, $x_hub_signature_256);
if (!hash_equals($x_hub_signature_256, $hmac)) {
ray('Invalid signature');
continue;
}
$isFunctional = $application->destination->server->isFunctional();
if (!$isFunctional) {
ray('Server is not functional: ' . $application->destination->server->name);
continue;
}
if ($x_github_event === 'push') {
if ($application->isDeployable()) {
ray('Deploying ' . $application->name . ' with branch ' . $branch);
$deployment_uuid = new Cuid2(7);
queue_application_deployment(
application_id: $application->id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true
);
} else {
ray('Deployments disabled for ' . $application->name);
}
}
if ($x_github_event === 'pull_request') {
if ($action === 'opened' || $action === 'synchronize' || $action === 'reopened') {
if ($application->isPRDeployable()) {
$deployment_uuid = new Cuid2(7);
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) {
ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id,
'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url,
]);
}
queue_application_deployment(
application_id: $application->id,
pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid,
force_rebuild: false,
is_webhook: true,
git_type: 'github'
);
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.');
} else {
ray('Preview deployments disabled for ' . $application->name);
return response('Nothing to do. Preview Deployments disabled.');
}
}
if ($action === 'closed') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) {
$found->delete();
$container_name = generateApplicationContainerName($application, $pull_request_id);
// ray('Stopping container: ' . $container_name);
instant_remote_process(["docker rm -f $container_name"], $application->destination->server);
return response('Preview Deployment closed.');
}
return response('Nothing to do. No Preview Deployment found');
}
}
}
} catch (Exception $e) {
ray($e->getMessage());
return handleError($e);
}
});
Route::post('/source/github/events', function () { Route::post('/source/github/events', function () {
try { try {
$id = null; $id = null;
@@ -150,6 +388,7 @@ Route::post('/source/github/events', function () {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if (!$found) { if (!$found) {
ApplicationPreview::create([ ApplicationPreview::create([
'git_type' => 'github',
'application_id' => $application->id, 'application_id' => $application->id,
'pull_request_id' => $pull_request_id, 'pull_request_id' => $pull_request_id,
'pull_request_html_url' => $pull_request_html_url, 'pull_request_html_url' => $pull_request_html_url,
@@ -160,7 +399,8 @@ Route::post('/source/github/events', function () {
pull_request_id: $pull_request_id, pull_request_id: $pull_request_id,
deployment_uuid: $deployment_uuid, deployment_uuid: $deployment_uuid,
force_rebuild: false, force_rebuild: false,
is_webhook: true is_webhook: true,
git_type: 'github'
); );
ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id); ray('Deploying preview for ' . $application->name . ' with branch ' . $branch . ' and base branch ' . $base_branch . ' and pull request id ' . $pull_request_id);
return response('Preview Deployment queued.'); return response('Preview Deployment queued.');
@@ -169,7 +409,7 @@ Route::post('/source/github/events', function () {
return response('Nothing to do. Preview Deployments disabled.'); return response('Nothing to do. Preview Deployments disabled.');
} }
} }
if ($action === 'closed') { if ($action === 'closed' || $action === 'close') {
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
if ($found) { if ($found) {
$found->delete(); $found->delete();

View File

@@ -4,7 +4,7 @@
"version": "3.12.36" "version": "3.12.36"
}, },
"v4": { "v4": {
"version": "4.0.0-beta.128" "version": "4.0.0-beta.140"
} }
} }
} }