mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 02:16:43 +00:00
Merge remote-tracking branch 'origin/next' into env-var-descriptions
This commit is contained in:
commit
d9e39ba211
404 files changed
+29406
-26356
No files matched your search
+18
-73
@@ -6,6 +6,7 @@ use App\Enums\ApplicationDeploymentStatus;
|
||||
use App\Services\ConfigurationGenerator;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasConfiguration;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -60,6 +61,8 @@ use Visus\Cuid2\Cuid2;
|
||||
'health_check_timeout' => ['type' => 'integer', 'description' => 'Health check timeout in seconds.'],
|
||||
'health_check_retries' => ['type' => 'integer', 'description' => 'Health check retries count.'],
|
||||
'health_check_start_period' => ['type' => 'integer', 'description' => 'Health check start period in seconds.'],
|
||||
'health_check_type' => ['type' => 'string', 'description' => 'Health check type: http or cmd.', 'enum' => ['http', 'cmd']],
|
||||
'health_check_command' => ['type' => 'string', 'nullable' => true, 'description' => 'Health check command for CMD type.'],
|
||||
'limits_memory' => ['type' => 'string', 'description' => 'Memory limit.'],
|
||||
'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit.'],
|
||||
'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness.'],
|
||||
@@ -111,7 +114,7 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Application extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasConfiguration, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
private static $parserVersion = '5';
|
||||
|
||||
@@ -844,15 +847,7 @@ class Application extends BaseModel
|
||||
public function environment_variables()
|
||||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||
->where('is_preview', false)
|
||||
->orderByRaw("
|
||||
CASE
|
||||
WHEN is_required = true THEN 1
|
||||
WHEN LOWER(key) LIKE 'service_%' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
LOWER(key) ASC
|
||||
");
|
||||
->where('is_preview', false);
|
||||
}
|
||||
|
||||
public function runtime_environment_variables()
|
||||
@@ -997,7 +992,7 @@ class Application extends BaseModel
|
||||
if (isDev() && data_get($this, 'private_key_id') === 0) {
|
||||
return 'deploy_key';
|
||||
}
|
||||
if (data_get($this, 'private_key_id')) {
|
||||
if (! is_null(data_get($this, 'private_key_id'))) {
|
||||
return 'deploy_key';
|
||||
} elseif (data_get($this, 'source')) {
|
||||
return 'source';
|
||||
@@ -1584,6 +1579,11 @@ class Application extends BaseModel
|
||||
try {
|
||||
$composeFileContent = instant_remote_process($commands, $this->destination->server);
|
||||
} catch (\Exception $e) {
|
||||
// Restore original values on failure only
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
|
||||
if (str($e->getMessage())->contains('No such file')) {
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
@@ -1595,9 +1595,7 @@ class Application extends BaseModel
|
||||
}
|
||||
throw new \RuntimeException($e->getMessage());
|
||||
} finally {
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
// Cleanup only - restoration happens in catch block
|
||||
$commands = collect([
|
||||
"rm -rf /tmp/{$uuid}",
|
||||
]);
|
||||
@@ -1643,6 +1641,11 @@ class Application extends BaseModel
|
||||
'initialDockerComposeLocation' => $this->docker_compose_location,
|
||||
];
|
||||
} else {
|
||||
// Restore original values before throwing
|
||||
$this->docker_compose_location = $initialDockerComposeLocation;
|
||||
$this->base_directory = $initialBaseDirectory;
|
||||
$this->save();
|
||||
|
||||
throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
|
||||
}
|
||||
}
|
||||
@@ -1658,7 +1661,7 @@ class Application extends BaseModel
|
||||
$this->custom_labels = base64_encode($customLabels);
|
||||
}
|
||||
$customLabels = base64_decode($this->custom_labels);
|
||||
if (mb_detect_encoding($customLabels, 'ASCII', true) === false) {
|
||||
if (mb_detect_encoding($customLabels, 'UTF-8', true) === false) {
|
||||
$customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n");
|
||||
}
|
||||
$this->custom_labels = base64_encode($customLabels);
|
||||
@@ -1958,64 +1961,6 @@ class Application extends BaseModel
|
||||
}
|
||||
}
|
||||
|
||||
public static function getDomainsByUuid(string $uuid): array
|
||||
{
|
||||
$application = self::where('uuid', $uuid)->first();
|
||||
|
||||
if ($application) {
|
||||
return $application->fqdns;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
$server = $this->destination->server;
|
||||
$container_name = $this->uuid;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$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;
|
||||
if ($server->isMetricsEnabled()) {
|
||||
$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 getLimits(): array
|
||||
{
|
||||
|
||||
@@ -41,6 +41,10 @@ class ApplicationDeploymentQueue extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'finished_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function application()
|
||||
{
|
||||
return $this->belongsTo(Application::class);
|
||||
|
||||
@@ -147,6 +147,12 @@ class EnvironmentVariable extends BaseModel
|
||||
}
|
||||
|
||||
$real_value = $this->get_real_environment_variables($this->value, $resource);
|
||||
|
||||
// Skip escaping for valid JSON objects/arrays to prevent quote corruption (see #6160)
|
||||
if (json_validate($real_value) && (str_starts_with($real_value, '{') || str_starts_with($real_value, '['))) {
|
||||
return $real_value;
|
||||
}
|
||||
|
||||
if ($this->is_literal || $this->is_multiline) {
|
||||
$real_value = '\''.$real_value.'\'';
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Once;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class InstanceSettings extends Model
|
||||
@@ -35,6 +36,9 @@ class InstanceSettings extends Model
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::updated(function ($settings) {
|
||||
// Clear once() cache so subsequent calls get fresh data
|
||||
Once::flush();
|
||||
|
||||
// Clear trusted hosts cache when FQDN changes
|
||||
if ($settings->wasChanged('fqdn')) {
|
||||
\Cache::forget('instance_settings_fqdn_host');
|
||||
@@ -82,7 +86,7 @@ class InstanceSettings extends Model
|
||||
|
||||
public static function get()
|
||||
{
|
||||
return InstanceSettings::findOrFail(0);
|
||||
return once(fn () => InstanceSettings::findOrFail(0));
|
||||
}
|
||||
|
||||
// public function getRecipients($notification)
|
||||
|
||||
@@ -237,7 +237,7 @@ class PrivateKey extends BaseModel
|
||||
$testSuccess = $disk->put($testFilename, 'test');
|
||||
|
||||
if (! $testSuccess) {
|
||||
throw new \Exception('SSH keys storage directory is not writable');
|
||||
throw new \Exception('SSH keys storage directory is not writable. Run on the host: sudo chown -R 9999 /data/coolify/ssh && sudo chmod -R 700 /data/coolify/ssh && docker restart coolify');
|
||||
}
|
||||
|
||||
// Clean up test file
|
||||
|
||||
@@ -15,12 +15,6 @@ use Visus\Cuid2\Cuid2;
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'description' => ['type' => 'string'],
|
||||
'environments' => new OA\Property(
|
||||
property: 'environments',
|
||||
type: 'array',
|
||||
items: new OA\Items(ref: '#/components/schemas/Environment'),
|
||||
description: 'The environments of the project.'
|
||||
),
|
||||
]
|
||||
)]
|
||||
class Project extends BaseModel
|
||||
|
||||
@@ -5,13 +5,35 @@ namespace App\Models;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Scheduled Task model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'id' => ['type' => 'integer', 'description' => 'The unique identifier of the scheduled task in the database.'],
|
||||
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the scheduled task.'],
|
||||
'enabled' => ['type' => 'boolean', 'description' => 'The flag to indicate if the scheduled task is enabled.'],
|
||||
'name' => ['type' => 'string', 'description' => 'The name of the scheduled task.'],
|
||||
'command' => ['type' => 'string', 'description' => 'The command to execute.'],
|
||||
'frequency' => ['type' => 'string', 'description' => 'The frequency of the scheduled task.'],
|
||||
'container' => ['type' => 'string', 'nullable' => true, 'description' => 'The container where the command should be executed.'],
|
||||
'timeout' => ['type' => 'integer', 'description' => 'The timeout of the scheduled task in seconds.'],
|
||||
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the scheduled task was created.'],
|
||||
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the scheduled task was last updated.'],
|
||||
],
|
||||
)]
|
||||
class ScheduledTask extends BaseModel
|
||||
{
|
||||
use HasSafeStringAttribute;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
public static function ownedByCurrentTeamAPI(int $teamId)
|
||||
{
|
||||
return static::where('team_id', $teamId)->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -3,7 +3,23 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Scheduled Task Execution model',
|
||||
type: 'object',
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string', 'description' => 'The unique identifier of the execution.'],
|
||||
'status' => ['type' => 'string', 'enum' => ['success', 'failed', 'running'], 'description' => 'The status of the execution.'],
|
||||
'message' => ['type' => 'string', 'nullable' => true, 'description' => 'The output message of the execution.'],
|
||||
'retry_count' => ['type' => 'integer', 'description' => 'The number of retries.'],
|
||||
'duration' => ['type' => 'number', 'nullable' => true, 'description' => 'Duration in seconds.'],
|
||||
'started_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'When the execution started.'],
|
||||
'finished_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'When the execution finished.'],
|
||||
'created_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'When the record was created.'],
|
||||
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'When the record was last updated.'],
|
||||
],
|
||||
)]
|
||||
class ScheduledTaskExecution extends BaseModel
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
+45
-56
@@ -16,6 +16,7 @@ use App\Notifications\Server\Reachable;
|
||||
use App\Notifications\Server\Unreachable;
|
||||
use App\Services\ConfigurationRepository;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
@@ -103,10 +104,16 @@ use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Server extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, SchemalessAttributesTrait, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, SchemalessAttributesTrait, SoftDeletes;
|
||||
|
||||
public static $batch_counter = 0;
|
||||
|
||||
/**
|
||||
* Identity map cache for request-scoped Server lookups.
|
||||
* Prevents N+1 queries when the same Server is accessed multiple times.
|
||||
*/
|
||||
private static ?array $identityMapCache = null;
|
||||
|
||||
protected $appends = ['is_coolify_host'];
|
||||
|
||||
protected static function booted()
|
||||
@@ -185,6 +192,40 @@ class Server extends BaseModel
|
||||
$server->settings()->delete();
|
||||
$server->sslCertificates()->delete();
|
||||
});
|
||||
|
||||
static::updated(function () {
|
||||
static::flushIdentityMap();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a Server by ID using the identity map cache.
|
||||
* This prevents N+1 queries when the same Server is accessed multiple times.
|
||||
*/
|
||||
public static function findCached($id): ?static
|
||||
{
|
||||
if ($id === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (static::$identityMapCache === null) {
|
||||
static::$identityMapCache = [];
|
||||
}
|
||||
|
||||
if (! isset(static::$identityMapCache[$id])) {
|
||||
static::$identityMapCache[$id] = static::query()->find($id);
|
||||
}
|
||||
|
||||
return static::$identityMapCache[$id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush the identity map cache.
|
||||
* Called automatically on update, and should be called in tests.
|
||||
*/
|
||||
public static function flushIdentityMap(): void
|
||||
{
|
||||
static::$identityMapCache = null;
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
@@ -269,15 +310,6 @@ class Server extends BaseModel
|
||||
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_swarm_worker', false)->whereRelation('settings', 'is_build_server', false)->whereRelation('settings', 'force_disabled', false);
|
||||
}
|
||||
|
||||
public static function destinationsByServer(string $server_id)
|
||||
{
|
||||
$server = Server::ownedByCurrentTeam()->get()->where('id', $server_id)->firstOrFail();
|
||||
$standaloneDocker = collect($server->standaloneDockers->all());
|
||||
$swarmDocker = collect($server->swarmDockers->all());
|
||||
|
||||
return $standaloneDocker->concat($swarmDocker);
|
||||
}
|
||||
|
||||
public function settings()
|
||||
{
|
||||
return $this->hasOne(ServerSetting::class);
|
||||
@@ -667,51 +699,6 @@ $schema://$host {
|
||||
CheckAndStartSentinelJob::dispatch($this);
|
||||
}
|
||||
|
||||
public function getCpuMetrics(int $mins = 5)
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
|
||||
if (str($cpu)->contains('error')) {
|
||||
$error = json_decode($cpu, 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);
|
||||
}
|
||||
$cpu = json_decode($cpu, true);
|
||||
|
||||
return collect($cpu)->map(function ($metric) {
|
||||
return [(int) $metric['time'], (float) $metric['percent']];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getMemoryMetrics(int $mins = 5)
|
||||
{
|
||||
if ($this->isMetricsEnabled()) {
|
||||
$from = now()->subMinutes($mins)->toIso8601ZuluString();
|
||||
$memory = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/memory/history?from=$from'"], $this, false);
|
||||
if (str($memory)->contains('error')) {
|
||||
$error = json_decode($memory, 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);
|
||||
}
|
||||
$memory = json_decode($memory, true);
|
||||
$parsedCollection = collect($memory)->map(function ($metric) {
|
||||
$usedPercent = $metric['usedPercent'] ?? 0.0;
|
||||
|
||||
return [(int) $metric['time'], (float) $usedPercent];
|
||||
});
|
||||
|
||||
return $parsedCollection->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
public function getDiskUsage(): ?string
|
||||
{
|
||||
return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false);
|
||||
@@ -1465,12 +1452,14 @@ $schema://$host {
|
||||
$certificateContent = $caCertificate->ssl_certificate;
|
||||
$caCertPath = config('constants.coolify.base_config_path').'/ssl/';
|
||||
|
||||
$base64Cert = base64_encode($certificateContent);
|
||||
|
||||
$commands = collect([
|
||||
"mkdir -p $caCertPath",
|
||||
"chown -R 9999:root $caCertPath",
|
||||
"chmod -R 700 $caCertPath",
|
||||
"rm -rf $caCertPath/coolify-ca.crt",
|
||||
"echo '{$certificateContent}' > $caCertPath/coolify-ca.crt",
|
||||
"echo '{$base64Cert}' | base64 -d | tee $caCertPath/coolify-ca.crt > /dev/null",
|
||||
"chmod 644 $caCertPath/coolify-ca.crt",
|
||||
]);
|
||||
|
||||
|
||||
+109
-9
@@ -515,6 +515,31 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('RabbitMQ', $data->toArray());
|
||||
break;
|
||||
case $image->is('registry'):
|
||||
$data = collect([]);
|
||||
$registry_user = $this->environment_variables()->where('key', 'SERVICE_USER_REGISTRY')->first();
|
||||
$registry_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_REGISTRY')->first();
|
||||
if ($registry_user) {
|
||||
$data = $data->merge([
|
||||
'Registry User' => [
|
||||
'key' => data_get($registry_user, 'key'),
|
||||
'value' => data_get($registry_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($registry_password) {
|
||||
$data = $data->merge([
|
||||
'Registry Password' => [
|
||||
'key' => data_get($registry_password, 'key'),
|
||||
'value' => data_get($registry_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Docker Registry', $data->toArray());
|
||||
break;
|
||||
case $image->contains('tolgee'):
|
||||
$data = collect([]);
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_TOLGEE')->first();
|
||||
@@ -844,6 +869,30 @@ class Service extends BaseModel
|
||||
}
|
||||
$fields->put('Meilisearch', $data->toArray());
|
||||
break;
|
||||
case $image->contains('linkding'):
|
||||
$data = collect([]);
|
||||
$SERVICE_USER_LINKDING = $this->environment_variables()->where('key', 'SERVICE_USER_LINKDING')->first();
|
||||
$SERVICE_PASSWORD_LINKDING = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LINKDING')->first();
|
||||
if ($SERVICE_USER_LINKDING) {
|
||||
$data = $data->merge([
|
||||
'Superuser Name' => [
|
||||
'key' => data_get($SERVICE_USER_LINKDING, 'key'),
|
||||
'value' => data_get($SERVICE_USER_LINKDING, 'value'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($SERVICE_PASSWORD_LINKDING) {
|
||||
$data = $data->merge([
|
||||
'Superuser Password' => [
|
||||
'key' => data_get($SERVICE_PASSWORD_LINKDING, 'key'),
|
||||
'value' => data_get($SERVICE_PASSWORD_LINKDING, 'value'),
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$fields->put('Linkding', $data->toArray());
|
||||
break;
|
||||
case $image->contains('ghost'):
|
||||
$data = collect([]);
|
||||
$MAIL_OPTIONS_AUTH_PASS = $this->environment_variables()->where('key', 'MAIL_OPTIONS_AUTH_PASS')->first();
|
||||
@@ -1044,6 +1093,65 @@ class Service extends BaseModel
|
||||
|
||||
$fields->put('Strapi', $data->toArray());
|
||||
break;
|
||||
case $image->contains('marckohlbrugge/sessy'):
|
||||
$data = collect([]);
|
||||
$username = $this->environment_variables()->where('key', 'SERVICE_USER_SESSY')->first();
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_SESSY')->first();
|
||||
if ($username) {
|
||||
$data = $data->merge([
|
||||
'HTTP Auth Username' => [
|
||||
'key' => data_get($username, 'key'),
|
||||
'value' => data_get($username, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'HTTP Auth Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Sessy', $data->toArray());
|
||||
break;
|
||||
case $image->contains('coollabsio/openclaw'):
|
||||
$data = collect([]);
|
||||
$username = $this->environment_variables()->where('key', 'AUTH_USERNAME')->first();
|
||||
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_OPENCLAW')->first();
|
||||
$gateway_token = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_64_GATEWAYTOKEN')->first();
|
||||
if ($username) {
|
||||
$data = $data->merge([
|
||||
'Username' => [
|
||||
'key' => data_get($username, 'key'),
|
||||
'value' => data_get($username, 'value'),
|
||||
'readonly' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($password) {
|
||||
$data = $data->merge([
|
||||
'Password' => [
|
||||
'key' => data_get($password, 'key'),
|
||||
'value' => data_get($password, 'value'),
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
if ($gateway_token) {
|
||||
$data = $data->merge([
|
||||
'Gateway Token' => [
|
||||
'key' => data_get($gateway_token, 'key'),
|
||||
'value' => data_get($gateway_token, 'value'),
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
$fields->put('Openclaw', $data->toArray());
|
||||
break;
|
||||
default:
|
||||
$data = collect([]);
|
||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
|
||||
@@ -1408,15 +1516,7 @@ class Service extends BaseModel
|
||||
|
||||
public function environment_variables()
|
||||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||
->orderByRaw("
|
||||
CASE
|
||||
WHEN is_required = true THEN 1
|
||||
WHEN LOWER(key) LIKE 'service_%' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
LOWER(key) ASC
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
|
||||
@@ -88,7 +88,7 @@ class ServiceApplication extends BaseModel
|
||||
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
return data_get($this, 'service.environment.project.team');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
|
||||
@@ -124,7 +124,7 @@ class ServiceDatabase extends BaseModel
|
||||
|
||||
public function team()
|
||||
{
|
||||
return data_get($this, 'environment.project.team');
|
||||
return data_get($this, 'service.environment.project.team');
|
||||
}
|
||||
|
||||
public function workdir()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneClickhouse extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -294,15 +295,7 @@ class StandaloneClickhouse extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
public function runtime_environment_variables()
|
||||
@@ -320,50 +313,6 @@ class StandaloneClickhouse extends BaseModel
|
||||
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;
|
||||
|
||||
@@ -73,6 +73,28 @@ class StandaloneDocker extends BaseModel
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server attribute using identity map caching.
|
||||
* This intercepts lazy-loading to use cached Server lookups.
|
||||
*/
|
||||
public function getServerAttribute(): ?Server
|
||||
{
|
||||
// Use eager loaded data if available
|
||||
if ($this->relationLoaded('server')) {
|
||||
return $this->getRelation('server');
|
||||
}
|
||||
|
||||
// Use identity map for lazy loading
|
||||
$server = Server::findCached($this->server_id);
|
||||
|
||||
// Cache in relation for future access on this instance
|
||||
if ($server) {
|
||||
$this->setRelation('server', $server);
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
public function services()
|
||||
{
|
||||
return $this->morphMany(Service::class, 'destination');
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneDragonfly extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -316,50 +317,6 @@ class StandaloneDragonfly extends BaseModel
|
||||
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;
|
||||
@@ -367,14 +324,6 @@ class StandaloneDragonfly extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneKeydb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -316,50 +317,6 @@ class StandaloneKeydb extends BaseModel
|
||||
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;
|
||||
@@ -367,14 +324,6 @@ class StandaloneKeydb extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -11,7 +12,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -288,15 +289,7 @@ class StandaloneMariadb extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
public function runtime_environment_variables()
|
||||
@@ -319,50 +312,6 @@ class StandaloneMariadb extends BaseModel
|
||||
return $this->morphMany(SslCertificate::class, 'resource');
|
||||
}
|
||||
|
||||
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 true;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMongodb extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -341,50 +342,6 @@ class StandaloneMongodb extends BaseModel
|
||||
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 true;
|
||||
@@ -392,14 +349,6 @@ class StandaloneMongodb extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMysql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -320,50 +321,6 @@ class StandaloneMysql extends BaseModel
|
||||
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 true;
|
||||
@@ -371,14 +328,6 @@ class StandaloneMysql extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandalonePostgresql extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -27,9 +28,23 @@ class StandalonePostgresql extends BaseModel
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function ($database) {
|
||||
// This is really stupid and it took me 1h to figure out why the image was not loading properly. This is exactly the reason why we need to use the action pattern because Model events and Accessors are a fragile mess!
|
||||
$image = (string) ($database->getAttributes()['image'] ?? '');
|
||||
$majorVersion = 0;
|
||||
|
||||
if (preg_match('/:(?:pg)?(\d+)/i', $image, $matches)) {
|
||||
$majorVersion = (int) $matches[1];
|
||||
}
|
||||
|
||||
// PostgreSQL 18+ uses /var/lib/postgresql as mount path
|
||||
// Older versions use /var/lib/postgresql/data
|
||||
$mountPath = $majorVersion >= 18
|
||||
? '/var/lib/postgresql'
|
||||
: '/var/lib/postgresql/data';
|
||||
|
||||
LocalPersistentVolume::create([
|
||||
'name' => 'postgres-data-'.$database->uuid,
|
||||
'mount_path' => '/var/lib/postgresql/data',
|
||||
'mount_path' => $mountPath,
|
||||
'host_path' => null,
|
||||
'resource_id' => $database->id,
|
||||
'resource_type' => $database->getMorphClass(),
|
||||
@@ -322,66 +337,11 @@ class StandalonePostgresql extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
|
||||
public function isBackupSolutionAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
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'] ?? 0.0),
|
||||
];
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasMetrics;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
@@ -10,7 +11,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneRedis extends BaseModel
|
||||
{
|
||||
use ClearsGlobalSearchCache, HasFactory, HasSafeStringAttribute, SoftDeletes;
|
||||
use ClearsGlobalSearchCache, HasFactory, HasMetrics, HasSafeStringAttribute, SoftDeletes;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
@@ -332,50 +333,6 @@ class StandaloneRedis extends BaseModel
|
||||
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;
|
||||
@@ -417,14 +374,6 @@ class StandaloneRedis extends BaseModel
|
||||
|
||||
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
|
||||
");
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,28 @@ class SwarmDocker extends BaseModel
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server attribute using identity map caching.
|
||||
* This intercepts lazy-loading to use cached Server lookups.
|
||||
*/
|
||||
public function getServerAttribute(): ?Server
|
||||
{
|
||||
// Use eager loaded data if available
|
||||
if ($this->relationLoaded('server')) {
|
||||
return $this->getRelation('server');
|
||||
}
|
||||
|
||||
// Use identity map for lazy loading
|
||||
$server = Server::findCached($this->server_id);
|
||||
|
||||
// Cache in relation for future access on this instance
|
||||
if ($server) {
|
||||
$this->setRelation('server', $server);
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
public function services()
|
||||
{
|
||||
return $this->morphMany(Service::class, 'destination');
|
||||
|
||||
+2
-1
@@ -191,7 +191,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
$this->getNotificationSettings('discord')?->isEnabled() ||
|
||||
$this->getNotificationSettings('slack')?->isEnabled() ||
|
||||
$this->getNotificationSettings('telegram')?->isEnabled() ||
|
||||
$this->getNotificationSettings('pushover')?->isEnabled();
|
||||
$this->getNotificationSettings('pushover')?->isEnabled() ||
|
||||
$this->getNotificationSettings('webhook')?->isEnabled();
|
||||
}
|
||||
|
||||
public function subscriptionEnded()
|
||||
|
||||
+43
-21
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Jobs\UpdateStripeCustomerEmailJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword;
|
||||
use App\Traits\DeletesUserSessions;
|
||||
@@ -295,9 +296,10 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
public function isInstanceAdmin()
|
||||
{
|
||||
$found_root_team = Auth::user()->teams->filter(function ($team) {
|
||||
$found_root_team = $this->teams->filter(function ($team) {
|
||||
if ($team->id == 0) {
|
||||
if (! Auth::user()->isAdmin()) {
|
||||
$role = $team->pivot->role;
|
||||
if ($role !== 'admin' && $role !== 'owner') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -310,32 +312,51 @@ class User extends Authenticatable implements SendsEmail
|
||||
return $found_root_team->count() > 0;
|
||||
}
|
||||
|
||||
public function currentTeam()
|
||||
public function currentTeam(): ?Team
|
||||
{
|
||||
return Cache::remember('team:'.Auth::id(), 3600, function () {
|
||||
if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) {
|
||||
return Auth::user()->teams[0];
|
||||
}
|
||||
$sessionTeamId = data_get(session('currentTeam'), 'id');
|
||||
|
||||
return Team::find(session('currentTeam')->id);
|
||||
if (is_null($sessionTeamId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if user actually belongs to this team
|
||||
if (! $this->teams->contains('id', $sessionTeamId)) {
|
||||
session()->forget('currentTeam');
|
||||
Cache::forget('user:'.$this->id.':team:'.$sessionTeamId);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return Cache::remember('user:'.$this->id.':team:'.$sessionTeamId, 3600, function () use ($sessionTeamId) {
|
||||
return Team::find($sessionTeamId);
|
||||
});
|
||||
}
|
||||
|
||||
public function otherTeams()
|
||||
{
|
||||
return Auth::user()->teams->filter(function ($team) {
|
||||
return $team->id != currentTeam()->id;
|
||||
});
|
||||
}
|
||||
|
||||
public function role()
|
||||
public function role(): ?string
|
||||
{
|
||||
if (data_get($this, 'pivot')) {
|
||||
return $this->pivot->role;
|
||||
}
|
||||
$user = Auth::user()->teams->where('id', currentTeam()->id)->first();
|
||||
|
||||
return data_get($user, 'pivot.role');
|
||||
$current = $this->currentTeam();
|
||||
if (is_null($current)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$team = $this->teams->where('id', $current->id)->first();
|
||||
|
||||
return data_get($team, 'pivot.role');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's role in a specific team
|
||||
*/
|
||||
public function roleInTeam(int $teamId): ?string
|
||||
{
|
||||
$team = $this->teams->where('id', $teamId)->first();
|
||||
|
||||
return data_get($team, 'pivot.role');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,9 +436,10 @@ class User extends Authenticatable implements SendsEmail
|
||||
]);
|
||||
|
||||
// For cloud users, dispatch job to update Stripe customer email asynchronously
|
||||
if (isCloud() && $this->currentTeam()->subscription) {
|
||||
dispatch(new \App\Jobs\UpdateStripeCustomerEmailJob(
|
||||
$this->currentTeam(),
|
||||
$currentTeam = $this->currentTeam();
|
||||
if (isCloud() && $currentTeam?->subscription) {
|
||||
dispatch(new UpdateStripeCustomerEmailJob(
|
||||
$currentTeam,
|
||||
$this->id,
|
||||
$newEmail,
|
||||
$oldEmail
|
||||
|
||||
Reference in new issue
Block a user