sentinel updates

This commit is contained in:
Andras Bacsai
2024-10-14 12:07:37 +02:00
parent c137620b81
commit 1f193d465d
23 changed files with 293 additions and 103 deletions

View File

@@ -2,6 +2,7 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -16,11 +17,25 @@ class StartSentinel
} }
$metrics_history = $server->settings->metrics_history_days; $metrics_history = $server->settings->metrics_history_days;
$refresh_rate = $server->settings->metrics_refresh_rate_seconds; $refresh_rate = $server->settings->metrics_refresh_rate_seconds;
$token = $server->settings->metrics_token; $token = $server->settings->sentinel_token;
instant_remote_process([ $fqdn = InstanceSettings::get()->fqdn;
"docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --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", if (str($fqdn)->startsWith('http')) {
'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', throw new \Exception('You should use https to run Sentinel.');
'chmod -R 700 /data/coolify/metrics /data/coolify/logs', }
], $server, true); $environments = [
'TOKEN' => $token,
'ENDPOINT' => InstanceSettings::get()->fqdn,
'COLLECTOR_ENABLED' => 'true',
'COLLECTOR_REFRESH_RATE_SECONDS' => $refresh_rate,
'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);
} }
} }

View File

@@ -23,7 +23,7 @@ class ServersController extends Controller
return serializeApiResponse($settings); return serializeApiResponse($settings);
} }
$settings = $settings->makeHidden([ $settings = $settings->makeHidden([
'metrics_token', 'sentinel_token',
]); ]);
return serializeApiResponse($settings); return serializeApiResponse($settings);

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
class PushServerUpdateJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server, public $data) {}
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;
}
foreach ($containers as $container) {
$containerStatus = data_get($container, 'status', 'exited');
$containerHealth = data_get($container, 'health', 'unhealthy');
$containerStatus = "$containerStatus ($containerHealth)";
$labels = collect(data_get($container, 'labels'));
if ($labels->has('coolify.applicationId')) {
$applicationId = $labels->get('coolify.applicationId');
}
Log::info("$applicationId, $containerStatus");
}
}
}

View File

@@ -7,6 +7,7 @@ use App\Actions\Server\StopSentinel;
use App\Jobs\DockerCleanupJob; use App\Jobs\DockerCleanupJob;
use App\Jobs\PullSentinelImageJob; use App\Jobs\PullSentinelImageJob;
use App\Models\Server; use App\Models\Server;
use Illuminate\Support\Facades\Http;
use Livewire\Component; use Livewire\Component;
class Form extends Component class Form extends Component
@@ -54,9 +55,9 @@ class Form extends Component
'server.settings.concurrent_builds' => 'required|integer|min:1', 'server.settings.concurrent_builds' => 'required|integer|min:1',
'server.settings.dynamic_timeout' => 'required|integer|min:1', 'server.settings.dynamic_timeout' => 'required|integer|min:1',
'server.settings.is_metrics_enabled' => 'required|boolean', 'server.settings.is_metrics_enabled' => 'required|boolean',
'server.settings.metrics_token' => 'required', 'server.settings.sentinel_token' => 'required',
'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'required|integer|min:1',
'server.settings.metrics_history_days' => 'required|integer|min:1', 'server.settings.sentinel_metrics_history_days' => 'required|integer|min:1',
'wildcard_domain' => 'nullable|url', 'wildcard_domain' => 'nullable|url',
'server.settings.is_server_api_enabled' => 'required|boolean', 'server.settings.is_server_api_enabled' => 'required|boolean',
'server.settings.server_timezone' => 'required|string|timezone', 'server.settings.server_timezone' => 'required|string|timezone',
@@ -81,9 +82,9 @@ class Form extends Component
'server.settings.concurrent_builds' => 'Concurrent Builds', 'server.settings.concurrent_builds' => 'Concurrent Builds',
'server.settings.dynamic_timeout' => 'Dynamic Timeout', 'server.settings.dynamic_timeout' => 'Dynamic Timeout',
'server.settings.is_metrics_enabled' => 'Metrics', 'server.settings.is_metrics_enabled' => 'Metrics',
'server.settings.metrics_token' => 'Metrics Token', 'server.settings.sentinel_token' => 'Metrics Token',
'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', 'server.settings.sentinel_metrics_refresh_rate_seconds' => 'Metrics Interval',
'server.settings.metrics_history_days' => 'Metrics History', 'server.settings.sentinel_metrics_history_days' => 'Metrics History',
'server.settings.is_server_api_enabled' => 'Server API', 'server.settings.is_server_api_enabled' => 'Server API',
'server.settings.server_timezone' => 'Server Timezone', 'server.settings.server_timezone' => 'Server Timezone',
'server.settings.delete_unused_volumes' => 'Delete Unused Volumes', 'server.settings.delete_unused_volumes' => 'Delete Unused Volumes',
@@ -100,7 +101,15 @@ class Form extends Component
$this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes; $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes;
$this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks; $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks;
} }
public function regenerateSentinelToken() {
try {
$this->server->generateSentinelToken();
$this->server->settings->refresh();
$this->dispatch('success', 'Metrics token regenerated.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function updated($field) public function updated($field)
{ {
if ($field === 'server.settings.docker_cleanup_frequency') { if ($field === 'server.settings.docker_cleanup_frequency') {
@@ -174,6 +183,28 @@ class Form extends Component
} }
} }
public function getPushData()
{
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',
]);
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() public function restartSentinel()
{ {
try { try {

View File

@@ -1406,7 +1406,7 @@ class Application extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -21,6 +21,7 @@ class InstanceSettings extends Model implements SendsEmail
'is_auto_update_enabled' => 'boolean', 'is_auto_update_enabled' => 'boolean',
'auto_update_frequency' => 'string', 'auto_update_frequency' => 'string',
'update_check_frequency' => 'string', 'update_check_frequency' => 'string',
'sentinel_token' => 'encrypted',
]; ];
public function fqdn(): Attribute public function fqdn(): Attribute

View File

@@ -17,6 +17,8 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait; use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url; use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml; use Symfony\Component\Yaml\Yaml;
use Illuminate\Support\Str;
#[OA\Schema( #[OA\Schema(
description: 'Server model', description: 'Server model',
@@ -166,7 +168,7 @@ class Server extends BaseModel
public function setupDefault404Redirect() public function setupDefault404Redirect()
{ {
$dynamic_conf_path = $this->proxyPath().'/dynamic'; $dynamic_conf_path = $this->proxyPath() . '/dynamic';
$proxy_type = $this->proxyType(); $proxy_type = $this->proxyType();
$redirect_url = $this->proxy->redirect_url; $redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) { if ($proxy_type === ProxyTypes::TRAEFIK->value) {
@@ -180,8 +182,8 @@ class Server extends BaseModel
respond 404 respond 404
}'; }';
$conf = $conf =
"# This file is automatically generated by Coolify.\n". "# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n". "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf; $conf;
$base64 = base64_encode($conf); $base64 = base64_encode($conf);
instant_remote_process([ instant_remote_process([
@@ -243,8 +245,8 @@ respond 404
]; ];
$conf = Yaml::dump($dynamic_conf, 12, 2); $conf = Yaml::dump($dynamic_conf, 12, 2);
$conf = $conf =
"# This file is automatically generated by Coolify.\n". "# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n". "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf; $conf;
$base64 = base64_encode($conf); $base64 = base64_encode($conf);
@@ -253,8 +255,8 @@ respond 404
redir $redirect_url redir $redirect_url
}"; }";
$conf = $conf =
"# This file is automatically generated by Coolify.\n". "# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n". "# Do not edit it manually (only if you know what are you doing).\n\n" .
$conf; $conf;
$base64 = base64_encode($conf); $base64 = base64_encode($conf);
} }
@@ -272,7 +274,7 @@ respond 404
public function setupDynamicProxyConfiguration() public function setupDynamicProxyConfiguration()
{ {
$settings = instanceSettings(); $settings = instanceSettings();
$dynamic_config_path = $this->proxyPath().'/dynamic'; $dynamic_config_path = $this->proxyPath() . '/dynamic';
if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml"; $file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) { if (empty($settings->fqdn) || (isCloud() && $this->id !== 0) || ! $this->isLocalhost()) {
@@ -391,8 +393,8 @@ respond 404
} }
$yaml = Yaml::dump($traefik_dynamic_conf, 12, 2); $yaml = Yaml::dump($traefik_dynamic_conf, 12, 2);
$yaml = $yaml =
"# This file is automatically generated by Coolify.\n". "# This file is automatically generated by Coolify.\n" .
"# Do not edit it manually (only if you know what are you doing).\n\n". "# Do not edit it manually (only if you know what are you doing).\n\n" .
$yaml; $yaml;
$base64 = base64_encode($yaml); $base64 = base64_encode($yaml);
@@ -456,13 +458,13 @@ $schema://$host {
if (isDev()) { if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'; $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
} else { } else {
$proxy_path = $proxy_path.'/caddy'; $proxy_path = $proxy_path . '/caddy';
} }
} elseif ($proxyType === ProxyTypes::NGINX->value) { } elseif ($proxyType === ProxyTypes::NGINX->value) {
if (isDev()) { if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx'; $proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
} else { } else {
$proxy_path = $proxy_path.'/nginx'; $proxy_path = $proxy_path . '/nginx';
} }
} }
@@ -525,6 +527,17 @@ $schema://$host {
Storage::disk('ssh-mux')->delete($this->muxFilename()); Storage::disk('ssh-mux')->delete($this->muxFilename());
} }
public function generateSentinelToken()
{
$data = [
'server_uuid' => $this->uuid,
];
$token = json_encode($data);
$encrypted = encrypt($token);
$this->settings->sentinel_token = $encrypted;
$this->settings->save();
return $encrypted;
}
public function isSentinelEnabled() public function isSentinelEnabled()
{ {
return $this->isMetricsEnabled() || $this->isServerApiEnabled(); return $this->isMetricsEnabled() || $this->isServerApiEnabled();
@@ -555,7 +568,6 @@ $schema://$host {
ray($process->exitCode(), $process->output(), $process->errorOutput()); ray($process->exitCode(), $process->output(), $process->errorOutput());
throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); throw new \Exception("Server API is not reachable on http://{$server_ip}:12172");
} }
} }
} }
@@ -579,7 +591,7 @@ $schema://$host {
{ {
if ($this->isMetricsEnabled()) { if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); $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')) { if (str($cpu)->contains('error')) {
$error = json_decode($cpu, true); $error = json_decode($cpu, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');
@@ -606,7 +618,7 @@ $schema://$host {
{ {
if ($this->isMetricsEnabled()) { if ($this->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false); $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')) { if (str($memory)->contains('error')) {
$error = json_decode($memory, true); $error = json_decode($memory, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -35,9 +35,9 @@ use OpenApi\Attributes as OA;
'logdrain_highlight_project_id' => ['type' => 'string'], 'logdrain_highlight_project_id' => ['type' => 'string'],
'logdrain_newrelic_base_uri' => ['type' => 'string'], 'logdrain_newrelic_base_uri' => ['type' => 'string'],
'logdrain_newrelic_license_key' => ['type' => 'string'], 'logdrain_newrelic_license_key' => ['type' => 'string'],
'metrics_history_days' => ['type' => 'integer'], 'sentinel_metrics_history_days' => ['type' => 'integer'],
'metrics_refresh_rate_seconds' => ['type' => 'integer'], 'sentinel_metrics_refresh_rate_seconds' => ['type' => 'integer'],
'metrics_token' => ['type' => 'string'], 'sentinel_token' => ['type' => 'string'],
'docker_cleanup_frequency' => ['type' => 'string'], 'docker_cleanup_frequency' => ['type' => 'string'],
'docker_cleanup_threshold' => ['type' => 'integer'], 'docker_cleanup_threshold' => ['type' => 'integer'],
'server_id' => ['type' => 'integer'], 'server_id' => ['type' => 'integer'],
@@ -53,6 +53,7 @@ class ServerSetting extends Model
protected $casts = [ protected $casts = [
'force_docker_cleanup' => 'boolean', 'force_docker_cleanup' => 'boolean',
'docker_cleanup_threshold' => 'integer', 'docker_cleanup_threshold' => 'integer',
'sentinel_token' => 'encrypted',
]; ];
public function server() public function server()

View File

@@ -272,7 +272,7 @@ class StandaloneClickhouse extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneDragonfly extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneKeydb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -272,7 +272,7 @@ class StandaloneMariadb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -292,7 +292,7 @@ class StandaloneMongodb extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -273,7 +273,7 @@ class StandaloneMysql extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -274,7 +274,7 @@ class StandalonePostgresql extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -268,7 +268,7 @@ class StandaloneRedis extends BaseModel
$container_name = $this->uuid; $container_name = $this->uuid;
if ($server->isMetricsEnabled()) { if ($server->isMetricsEnabled()) {
$from = now()->subMinutes($mins)->toIso8601ZuluString(); $from = now()->subMinutes($mins)->toIso8601ZuluString();
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); $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 (str($metrics)->contains('error')) { if (str($metrics)->contains('error')) {
$error = json_decode($metrics, true); $error = json_decode($metrics, true);
$error = data_get($error, 'error', 'Something is not okay, are you okay?'); $error = data_get($error, 'error', 'Something is not okay, are you okay?');

View File

@@ -1338,13 +1338,6 @@ function isAnyDeploymentInprogress()
exit(0); exit(0);
} }
function generateSentinelToken()
{
$token = Str::random(64);
return $token;
}
function isBase64Encoded($strValue) function isBase64Encoded($strValue)
{ {
return base64_encode(base64_decode($strValue, true)) === $strValue; return base64_encode(base64_decode($strValue, true)) === $strValue;

View File

@@ -18,7 +18,7 @@ return new class extends Migration
$table->boolean('is_metrics_enabled')->default(false); $table->boolean('is_metrics_enabled')->default(false);
$table->integer('metrics_refresh_rate_seconds')->default(5); $table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30); $table->integer('metrics_history_days')->default(30);
$table->string('metrics_token')->default(generateSentinelToken()); $table->string('metrics_token')->nullable();
}); });
} }

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->dropColumn('metrics_token');
$table->dropColumn('metrics_refresh_rate_seconds');
$table->dropColumn('metrics_history_days');
$table->text('sentinel_token')->nullable();
$table->integer('sentinel_metrics_refresh_rate_seconds')->default(5);
$table->integer('sentinel_metrics_history_days')->default(30);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('server_settings', function (Blueprint $table) {
$table->string('metrics_token')->nullable();
$table->integer('metrics_refresh_rate_seconds')->default(5);
$table->integer('metrics_history_days')->default(30);
$table->dropColumn('sentinel_token');
$table->dropColumn('sentinel_metrics_refresh_rate_seconds');
$table->dropColumn('sentinel_metrics_history_days');
});
}
};

View File

@@ -4981,11 +4981,11 @@ components:
type: string type: string
logdrain_newrelic_license_key: logdrain_newrelic_license_key:
type: string type: string
metrics_history_days: sentinel_metrics_refresh_rate_seconds:
type: integer type: integer
metrics_refresh_rate_seconds: sentinel_metrics_history_days:
type: integer type: integer
metrics_token: sentinel_token:
type: string type: string
docker_cleanup_frequency: docker_cleanup_frequency:
type: string type: string

View File

@@ -68,7 +68,8 @@
</div> </div>
<div class="flex flex-col gap-2 w-full lg:flex-row"> <div class="flex flex-col gap-2 w-full lg:flex-row">
<x-forms.input type="password" id="server.ip" label="IP Address/Domain" <x-forms.input type="password" id="server.ip" label="IP Address/Domain"
helper="An IP Address (127.0.0.1) or domain (example.com). Make sure there is no protocol like http(s):// so you provide a FQDN not a URL." required /> helper="An IP Address (127.0.0.1) or domain (example.com). Make sure there is no protocol like http(s):// so you provide a FQDN not a URL."
required />
<div class="flex gap-2"> <div class="flex gap-2">
<x-forms.input id="server.user" label="User" required /> <x-forms.input id="server.user" label="User" required />
<x-forms.input type="number" id="server.port" label="Port" required /> <x-forms.input type="number" id="server.port" label="Port" required />
@@ -94,7 +95,8 @@
</div> </div>
<div class="relative"> <div class="relative">
<div class="inline-flex relative items-center w-64"> <div class="inline-flex relative items-center w-64">
<input autocomplete="off" wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300' <input autocomplete="off"
wire:dirty.class.remove='dark:focus:ring-coolgray-300 dark:ring-coolgray-300'
wire:dirty.class="dark:focus:ring-warning dark:ring-warning" x-model="search" wire:dirty.class="dark:focus:ring-warning dark:ring-warning" x-model="search"
@focus="open = true" @click.away="open = false" @input="open = true" class="w-full input" @focus="open = true" @click.away="open = false" @input="open = true" class="w-full input"
:placeholder="placeholder" wire:model.debounce.300ms="server.settings.server_timezone"> :placeholder="placeholder" wire:model.debounce.300ms="server.settings.server_timezone">
@@ -129,23 +131,32 @@
</div> </div>
@if ($server->settings->is_cloudflare_tunnel) @if ($server->settings->is_cloudflare_tunnel)
<div class="w-64"> <div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel" label="Enabled" /> <x-forms.checkbox instantSave id="server.settings.is_cloudflare_tunnel"
label="Enabled" />
</div> </div>
@elseif (!$server->isFunctional()) @elseif (!$server->isFunctional())
<div class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300"> <div
To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please validate your server first.</span> Then you will need a Cloudflare token and an SSH domain configured. class="p-4 mb-4 w-full text-sm text-yellow-800 bg-yellow-100 rounded dark:bg-yellow-900 dark:text-yellow-300">
<br/> To <span class="font-semibold">automatically</span> configure Cloudflare Tunnels, please
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please click <span wire:click="manualCloudflareConfig" class="underline cursor-pointer">here</span>, then you should validate the server. validate your server first.</span> Then you will need a Cloudflare token and an SSH
<br/><br/> domain configured.
For more information, please read our <a href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank" class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>. <br />
To <span class="font-semibold">manually</span> configure Cloudflare Tunnels, please
click <span wire:click="manualCloudflareConfig"
class="underline cursor-pointer">here</span>, then you should validate the server.
<br /><br />
For more information, please read our <a
href="https://coolify.io/docs/knowledge-base/cloudflare/tunnels/" target="_blank"
class="font-medium underline hover:text-yellow-600 dark:hover:text-yellow-200">documentation</a>.
</div> </div>
@endif @endif
@if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) @if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional())
<x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels" class="w-full" :closeOutside="false"> <x-modal-input buttonTitle="Automated Configuration" title="Cloudflare Tunnels"
class="w-full" :closeOutside="false">
<livewire:server.configure-cloudflare-tunnels :server_id="$server->id" /> <livewire:server.configure-cloudflare-tunnels :server_id="$server->id" />
</x-modal-input> </x-modal-input>
@endif @endif
@if ($server->isFunctional() &&!$server->settings->is_cloudflare_tunnel) @if ($server->isFunctional() && !$server->settings->is_cloudflare_tunnel)
<div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer"> <div wire:click="manualCloudflareConfig" class="w-full underline cursor-pointer">
I have configured Cloudflare Tunnels manually I have configured Cloudflare Tunnels manually
</div> </div>
@@ -201,57 +212,58 @@
</ul>" </ul>"
instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" /> instantSave id="server.settings.force_docker_cleanup" label="Force Docker Cleanup" />
</div> </div>
<x-modal-confirmation <x-modal-confirmation title="Confirm Docker Cleanup?" buttonTitle="Trigger Docker Cleanup"
title="Confirm Docker Cleanup?" submitAction="manualCleanup" :actions="[
buttonTitle="Trigger Docker Cleanup"
submitAction="manualCleanup"
:actions="[
'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)', 'Permanently deletes all stopped containers managed by Coolify (as containers are non-persistent, no data will be lost)',
'Permanently deletes all unused images', 'Permanently deletes all unused images',
'Clears build cache', 'Clears build cache',
'Removes old versions of the Coolify helper image', 'Removes old versions of the Coolify helper image',
'Optionally permanently deletes all unused volumes (if enabled in advanced options).', 'Optionally permanently deletes all unused volumes (if enabled in advanced options).',
'Optionally permanently deletes all unused networks (if enabled in advanced options).' 'Optionally permanently deletes all unused networks (if enabled in advanced options).',
]" ]" :confirmWithText="false" :confirmWithPassword="false"
:confirmWithText="false" step2ButtonText="Trigger Docker Cleanup" />
:confirmWithPassword="false"
step2ButtonText="Trigger Docker Cleanup"
/>
</div> </div>
@if ($server->settings->force_docker_cleanup) @if ($server->settings->force_docker_cleanup)
<x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency" <x-forms.input placeholder="*/10 * * * *" id="server.settings.docker_cleanup_frequency"
label="Docker cleanup frequency" required label="Docker cleanup frequency" required
helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." /> helper="Cron expression for Docker Cleanup.<br>You can use every_minute, hourly, daily, weekly, monthly, yearly.<br><br>Default is every night at midnight." />
@else @else
<x-forms.input id="server.settings.docker_cleanup_threshold" <x-forms.input id="server.settings.docker_cleanup_threshold"
label="Docker cleanup threshold (%)" required label="Docker cleanup threshold (%)" required
helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." /> helper="The Docker cleanup tasks will run when the disk usage exceeds this threshold." />
@endif @endif
<div x-data="{ open: false }" class="mt-4 max-w-md"> <div x-data="{ open: false }" class="mt-4 max-w-md">
<button @click="open = !open" type="button" class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100"> <button @click="open = !open" type="button"
class="flex items-center justify-between w-full text-left text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100">
<span>Advanced Options</span> <span>Advanced Options</span>
<svg :class="{'rotate-180': open}" class="w-5 h-5 transition-transform duration-200" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"> <svg :class="{ 'rotate-180': open }" class="w-5 h-5 transition-transform duration-200"
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" /> xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg> </svg>
</button> </button>
<div x-show="open" class="mt-2 space-y-2"> <div x-show="open" class="mt-2 space-y-2">
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these options only if you fully understand their implications and consequences!</strong><br>Improper use will result in data loss and could cause functional issues.</p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-2"><strong>Warning: Enable these
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes" label="Delete Unused Volumes" options only if you fully understand their implications and
consequences!</strong><br>Improper use will result in data loss and could cause
functional issues.</p>
<x-forms.checkbox instantSave id="server.settings.delete_unused_volumes"
label="Delete Unused Volumes"
helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br> helper="This option will remove all unused Docker volumes during cleanup.<br><br><strong>Warning: Data form stopped containers will be lost!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'> <ul class='list-disc pl-4 mt-2'>
<li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li> <li>Volumes not attached to running containers will be deleted and data will be permanently lost (stopped containers are affected).</li>
<li>Data from stopped containers volumes will be permanently lost.</li> <li>Data from stopped containers volumes will be permanently lost.</li>
<li>No way to recover deleted volume data.</li> <li>No way to recover deleted volume data.</li>
</ul>" </ul>" />
/> <x-forms.checkbox instantSave id="server.settings.delete_unused_networks"
<x-forms.checkbox instantSave id="server.settings.delete_unused_networks" label="Delete Unused Networks" label="Delete Unused Networks"
helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br> helper="This option will remove all unused Docker networks during cleanup.<br><br><strong>Warning: Functionality may be lost and containers may not be able to communicate with each other!</strong><br><br>Consequences include:<br>
<ul class='list-disc pl-4 mt-2'> <ul class='list-disc pl-4 mt-2'>
<li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li> <li>Networks not attached to running containers will be permanently deleted (stopped containers are affected).</li>
<li>Custom networks for stopped containers will be permanently deleted.</li> <li>Custom networks for stopped containers will be permanently deleted.</li>
<li>Functionality may be lost and containers may not be able to communicate with each other.</li> <li>Functionality may be lost and containers may not be able to communicate with each other.</li>
</ul>" </ul>" />
/>
</div> </div>
</div> </div>
</div> </div>
@@ -269,21 +281,30 @@
{{-- <x-forms.button wire:click='restartSentinel'>Restart</x-forms.button> --}} {{-- <x-forms.button wire:click='restartSentinel'>Restart</x-forms.button> --}}
{{-- @endif --}} {{-- @endif --}}
</div> </div>
<div>Metrics are disabled until a few bugs are fixed.</div> @if (isDev())
{{-- <div class="w-64"> <x-forms.button wire:click="getPushData">Get Push Data</x-forms.button>
{{-- <div class="w-64">
<x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable Metrics" /> <x-forms.checkbox instantSave id="server.settings.is_metrics_enabled" label="Enable Metrics" />
</div> <x-forms.button>Start Sentinel</x-forms.button>
<div class="pt-4"> </div> --}}
<div class="flex flex-wrap gap-2 sm:flex-nowrap"> <div class="flex flex-col gap-2">
<x-forms.input type="password" id="server.settings.metrics_token" label="Metrics token" required <div class="flex flex-wrap gap-2 sm:flex-nowrap items-end">
helper="Token for collector (Sentinel)." /> <x-forms.input type="password" id="server.settings.sentinel_token" label="Metrics token"
<x-forms.input id="server.settings.metrics_refresh_rate_seconds" label="Metrics rate (seconds)" required helper="Token for collector (Sentinel)." />
required <x-forms.button wire:click="regenerateSentinelToken">Regenerate</x-forms.button>
helper="The interval for gathering metrics. Lower means more disk space will be used." /> </div>
<x-forms.input id="server.settings.metrics_history_days" label="Metrics history (days)" required <div class="flex flex-wrap gap-2 sm:flex-nowrap">
helper="How many days should the metrics data should be reserved." /> <x-forms.input id="server.settings.sentinel_metrics_refresh_rate_seconds"
label="Metrics rate (seconds)" required
helper="The interval for gathering metrics. Lower means more disk space will be used." />
<x-forms.input id="server.settings.sentinel_metrics_history_days"
label="Metrics history (days)" required
helper="How many days should the metrics data should be reserved." />
</div>
</div> </div>
</div> --}} @else
<div>Metrics are disabled until a few bugs are fixed.</div>
@endif
@endif @endif
</form> </form>
</div> </div>

View File

@@ -13,6 +13,9 @@ use App\Http\Controllers\Api\TeamController;
use App\Http\Middleware\ApiAllowed; use App\Http\Middleware\ApiAllowed;
use App\Http\Middleware\IgnoreReadOnlyApiToken; use App\Http\Middleware\IgnoreReadOnlyApiToken;
use App\Http\Middleware\OnlyRootApiToken; use App\Http\Middleware\OnlyRootApiToken;
use App\Jobs\PushServerUpdateJob;
use App\Models\Server;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/health', [OtherController::class, 'healthcheck']); Route::get('/health', [OtherController::class, 'healthcheck']);
@@ -129,6 +132,28 @@ Route::group([
}); });
Route::group([
'prefix' => 'v1',
], function () {
Route::post('/sentinel/push', function () {
$token = request()->header('Authorization');
if (!$token) {
return response()->json(['message' => 'Unauthorized'], 401);
}
$naked_token = str_replace('Bearer ', '', $token);
$decrypted = decrypt($naked_token);
$decrypted_token = json_decode($decrypted, true);
$server_uuid = data_get($decrypted_token, 'server_uuid');
$server = Server::where('uuid', $server_uuid)->first();
if (!$server) {
return response()->json(['message' => 'Server not found'], 404);
}
$data = request()->all();
PushServerUpdateJob::dispatch($server, $data);
return response()->json(['message' => 'ok'], 200);
});
});
Route::any('/{any}', function () { Route::any('/{any}', function () {
return response()->json(['message' => 'Not found.', 'docs' => 'https://coolify.io/docs'], 404); return response()->json(['message' => 'Not found.', 'docs' => 'https://coolify.io/docs'], 404);
})->where('any', '.*'); })->where('any', '.*');

View File

@@ -13,7 +13,7 @@ DOCKER_VERSION="26.0"
# TODO: Ask for a user # TODO: Ask for a user
CURRENT_USER=$USER CURRENT_USER=$USER
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel}
mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/ssh/{keys,mux}
mkdir -p /data/coolify/proxy/dynamic mkdir -p /data/coolify/proxy/dynamic