mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-13 19:09:50 +00:00
feat(database): configure standalone health checks (#10481)
This commit is contained in:
@@ -50,13 +50,9 @@ class StartClickhouse
|
||||
],
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'clickhouse-client', '--user', (string) $this->database->clickhouse_admin_user, '--password', (string) $this->database->clickhouse_admin_password, '--query', 'SELECT 1'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'clickhouse-client', '--user', (string) $this->database->clickhouse_admin_user, '--password', (string) $this->database->clickhouse_admin_password, '--query', 'SELECT 1',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -98,6 +94,9 @@ class StartClickhouse
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -106,13 +106,9 @@ class StartDragonfly
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'redis-cli', '-a', (string) $this->database->dragonfly_password, 'ping'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'redis-cli', '-a', (string) $this->database->dragonfly_password, 'ping',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -182,6 +178,9 @@ class StartDragonfly
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -108,13 +108,9 @@ class StartKeydb
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'keydb-cli', '--pass', (string) $this->database->keydb_password, 'ping'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'keydb-cli', '--pass', (string) $this->database->keydb_password, 'ping',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -197,6 +193,9 @@ class StartKeydb
|
||||
// Add custom docker run options
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -103,13 +103,9 @@ class StartMariadb
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'healthcheck.sh', '--connect', '--innodb_initialized',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -202,6 +198,9 @@ class StartMariadb
|
||||
];
|
||||
}
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -109,17 +109,11 @@ class StartMongodb
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD',
|
||||
'echo',
|
||||
'ok',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD',
|
||||
'echo',
|
||||
'ok',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -253,6 +247,9 @@ class StartMongodb
|
||||
$docker_compose['services'][$container_name]['command'] = $commandParts;
|
||||
}
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -103,13 +103,9 @@ class StartMysql
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}",
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -203,6 +199,9 @@ class StartMysql
|
||||
];
|
||||
}
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -110,13 +110,9 @@ class StartPostgresql
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'psql', '-U', (string) $this->database->postgres_user, '-d', (string) $this->database->postgres_db, '-c', 'SELECT 1'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD', 'psql', '-U', (string) $this->database->postgres_user, '-d', (string) $this->database->postgres_db, '-c', 'SELECT 1',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -213,6 +209,9 @@ class StartPostgresql
|
||||
$docker_compose['services'][$container_name]['command'] = $command;
|
||||
}
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -105,17 +105,11 @@ class StartRedis
|
||||
$this->database->destination->network,
|
||||
],
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => [
|
||||
'CMD-SHELL',
|
||||
'redis-cli',
|
||||
'ping',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
],
|
||||
'healthcheck' => $this->database->healthCheckConfiguration([
|
||||
'CMD-SHELL',
|
||||
'redis-cli',
|
||||
'ping',
|
||||
]),
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
'mem_swappiness' => $this->database->limits_memory_swappiness,
|
||||
@@ -194,6 +188,9 @@ class StartRedis
|
||||
$docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options);
|
||||
$docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network);
|
||||
|
||||
if (! $this->database->isHealthcheckEnabled()) {
|
||||
unset($docker_compose['services'][$container_name]['healthcheck']);
|
||||
}
|
||||
$docker_compose = Yaml::dump($docker_compose, 10);
|
||||
$docker_compose_base64 = base64_encode($docker_compose);
|
||||
$this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null";
|
||||
|
||||
@@ -299,6 +299,11 @@ class DatabasesController extends Controller
|
||||
'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'],
|
||||
'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'],
|
||||
'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'],
|
||||
'health_check_enabled' => ['type' => 'boolean', 'description' => 'Enable the database healthcheck probe.', 'default' => true],
|
||||
'health_check_interval' => ['type' => 'integer', 'description' => 'Healthcheck interval in seconds.', 'minimum' => 1, 'default' => 15],
|
||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.', 'minimum' => 1, 'default' => 5],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.', 'minimum' => 1, 'default' => 5],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.', 'minimum' => 0, 'default' => 5],
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -565,9 +570,17 @@ class DatabasesController extends Controller
|
||||
}
|
||||
break;
|
||||
}
|
||||
$allowedFields = array_merge($allowedFields, ['health_check_enabled', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period']);
|
||||
$healthCheckValidator = customApiValidator($request->all(), [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer|min:1',
|
||||
'health_check_timeout' => 'integer|min:1',
|
||||
'health_check_retries' => 'integer|min:1',
|
||||
'health_check_start_period' => 'integer|min:0',
|
||||
]);
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
if ($validator->fails() || $healthCheckValidator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors()->merge($healthCheckValidator->errors());
|
||||
if (! empty($extraFields)) {
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class Health extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $database;
|
||||
|
||||
#[Validate(['boolean'])]
|
||||
public bool $healthCheckEnabled = true;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckInterval = 15;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckTimeout = 5;
|
||||
|
||||
#[Validate(['integer', 'min:1'])]
|
||||
public int $healthCheckRetries = 5;
|
||||
|
||||
#[Validate(['integer', 'min:0'])]
|
||||
public int $healthCheckStartPeriod = 5;
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('view', $this->database);
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->validate();
|
||||
$this->database->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->database->health_check_interval = $this->healthCheckInterval;
|
||||
$this->database->health_check_timeout = $this->healthCheckTimeout;
|
||||
$this->database->health_check_retries = $this->healthCheckRetries;
|
||||
$this->database->health_check_start_period = $this->healthCheckStartPeriod;
|
||||
$this->database->save();
|
||||
} else {
|
||||
$this->healthCheckEnabled = $this->database->health_check_enabled;
|
||||
$this->healthCheckInterval = $this->database->health_check_interval;
|
||||
$this->healthCheckTimeout = $this->database->health_check_timeout;
|
||||
$this->healthCheckRetries = $this->database->health_check_retries;
|
||||
$this->healthCheckStartPeriod = $this->database->health_check_start_period;
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave(): void
|
||||
{
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function submit(): void
|
||||
{
|
||||
$updateSuccessful = false;
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->syncData(true);
|
||||
$updateSuccessful = true;
|
||||
$this->dispatch('success', 'Health check updated. Restart the database to apply the changes.');
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
|
||||
if (! $updateSuccessful) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->markConfigurationChanged();
|
||||
}
|
||||
|
||||
public function toggleHealthcheck(): void
|
||||
{
|
||||
$updateSuccessful = false;
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->healthCheckEnabled = ! $this->healthCheckEnabled;
|
||||
$this->syncData(true);
|
||||
$updateSuccessful = true;
|
||||
$this->dispatch('success', 'Health check '.($this->healthCheckEnabled ? 'enabled' : 'disabled').'. Restart the database to apply the changes.');
|
||||
} catch (\Throwable $e) {
|
||||
handleError($e, $this);
|
||||
}
|
||||
|
||||
if (! $updateSuccessful) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->markConfigurationChanged();
|
||||
}
|
||||
|
||||
private function markConfigurationChanged(): void
|
||||
{
|
||||
if (is_null($this->database->config_hash)) {
|
||||
$this->database->isConfigurationChanged(true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.project.database.health');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -44,11 +45,21 @@ class StandaloneClickhouse extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'clickhouse_admin_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
@@ -111,6 +122,7 @@ class StandaloneClickhouse extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -43,11 +44,21 @@ class StandaloneDragonfly extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'dragonfly_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
@@ -110,6 +121,7 @@ class StandaloneDragonfly extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -44,11 +45,21 @@ class StandaloneKeydb extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'keydb_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
@@ -111,6 +122,7 @@ class StandaloneKeydb extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->keydb_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -12,7 +13,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -47,11 +48,21 @@ class StandaloneMariadb extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'mariadb_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
@@ -114,6 +125,7 @@ class StandaloneMariadb extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mariadb_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -47,11 +48,21 @@ class StandaloneMongodb extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
@@ -120,6 +131,7 @@ class StandaloneMongodb extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mongo_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMysql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -48,11 +49,21 @@ class StandaloneMysql extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'mysql_password' => 'encrypted',
|
||||
'mysql_root_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
@@ -116,6 +127,7 @@ class StandaloneMysql extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->mysql_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -50,11 +51,21 @@ class StandalonePostgresql extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'init_scripts' => 'array',
|
||||
'postgres_password' => 'encrypted',
|
||||
'public_port_timeout' => 'integer',
|
||||
@@ -158,6 +169,7 @@ class StandalonePostgresql extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->postgres_initdb_args.$this->postgres_host_auth_method;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneRedis extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
@@ -43,11 +44,21 @@ class StandaloneRedis extends BaseModel
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
||||
|
||||
protected $casts = [
|
||||
'health_check_enabled' => 'boolean',
|
||||
'health_check_interval' => 'integer',
|
||||
'health_check_timeout' => 'integer',
|
||||
'health_check_retries' => 'integer',
|
||||
'health_check_start_period' => 'integer',
|
||||
'public_port_timeout' => 'integer',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
@@ -115,6 +126,7 @@ class StandaloneRedis extends BaseModel
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->ports_mappings.$this->redis_conf;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
/**
|
||||
* Shared healthcheck behaviour for standalone database models.
|
||||
*
|
||||
* Standalone databases use a fixed, type-specific probe command (psql, redis-cli, ...),
|
||||
* so only the timing fields and the enable/disable flag are configurable.
|
||||
*/
|
||||
trait HasDatabaseHealthCheck
|
||||
{
|
||||
public function isHealthcheckEnabled(): bool
|
||||
{
|
||||
return (bool) ($this->health_check_enabled ?? true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Docker Compose healthcheck block for the given probe command.
|
||||
*
|
||||
* @param array<int, string> $test The Docker `test` array (e.g. ['CMD', 'pg_isready']).
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function healthCheckConfiguration(array $test): array
|
||||
{
|
||||
return [
|
||||
'test' => $test,
|
||||
'interval' => ($this->health_check_interval ?? 15).'s',
|
||||
'timeout' => ($this->health_check_timeout ?? 5).'s',
|
||||
'retries' => $this->health_check_retries ?? 5,
|
||||
'start_period' => ($this->health_check_start_period ?? 5).'s',
|
||||
];
|
||||
}
|
||||
|
||||
protected function healthCheckConfigurationHash(): string
|
||||
{
|
||||
return implode('|', [
|
||||
(int) ($this->health_check_enabled ?? true),
|
||||
$this->health_check_interval ?? 15,
|
||||
$this->health_check_timeout ?? 5,
|
||||
$this->health_check_retries ?? 5,
|
||||
$this->health_check_start_period ?? 5,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private array $tables = [
|
||||
'standalone_postgresqls',
|
||||
'standalone_mysqls',
|
||||
'standalone_mariadbs',
|
||||
'standalone_redis',
|
||||
'standalone_clickhouses',
|
||||
'standalone_dragonflies',
|
||||
'standalone_keydbs',
|
||||
'standalone_mongodbs',
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::table($table, function (Blueprint $table) {
|
||||
$table->boolean('health_check_enabled')->default(true);
|
||||
$table->integer('health_check_interval')->default(15);
|
||||
$table->integer('health_check_timeout')->default(5);
|
||||
$table->integer('health_check_retries')->default(5);
|
||||
$table->integer('health_check_start_period')->default(5);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
foreach ($this->tables as $table) {
|
||||
Schema::table($table, function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4605,6 +4605,35 @@
|
||||
"mysql_conf": {
|
||||
"type": "string",
|
||||
"description": "MySQL conf"
|
||||
},
|
||||
"health_check_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable the database healthcheck probe.",
|
||||
"default": true
|
||||
},
|
||||
"health_check_interval": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck interval in seconds.",
|
||||
"minimum": 1,
|
||||
"default": 15
|
||||
},
|
||||
"health_check_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck timeout in seconds.",
|
||||
"minimum": 1,
|
||||
"default": 5
|
||||
},
|
||||
"health_check_retries": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck retries count.",
|
||||
"minimum": 1,
|
||||
"default": 5
|
||||
},
|
||||
"health_check_start_period": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck start period in seconds.",
|
||||
"minimum": 0,
|
||||
"default": 5
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -2950,6 +2950,30 @@ paths:
|
||||
mysql_conf:
|
||||
type: string
|
||||
description: 'MySQL conf'
|
||||
health_check_enabled:
|
||||
type: boolean
|
||||
description: 'Enable the database healthcheck probe.'
|
||||
default: true
|
||||
health_check_interval:
|
||||
type: integer
|
||||
description: 'Healthcheck interval in seconds.'
|
||||
minimum: 1
|
||||
default: 15
|
||||
health_check_timeout:
|
||||
type: integer
|
||||
description: 'Healthcheck timeout in seconds.'
|
||||
minimum: 1
|
||||
default: 5
|
||||
health_check_retries:
|
||||
type: integer
|
||||
description: 'Healthcheck retries count.'
|
||||
minimum: 1
|
||||
default: 5
|
||||
health_check_start_period:
|
||||
type: integer
|
||||
description: 'Healthcheck start period in seconds.'
|
||||
minimum: 0
|
||||
default: 5
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
@endcan
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.webhooks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Webhooks</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.healthcheck', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Healthcheck</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.resource-limits', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Resource Limits</span></a>
|
||||
<a class="sub-menu-item" {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
@@ -57,6 +59,8 @@
|
||||
<livewire:project.shared.destination :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.persistent-storage')
|
||||
<livewire:project.service.storage :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.healthcheck')
|
||||
<livewire:project.database.health :database="$database" />
|
||||
@elseif ($currentRoute === 'project.database.import-backup')
|
||||
<livewire:project.database.import :resource="$database" />
|
||||
@elseif ($currentRoute === 'project.database.webhooks')
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Healthcheck</h2>
|
||||
<x-forms.button canGate="update" :canResource="$database" type="submit">Save</x-forms.button>
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
submitAction="toggleHealthcheck" :actions="['Enable healthcheck for this database.']"
|
||||
warningMessage="If the health check fails, this database will be marked unhealthy. Please review the <a href='https://coolify.io/docs/knowledge-base/health-checks' target='_blank' class='underline text-white'>Health Checks</a> guide before proceeding!"
|
||||
step2ButtonText="Enable Healthcheck" :confirmWithText="false" :confirmWithPassword="false"
|
||||
isHighlightedButton>
|
||||
</x-modal-confirmation>
|
||||
@else
|
||||
<x-forms.button canGate="update" :canResource="$database" wire:click="toggleHealthcheck">Disable Healthcheck</x-forms.button>
|
||||
@endif
|
||||
</div>
|
||||
<div class="mt-1 pb-4">Define how your resource's health should be checked.</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-callout type="warning" title="Healthcheck disabled">
|
||||
<p>Docker runs no healthcheck probe for this database and Coolify can no longer report a healthy/unhealthy state.</p>
|
||||
</x-callout>
|
||||
@endif
|
||||
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckInterval"
|
||||
placeholder="15" label="Interval (s)" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckTimeout"
|
||||
placeholder="5" label="Timeout (s)" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="1" type="number" id="healthCheckRetries"
|
||||
placeholder="5" label="Retries" required />
|
||||
<x-forms.input canGate="update" :canResource="$database" min="0" type="number"
|
||||
id="healthCheckStartPeriod" placeholder="5" label="Start Period (s)" required />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,6 +1,6 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Healthchecks</h2>
|
||||
<h2>Healthcheck</h2>
|
||||
<x-forms.button canGate="update" :canResource="$resource" type="submit">Save</x-forms.button>
|
||||
@if (!$healthCheckEnabled)
|
||||
<x-modal-confirmation title="Confirm Healthcheck Enable?" buttonTitle="Enable Healthcheck"
|
||||
|
||||
@@ -243,6 +243,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/servers', DatabaseConfiguration::class)->name('project.database.servers');
|
||||
Route::get('/import-backup', DatabaseConfiguration::class)->name('project.database.import-backup')->middleware('can.update.resource');
|
||||
Route::get('/persistent-storage', DatabaseConfiguration::class)->name('project.database.persistent-storage');
|
||||
Route::get('/healthcheck', DatabaseConfiguration::class)->name('project.database.healthcheck');
|
||||
Route::get('/webhooks', DatabaseConfiguration::class)->name('project.database.webhooks');
|
||||
Route::get('/resource-limits', DatabaseConfiguration::class)->name('project.database.resource-limits');
|
||||
Route::get('/resource-operations', DatabaseConfiguration::class)->name('project.database.resource-operations');
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Database\Health;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
|
||||
it('defaults to an enabled healthcheck when nothing is configured', function () {
|
||||
$database = new StandalonePostgresql;
|
||||
|
||||
expect($database->isHealthcheckEnabled())->toBeTrue();
|
||||
});
|
||||
|
||||
it('builds the compose healthcheck block from the model timing fields', function () {
|
||||
$database = new StandalonePostgresql([
|
||||
'health_check_interval' => 30,
|
||||
'health_check_timeout' => 7,
|
||||
'health_check_retries' => 4,
|
||||
'health_check_start_period' => 12,
|
||||
]);
|
||||
|
||||
$config = $database->healthCheckConfiguration(['CMD', 'pg_isready']);
|
||||
|
||||
expect($config)->toBe([
|
||||
'test' => ['CMD', 'pg_isready'],
|
||||
'interval' => '30s',
|
||||
'timeout' => '7s',
|
||||
'retries' => 4,
|
||||
'start_period' => '12s',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to safe defaults when timing fields are missing', function () {
|
||||
$database = new StandalonePostgresql;
|
||||
|
||||
$config = $database->healthCheckConfiguration(['CMD', 'pg_isready']);
|
||||
|
||||
expect($config['interval'])->toBe('15s')
|
||||
->and($config['timeout'])->toBe('5s')
|
||||
->and($config['retries'])->toBe(5)
|
||||
->and($config['start_period'])->toBe('5s');
|
||||
});
|
||||
|
||||
it('reports the healthcheck as disabled when the flag is false', function () {
|
||||
$database = new StandalonePostgresql(['health_check_enabled' => false]);
|
||||
|
||||
expect($database->isHealthcheckEnabled())->toBeFalse();
|
||||
});
|
||||
|
||||
it('uses distinct hash fragments for ambiguous healthcheck values', function () {
|
||||
$enabledDatabase = new StandalonePostgresql([
|
||||
'health_check_enabled' => true,
|
||||
'health_check_interval' => 5,
|
||||
'health_check_timeout' => 5,
|
||||
'health_check_retries' => 5,
|
||||
'health_check_start_period' => 5,
|
||||
]);
|
||||
|
||||
$disabledDatabase = new StandalonePostgresql([
|
||||
'health_check_enabled' => false,
|
||||
'health_check_interval' => 15,
|
||||
'health_check_timeout' => 5,
|
||||
'health_check_retries' => 5,
|
||||
'health_check_start_period' => 5,
|
||||
]);
|
||||
|
||||
$getHashFragment = function () {
|
||||
return $this->healthCheckConfigurationHash();
|
||||
};
|
||||
|
||||
expect($getHashFragment->call($enabledDatabase))
|
||||
->toBe('1|5|5|5|5')
|
||||
->not->toBe($getHashFragment->call($disabledDatabase))
|
||||
->and($getHashFragment->call($disabledDatabase))->toBe('0|15|5|5|5');
|
||||
});
|
||||
|
||||
it('does not mark configuration changed when health update authorization fails', function () {
|
||||
$database = new class
|
||||
{
|
||||
public ?string $config_hash = null;
|
||||
|
||||
public int $configurationChangedChecks = 0;
|
||||
|
||||
public function isConfigurationChanged(bool $save = false): bool
|
||||
{
|
||||
$this->configurationChangedChecks++;
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
$component = new class extends Health
|
||||
{
|
||||
public array $dispatchedEvents = [];
|
||||
|
||||
public function authorize($ability, $arguments = [])
|
||||
{
|
||||
throw new AuthorizationException('This action is unauthorized.');
|
||||
}
|
||||
|
||||
public function dispatch($event, ...$params)
|
||||
{
|
||||
$this->dispatchedEvents[] = $event;
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
$component->database = $database;
|
||||
$component->submit();
|
||||
|
||||
expect($database->configurationChangedChecks)->toBe(0)
|
||||
->and($component->dispatchedEvents)->toBe(['error']);
|
||||
});
|
||||
|
||||
it('toggles database healthcheck and marks configuration changed', function () {
|
||||
$database = new class
|
||||
{
|
||||
public ?string $config_hash = 'existing';
|
||||
|
||||
public bool $health_check_enabled = false;
|
||||
|
||||
public int $health_check_interval = 15;
|
||||
|
||||
public int $health_check_timeout = 5;
|
||||
|
||||
public int $health_check_retries = 5;
|
||||
|
||||
public int $health_check_start_period = 5;
|
||||
|
||||
public int $saveCalls = 0;
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->saveCalls++;
|
||||
}
|
||||
};
|
||||
|
||||
$component = new class extends Health
|
||||
{
|
||||
public array $dispatchedEvents = [];
|
||||
|
||||
public function authorize($ability, $arguments = [])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function dispatch($event, ...$params)
|
||||
{
|
||||
$this->dispatchedEvents[] = $event;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->database->health_check_enabled = $this->healthCheckEnabled;
|
||||
$this->database->save();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$component->database = $database;
|
||||
$component->healthCheckEnabled = false;
|
||||
$component->healthCheckInterval = 15;
|
||||
$component->healthCheckTimeout = 5;
|
||||
$component->healthCheckRetries = 5;
|
||||
$component->healthCheckStartPeriod = 5;
|
||||
|
||||
$component->toggleHealthcheck();
|
||||
|
||||
expect($database->health_check_enabled)->toBeTrue()
|
||||
->and($database->saveCalls)->toBe(1)
|
||||
->and($component->dispatchedEvents)->toBe(['success', 'configurationChanged']);
|
||||
});
|
||||
Reference in New Issue
Block a user