mirror of
https://github.com/coollabsio/coolify.git
synced 2026-10-05 21:47:10 -04:00
feat(database): add StandaloneSqlite model and migration
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\Auditable;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasDatabaseHealthCheck;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use App\Traits\HasSecretManager;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneSqlite extends BaseModel
|
||||
{
|
||||
use Auditable, ClearsGlobalSearchCache, HasDatabaseHealthCheck, HasFactory, HasMetrics, HasSafeStringAttribute, HasSecretManager, SoftDeletes;
|
||||
|
||||
public const DATA_DIRECTORY = '/var/lib/sqlite';
|
||||
|
||||
public const DATABASES_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9._-]*(,[A-Za-z0-9][A-Za-z0-9._-]*)*$/';
|
||||
|
||||
protected array $auditExclude = ['last_online_at'];
|
||||
|
||||
protected $fillable = [
|
||||
'uuid',
|
||||
'name',
|
||||
'description',
|
||||
'sqlite_databases',
|
||||
'is_log_drain_enabled',
|
||||
'is_include_timestamps',
|
||||
'status',
|
||||
'image',
|
||||
'limits_memory',
|
||||
'limits_memory_swap',
|
||||
'limits_memory_swappiness',
|
||||
'limits_memory_reservation',
|
||||
'limits_cpus',
|
||||
'limits_cpuset',
|
||||
'limits_cpu_shares',
|
||||
'started_at',
|
||||
'restart_count',
|
||||
'last_restart_at',
|
||||
'last_restart_type',
|
||||
'last_online_at',
|
||||
'custom_docker_run_options',
|
||||
'destination_type',
|
||||
'destination_id',
|
||||
'environment_id',
|
||||
'health_check_enabled',
|
||||
'health_check_interval',
|
||||
'health_check_timeout',
|
||||
'health_check_retries',
|
||||
'health_check_start_period',
|
||||
];
|
||||
|
||||
protected $appends = ['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',
|
||||
'restart_count' => 'integer',
|
||||
'last_restart_at' => 'datetime',
|
||||
'last_restart_type' => 'string',
|
||||
];
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
LocalPersistentVolume::create([
|
||||
'name' => 'sqlite-data-'.$database->uuid,
|
||||
'mount_path' => self::DATA_DIRECTORY,
|
||||
'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->last_online_at = now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get query builder for SQLite databases owned by current team.
|
||||
* If you need all databases without further query chaining, use ownedByCurrentTeamCached() instead.
|
||||
*/
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return StandaloneSqlite::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all SQLite databases owned by current team (cached for request duration).
|
||||
*/
|
||||
public static function ownedByCurrentTeamCached()
|
||||
{
|
||||
return once(function () {
|
||||
return StandaloneSqlite::ownedByCurrentTeam()->get();
|
||||
});
|
||||
}
|
||||
|
||||
protected function serverStatus(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: function () {
|
||||
return $this->destination->server->isFunctional();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function isConfigurationChanged(bool $save = false)
|
||||
{
|
||||
$newConfigHash = $this->image.$this->sqlite_databases;
|
||||
$newConfigHash .= $this->healthCheckConfigurationHash();
|
||||
$newConfigHash .= json_encode($this->environment_variables()->get('value')->makeVisible('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 '.escapeshellarg($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 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 team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
}
|
||||
|
||||
public function databaseType(): Attribute
|
||||
{
|
||||
return new Attribute(
|
||||
get: fn () => $this->type(),
|
||||
);
|
||||
}
|
||||
|
||||
public function type(): string
|
||||
{
|
||||
return 'standalone-sqlite';
|
||||
}
|
||||
|
||||
public function databaseFilePath(): string
|
||||
{
|
||||
return self::DATA_DIRECTORY.'/'.str($this->sqlite_databases)->before(',')->trim();
|
||||
}
|
||||
|
||||
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 environment_variables()
|
||||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
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 isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('standalone_sqlites', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->string('name');
|
||||
$table->string('description')->nullable();
|
||||
|
||||
$table->string('sqlite_databases')->default('database.sqlite');
|
||||
|
||||
$table->boolean('is_log_drain_enabled')->default(false);
|
||||
$table->boolean('is_include_timestamps')->default(false);
|
||||
$table->softDeletes();
|
||||
|
||||
$table->string('status')->default('exited');
|
||||
$table->integer('restart_count')->default(0);
|
||||
$table->timestamp('last_restart_at')->nullable();
|
||||
$table->string('last_restart_type', 10)->nullable();
|
||||
|
||||
$table->string('image')->default('peakimages/sqlite:3.53.4-v0.1.0');
|
||||
|
||||
$table->boolean('is_public')->default(false);
|
||||
|
||||
$table->string('limits_memory')->default('0');
|
||||
$table->string('limits_memory_swap')->default('0');
|
||||
$table->integer('limits_memory_swappiness')->default(60);
|
||||
$table->string('limits_memory_reservation')->default('0');
|
||||
|
||||
$table->string('limits_cpus')->default('0');
|
||||
$table->string('limits_cpuset')->nullable()->default(null);
|
||||
$table->integer('limits_cpu_shares')->default(1024);
|
||||
|
||||
$table->timestamp('started_at')->nullable();
|
||||
$table->morphs('destination');
|
||||
$table->foreignId('environment_id')->nullable();
|
||||
|
||||
$table->string('config_hash')->nullable();
|
||||
$table->text('custom_docker_run_options')->nullable();
|
||||
|
||||
$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);
|
||||
|
||||
$table->timestamps();
|
||||
$table->timestamp('last_online_at')->default(now());
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user