Merge branch 'next' into feat/disable-default-redirect
This commit is contained in:
@@ -12,6 +12,7 @@ class GenerateConfig
|
||||
public function handle(Application $application, bool $is_json = false)
|
||||
{
|
||||
ray()->clearAll();
|
||||
|
||||
return $application->generateConfig(is_json: $is_json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ class StartRedis
|
||||
{
|
||||
$this->database = $database;
|
||||
|
||||
$startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
|
||||
$container_name = $this->database->uuid;
|
||||
$this->configuration_dir = database_configuration_dir().'/'.$container_name;
|
||||
|
||||
@@ -37,6 +35,8 @@ class StartRedis
|
||||
$environment_variables = $this->generate_environment_variables();
|
||||
$this->add_custom_redis();
|
||||
|
||||
$startCommand = $this->buildStartCommand();
|
||||
|
||||
$docker_compose = [
|
||||
'services' => [
|
||||
$container_name => [
|
||||
@@ -105,7 +105,6 @@ class StartRedis
|
||||
'target' => '/usr/local/etc/redis/redis.conf',
|
||||
'read_only' => true,
|
||||
];
|
||||
$docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
// Add custom docker run options
|
||||
@@ -160,12 +159,26 @@ class StartRedis
|
||||
private function generate_environment_variables()
|
||||
{
|
||||
$environment_variables = collect();
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
|
||||
if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) {
|
||||
$environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}");
|
||||
foreach ($this->database->runtime_environment_variables as $env) {
|
||||
if ($env->is_shared) {
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$this->database->update(['redis_password' => $env->real_value]);
|
||||
}
|
||||
|
||||
if ($env->key === 'REDIS_USERNAME') {
|
||||
$this->database->update(['redis_username' => $env->real_value]);
|
||||
}
|
||||
} else {
|
||||
if ($env->key === 'REDIS_PASSWORD') {
|
||||
$env->update(['value' => $this->database->redis_password]);
|
||||
} elseif ($env->key === 'REDIS_USERNAME') {
|
||||
$env->update(['value' => $this->database->redis_username]);
|
||||
}
|
||||
$environment_variables->push("$env->key=$env->real_value");
|
||||
}
|
||||
}
|
||||
|
||||
add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables);
|
||||
@@ -173,6 +186,27 @@ class StartRedis
|
||||
return $environment_variables->all();
|
||||
}
|
||||
|
||||
private function buildStartCommand(): string
|
||||
{
|
||||
$hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf);
|
||||
$redisConfPath = '/usr/local/etc/redis/redis.conf';
|
||||
|
||||
if ($hasRedisConf) {
|
||||
$confContent = $this->database->redis_conf;
|
||||
$hasRequirePass = str_contains($confContent, 'requirepass');
|
||||
|
||||
if ($hasRequirePass) {
|
||||
$command = "redis-server $redisConfPath";
|
||||
} else {
|
||||
$command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}";
|
||||
}
|
||||
} else {
|
||||
$command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes";
|
||||
}
|
||||
|
||||
return $command;
|
||||
}
|
||||
|
||||
private function add_custom_redis()
|
||||
{
|
||||
if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) {
|
||||
|
||||
@@ -651,31 +651,5 @@ class GetContainersStatus
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
if (! $this->server->proxySet() || $this->server->proxy->force_stop) {
|
||||
return;
|
||||
}
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
app/Actions/Server/DeleteServer.php
Normal file
17
app/Actions/Server/DeleteServer.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class DeleteServer
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server)
|
||||
{
|
||||
StopSentinel::run($server);
|
||||
$server->forceDelete();
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Actions\Server;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
@@ -10,32 +9,48 @@ class StartSentinel
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, $version = 'latest', bool $restart = false)
|
||||
public function handle(Server $server, $version = 'next', bool $restart = false)
|
||||
{
|
||||
if ($restart) {
|
||||
StopSentinel::run($server);
|
||||
}
|
||||
$metrics_history = $server->settings->metrics_history_days;
|
||||
$refresh_rate = $server->settings->metrics_refresh_rate_seconds;
|
||||
$token = $server->settings->sentinel_token;
|
||||
$fqdn = InstanceSettings::get()->fqdn;
|
||||
if (str($fqdn)->startsWith('http')) {
|
||||
throw new \Exception('You should use https to run Sentinel.');
|
||||
$metrics_history = data_get($server, 'settings.sentinel_metrics_history_days');
|
||||
$refresh_rate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds');
|
||||
$push_interval = data_get($server, 'settings.sentinel_push_interval_seconds');
|
||||
$token = data_get($server, 'settings.sentinel_token');
|
||||
$endpoint = data_get($server, 'settings.sentinel_custom_url');
|
||||
$mount_dir = '/data/coolify/sentinel';
|
||||
$image = "ghcr.io/coollabsio/sentinel:$version";
|
||||
if (! $endpoint) {
|
||||
throw new \Exception('You should set FQDN in Instance Settings.');
|
||||
}
|
||||
$environments = [
|
||||
'TOKEN' => $token,
|
||||
'ENDPOINT' => InstanceSettings::get()->fqdn,
|
||||
'COLLECTOR_ENABLED' => 'true',
|
||||
'PUSH_ENDPOINT' => $endpoint,
|
||||
'PUSH_INTERVAL_SECONDS' => $push_interval,
|
||||
'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false',
|
||||
'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history
|
||||
'COLLECTOR_RETENTION_PERIOD_DAYS' => $metrics_history,
|
||||
];
|
||||
$docker_environments = "-e \"" . implode("\" -e \"", array_map(fn($key, $value) => "$key=$value", array_keys($environments), $environments)) . "\"";
|
||||
ray($docker_environments);
|
||||
return true;
|
||||
// instant_remote_process([
|
||||
// "docker run --rm --pull always -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/sentinel:/app/sentinel --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version",
|
||||
// 'chown -R 9999:root /data/coolify/sentinel',
|
||||
// 'chmod -R 700 /data/coolify/sentinel',
|
||||
// ], $server, true);
|
||||
if (isDev()) {
|
||||
// data_set($environments, 'DEBUG', 'true');
|
||||
$mount_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel';
|
||||
// $image = 'sentinel';
|
||||
}
|
||||
$docker_environments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"';
|
||||
|
||||
$docker_command = "docker run -d $docker_environments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mount_dir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway $image";
|
||||
|
||||
instant_remote_process([
|
||||
'docker rm -f coolify-sentinel || true',
|
||||
"mkdir -p $mount_dir",
|
||||
$docker_command,
|
||||
"chown -R 9999:root $mount_dir",
|
||||
"chmod -R 700 $mount_dir",
|
||||
], $server);
|
||||
|
||||
$server->settings->is_sentinel_enabled = true;
|
||||
$server->settings->save();
|
||||
$server->sentinelHeartbeat();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ class StopSentinel
|
||||
public function handle(Server $server)
|
||||
{
|
||||
instant_remote_process(['docker rm -f coolify-sentinel'], $server, false);
|
||||
$server->sentinelHeartbeat(isReset: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
namespace App\Console;
|
||||
|
||||
use App\Jobs\CheckForUpdatesJob;
|
||||
use App\Jobs\CheckHelperImageJob;
|
||||
use App\Jobs\CleanupInstanceStuffsJob;
|
||||
use App\Jobs\CleanupStaleMultiplexedConnections;
|
||||
use App\Jobs\DatabaseBackupJob;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Jobs\PullTemplatesFromCDN;
|
||||
use App\Jobs\ScheduledTaskJob;
|
||||
use App\Jobs\ServerCheckJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Jobs\UpdateCoolifyJob;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use App\Models\ScheduledTask;
|
||||
@@ -20,6 +19,7 @@ use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class Kernel extends ConsoleKernel
|
||||
{
|
||||
@@ -38,13 +38,13 @@ class Kernel extends ConsoleKernel
|
||||
$schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer();
|
||||
// Server Jobs
|
||||
$this->check_scheduled_backups($schedule);
|
||||
// $this->check_resources($schedule);
|
||||
$this->check_resources($schedule);
|
||||
$this->check_scheduled_tasks($schedule);
|
||||
$schedule->command('uploads:clear')->everyTwoMinutes();
|
||||
|
||||
$schedule->command('telescope:prune')->daily();
|
||||
|
||||
$schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
$schedule->job(new CheckHelperImageJob)->everyFiveMinutes()->onOneServer();
|
||||
} else {
|
||||
// Instance Jobs
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
@@ -80,7 +80,7 @@ class Kernel extends ConsoleKernel
|
||||
})->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
|
||||
}
|
||||
}
|
||||
$schedule->job(new PullHelperImageJob)
|
||||
$schedule->job(new CheckHelperImageJob)
|
||||
->cron($settings->update_check_frequency)
|
||||
->timezone($settings->instance_timezone)
|
||||
->onOneServer();
|
||||
@@ -115,7 +115,10 @@ class Kernel extends ConsoleKernel
|
||||
$servers = $this->all_servers->where('ip', '!=', '1.2.3.4');
|
||||
}
|
||||
foreach ($servers as $server) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
$last_sentinel_update = $server->sentinel_updated_at;
|
||||
if (Carbon::parse($last_sentinel_update)->isBefore(now()->subMinutes(4))) {
|
||||
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
|
||||
}
|
||||
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
|
||||
$serverTimezone = $server->settings->server_timezone;
|
||||
if ($server->settings->force_docker_cleanup) {
|
||||
|
||||
@@ -1579,11 +1579,16 @@ class ApplicationsController extends Controller
|
||||
$request->offsetUnset('docker_compose_domains');
|
||||
}
|
||||
$instantDeploy = $request->instant_deploy;
|
||||
$isStatic = $request->is_static;
|
||||
$useBuildServer = $request->use_build_server;
|
||||
|
||||
$use_build_server = $request->use_build_server;
|
||||
if (isset($useBuildServer)) {
|
||||
$application->settings->is_build_server_enabled = $useBuildServer;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
if (isset($use_build_server)) {
|
||||
$application->settings->is_build_server_enabled = $use_build_server;
|
||||
if (isset($isStatic)) {
|
||||
$application->settings->is_static = $isStatic;
|
||||
$application->settings->save();
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class OtherController extends Controller
|
||||
#[OA\Get(
|
||||
summary: 'Healthcheck',
|
||||
description: 'Healthcheck endpoint.',
|
||||
path: '/healthcheck',
|
||||
path: '/health',
|
||||
operationId: 'healthcheck',
|
||||
responses: [
|
||||
new OA\Response(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
@@ -726,6 +727,7 @@ class ServersController extends Controller
|
||||
return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400);
|
||||
}
|
||||
$server->delete();
|
||||
DeleteServer::dispatch($server);
|
||||
|
||||
return response()->json(['message' => 'Server deleted.']);
|
||||
}
|
||||
|
||||
40
app/Jobs/CheckHelperImageJob.php
Normal file
40
app/Jobs/CheckHelperImageJob.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CheckHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -504,8 +504,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$network = $this->database->destination->network;
|
||||
}
|
||||
|
||||
$this->ensureHelperImageAvailable();
|
||||
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
if (isDev()) {
|
||||
@@ -538,35 +536,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureHelperImageAvailable(): void
|
||||
{
|
||||
$fullImageName = $this->getFullImageName();
|
||||
|
||||
$imageExists = $this->checkImageExists($fullImageName);
|
||||
|
||||
if (! $imageExists) {
|
||||
$this->pullHelperImage($fullImageName);
|
||||
}
|
||||
}
|
||||
|
||||
private function checkImageExists(string $fullImageName): bool
|
||||
{
|
||||
$result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false);
|
||||
|
||||
return trim($result) === 'exists';
|
||||
}
|
||||
|
||||
private function pullHelperImage(string $fullImageName): void
|
||||
{
|
||||
try {
|
||||
instant_remote_process(["docker pull {$fullImageName}"], $this->server);
|
||||
} catch (\Exception $e) {
|
||||
$errorMessage = 'Failed to pull helper image: '.$e->getMessage();
|
||||
$this->add_to_backup_output($errorMessage);
|
||||
throw new \RuntimeException($errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private function getFullImageName(): string
|
||||
{
|
||||
$settings = instanceSettings();
|
||||
|
||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
@@ -17,28 +16,15 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
public $timeout = 1000;
|
||||
|
||||
public function __construct() {}
|
||||
public function __construct(public Server $server) {}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
try {
|
||||
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
|
||||
if ($response->successful()) {
|
||||
$versions = $response->json();
|
||||
$settings = instanceSettings();
|
||||
$latest_version = data_get($versions, 'coolify.helper.version');
|
||||
$current_version = $settings->helper_version;
|
||||
if (version_compare($latest_version, $current_version, '>')) {
|
||||
// New version available
|
||||
// $helperImage = config('coolify.helper_image');
|
||||
// instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server);
|
||||
$settings->update(['helper_version' => $latest_version]);
|
||||
}
|
||||
}
|
||||
|
||||
$helperImage = config('coolify.helper_image');
|
||||
$latest_version = instanceSettings()->helper_version;
|
||||
instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false);
|
||||
} catch (\Throwable $e) {
|
||||
send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage());
|
||||
ray($e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,23 @@
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Actions\Proxy\CheckProxy;
|
||||
use App\Actions\Proxy\StartProxy;
|
||||
use App\Actions\Server\InstallLogDrain;
|
||||
use App\Actions\Shared\ComplexStatusCheck;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PushServerUpdateJob implements ShouldQueue
|
||||
{
|
||||
@@ -20,7 +26,45 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 60;
|
||||
public $timeout = 30;
|
||||
|
||||
public Collection $containers;
|
||||
|
||||
public Collection $applications;
|
||||
|
||||
public Collection $previews;
|
||||
|
||||
public Collection $databases;
|
||||
|
||||
public Collection $services;
|
||||
|
||||
public Collection $allApplicationIds;
|
||||
|
||||
public Collection $allDatabaseUuids;
|
||||
|
||||
public Collection $allTcpProxyUuids;
|
||||
|
||||
public Collection $allServiceApplicationIds;
|
||||
|
||||
public Collection $allApplicationPreviewsIds;
|
||||
|
||||
public Collection $allServiceDatabaseIds;
|
||||
|
||||
public Collection $allApplicationsWithAdditionalServers;
|
||||
|
||||
public Collection $foundApplicationIds;
|
||||
|
||||
public Collection $foundDatabaseUuids;
|
||||
|
||||
public Collection $foundServiceApplicationIds;
|
||||
|
||||
public Collection $foundServiceDatabaseIds;
|
||||
|
||||
public Collection $foundApplicationPreviewsIds;
|
||||
|
||||
public bool $foundProxy = false;
|
||||
|
||||
public bool $foundLogDrainContainer = false;
|
||||
|
||||
public function backoff(): int
|
||||
{
|
||||
@@ -29,108 +73,335 @@ class PushServerUpdateJob implements ShouldQueue
|
||||
|
||||
public function __construct(public Server $server, public $data)
|
||||
{
|
||||
// TODO: Handle multiple servers
|
||||
// TODO: Handle Preview deployments
|
||||
// TODO: Handle DB TCP proxies
|
||||
// TODO: Handle DBs
|
||||
// TODO: Handle services
|
||||
// TODO: Handle proxies
|
||||
$this->containers = collect();
|
||||
$this->foundApplicationIds = collect();
|
||||
$this->foundDatabaseUuids = collect();
|
||||
$this->foundServiceApplicationIds = collect();
|
||||
$this->foundApplicationPreviewsIds = collect();
|
||||
$this->foundServiceDatabaseIds = collect();
|
||||
$this->allApplicationIds = collect();
|
||||
$this->allDatabaseUuids = collect();
|
||||
$this->allTcpProxyUuids = collect();
|
||||
$this->allServiceApplicationIds = collect();
|
||||
$this->allServiceDatabaseIds = collect();
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
if (! $this->data) {
|
||||
throw new \Exception('No data provided');
|
||||
}
|
||||
$data = collect($this->data);
|
||||
$containers = collect(data_get($data, 'containers'));
|
||||
if ($containers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$foundApplicationIds = collect();
|
||||
$foundServiceIds = collect();
|
||||
$foundProxy = false;
|
||||
foreach ($containers as $container) {
|
||||
$containerStatus = data_get($container, 'state', 'exited');
|
||||
$containerHealth = data_get($container, 'health_status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$labels = collect(data_get($container, 'labels'));
|
||||
$coolify_managed = $labels->has('coolify.managed');
|
||||
if ($coolify_managed) {
|
||||
if ($labels->has('coolify.applicationId')) {
|
||||
$applicationId = $labels->get('coolify.applicationId');
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
|
||||
$foundApplicationIds->push($applicationId);
|
||||
try {
|
||||
$this->updateApplicationStatus($applicationId, $pullRequestId, $containerStatus);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
} elseif ($labels->has('coolify.serviceId')) {
|
||||
$serviceId = $labels->get('coolify.serviceId');
|
||||
$foundServiceIds->push($serviceId);
|
||||
Log::info("Service: $serviceId, $containerStatus");
|
||||
} else {
|
||||
try {
|
||||
if (! $this->data) {
|
||||
throw new \Exception('No data provided');
|
||||
}
|
||||
$data = collect($this->data);
|
||||
|
||||
$this->serverStatus();
|
||||
|
||||
$this->server->sentinelHeartbeat();
|
||||
|
||||
$this->containers = collect(data_get($data, 'containers'));
|
||||
if ($this->containers->isEmpty()) {
|
||||
return;
|
||||
}
|
||||
$this->applications = $this->server->applications();
|
||||
$this->databases = $this->server->databases();
|
||||
$this->previews = $this->server->previews();
|
||||
$this->services = $this->server->services()->get();
|
||||
$this->allApplicationIds = $this->applications->filter(function ($application) {
|
||||
return $application->additional_servers->count() === 0;
|
||||
})->pluck('id');
|
||||
$this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) {
|
||||
return $application->additional_servers->count() > 0;
|
||||
});
|
||||
$this->allApplicationPreviewsIds = $this->previews->pluck('id');
|
||||
$this->allDatabaseUuids = $this->databases->pluck('uuid');
|
||||
$this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid');
|
||||
$this->services->each(function ($service) {
|
||||
$service->applications()->pluck('id')->each(function ($applicationId) {
|
||||
$this->allServiceApplicationIds->push($applicationId);
|
||||
});
|
||||
$service->databases()->pluck('id')->each(function ($databaseId) {
|
||||
$this->allServiceDatabaseIds->push($databaseId);
|
||||
});
|
||||
});
|
||||
|
||||
ray('allServiceApplicationIds', ['allServiceApplicationIds' => $this->allServiceApplicationIds]);
|
||||
|
||||
foreach ($this->containers as $container) {
|
||||
$containerStatus = data_get($container, 'state', 'exited');
|
||||
$containerHealth = data_get($container, 'health_status', 'unhealthy');
|
||||
$containerStatus = "$containerStatus ($containerHealth)";
|
||||
$labels = collect(data_get($container, 'labels'));
|
||||
$coolify_managed = $labels->has('coolify.managed');
|
||||
if ($coolify_managed) {
|
||||
$name = data_get($container, 'name');
|
||||
$uuid = $labels->get('com.docker.compose.service');
|
||||
$type = $labels->get('coolify.type');
|
||||
if ($name === 'coolify-proxy') {
|
||||
$foundProxy = true;
|
||||
Log::info("Proxy: $uuid, $containerStatus");
|
||||
} elseif ($type === 'service') {
|
||||
Log::info("Service: $uuid, $containerStatus");
|
||||
if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) {
|
||||
$this->foundLogDrainContainer = true;
|
||||
}
|
||||
if ($labels->has('coolify.applicationId')) {
|
||||
$applicationId = $labels->get('coolify.applicationId');
|
||||
$pullRequestId = data_get($labels, 'coolify.pullRequestId', '0');
|
||||
try {
|
||||
if ($pullRequestId === '0') {
|
||||
if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationIds->push($applicationId);
|
||||
}
|
||||
$this->updateApplicationStatus($applicationId, $containerStatus);
|
||||
} else {
|
||||
if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) {
|
||||
$this->foundApplicationPreviewsIds->push($applicationId);
|
||||
}
|
||||
$this->updateApplicationPreviewStatus($applicationId, $containerStatus);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
ray()->error($e);
|
||||
}
|
||||
} elseif ($labels->has('coolify.serviceId')) {
|
||||
$serviceId = $labels->get('coolify.serviceId');
|
||||
$subType = $labels->get('coolify.service.subType');
|
||||
$subId = $labels->get('coolify.service.subId');
|
||||
if ($subType === 'application' && $this->isRunning($containerStatus)) {
|
||||
$this->foundServiceApplicationIds->push($subId);
|
||||
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
|
||||
} elseif ($subType === 'database' && $this->isRunning($containerStatus)) {
|
||||
$this->foundServiceDatabaseIds->push($subId);
|
||||
$this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus);
|
||||
}
|
||||
|
||||
} else {
|
||||
Log::info("Database: $uuid, $containerStatus");
|
||||
$uuid = $labels->get('com.docker.compose.service');
|
||||
$type = $labels->get('coolify.type');
|
||||
if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) {
|
||||
$this->foundProxy = true;
|
||||
} elseif ($type === 'service' && $this->isRunning($containerStatus)) {
|
||||
ray("Service: $uuid, $containerStatus");
|
||||
} else {
|
||||
if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
$this->foundDatabaseUuids->push($uuid);
|
||||
if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) {
|
||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true);
|
||||
} else {
|
||||
$this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateProxyStatus();
|
||||
|
||||
$this->updateNotFoundApplicationStatus();
|
||||
$this->updateNotFoundApplicationPreviewStatus();
|
||||
$this->updateNotFoundDatabaseStatus();
|
||||
$this->updateNotFoundServiceStatus();
|
||||
|
||||
$this->updateAdditionalServersStatus();
|
||||
|
||||
$this->checkLogDrainContainer();
|
||||
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// If proxy is not found, start it
|
||||
if (! $foundProxy && $this->server->isProxyShouldRun()) {
|
||||
Log::info('Proxy not found, starting it');
|
||||
StartProxy::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
|
||||
// Update not found applications
|
||||
$allApplicationIds = $this->server->applications()->pluck('id');
|
||||
$notFoundApplicationIds = $allApplicationIds->diff($foundApplicationIds);
|
||||
private function serverStatus()
|
||||
{
|
||||
if ($this->server->isFunctional() === false) {
|
||||
throw new \Exception('Server is not ready.');
|
||||
}
|
||||
if ($this->server->status() === false) {
|
||||
throw new \Exception('Server is not reachable.');
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationStatus(string $applicationId, string $containerStatus)
|
||||
{
|
||||
$application = $this->applications->where('id', $applicationId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
}
|
||||
|
||||
private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus)
|
||||
{
|
||||
$application = $this->previews->where('id', $applicationId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray('Application preview updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
}
|
||||
|
||||
private function updateNotFoundApplicationStatus()
|
||||
{
|
||||
$notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds);
|
||||
if ($notFoundApplicationIds->isNotEmpty()) {
|
||||
Log::info('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
|
||||
$this->updateNotFoundApplications($notFoundApplicationIds);
|
||||
ray('Not found application ids', ['application_ids' => $notFoundApplicationIds]);
|
||||
$notFoundApplicationIds->each(function ($applicationId) {
|
||||
ray('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
$application = Application::find($applicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
ray('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateApplicationStatus(string $applicationId, string $pullRequestId, string $containerStatus)
|
||||
private function updateNotFoundApplicationPreviewStatus()
|
||||
{
|
||||
if ($pullRequestId === '0') {
|
||||
$application = Application::find($applicationId);
|
||||
if (! $application) {
|
||||
return;
|
||||
$notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds);
|
||||
if ($notFoundApplicationPreviewsIds->isNotEmpty()) {
|
||||
ray('Not found application previews ids', ['application_previews_ids' => $notFoundApplicationPreviewsIds]);
|
||||
$notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) {
|
||||
ray('Updating application preview status', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
|
||||
$applicationPreview = ApplicationPreview::find($applicationPreviewId);
|
||||
if ($applicationPreview) {
|
||||
$applicationPreview->status = 'exited';
|
||||
$applicationPreview->save();
|
||||
ray('Application preview status updated', ['application_preview_id' => $applicationPreviewId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateProxyStatus()
|
||||
{
|
||||
// If proxy is not found, start it
|
||||
if ($this->server->isProxyShouldRun()) {
|
||||
if ($this->foundProxy === false) {
|
||||
try {
|
||||
if (CheckProxy::run($this->server)) {
|
||||
StartProxy::run($this->server, false);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
}
|
||||
} else {
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false)
|
||||
{
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if (! $database) {
|
||||
return;
|
||||
}
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => $containerStatus]);
|
||||
if ($this->isRunning($containerStatus) && $tcpProxy) {
|
||||
$tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) {
|
||||
return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running';
|
||||
})->first();
|
||||
if (! $tcpProxyContainerFound) {
|
||||
ray('Starting TCP proxy for database', ['database_uuid' => $databaseUuid]);
|
||||
StartDatabaseProxy::dispatch($database);
|
||||
} else {
|
||||
ray('TCP proxy for database found in containers', ['database_uuid' => $databaseUuid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundDatabaseStatus()
|
||||
{
|
||||
$notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids);
|
||||
if ($notFoundDatabaseUuids->isNotEmpty()) {
|
||||
ray('Not found database uuids', ['database_uuids' => $notFoundDatabaseUuids]);
|
||||
$notFoundDatabaseUuids->each(function ($databaseUuid) {
|
||||
ray('Updating database status', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
|
||||
$database = $this->databases->where('uuid', $databaseUuid)->first();
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
ray('Database status updated', ['database_uuid' => $databaseUuid, 'status' => 'exited']);
|
||||
ray('Database is public', ['database_uuid' => $databaseUuid, 'is_public' => $database->is_public]);
|
||||
if ($database->is_public) {
|
||||
ray('Stopping TCP proxy for database', ['database_uuid' => $databaseUuid]);
|
||||
StopDatabaseProxy::dispatch($database);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus)
|
||||
{
|
||||
$service = $this->services->where('id', $serviceId)->first();
|
||||
if (! $service) {
|
||||
return;
|
||||
}
|
||||
if ($subType === 'application') {
|
||||
$application = $service->applications()->where('id', $subId)->first();
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
Log::info('Application updated', ['application_id' => $applicationId, 'status' => $containerStatus]);
|
||||
ray('Service application updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
} elseif ($subType === 'database') {
|
||||
$database = $service->databases()->where('id', $subId)->first();
|
||||
$database->status = $containerStatus;
|
||||
$database->save();
|
||||
ray('Service database updated', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
} else {
|
||||
$application = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first();
|
||||
if (! $application) {
|
||||
return;
|
||||
}
|
||||
$application->status = $containerStatus;
|
||||
$application->save();
|
||||
ray()->warning('Unknown sub type', ['service_id' => $serviceId, 'sub_type' => $subType, 'sub_id' => $subId, 'status' => $containerStatus]);
|
||||
}
|
||||
}
|
||||
|
||||
private function updateNotFoundApplications(Collection $applicationIds)
|
||||
private function updateNotFoundServiceStatus()
|
||||
{
|
||||
$applicationIds->each(function ($applicationId) {
|
||||
Log::info('Updating application status', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
$application = Application::find($applicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
Log::info('Application status updated', ['application_id' => $applicationId, 'status' => 'exited']);
|
||||
}
|
||||
$notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds);
|
||||
$notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds);
|
||||
if ($notFoundServiceApplicationIds->isNotEmpty()) {
|
||||
ray('Not found service application ids', ['service_application_ids' => $notFoundServiceApplicationIds]);
|
||||
$notFoundServiceApplicationIds->each(function ($serviceApplicationId) {
|
||||
ray('Updating service application status', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
|
||||
$application = ServiceApplication::find($serviceApplicationId);
|
||||
if ($application) {
|
||||
$application->status = 'exited';
|
||||
$application->save();
|
||||
ray('Service application status updated', ['service_application_id' => $serviceApplicationId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($notFoundServiceDatabaseIds->isNotEmpty()) {
|
||||
ray('Not found service database ids', ['service_database_ids' => $notFoundServiceDatabaseIds]);
|
||||
$notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) {
|
||||
ray('Updating service database status', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
|
||||
$database = ServiceDatabase::find($serviceDatabaseId);
|
||||
if ($database) {
|
||||
$database->status = 'exited';
|
||||
$database->save();
|
||||
ray('Service database status updated', ['service_database_id' => $serviceDatabaseId, 'status' => 'exited']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function updateAdditionalServersStatus()
|
||||
{
|
||||
$this->allApplicationsWithAdditionalServers->each(function ($application) {
|
||||
ray('Updating additional servers status for application', ['application_id' => $application->id]);
|
||||
ComplexStatusCheck::run($application);
|
||||
});
|
||||
}
|
||||
|
||||
private function isRunning(string $containerStatus)
|
||||
{
|
||||
return str($containerStatus)->contains('running');
|
||||
}
|
||||
|
||||
private function checkLogDrainContainer()
|
||||
{
|
||||
if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) {
|
||||
InstallLogDrain::dispatch($this->server);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,32 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
if ($this->server->isLogDrainEnabled()) {
|
||||
$this->checkLogDrainContainer();
|
||||
}
|
||||
if ($this->server->proxySet() && ! $this->server->proxy->force_stop) {
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
@@ -387,31 +413,5 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
}
|
||||
// $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url));
|
||||
}
|
||||
|
||||
// Check if proxy is running
|
||||
$this->server->proxyType();
|
||||
$foundProxyContainer = $this->containers->filter(function ($value, $key) {
|
||||
if ($this->server->isSwarm()) {
|
||||
return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik';
|
||||
} else {
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
StartProxy::run($this->server, false);
|
||||
$this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
ray($e);
|
||||
}
|
||||
} else {
|
||||
$this->server->proxy->status = data_get($foundProxyContainer, 'State.Status');
|
||||
$this->server->save();
|
||||
$connectProxyToDockerNetworks = connectProxyToNetworks($this->server);
|
||||
instant_remote_process($connectProxyToDockerNetworks, $this->server, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class Show extends Component
|
||||
return ! $alreadyAddedNetworks->contains('network', $network['Name']);
|
||||
});
|
||||
if ($this->networks->count() === 0) {
|
||||
$this->dispatch('success', 'No new networks found.');
|
||||
$this->dispatch('success', 'No new destinations found on this server.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,7 +241,6 @@ class General extends Component
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedApplicationBuildPack()
|
||||
{
|
||||
if ($this->application->build_pack !== 'nixpacks') {
|
||||
@@ -314,7 +313,7 @@ class General extends Component
|
||||
public function set_redirect()
|
||||
{
|
||||
try {
|
||||
$has_www = collect($this->application->fqdns)->filter(fn($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
$has_www = collect($this->application->fqdns)->filter(fn ($fqdn) => str($fqdn)->contains('www.'))->count();
|
||||
if ($has_www === 0 && $this->application->redirect === 'www') {
|
||||
$this->dispatch('error', 'You want to redirect to www, but you do not have a www domain set.<br><br>Please add www to your domain list and as an A DNS record (if applicable).');
|
||||
|
||||
@@ -335,9 +334,15 @@ class General extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
$this->resetDefaultLabels();
|
||||
|
||||
if ($this->application->isDirty('redirect')) {
|
||||
@@ -403,17 +408,19 @@ class General extends Component
|
||||
}
|
||||
$this->application->custom_labels = base64_encode($this->customLabels);
|
||||
$this->application->save();
|
||||
$showToaster && $this->dispatch('success', 'Application settings updated!');
|
||||
$showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!');
|
||||
} catch (\Throwable $e) {
|
||||
$originalFqdn = $this->application->getOriginal('fqdn');
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadConfig()
|
||||
{
|
||||
$config = GenerateConfig::run($this->application, true);
|
||||
@@ -423,7 +430,7 @@ class General extends Component
|
||||
echo $config;
|
||||
}, $fileName, [
|
||||
'Content-Type' => 'application/json',
|
||||
'Content-Disposition' => 'attachment; filename=' . $fileName,
|
||||
'Content-Disposition' => 'attachment; filename='.$fileName,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,21 @@ use Livewire\Component;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
protected $listeners = ['refresh'];
|
||||
protected $listeners = [
|
||||
'envsUpdated' => 'refresh',
|
||||
'refresh',
|
||||
];
|
||||
|
||||
public Server $server;
|
||||
|
||||
public StandaloneRedis $database;
|
||||
|
||||
public string $redis_username;
|
||||
|
||||
public string $redis_password;
|
||||
|
||||
public string $redis_version;
|
||||
|
||||
public ?string $db_url = null;
|
||||
|
||||
public ?string $db_url_public = null;
|
||||
@@ -25,33 +34,33 @@ class General extends Component
|
||||
'database.name' => 'required',
|
||||
'database.description' => 'nullable',
|
||||
'database.redis_conf' => 'nullable',
|
||||
'database.redis_password' => 'required',
|
||||
'database.image' => 'required',
|
||||
'database.ports_mappings' => 'nullable',
|
||||
'database.is_public' => 'nullable|boolean',
|
||||
'database.public_port' => 'nullable|integer',
|
||||
'database.is_log_drain_enabled' => 'nullable|boolean',
|
||||
'database.custom_docker_run_options' => 'nullable',
|
||||
'redis_username' => 'required',
|
||||
'redis_password' => 'required',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'database.name' => 'Name',
|
||||
'database.description' => 'Description',
|
||||
'database.redis_conf' => 'Redis Configuration',
|
||||
'database.redis_password' => 'Redis Password',
|
||||
'database.image' => 'Image',
|
||||
'database.ports_mappings' => 'Port Mapping',
|
||||
'database.is_public' => 'Is Public',
|
||||
'database.public_port' => 'Public Port',
|
||||
'database.custom_docker_run_options' => 'Custom Docker Options',
|
||||
'redis_username' => 'Redis Username',
|
||||
'redis_password' => 'Redis Password',
|
||||
];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->server = data_get($this->database, 'destination.server');
|
||||
|
||||
$this->refreshView();
|
||||
}
|
||||
|
||||
public function instantSaveAdvanced()
|
||||
@@ -75,13 +84,24 @@ class General extends Component
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
if ($this->database->redis_conf === '') {
|
||||
$this->database->redis_conf = null;
|
||||
|
||||
if (version_compare($this->redis_version, '6.0', '>=')) {
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_USERNAME'],
|
||||
['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id]
|
||||
);
|
||||
}
|
||||
$this->database->runtime_environment_variables()->updateOrCreate(
|
||||
['key' => 'REDIS_PASSWORD'],
|
||||
['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id]
|
||||
);
|
||||
|
||||
$this->database->save();
|
||||
$this->dispatch('success', 'Database updated.');
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshEnvs');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,10 +139,25 @@ class General extends Component
|
||||
public function refresh(): void
|
||||
{
|
||||
$this->database->refresh();
|
||||
$this->refreshView();
|
||||
}
|
||||
|
||||
private function refreshView()
|
||||
{
|
||||
$this->db_url = $this->database->internal_db_url;
|
||||
$this->db_url_public = $this->database->external_db_url;
|
||||
$this->redis_version = $this->database->getRedisVersion();
|
||||
$this->redis_username = $this->database->redis_username;
|
||||
$this->redis_password = $this->database->redis_password;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.database.redis.general');
|
||||
}
|
||||
|
||||
public function isSharedVariable($name)
|
||||
{
|
||||
return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,22 @@ use Livewire\Component;
|
||||
|
||||
class DeleteEnvironment extends Component
|
||||
{
|
||||
public array $parameters;
|
||||
|
||||
public int $environment_id;
|
||||
|
||||
public bool $disabled = false;
|
||||
|
||||
public string $environmentName = '';
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
try {
|
||||
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
|
||||
$this->parameters = get_route_parameters();
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
@@ -30,7 +34,7 @@ class DeleteEnvironment extends Component
|
||||
if ($environment->isEmpty()) {
|
||||
$environment->delete();
|
||||
|
||||
return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', 'Environment has defined resources, please delete them first.');
|
||||
|
||||
@@ -18,7 +18,11 @@ class Index extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->private_keys = PrivateKey::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get();
|
||||
$this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) {
|
||||
$project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]);
|
||||
|
||||
return $project;
|
||||
});
|
||||
$this->servers = Server::ownedByCurrentTeam()->count();
|
||||
}
|
||||
|
||||
|
||||
@@ -317,6 +317,7 @@ class PublicGitRepository extends Component
|
||||
// $application->setConfig($config);
|
||||
// }
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_name' => $environment->name,
|
||||
|
||||
@@ -32,8 +32,11 @@ class Index extends Component
|
||||
|
||||
public $services = [];
|
||||
|
||||
public array $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
$project = currentTeam()->load(['projects'])->projects->where('uuid', request()->route('project_uuid'))->first();
|
||||
if (! $project) {
|
||||
return redirect()->route('dashboard');
|
||||
@@ -44,7 +47,6 @@ class Index extends Component
|
||||
}
|
||||
$this->project = $project;
|
||||
$this->environment = $environment;
|
||||
|
||||
$this->applications = $this->environment->applications->load(['tags']);
|
||||
$this->applications = $this->applications->map(function ($application) {
|
||||
if (data_get($application, 'environment.project.uuid')) {
|
||||
|
||||
@@ -21,6 +21,7 @@ class EditDomain extends Component
|
||||
{
|
||||
$this->application = ServiceApplication::find($this->applicationId);
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
@@ -28,9 +29,14 @@ class EditDomain extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
@@ -38,7 +44,7 @@ class EditDomain extends Component
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->application->service->parse();
|
||||
$this->dispatch('refresh');
|
||||
@@ -48,6 +54,7 @@ class EditDomain extends Component
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class Navbar extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted',
|
||||
"envsUpdated" => '$refresh',
|
||||
'envsUpdated' => '$refresh',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,11 +30,6 @@ class ServiceApplicationView extends Component
|
||||
'application.is_stripprefix_enabled' => 'nullable|boolean',
|
||||
];
|
||||
|
||||
public function updatedApplicationFqdn()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
$this->submit();
|
||||
@@ -82,10 +77,14 @@ class ServiceApplicationView extends Component
|
||||
$this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim();
|
||||
$this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) {
|
||||
Url::fromString($domain, ['http', 'https']);
|
||||
|
||||
return str($domain)->trim()->lower();
|
||||
});
|
||||
$this->application->fqdn = $this->application->fqdn->unique()->implode(',');
|
||||
|
||||
$warning = sslipDomainWarning($this->application->fqdn);
|
||||
if ($warning) {
|
||||
$this->dispatch('warning', __('warning.sslipdomain'));
|
||||
}
|
||||
check_domain_usage(resource: $this->application);
|
||||
$this->validate();
|
||||
$this->application->save();
|
||||
@@ -93,7 +92,7 @@ class ServiceApplicationView extends Component
|
||||
if (str($this->application->fqdn)->contains(',')) {
|
||||
$this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.<br><br>Only use multiple domains if you know what you are doing.');
|
||||
} else {
|
||||
$this->dispatch('success', 'Service saved.');
|
||||
! $warning && $this->dispatch('success', 'Service saved.');
|
||||
}
|
||||
$this->dispatch('generateDockerCompose');
|
||||
} catch (\Throwable $e) {
|
||||
@@ -101,6 +100,7 @@ class ServiceApplicationView extends Component
|
||||
if ($originalFqdn !== $this->application->fqdn) {
|
||||
$this->application->fqdn = $originalFqdn;
|
||||
}
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,13 +31,8 @@ class Metrics extends Component
|
||||
public function loadData()
|
||||
{
|
||||
try {
|
||||
$metrics = $this->resource->getMetrics($this->interval);
|
||||
$cpuMetrics = collect($metrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
$memoryMetrics = collect($metrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[2]];
|
||||
});
|
||||
$cpuMetrics = $this->resource->getCpuMetrics($this->interval);
|
||||
$memoryMetrics = $this->resource->getMemoryMetrics($this->interval);
|
||||
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
|
||||
'seriesData' => $cpuMetrics,
|
||||
]);
|
||||
|
||||
@@ -8,8 +8,11 @@ use Livewire\Component;
|
||||
class UploadConfig extends Component
|
||||
{
|
||||
public $config;
|
||||
|
||||
public $applicationId;
|
||||
public function mount() {
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (isDev()) {
|
||||
$this->config = '{
|
||||
"build_pack": "nixpacks",
|
||||
@@ -22,6 +25,7 @@ class UploadConfig extends Component
|
||||
}';
|
||||
}
|
||||
}
|
||||
|
||||
public function uploadConfig()
|
||||
{
|
||||
try {
|
||||
@@ -30,10 +34,12 @@ class UploadConfig extends Component
|
||||
$this->dispatch('success', 'Application settings updated');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.upload-config');
|
||||
|
||||
77
app/Livewire/Server/Advanced.php
Normal file
77
app/Livewire/Server/Advanced.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Advanced extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||
'server.settings.delete_unused_volumes' => 'boolean',
|
||||
'server.settings.delete_unused_networks' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
'server.settings.force_docker_cleanup' => 'Force Docker Cleanup',
|
||||
'server.settings.docker_cleanup_frequency' => 'Docker Cleanup Frequency',
|
||||
'server.settings.docker_cleanup_threshold' => 'Docker Cleanup Threshold',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCleanup()
|
||||
{
|
||||
try {
|
||||
DockerCleanupJob::dispatch($this->server, true);
|
||||
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
if (empty($frequency) || ! validate_cron_expression($frequency)) {
|
||||
$this->server->settings->docker_cleanup_frequency = '*/10 * * * *';
|
||||
throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.');
|
||||
}
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.advanced');
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@ class Charts extends Component
|
||||
try {
|
||||
$cpuMetrics = $this->server->getCpuMetrics($this->interval);
|
||||
$memoryMetrics = $this->server->getMemoryMetrics($this->interval);
|
||||
$cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
$memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
|
||||
return [$metric[0], $metric[1]];
|
||||
});
|
||||
// $cpuMetrics = collect($cpuMetrics)->map(function ($metric) {
|
||||
// return [$metric[0], $metric[1]];
|
||||
// });
|
||||
// $memoryMetrics = collect($memoryMetrics)->map(function ($metric) {
|
||||
// return [$metric[0], $metric[1]];
|
||||
// });
|
||||
$this->dispatch("refreshChartData-{$this->chartId}-cpu", [
|
||||
'seriesData' => $cpuMetrics,
|
||||
]);
|
||||
|
||||
44
app/Livewire/Server/CloudflareTunnels.php
Normal file
44
app/Livewire/Server/CloudflareTunnels.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudflareTunnels extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
protected $rules = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
];
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
$this->server->settings->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.server.cloudflare-tunnels');
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\DeleteServer;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -28,6 +29,7 @@ class Delete extends Component
|
||||
return;
|
||||
}
|
||||
$this->server->delete();
|
||||
DeleteServer::dispatch($this->server);
|
||||
|
||||
return redirect()->route('server.index');
|
||||
} catch (\Throwable $e) {
|
||||
|
||||
@@ -4,10 +4,7 @@ namespace App\Livewire\Server;
|
||||
|
||||
use App\Actions\Server\StartSentinel;
|
||||
use App\Actions\Server\StopSentinel;
|
||||
use App\Jobs\DockerCleanupJob;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Form extends Component
|
||||
@@ -47,25 +44,19 @@ class Form extends Component
|
||||
'server.ip' => 'required',
|
||||
'server.user' => 'required',
|
||||
'server.port' => 'required',
|
||||
'server.settings.is_cloudflare_tunnel' => 'required|boolean',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
'server.settings.is_reachable' => 'required',
|
||||
'server.settings.is_swarm_manager' => 'required|boolean',
|
||||
'server.settings.is_swarm_worker' => 'required|boolean',
|
||||
'server.settings.is_build_server' => 'required|boolean',
|
||||
'server.settings.concurrent_builds' => 'required|integer|min:1',
|
||||
'server.settings.dynamic_timeout' => 'required|integer|min:1',
|
||||
'server.settings.is_metrics_enabled' => 'required|boolean',
|
||||
'server.settings.sentinel_token' => 'required',
|
||||
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
|
||||
'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
|
||||
'wildcard_domain' => 'nullable|url',
|
||||
'server.settings.is_server_api_enabled' => 'required|boolean',
|
||||
'server.settings.sentinel_push_interval_seconds' => 'required|integer|min:10',
|
||||
'server.settings.sentinel_custom_url' => 'nullable|url',
|
||||
'server.settings.is_sentinel_enabled' => 'required|boolean',
|
||||
'server.settings.server_timezone' => 'required|string|timezone',
|
||||
'server.settings.force_docker_cleanup' => 'required|boolean',
|
||||
'server.settings.docker_cleanup_frequency' => 'required_if:server.settings.force_docker_cleanup,true|string',
|
||||
'server.settings.docker_cleanup_threshold' => 'required_if:server.settings.force_docker_cleanup,false|integer|min:1|max:100',
|
||||
'server.settings.delete_unused_volumes' => 'boolean',
|
||||
'server.settings.delete_unused_networks' => 'boolean',
|
||||
];
|
||||
|
||||
protected $validationAttributes = [
|
||||
@@ -74,21 +65,18 @@ class Form extends Component
|
||||
'server.ip' => 'IP address/Domain',
|
||||
'server.user' => 'User',
|
||||
'server.port' => 'Port',
|
||||
'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel',
|
||||
'server.settings.is_reachable' => 'Is reachable',
|
||||
'server.settings.is_swarm_manager' => 'Swarm Manager',
|
||||
'server.settings.is_swarm_worker' => 'Swarm Worker',
|
||||
'server.settings.is_build_server' => 'Build Server',
|
||||
'server.settings.concurrent_builds' => 'Concurrent Builds',
|
||||
'server.settings.dynamic_timeout' => 'Dynamic Timeout',
|
||||
'server.settings.is_metrics_enabled' => 'Metrics',
|
||||
'server.settings.sentinel_token' => 'Metrics Token',
|
||||
'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
|
||||
'server.settings.sentinel_metrics_history_days' => 'Metrics History',
|
||||
'server.settings.is_server_api_enabled' => 'Server API',
|
||||
'server.settings.sentinel_push_interval_seconds' => 'Push Interval',
|
||||
'server.settings.is_sentinel_enabled' => 'Server API',
|
||||
'server.settings.sentinel_custom_url' => 'Coolify URL',
|
||||
'server.settings.server_timezone' => 'Server Timezone',
|
||||
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
|
||||
'server.settings.delete_unused_networks' => 'Delete Unused Networks',
|
||||
];
|
||||
|
||||
public function mount(Server $server)
|
||||
@@ -96,20 +84,26 @@ class Form extends Component
|
||||
$this->server = $server;
|
||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
||||
$this->wildcard_domain = $this->server->settings->wildcard_domain;
|
||||
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
|
||||
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
|
||||
}
|
||||
public function regenerateSentinelToken() {
|
||||
|
||||
public function checkSyncStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->server->settings->refresh();
|
||||
}
|
||||
|
||||
public function regenerateSentinelToken()
|
||||
{
|
||||
try {
|
||||
$this->server->generateSentinelToken();
|
||||
$this->server->settings->generateSentinelToken();
|
||||
$this->server->settings->refresh();
|
||||
$this->dispatch('success', 'Metrics token regenerated.');
|
||||
$this->restartSentinel(notification: false);
|
||||
$this->dispatch('success', 'Token regenerated & Sentinel restarted.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updated($field)
|
||||
{
|
||||
if ($field === 'server.settings.docker_cleanup_frequency') {
|
||||
@@ -140,21 +134,35 @@ class Form extends Component
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
|
||||
public function checkPortForServerApi()
|
||||
public function updatedServerSettingsIsSentinelEnabled($value)
|
||||
{
|
||||
try {
|
||||
if ($this->server->settings->is_server_api_enabled === true) {
|
||||
$this->server->checkServerApi();
|
||||
$this->dispatch('success', 'Server API is reachable.');
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
if ($value === false) {
|
||||
StopSentinel::dispatch($this->server);
|
||||
$this->server->settings->is_metrics_enabled = false;
|
||||
$this->server->settings->save();
|
||||
$this->server->sentinelHeartbeat(isReset: true);
|
||||
} else {
|
||||
try {
|
||||
StartSentinel::run($this->server);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedServerSettingsIsMetricsEnabled()
|
||||
{
|
||||
$this->restartSentinel();
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->validate();
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->validateServer(false);
|
||||
|
||||
@@ -162,55 +170,27 @@ class Form extends Component
|
||||
$this->server->save();
|
||||
$this->dispatch('success', 'Server updated.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
if ($this->server->isSentinelEnabled()) {
|
||||
PullSentinelImageJob::dispatchSync($this->server);
|
||||
ray('Sentinel is enabled');
|
||||
if ($this->server->settings->isDirty('is_metrics_enabled')) {
|
||||
$this->dispatch('reloadWindow');
|
||||
}
|
||||
if ($this->server->settings->isDirty('is_server_api_enabled') && $this->server->settings->is_server_api_enabled === true) {
|
||||
ray('Starting sentinel');
|
||||
}
|
||||
} else {
|
||||
ray('Sentinel is not enabled');
|
||||
StopSentinel::dispatch($this->server);
|
||||
}
|
||||
$this->server->settings->save();
|
||||
// $this->checkPortForServerApi();
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->server->settings->refresh();
|
||||
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getPushData()
|
||||
public function restartSentinel($notification = true)
|
||||
{
|
||||
try {
|
||||
if (!isDev()) {
|
||||
throw new \Exception('This feature is only available in dev mode.');
|
||||
}
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $this->server->settings->sentinel_token,
|
||||
])->post('http://host.docker.internal:8888/api/push', [
|
||||
'data' => 'test',
|
||||
$this->validate();
|
||||
$this->validate([
|
||||
'server.settings.sentinel_custom_url' => 'required|url',
|
||||
]);
|
||||
if ($response->successful()) {
|
||||
$this->dispatch('success', 'Push data sent.');
|
||||
return;
|
||||
}
|
||||
$error = data_get($response->json(), 'error');
|
||||
throw new \Exception($error);
|
||||
|
||||
} catch(\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function restartSentinel()
|
||||
{
|
||||
try {
|
||||
$version = get_latest_sentinel_version();
|
||||
StartSentinel::run($this->server, $version, true);
|
||||
$this->dispatch('success', 'Sentinel restarted.');
|
||||
if ($notification) {
|
||||
$this->dispatch('success', 'Sentinel started.');
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -267,11 +247,11 @@ class Form extends Component
|
||||
}
|
||||
refresh_server_connection($this->server->privateKey);
|
||||
$this->server->settings->wildcard_domain = $this->wildcard_domain;
|
||||
if ($this->server->settings->force_docker_cleanup) {
|
||||
$this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
} else {
|
||||
$this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
}
|
||||
// if ($this->server->settings->force_docker_cleanup) {
|
||||
// $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency;
|
||||
// } else {
|
||||
// $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold;
|
||||
// }
|
||||
$currentTimezone = $this->server->settings->getOriginal('server_timezone');
|
||||
$newTimezone = $this->server->settings->server_timezone;
|
||||
if ($currentTimezone !== $newTimezone || $currentTimezone === '') {
|
||||
@@ -285,21 +265,4 @@ class Form extends Component
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
public function manualCleanup()
|
||||
{
|
||||
try {
|
||||
DockerCleanupJob::dispatch($this->server, true);
|
||||
$this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function manualCloudflareConfig()
|
||||
{
|
||||
$this->server->settings->is_cloudflare_tunnel = true;
|
||||
$this->server->settings->save();
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Server\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
class Modal extends Component
|
||||
{
|
||||
public Server $server;
|
||||
|
||||
public function proxyStatusUpdated()
|
||||
{
|
||||
$this->dispatch('proxyStatusUpdated');
|
||||
}
|
||||
}
|
||||
@@ -22,10 +22,7 @@ class Show extends Component
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ class Resources extends Component
|
||||
|
||||
public $parameters = [];
|
||||
|
||||
public Collection $unmanagedContainers;
|
||||
public Collection $containers;
|
||||
|
||||
public $activeTab = 'managed';
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
@@ -50,14 +52,29 @@ class Resources extends Component
|
||||
public function refreshStatus()
|
||||
{
|
||||
$this->server->refresh();
|
||||
$this->loadUnmanagedContainers();
|
||||
if ($this->activeTab === 'managed') {
|
||||
$this->loadManagedContainers();
|
||||
} else {
|
||||
$this->loadUnmanagedContainers();
|
||||
}
|
||||
$this->dispatch('success', 'Resource statuses refreshed.');
|
||||
}
|
||||
|
||||
public function loadManagedContainers()
|
||||
{
|
||||
try {
|
||||
$this->activeTab = 'managed';
|
||||
$this->containers = $this->server->refresh()->definedResources();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function loadUnmanagedContainers()
|
||||
{
|
||||
$this->activeTab = 'unmanaged';
|
||||
try {
|
||||
$this->unmanagedContainers = $this->server->loadUnmanagedContainers();
|
||||
$this->containers = $this->server->loadUnmanagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -65,13 +82,14 @@ class Resources extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->unmanagedContainers = collect();
|
||||
$this->containers = collect();
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->loadManagedContainers();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -10,20 +10,17 @@ class Show extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public ?Server $server = null;
|
||||
public Server $server;
|
||||
|
||||
public $parameters = [];
|
||||
public array $parameters;
|
||||
|
||||
protected $listeners = ['refreshServerShow'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
try {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first();
|
||||
if (is_null($this->server)) {
|
||||
return redirect()->route('server.index');
|
||||
}
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail();
|
||||
$this->parameters = get_route_parameters();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Livewire\Server;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -14,15 +13,29 @@ class ShowPrivateKey extends Component
|
||||
|
||||
public $parameters;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
|
||||
public function setPrivateKey($privateKeyId)
|
||||
{
|
||||
$originalPrivateKeyId = $this->server->getOriginal('private_key_id');
|
||||
try {
|
||||
$privateKey = PrivateKey::findOrFail($privateKeyId);
|
||||
$this->server->update(['private_key_id' => $privateKey->id]);
|
||||
$this->server->refresh();
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
$this->server->update(['private_key_id' => $privateKeyId]);
|
||||
['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection();
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Private key updated successfully.');
|
||||
} else {
|
||||
throw new \Exception('Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->server->update(['private_key_id' => $originalPrivateKeyId]);
|
||||
$this->server->validateConnection();
|
||||
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +46,15 @@ class ShowPrivateKey extends Component
|
||||
if ($uptime) {
|
||||
$this->dispatch('success', 'Server is reachable.');
|
||||
} else {
|
||||
ray($error);
|
||||
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
$this->dispatch('refreshServerShow');
|
||||
$this->server->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->parameters = get_route_parameters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,13 @@ class Index extends Component
|
||||
|
||||
public string $update_check_frequency;
|
||||
|
||||
public $timezones;
|
||||
|
||||
public bool $disable_two_step_confirmation;
|
||||
|
||||
protected string $dynamic_config_path = '/data/coolify/proxy/dynamic';
|
||||
|
||||
protected Server $server;
|
||||
public $timezones;
|
||||
|
||||
protected $rules = [
|
||||
'settings.fqdn' => 'nullable',
|
||||
@@ -39,6 +42,8 @@ class Index extends Component
|
||||
'settings.instance_name' => 'nullable',
|
||||
'settings.allowed_ips' => 'nullable',
|
||||
'settings.is_auto_update_enabled' => 'boolean',
|
||||
'settings.public_ipv4' => 'nullable',
|
||||
'settings.public_ipv6' => 'nullable',
|
||||
'auto_update_frequency' => 'string',
|
||||
'update_check_frequency' => 'string',
|
||||
'settings.instance_timezone' => 'required|string|timezone',
|
||||
@@ -52,16 +57,18 @@ class Index extends Component
|
||||
'settings.custom_dns_servers' => 'Custom DNS servers',
|
||||
'settings.allowed_ips' => 'Allowed IPs',
|
||||
'settings.is_auto_update_enabled' => 'Auto Update Enabled',
|
||||
'settings.public_ipv4' => 'IPv4',
|
||||
'settings.public_ipv6' => 'IPv6',
|
||||
'auto_update_frequency' => 'Auto Update Frequency',
|
||||
'update_check_frequency' => 'Update Check Frequency',
|
||||
'settings.instance_timezone' => 'Instance Timezone',
|
||||
];
|
||||
|
||||
|
||||
public function mount()
|
||||
{
|
||||
if (isInstanceAdmin()) {
|
||||
$this->settings = instanceSettings();
|
||||
loggy($this->settings);
|
||||
$this->do_not_track = $this->settings->do_not_track;
|
||||
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
|
||||
$this->is_registration_enabled = $this->settings->is_registration_enabled;
|
||||
@@ -70,6 +77,7 @@ class Index extends Component
|
||||
$this->auto_update_frequency = $this->settings->auto_update_frequency;
|
||||
$this->update_check_frequency = $this->settings->update_check_frequency;
|
||||
$this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray();
|
||||
$this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation;
|
||||
} else {
|
||||
return redirect()->route('dashboard');
|
||||
}
|
||||
@@ -84,6 +92,7 @@ class Index extends Component
|
||||
$this->settings->is_api_enabled = $this->is_api_enabled;
|
||||
$this->settings->auto_update_frequency = $this->auto_update_frequency;
|
||||
$this->settings->update_check_frequency = $this->update_check_frequency;
|
||||
$this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation;
|
||||
$this->settings->save();
|
||||
$this->dispatch('success', 'Settings updated!');
|
||||
}
|
||||
@@ -171,9 +180,16 @@ class Index extends Component
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.settings.index');
|
||||
}
|
||||
|
||||
public function toggleTwoStepConfirmation()
|
||||
{
|
||||
$this->settings->disable_two_step_confirmation = true;
|
||||
$this->settings->save();
|
||||
$this->disable_two_step_confirmation = true;
|
||||
$this->dispatch('success', 'Two step confirmation has been disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class Create extends Component
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = generate_random_name();
|
||||
$this->name = substr(generate_random_name(), 0, 34); // GitHub Apps names can only be 34 characters long
|
||||
}
|
||||
|
||||
public function createGitHubApp()
|
||||
|
||||
@@ -1400,13 +1400,21 @@ class Application extends BaseModel
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMetrics(int $mins = 5)
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false);
|
||||
if (isDev() && $server->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/cpu/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$metrics = $process->output();
|
||||
} else {
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
||||
}
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -1415,14 +1423,41 @@ class Application extends BaseModel
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = str($metrics)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($metrics)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line));
|
||||
$cpu_usage_percent = number_format($cpu_usage_percent, 2);
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage];
|
||||
});
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
if (isDev() && $server->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/container/{$container_name}/memory/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$metrics = $process->output();
|
||||
} else {
|
||||
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
||||
}
|
||||
if (str($metrics)->contains('error')) {
|
||||
$error = json_decode($metrics, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
if ($error == 'Unauthorized') {
|
||||
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$metrics = json_decode($metrics, true);
|
||||
$parsedCollection = collect($metrics)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['used']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
@@ -1459,7 +1494,9 @@ class Application extends BaseModel
|
||||
|
||||
return $config;
|
||||
}
|
||||
public function setConfig($config) {
|
||||
|
||||
public function setConfig($config)
|
||||
{
|
||||
|
||||
$config = $config;
|
||||
$validator = Validator::make(['config' => $config], [
|
||||
|
||||
@@ -74,6 +74,9 @@ class EnvironmentVariable extends Model
|
||||
'version' => config('version'),
|
||||
]);
|
||||
});
|
||||
static::saving(function (EnvironmentVariable $environmentVariable) {
|
||||
$environmentVariable->updateIsShared();
|
||||
});
|
||||
}
|
||||
|
||||
public function service()
|
||||
@@ -217,4 +220,11 @@ class EnvironmentVariable extends Model
|
||||
set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value,
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateIsShared(): void
|
||||
{
|
||||
$type = str($this->value)->after('{{')->before('.')->value;
|
||||
$isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}');
|
||||
$this->is_shared = $isShared;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\PullHelperImageJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@@ -24,6 +25,20 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
'sentinel_token' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updated(function ($settings) {
|
||||
if ($settings->isDirty('helper_version')) {
|
||||
Server::chunkById(100, function ($servers) {
|
||||
foreach ($servers as $server) {
|
||||
PullHelperImageJob::dispatch($server);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function fqdn(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
@@ -86,17 +101,4 @@ class InstanceSettings extends Model implements SendsEmail
|
||||
|
||||
return "[{$instanceName}]";
|
||||
}
|
||||
|
||||
public function helperVersion(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function ($value) {
|
||||
if (isDev()) {
|
||||
return 'latest';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ class ScheduledDatabaseBackup extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ use App\Enums\ProxyTypes;
|
||||
use App\Jobs\PullSentinelImageJob;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
@@ -43,7 +45,7 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use SchemalessAttributesTrait;
|
||||
use SchemalessAttributesTrait,SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
@@ -103,7 +105,8 @@ class Server extends BaseModel
|
||||
$server->proxy->redirect_enabled = true;
|
||||
}
|
||||
});
|
||||
static::deleting(function ($server) {
|
||||
|
||||
static::forceDeleting(function ($server) {
|
||||
$server->destinations()->each(function ($destination) {
|
||||
$destination->delete();
|
||||
});
|
||||
@@ -520,22 +523,20 @@ $schema://$host {
|
||||
Storage::disk('ssh-mux')->delete($this->muxFilename());
|
||||
}
|
||||
|
||||
public function generateSentinelToken()
|
||||
public function sentinelHeartbeat(bool $isReset = false)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->settings->sentinel_token = $encrypted;
|
||||
$this->settings->save();
|
||||
$this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now();
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $encrypted;
|
||||
public function isSentinelLive()
|
||||
{
|
||||
return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subMinutes(4));
|
||||
}
|
||||
|
||||
public function isSentinelEnabled()
|
||||
{
|
||||
return $this->isMetricsEnabled() || $this->isServerApiEnabled();
|
||||
return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer();
|
||||
}
|
||||
|
||||
public function isMetricsEnabled()
|
||||
@@ -545,7 +546,7 @@ $schema://$host {
|
||||
|
||||
public function isServerApiEnabled()
|
||||
{
|
||||
return $this->settings->is_server_api_enabled;
|
||||
return $this->settings->is_sentinel_enabled;
|
||||
}
|
||||
|
||||
public function checkServerApi()
|
||||
@@ -586,7 +587,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
if (isDev() && $this->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/cpu/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$cpu = $process->output();
|
||||
} else {
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
}
|
||||
if (str($cpu)->contains('error')) {
|
||||
$error = json_decode($cpu, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -595,17 +604,13 @@ $schema://$host {
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$cpu = str($cpu)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($cpu)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $cpu_usage_percent] = explode(',', trim($line));
|
||||
$cpu_usage_percent = number_format($cpu_usage_percent, 0);
|
||||
|
||||
return [(int) $time, (float) $cpu_usage_percent];
|
||||
});
|
||||
$cpu = json_decode($cpu, true);
|
||||
$parsedCollection = collect($cpu)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
return $parsedCollection;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,7 +618,15 @@ $schema://$host {
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
||||
if (isDev() && $this->id === 0) {
|
||||
$process = Process::run("curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://host.docker.internal:8888/api/memory/history?from=$from");
|
||||
if ($process->failed()) {
|
||||
throw new \Exception($process->errorOutput());
|
||||
}
|
||||
$memory = $process->output();
|
||||
} else {
|
||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
||||
}
|
||||
if (str($memory)->contains('error')) {
|
||||
$error = json_decode($memory, true);
|
||||
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
||||
@@ -622,14 +635,9 @@ $schema://$host {
|
||||
}
|
||||
throw new \Exception($error);
|
||||
}
|
||||
$memory = str($memory)->explode("\n")->skip(1)->all();
|
||||
$parsedCollection = collect($memory)->flatMap(function ($item) {
|
||||
return collect(explode("\n", trim($item)))->map(function ($line) {
|
||||
[$time, $used, $free, $usedPercent] = explode(',', trim($line));
|
||||
$usedPercent = number_format($usedPercent, 0);
|
||||
|
||||
return [(int) $time, (float) $usedPercent];
|
||||
});
|
||||
$memory = json_decode($memory, true);
|
||||
$parsedCollection = collect($memory)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['usedPercent']];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
@@ -1049,6 +1057,38 @@ $schema://$host {
|
||||
return data_get($this, 'settings.is_swarm_worker');
|
||||
}
|
||||
|
||||
public function status(): bool
|
||||
{
|
||||
['uptime' => $uptime] = $this->validateConnection(false);
|
||||
if ($uptime) {
|
||||
if ($this->unreachable_notification_sent === true) {
|
||||
$this->update(['unreachable_notification_sent' => false]);
|
||||
}
|
||||
} else {
|
||||
// $this->server->team?->notify(new Unreachable($this->server));
|
||||
foreach ($this->applications as $application) {
|
||||
$application->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->databases as $database) {
|
||||
$database->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($this->services as $service) {
|
||||
$apps = $service->applications()->get();
|
||||
$dbs = $service->databases()->get();
|
||||
foreach ($apps as $app) {
|
||||
$app->update(['status' => 'exited']);
|
||||
}
|
||||
foreach ($dbs as $db) {
|
||||
$db->update(['status' => 'exited']);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateConnection($isManualCheck = true)
|
||||
{
|
||||
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
|
||||
|
||||
@@ -24,7 +24,7 @@ use OpenApi\Attributes as OA;
|
||||
'is_logdrain_newrelic_enabled' => ['type' => 'boolean'],
|
||||
'is_metrics_enabled' => ['type' => 'boolean'],
|
||||
'is_reachable' => ['type' => 'boolean'],
|
||||
'is_server_api_enabled' => ['type' => 'boolean'],
|
||||
'is_sentinel_enabled' => ['type' => 'boolean'],
|
||||
'is_swarm_manager' => ['type' => 'boolean'],
|
||||
'is_swarm_worker' => ['type' => 'boolean'],
|
||||
'is_usable' => ['type' => 'boolean'],
|
||||
@@ -56,6 +56,63 @@ class ServerSetting extends Model
|
||||
'sentinel_token' => 'encrypted',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::creating(function ($setting) {
|
||||
try {
|
||||
if (str($setting->sentinel_token)->isEmpty()) {
|
||||
$setting->generateSentinelToken(save: false);
|
||||
}
|
||||
if (str($setting->sentinel_custom_url)->isEmpty()) {
|
||||
$url = $setting->generateSentinelUrl(save: false);
|
||||
if (str($url)->isEmpty()) {
|
||||
$setting->is_sentinel_enabled = false;
|
||||
} else {
|
||||
$setting->is_sentinel_enabled = true;
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
loggy('Error creating server setting: '.$e->getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function generateSentinelToken(bool $save = true)
|
||||
{
|
||||
$data = [
|
||||
'server_uuid' => $this->server->uuid,
|
||||
];
|
||||
$token = json_encode($data);
|
||||
$encrypted = encrypt($token);
|
||||
$this->sentinel_token = $encrypted;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $encrypted;
|
||||
}
|
||||
|
||||
public function generateSentinelUrl(bool $save = true)
|
||||
{
|
||||
$domain = null;
|
||||
$settings = InstanceSettings::get();
|
||||
if ($this->server->isLocalhost()) {
|
||||
$domain = 'http://host.docker.internal:8000';
|
||||
} elseif ($settings->fqdn) {
|
||||
$domain = $settings->fqdn;
|
||||
} elseif ($settings->ipv4) {
|
||||
$domain = $settings->ipv4.':8000';
|
||||
} elseif ($settings->ipv6) {
|
||||
$domain = $settings->ipv6.':8000';
|
||||
}
|
||||
$this->sentinel_custom_url = $domain;
|
||||
if ($save) {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
public function server()
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
|
||||
@@ -297,7 +297,7 @@ class Service extends BaseModel
|
||||
'key' => 'CP_DISABLE_HTTPS',
|
||||
'value' => data_get($disable_https, 'value'),
|
||||
'rules' => 'required',
|
||||
'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS",
|
||||
'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS',
|
||||
],
|
||||
]);
|
||||
}
|
||||
@@ -997,8 +997,8 @@ class Service extends BaseModel
|
||||
break;
|
||||
case $image->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
@@ -1326,9 +1326,9 @@ class Service extends BaseModel
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -210,7 +210,12 @@ class StandaloneRedis extends BaseModel
|
||||
protected function internalDbUrl(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0",
|
||||
get: function () {
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0";
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -219,7 +224,10 @@ class StandaloneRedis extends BaseModel
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
if ($this->is_public && $this->public_port) {
|
||||
return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
$redis_version = $this->getRedisVersion();
|
||||
$username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : '';
|
||||
|
||||
return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -227,6 +235,13 @@ class StandaloneRedis extends BaseModel
|
||||
);
|
||||
}
|
||||
|
||||
public function getRedisVersion()
|
||||
{
|
||||
$image_parts = explode(':', $this->image);
|
||||
|
||||
return $image_parts[1] ?? '0.0';
|
||||
}
|
||||
|
||||
public function environment()
|
||||
{
|
||||
return $this->belongsTo(Environment::class);
|
||||
@@ -295,4 +310,33 @@ class StandaloneRedis extends BaseModel
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function redisPassword(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first();
|
||||
if (! $password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $password->value;
|
||||
},
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
public function redisUsername(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: function () {
|
||||
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
|
||||
if (! $username) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $username->value;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user