mirror of
https://github.com/coollabsio/coolify.git
synced 2026-06-13 19:09:50 +00:00
feat(database): configure standalone health checks
Add configurable health check settings for standalone databases and apply them to generated Docker Compose services. Allow disabling health checks and cover the behavior with feature tests.
This commit is contained in:
@@ -52,10 +52,10 @@ 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',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -98,6 +98,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";
|
||||
|
||||
@@ -108,10 +108,10 @@ class StartDragonfly
|
||||
'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',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -182,6 +182,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";
|
||||
|
||||
@@ -110,10 +110,10 @@ class StartKeydb
|
||||
'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',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -197,6 +197,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";
|
||||
|
||||
@@ -105,10 +105,10 @@ class StartMariadb
|
||||
'labels' => defaultDatabaseLabels($this->database)->toArray(),
|
||||
'healthcheck' => [
|
||||
'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -202,6 +202,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";
|
||||
|
||||
@@ -115,10 +115,10 @@ class StartMongodb
|
||||
'echo',
|
||||
'ok',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -253,6 +253,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";
|
||||
|
||||
@@ -105,10 +105,10 @@ class StartMysql
|
||||
'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',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -203,6 +203,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";
|
||||
|
||||
@@ -112,10 +112,10 @@ class StartPostgresql
|
||||
'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',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -213,6 +213,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";
|
||||
|
||||
@@ -111,10 +111,10 @@ class StartRedis
|
||||
'redis-cli',
|
||||
'ping',
|
||||
],
|
||||
'interval' => '5s',
|
||||
'timeout' => '5s',
|
||||
'retries' => 10,
|
||||
'start_period' => '5s',
|
||||
'interval' => "{$this->database->health_check_interval}s",
|
||||
'timeout' => "{$this->database->health_check_timeout}s",
|
||||
'retries' => $this->database->health_check_retries,
|
||||
'start_period' => "{$this->database->health_check_start_period}s",
|
||||
],
|
||||
'mem_limit' => $this->database->limits_memory,
|
||||
'memswap_limit' => $this->database->limits_memory_swap,
|
||||
@@ -194,6 +194,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.'],
|
||||
'health_check_interval' => ['type' => 'integer', 'description' => 'Healthcheck interval in seconds.'],
|
||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.'],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Healthcheck retries count.'],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Healthcheck start period in seconds.'],
|
||||
],
|
||||
),
|
||||
)
|
||||
@@ -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,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
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()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
$this->submit();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->syncData(true);
|
||||
$this->dispatch('success', 'Health check updated. Restart the database to apply the changes.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
if (is_null($this->database->config_hash)) {
|
||||
$this->database->isConfigurationChanged(true);
|
||||
} else {
|
||||
$this->dispatch('configurationChanged');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$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->health_check_enabled.$this->health_check_interval.$this->health_check_timeout.$this->health_check_retries.$this->health_check_start_period;
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
||||
$newConfigHash = md5($newConfigHash);
|
||||
$oldConfigHash = data_get($this, 'config_hash');
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,26 @@
|
||||
"mysql_conf": {
|
||||
"type": "string",
|
||||
"description": "MySQL conf"
|
||||
},
|
||||
"health_check_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Enable the database healthcheck probe."
|
||||
},
|
||||
"health_check_interval": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck interval in seconds."
|
||||
},
|
||||
"health_check_timeout": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck timeout in seconds."
|
||||
},
|
||||
"health_check_retries": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck retries count."
|
||||
},
|
||||
"health_check_start_period": {
|
||||
"type": "integer",
|
||||
"description": "Healthcheck start period in seconds."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -2950,6 +2950,21 @@ paths:
|
||||
mysql_conf:
|
||||
type: string
|
||||
description: 'MySQL conf'
|
||||
health_check_enabled:
|
||||
type: boolean
|
||||
description: 'Enable the database healthcheck probe.'
|
||||
health_check_interval:
|
||||
type: integer
|
||||
description: 'Healthcheck interval in seconds.'
|
||||
health_check_timeout:
|
||||
type: integer
|
||||
description: 'Healthcheck timeout in seconds.'
|
||||
health_check_retries:
|
||||
type: integer
|
||||
description: 'Healthcheck retries count.'
|
||||
health_check_start_period:
|
||||
type: integer
|
||||
description: 'Healthcheck start period in seconds.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Servers</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.persistent-storage', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Persistent Storage</span></a>
|
||||
<a class='sub-menu-item' {{ wireNavigate() }} wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.health-checks', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Health Checks</span></a>
|
||||
@can('update', $database)
|
||||
<a class='sub-menu-item' wire:current.exact="menu-item-active"
|
||||
href="{{ route('project.database.import-backup', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}"><span class="menu-item-label">Import Backup</span></a>
|
||||
@@ -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.health-checks')
|
||||
<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,26 @@
|
||||
<form wire:submit='submit' class="flex flex-col">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2>Health Checks</h2>
|
||||
<x-forms.button canGate="update" :canResource="$database" type="submit">Save</x-forms.button>
|
||||
</div>
|
||||
<div class="mt-1 pb-4">Configure how Docker checks this database's health. A higher interval lowers
|
||||
<code>dockerd</code>/<code>containerd</code> CPU and load on servers running many databases. Restart the
|
||||
database to apply changes.</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<x-forms.checkbox canGate="update" :canResource="$database" instantSave id="healthCheckEnabled"
|
||||
label="Enabled"
|
||||
helper="When disabled, Docker runs no healthcheck probe for this database and Coolify can no longer report a healthy/unhealthy state." />
|
||||
@if ($healthCheckEnabled)
|
||||
<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>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
@@ -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('/health-checks', DatabaseConfiguration::class)->name('project.database.health-checks');
|
||||
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,45 @@
|
||||
<?php
|
||||
|
||||
use App\Models\StandalonePostgresql;
|
||||
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user