mirror of
https://github.com/coollabsio/coolify.git
synced 2026-08-28 10:05:25 -05:00
Integrates Valkey as a new standalone database type. This includes adding the necessary model, updating actions to handle Valkey instances, and documenting the integration process.
387 lines
12 KiB
PHP
387 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use App\Traits\ClearsGlobalSearchCache;
|
|
use App\Traits\HasSafeStringAttribute;
|
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
|
|
class StandaloneValkey extends BaseModel
|
|
{
|
|
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
|
|
|
protected $guarded = [];
|
|
|
|
protected $appends = ['internal_db_url', 'external_db_url', 'database_type', 'server_status'];
|
|
|
|
protected static function booted()
|
|
{
|
|
static::created(function ($database) {
|
|
LocalPersistentVolume::create([
|
|
'name' => 'valkey-data-'.$database->uuid,
|
|
'mount_path' => '/data',
|
|
'host_path' => null,
|
|
'resource_id' => $database->id,
|
|
'resource_type' => $database->getMorphClass(),
|
|
]);
|
|
});
|
|
static::forceDeleting(function ($database) {
|
|
$database->persistentStorages()->delete();
|
|
$database->scheduledBackups()->delete();
|
|
$database->environment_variables()->delete();
|
|
$database->tags()->detach();
|
|
});
|
|
static::saving(function ($database) {
|
|
if ($database->isDirty('status')) {
|
|
$database->forceFill(['last_online_at' => now()]);
|
|
}
|
|
});
|
|
|
|
}
|
|
|
|
public static function ownedByCurrentTeam()
|
|
{
|
|
return StandaloneValkey::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
|
}
|
|
|
|
protected function serverStatus(): Attribute
|
|
{
|
|
return Attribute::make(
|
|
get: function () {
|
|
return $this->destination->server->isFunctional();
|
|
}
|
|
);
|
|
}
|
|
|
|
public function isConfigurationChanged(bool $save = false)
|
|
{
|
|
$newConfigHash = $this->image.$this->ports_mappings.$this->valkey_conf;
|
|
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
|
|
$newConfigHash = md5($newConfigHash);
|
|
$oldConfigHash = data_get($this, 'config_hash');
|
|
if ($oldConfigHash === null) {
|
|
if ($save) {
|
|
$this->config_hash = $newConfigHash;
|
|
$this->save();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
if ($oldConfigHash === $newConfigHash) {
|
|
return false;
|
|
} else {
|
|
if ($save) {
|
|
$this->config_hash = $newConfigHash;
|
|
$this->save();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public function isRunning()
|
|
{
|
|
return (bool) str($this->status)->contains('running');
|
|
}
|
|
|
|
public function isExited()
|
|
{
|
|
return (bool) str($this->status)->startsWith('exited');
|
|
}
|
|
|
|
public function workdir()
|
|
{
|
|
return database_configuration_dir()."/{$this->uuid}";
|
|
}
|
|
|
|
public function deleteConfigurations()
|
|
{
|
|
$server = data_get($this, 'destination.server');
|
|
$workdir = $this->workdir();
|
|
if (str($workdir)->endsWith($this->uuid)) {
|
|
instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
|
|
}
|
|
}
|
|
|
|
public function deleteVolumes()
|
|
{
|
|
$persistentStorages = $this->persistentStorages()->get() ?? collect();
|
|
if ($persistentStorages->count() === 0) {
|
|
return;
|
|
}
|
|
$server = data_get($this, 'destination.server');
|
|
foreach ($persistentStorages as $storage) {
|
|
instant_remote_process(["docker volume rm -f $storage->name"], $server, false);
|
|
}
|
|
}
|
|
|
|
public function realStatus()
|
|
{
|
|
return $this->getRawOriginal('status');
|
|
}
|
|
|
|
public function status(): Attribute
|
|
{
|
|
return Attribute::make(
|
|
set: function ($value) {
|
|
if (str($value)->contains('(')) {
|
|
$status = str($value)->before('(')->trim()->value();
|
|
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
|
} elseif (str($value)->contains(':')) {
|
|
$status = str($value)->before(':')->trim()->value();
|
|
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
|
} else {
|
|
$status = $value;
|
|
$health = 'unhealthy';
|
|
}
|
|
|
|
return "$status:$health";
|
|
},
|
|
get: function ($value) {
|
|
if (str($value)->contains('(')) {
|
|
$status = str($value)->before('(')->trim()->value();
|
|
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
|
|
} elseif (str($value)->contains(':')) {
|
|
$status = str($value)->before(':')->trim()->value();
|
|
$health = str($value)->after(':')->trim()->value() ?? 'unhealthy';
|
|
} else {
|
|
$status = $value;
|
|
$health = 'unhealthy';
|
|
}
|
|
|
|
return "$status:$health";
|
|
},
|
|
);
|
|
}
|
|
|
|
public function tags()
|
|
{
|
|
return $this->morphToMany(Tag::class, 'taggable');
|
|
}
|
|
|
|
public function project()
|
|
{
|
|
return data_get($this, 'environment.project');
|
|
}
|
|
|
|
public function team()
|
|
{
|
|
return data_get($this, 'environment.project.team');
|
|
}
|
|
|
|
public function sslCertificates()
|
|
{
|
|
return $this->morphMany(SslCertificate::class, 'resource');
|
|
}
|
|
|
|
public function link()
|
|
{
|
|
if (data_get($this, 'environment.project.uuid')) {
|
|
return route('project.database.configuration', [
|
|
'project_uuid' => data_get($this, 'environment.project.uuid'),
|
|
'environment_uuid' => data_get($this, 'environment.uuid'),
|
|
'database_uuid' => data_get($this, 'uuid'),
|
|
]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function isLogDrainEnabled()
|
|
{
|
|
return data_get($this, 'is_log_drain_enabled', false);
|
|
}
|
|
|
|
public function portsMappings(): Attribute
|
|
{
|
|
return Attribute::make(
|
|
set: fn ($value) => $value === '' ? null : $value,
|
|
);
|
|
}
|
|
|
|
public function portsMappingsArray(): Attribute
|
|
{
|
|
return Attribute::make(
|
|
get: fn () => is_null($this->ports_mappings)
|
|
? []
|
|
: explode(',', $this->ports_mappings),
|
|
|
|
);
|
|
}
|
|
|
|
public function type(): string
|
|
{
|
|
return 'standalone-valkey';
|
|
}
|
|
|
|
public function databaseType(): Attribute
|
|
{
|
|
return new Attribute(
|
|
get: fn () => $this->type(),
|
|
);
|
|
}
|
|
|
|
protected function internalDbUrl(): Attribute
|
|
{
|
|
return new Attribute(
|
|
get: function () {
|
|
$valkey_version = $this->getValkeyVersion();
|
|
$username_part = version_compare($valkey_version, '6.0', '>=') ? rawurlencode($this->valkey_username).':' : '';
|
|
$encodedPass = rawurlencode($this->valkey_password);
|
|
$scheme = $this->enable_ssl ? 'valkey+ssl' : 'valkey';
|
|
$port = $this->enable_ssl ? 6380 : 6379;
|
|
$url = "{$scheme}://{$username_part}{$encodedPass}@{$this->uuid}:{$port}/0";
|
|
|
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
);
|
|
}
|
|
|
|
protected function externalDbUrl(): Attribute
|
|
{
|
|
return new Attribute(
|
|
get: function () {
|
|
if ($this->is_public && $this->public_port) {
|
|
$serverIp = $this->destination->server->getIp;
|
|
if (empty($serverIp)) {
|
|
return null;
|
|
}
|
|
$valkey_version = $this->getValkeyVersion();
|
|
$username_part = version_compare($valkey_version, '6.0', '>=') ? rawurlencode($this->valkey_username).':' : '';
|
|
$encodedPass = rawurlencode($this->valkey_password);
|
|
$scheme = $this->enable_ssl ? 'valkey+ssl' : 'valkey';
|
|
$url = "{$scheme}://{$username_part}{$encodedPass}@{$serverIp}:{$this->public_port}/0";
|
|
|
|
if ($this->enable_ssl && $this->ssl_mode === 'verify-ca') {
|
|
$url .= '?cacert=/etc/ssl/certs/coolify-ca.crt';
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
);
|
|
}
|
|
|
|
public function getValkeyVersion()
|
|
{
|
|
$image_parts = explode(':', $this->image);
|
|
|
|
return $image_parts[1] ?? '0.0';
|
|
}
|
|
|
|
public function environment()
|
|
{
|
|
return $this->belongsTo(Environment::class);
|
|
}
|
|
|
|
public function fileStorages()
|
|
{
|
|
return $this->morphMany(LocalFileVolume::class, 'resource');
|
|
}
|
|
|
|
public function destination()
|
|
{
|
|
return $this->morphTo();
|
|
}
|
|
|
|
public function runtime_environment_variables()
|
|
{
|
|
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
|
}
|
|
|
|
public function persistentStorages()
|
|
{
|
|
return $this->morphMany(LocalPersistentVolume::class, 'resource');
|
|
}
|
|
|
|
public function scheduledBackups()
|
|
{
|
|
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
|
|
}
|
|
|
|
public function getCpuMetrics(int $mins = 5)
|
|
{
|
|
$server = $this->destination->server;
|
|
$container_name = $this->uuid;
|
|
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false);
|
|
if (str($metrics)->contains('error')) {
|
|
$error = json_decode($metrics, true);
|
|
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
if ($error === 'Unauthorized') {
|
|
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
}
|
|
throw new \Exception($error);
|
|
}
|
|
$metrics = json_decode($metrics, true);
|
|
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
return [(int) $metric['time'], (float) $metric['percent']];
|
|
});
|
|
|
|
return $parsedCollection->toArray();
|
|
}
|
|
|
|
public function getMemoryMetrics(int $mins = 5)
|
|
{
|
|
$server = $this->destination->server;
|
|
$container_name = $this->uuid;
|
|
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
|
$metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false);
|
|
if (str($metrics)->contains('error')) {
|
|
$error = json_decode($metrics, true);
|
|
$error = data_get($error, 'error', 'Something is not okay, are you okay?');
|
|
if ($error === 'Unauthorized') {
|
|
$error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.';
|
|
}
|
|
throw new \Exception($error);
|
|
}
|
|
$metrics = json_decode($metrics, true);
|
|
$parsedCollection = collect($metrics)->map(function ($metric) {
|
|
return [(int) $metric['time'], (float) $metric['used']];
|
|
});
|
|
|
|
return $parsedCollection->toArray();
|
|
}
|
|
|
|
public function isBackupSolutionAvailable()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
public function valkeyPassword(): Attribute
|
|
{
|
|
return new Attribute(
|
|
get: function () {
|
|
$password = $this->runtime_environment_variables()->where('key', 'VALKEY_PASSWORD')->first();
|
|
if (! $password) {
|
|
return null;
|
|
}
|
|
|
|
return $password->value;
|
|
},
|
|
|
|
);
|
|
}
|
|
|
|
public function environment_variables()
|
|
{
|
|
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
|
->orderByRaw("
|
|
CASE
|
|
WHEN LOWER(key) LIKE 'service_%' THEN 1
|
|
WHEN is_required = true AND (value IS NULL OR value = '') THEN 2
|
|
ELSE 3
|
|
END,
|
|
LOWER(key) ASC
|
|
");
|
|
}
|
|
}
|