diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 30cae71f1..1128b8f8f 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -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"; diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index addc30be4..530ba7d23 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -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"; diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index e59d6f697..e9acd0b3c 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -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"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index ceb1e8b85..17ed2a9a8 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -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"; diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index c79789718..e5973e807 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -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"; diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index 0394d50b6..f9d75e0c8 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -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"; diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index da8b5dc4e..520cbdec4 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -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"; diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index c31b099e4..e4bfd98e1 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -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"; diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index dc9b6f5b5..e2d2aad92 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -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.'); diff --git a/app/Livewire/Project/Database/Health.php b/app/Livewire/Project/Database/Health.php new file mode 100644 index 000000000..33540d74e --- /dev/null +++ b/app/Livewire/Project/Database/Health.php @@ -0,0 +1,81 @@ +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'); + } +} diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index 784e2c937..1e68046f9 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -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'); diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index e07053c03..5f76be884 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -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'); diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 979f45a3d..6d3b5a82b 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -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'); diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index dba8a52f5..1058d8721 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -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'); diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index e72f4f1c6..4657f1d5e 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -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'); diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index 1c522d200..f3d0ad55f 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -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'); diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 57dfe5988..39482892d 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -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'); diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index ef42d7f18..3d1d11138 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -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'); diff --git a/app/Traits/HasDatabaseHealthCheck.php b/app/Traits/HasDatabaseHealthCheck.php new file mode 100644 index 000000000..2602ecb23 --- /dev/null +++ b/app/Traits/HasDatabaseHealthCheck.php @@ -0,0 +1,34 @@ +health_check_enabled ?? true); + } + + /** + * Build the Docker Compose healthcheck block for the given probe command. + * + * @param array $test The Docker `test` array (e.g. ['CMD', 'pg_isready']). + * @return array + */ + 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', + ]; + } +} diff --git a/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php b/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php new file mode 100644 index 000000000..63d7c3497 --- /dev/null +++ b/database/migrations/2026_05_31_000000_add_health_check_to_standalone_databases.php @@ -0,0 +1,47 @@ +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', + ]); + }); + } + } +}; diff --git a/openapi.json b/openapi.json index e83538f2b..d3d25bacc 100644 --- a/openapi.json +++ b/openapi.json @@ -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" diff --git a/openapi.yaml b/openapi.yaml index 523d453ff..469a0c38d 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -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': diff --git a/resources/views/livewire/project/database/configuration.blade.php b/resources/views/livewire/project/database/configuration.blade.php index 73f87c0e3..c58232200 100644 --- a/resources/views/livewire/project/database/configuration.blade.php +++ b/resources/views/livewire/project/database/configuration.blade.php @@ -15,6 +15,8 @@ href="{{ route('project.database.servers', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'database_uuid' => $database->uuid]) }}">Servers Persistent Storage + Health Checks @can('update', $database) Import Backup @@ -57,6 +59,8 @@ @elseif ($currentRoute === 'project.database.persistent-storage') + @elseif ($currentRoute === 'project.database.health-checks') + @elseif ($currentRoute === 'project.database.import-backup') @elseif ($currentRoute === 'project.database.webhooks') diff --git a/resources/views/livewire/project/database/health.blade.php b/resources/views/livewire/project/database/health.blade.php new file mode 100644 index 000000000..2e70f79b2 --- /dev/null +++ b/resources/views/livewire/project/database/health.blade.php @@ -0,0 +1,26 @@ +
+
+

Health Checks

+ Save +
+
Configure how Docker checks this database's health. A higher interval lowers + dockerd/containerd CPU and load on servers running many databases. Restart the + database to apply changes.
+
+ + @if ($healthCheckEnabled) +
+ + + + +
+ @endif +
+
diff --git a/routes/web.php b/routes/web.php index aed37a086..e4d6f477f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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'); diff --git a/tests/Feature/DatabaseHealthCheckTest.php b/tests/Feature/DatabaseHealthCheckTest.php new file mode 100644 index 000000000..8bc1e4e92 --- /dev/null +++ b/tests/Feature/DatabaseHealthCheckTest.php @@ -0,0 +1,45 @@ +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(); +});