Merge branch 'next' into docker-network-aliases

This commit is contained in:
Piotr Wójcik
2025-03-16 14:50:26 +01:00
committed by GitHub
556 changed files with 74674 additions and 5618 deletions

View File

@@ -3,6 +3,8 @@
namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use App\Services\ConfigurationGenerator;
use App\Traits\HasConfiguration;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -106,7 +108,7 @@ use Visus\Cuid2\Cuid2;
class Application extends BaseModel
{
use HasFactory, SoftDeletes;
use HasConfiguration, HasFactory, SoftDeletes;
private static $parserVersion = '4';
@@ -328,7 +330,7 @@ class Application extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'application_uuid' => data_get($this, 'uuid'),
]);
}
@@ -341,7 +343,7 @@ class Application extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
$route = route('project.application.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
@@ -613,7 +615,7 @@ class Application extends BaseModel
},
get: function ($value) {
if ($this->additional_servers->count() === 0) {
//running (healthy)
// running (healthy)
if (str($value)->contains('(')) {
$status = str($value)->before('(')->trim()->value();
$health = str($value)->after('(')->before(')')->trim()->value() ?? 'unhealthy';
@@ -698,46 +700,62 @@ class Application extends BaseModel
return $this->settings->is_static ? [80] : $this->ports_exposes_array;
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->orderBy('key', 'asc');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('key', 'not like', 'NIXPACKS_%');
}
// Preview Deployments
public function build_environment_variables(): HasMany
public function build_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('is_build_time', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function nixpacks_environment_variables(): HasMany
public function nixpacks_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', false)->where('key', 'like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', false)
->where('key', 'like', 'NIXPACKS_%');
}
public function environment_variables_preview(): HasMany
public function environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function runtime_environment_variables_preview(): HasMany
public function runtime_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function build_environment_variables_preview(): HasMany
public function build_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('is_build_time', true)->where('key', 'not like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('is_build_time', true)
->where('key', 'not like', 'NIXPACKS_%');
}
public function nixpacks_environment_variables_preview(): HasMany
public function nixpacks_environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->where('key', 'like', 'NIXPACKS_%');
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->where('key', 'like', 'NIXPACKS_%');
}
public function scheduled_tasks(): HasMany
@@ -986,7 +1004,7 @@ class Application extends BaseModel
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$this->source->html_url}/{$customRepository}";
} else {
$github_access_token = generate_github_installation_token($this->source);
$github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
@@ -1098,7 +1116,7 @@ class Application extends BaseModel
$commands->push($git_clone_command);
}
} else {
$github_access_token = generate_github_installation_token($this->source);
$github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
@@ -1627,35 +1645,28 @@ class Application extends BaseModel
}
}
public function getLimits(): array
{
return [
'limits_memory' => $this->limits_memory,
'limits_memory_swap' => $this->limits_memory_swap,
'limits_memory_swappiness' => $this->limits_memory_swappiness,
'limits_memory_reservation' => $this->limits_memory_reservation,
'limits_cpus' => $this->limits_cpus,
'limits_cpuset' => $this->limits_cpuset,
'limits_cpu_shares' => $this->limits_cpu_shares,
];
}
public function generateConfig($is_json = false)
{
$config = collect([]);
if ($this->build_pack = 'nixpacks') {
$config = collect([
'build_pack' => 'nixpacks',
'docker_registry_image_name' => $this->docker_registry_image_name,
'docker_registry_image_tag' => $this->docker_registry_image_tag,
'install_command' => $this->install_command,
'build_command' => $this->build_command,
'start_command' => $this->start_command,
'base_directory' => $this->base_directory,
'publish_directory' => $this->publish_directory,
'custom_docker_run_options' => $this->custom_docker_run_options,
'ports_exposes' => $this->ports_exposes,
'ports_mappings' => $this->ports_mapping,
'settings' => collect([
'is_static' => $this->settings->is_static,
]),
]);
}
$config = $config->filter(function ($value) {
return str($value)->isNotEmpty();
});
$generator = new ConfigurationGenerator($this);
if ($is_json) {
return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
return $generator->toJson();
}
return $config;
return $generator->toArray();
}
public function setConfig($config)

View File

@@ -70,13 +70,18 @@ class ApplicationDeploymentQueue extends Model
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function getHorizonJobStatus()
{
return getJobStatus($this->horizon_job_id);
}
public function commitMessage()
{
if (empty($this->commit_message) || is_null($this->commit_message)) {
return null;
}
return str($this->commit_message)->trim()->limit(50)->value();
return str($this->commit_message)->value();
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class DockerCleanupExecution extends BaseModel
{
protected $guarded = [];
public function server(): BelongsTo
{
return $this->belongsTo(Server::class);
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -18,7 +17,7 @@ use OpenApi\Attributes as OA;
'description' => ['type' => 'string'],
]
)]
class Environment extends Model
class Environment extends BaseModel
{
protected $guarded = [];

View File

@@ -4,9 +4,7 @@ namespace App\Models;
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Environment Variable model',
@@ -14,9 +12,8 @@ use Visus\Cuid2\Cuid2;
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'application_id' => ['type' => 'integer'],
'service_id' => ['type' => 'integer'],
'database_id' => ['type' => 'integer'],
'resourceable_type' => ['type' => 'string'],
'resourceable_id' => ['type' => 'integer'],
'is_build_time' => ['type' => 'boolean'],
'is_literal' => ['type' => 'boolean'],
'is_multiline' => ['type' => 'boolean'],
@@ -31,7 +28,7 @@ use Visus\Cuid2\Cuid2;
'updated_at' => ['type' => 'string'],
]
)]
class EnvironmentVariable extends Model
class EnvironmentVariable extends BaseModel
{
protected $guarded = [];
@@ -42,29 +39,32 @@ class EnvironmentVariable extends Model
'is_multiline' => 'boolean',
'is_preview' => 'boolean',
'version' => 'string',
'resourceable_type' => 'string',
'resourceable_id' => 'integer',
];
protected $appends = ['real_value', 'is_shared', 'is_really_required'];
protected static function booted()
{
static::creating(function (Model $model) {
if (! $model->uuid) {
$model->uuid = (string) new Cuid2;
}
});
static::created(function (EnvironmentVariable $environment_variable) {
if ($environment_variable->application_id && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)->where('application_id', $environment_variable->application_id)->where('is_preview', true)->first();
if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) {
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)
->where('resourceable_type', Application::class)
->where('resourceable_id', $environment_variable->resourceable_id)
->where('is_preview', true)
->first();
if (! $found) {
$application = Application::find($environment_variable->application_id);
if ($application->build_pack !== 'dockerfile') {
$application = Application::find($environment_variable->resourceable_id);
if ($application && $application->build_pack !== 'dockerfile') {
ModelsEnvironmentVariable::create([
'key' => $environment_variable->key,
'value' => $environment_variable->value,
'is_build_time' => $environment_variable->is_build_time,
'is_multiline' => $environment_variable->is_multiline ?? false,
'application_id' => $environment_variable->application_id,
'resourceable_type' => Application::class,
'resourceable_id' => $environment_variable->resourceable_id,
'is_preview' => true,
]);
}
@@ -74,6 +74,7 @@ class EnvironmentVariable extends Model
'version' => config('constants.coolify.version'),
]);
});
static::saving(function (EnvironmentVariable $environmentVariable) {
$environmentVariable->updateIsShared();
});
@@ -92,43 +93,32 @@ class EnvironmentVariable extends Model
);
}
/**
* Get the parent resourceable model.
*/
public function resourceable()
{
return $this->morphTo();
}
public function resource()
{
$resource = null;
if ($this->application_id) {
$resource = Application::find($this->application_id);
} elseif ($this->service_id) {
$resource = Service::find($this->service_id);
} elseif ($this->standalone_postgresql_id) {
$resource = StandalonePostgresql::find($this->standalone_postgresql_id);
} elseif ($this->standalone_redis_id) {
$resource = StandaloneRedis::find($this->standalone_redis_id);
} elseif ($this->standalone_mongodb_id) {
$resource = StandaloneMongodb::find($this->standalone_mongodb_id);
} elseif ($this->standalone_mysql_id) {
$resource = StandaloneMysql::find($this->standalone_mysql_id);
} elseif ($this->standalone_mariadb_id) {
$resource = StandaloneMariadb::find($this->standalone_mariadb_id);
} elseif ($this->standalone_keydb_id) {
$resource = StandaloneKeydb::find($this->standalone_keydb_id);
} elseif ($this->standalone_dragonfly_id) {
$resource = StandaloneDragonfly::find($this->standalone_dragonfly_id);
} elseif ($this->standalone_clickhouse_id) {
$resource = StandaloneClickhouse::find($this->standalone_clickhouse_id);
}
return $resource;
return $this->resourceable;
}
public function realValue(): Attribute
{
$resource = $this->resource();
return Attribute::make(
get: function () use ($resource) {
$env = $this->get_real_environment_variables($this->value, $resource);
get: function () {
if (! $this->relationLoaded('resourceable')) {
$this->load('resourceable');
}
$resource = $this->resourceable;
if (! $resource) {
return null;
}
return data_get($env, 'value', $env);
return $this->get_real_environment_variables($this->value, $resource);
}
);
}
@@ -164,7 +154,6 @@ class EnvironmentVariable extends Model
if ($sharedEnvsFound->isEmpty()) {
return $environment_variable;
}
foreach ($sharedEnvsFound as $sharedEnv) {
$type = str($sharedEnv)->match('/(.*?)\./');
if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {

View File

@@ -33,17 +33,30 @@ class GithubApp extends BaseModel
public static function ownedByCurrentTeam()
{
return GithubApp::whereTeamId(currentTeam()->id);
return GithubApp::where(function ($query) {
$query->where('team_id', currentTeam()->id)
->orWhere('is_system_wide', true);
});
}
public static function public()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get();
return GithubApp::where(function ($query) {
$query->where(function ($q) {
$q->where('team_id', currentTeam()->id)
->orWhere('is_system_wide', true);
})->where('is_public', true);
})->whereNotNull('app_id')->get();
}
public static function private()
{
return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(false)->whereNotNull('app_id')->get();
return GithubApp::where(function ($query) {
$query->where(function ($q) {
$q->where('team_id', currentTeam()->id)
->orWhere('is_system_wide', true);
})->where('is_public', false);
})->whereNotNull('app_id')->get();
}
public function team()

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use App\Events\FileStorageChanged;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class LocalFileVolume extends BaseModel
@@ -11,6 +12,8 @@ class LocalFileVolume extends BaseModel
protected $guarded = [];
public $appends = ['is_binary'];
protected static function booted()
{
static::created(function (LocalFileVolume $fileVolume) {
@@ -19,6 +22,15 @@ class LocalFileVolume extends BaseModel
});
}
protected function isBinary(): Attribute
{
return Attribute::make(
get: function () {
return $this->content === '[binary file]';
}
);
}
public function service()
{
return $this->morphTo('resource');
@@ -44,6 +56,10 @@ class LocalFileVolume extends BaseModel
$isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server);
if ($isFile === 'OK') {
$content = instant_remote_process(["cat $path"], $server, false);
// Check if content contains binary data by looking for null bytes or non-printable characters
if (str_contains($content, "\0") || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', $content)) {
$content = '[binary file]';
}
$this->content = $content;
$this->is_directory = false;
$this->save();

View File

@@ -40,6 +40,8 @@ class PrivateKey extends BaseModel
'private_key' => 'encrypted',
];
protected $appends = ['public_key'];
protected static function booted()
{
static::saving(function ($key) {
@@ -64,6 +66,11 @@ class PrivateKey extends BaseModel
});
}
public function getPublicKeyAttribute()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
}
public function getPublicKey()
{
return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
@@ -208,15 +215,14 @@ class PrivateKey extends BaseModel
{
try {
$key = PublicKeyLoader::load($privateKey);
$publicKey = $key->getPublicKey();
return $publicKey->getFingerprint('sha256');
return $key->getPublicKey()->getFingerprint('sha256');
} catch (\Throwable $e) {
return null;
}
}
private static function fingerprintExists($fingerprint, $excludeId = null)
public static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::query()
->where('fingerprint', $fingerprint)

View File

@@ -3,6 +3,7 @@
namespace App\Models;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Project model',
@@ -24,8 +25,6 @@ class Project extends BaseModel
{
protected $guarded = [];
protected $appends = ['default_environment'];
public static function ownedByCurrentTeam()
{
return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
@@ -40,6 +39,7 @@ class Project extends BaseModel
Environment::create([
'name' => 'production',
'project_id' => $project->id,
'uuid' => (string) new Cuid2,
]);
});
static::deleting(function ($project) {
@@ -141,17 +141,15 @@ class Project extends BaseModel
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
}
public function getDefaultEnvironmentAttribute()
public function navigateTo()
{
$default = $this->environments()->where('name', 'production')->first();
if ($default) {
return $default->name;
}
$default = $this->environments()->get();
if ($default->count() > 0) {
return $default->sortBy('created_at')->first()->name;
if ($this->environments->count() === 1) {
return route('project.resource.index', [
'project_uuid' => $this->uuid,
'environment_uuid' => $this->environments->first()->uuid,
]);
}
return null;
return route('project.show', ['project_uuid' => $this->uuid]);
}
}

View File

@@ -40,21 +40,21 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}";
}
public function isHetzner()
{
return str($this->endpoint)->contains('your-objectstorage.com');
}
public function isDigitalOcean()
{
return str($this->endpoint)->contains('digitaloceanspaces.com');
}
public function testConnection(bool $shouldSave = false)
{
try {
set_s3_target($this);
Storage::disk('custom-s3')->files();
$disk = Storage::build([
'driver' => 's3',
'region' => $this['region'],
'key' => $this['key'],
'secret' => $this['secret'],
'bucket' => $this['bucket'],
'endpoint' => $this['endpoint'],
'use_path_style_endpoint' => true,
]);
// Test the connection by listing files with ListObjectsV2 (S3)
$disk->files();
$this->unusable_email_sent = false;
$this->is_usable = true;
} catch (\Throwable $e) {
@@ -63,13 +63,14 @@ class S3Storage extends BaseModel
$mail = new MailMessage;
$mail->subject('Coolify: S3 Storage Connection Error');
$mail->view('emails.s3-connection-error', ['name' => $this->name, 'reason' => $e->getMessage(), 'url' => route('storage.show', ['storage_uuid' => $this->uuid])]);
$users = collect([]);
$members = $this->team->members()->get();
foreach ($members as $user) {
if ($user->isAdmin()) {
$users->push($user);
}
}
// Load the team with its members and their roles explicitly
$team = $this->team()->with(['members' => function ($query) {
$query->withPivot('role');
}])->first();
// Get admins directly from the pivot relationship for this specific team
$users = $team->members()->wherePivotIn('role', ['admin', 'owner'])->get(['users.id', 'users.email']);
foreach ($users as $user) {
send_user_an_email($mail, $user->email);
}

View File

@@ -17,6 +17,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Stringable;
use OpenApi\Attributes as OA;
@@ -24,6 +25,7 @@ use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
use Spatie\SchemalessAttributes\SchemalessAttributesTrait;
use Spatie\Url\Url;
use Symfony\Component\Yaml\Yaml;
use Visus\Cuid2\Cuid2;
#[OA\Schema(
description: 'Server model',
@@ -54,6 +56,8 @@ class Server extends BaseModel
public static $batch_counter = 0;
protected $appends = ['is_coolify_host'];
protected static function booted()
{
static::saving(function ($server) {
@@ -99,11 +103,13 @@ class Server extends BaseModel
'server_id' => $server->id,
]);
} else {
StandaloneDocker::create([
$standaloneDocker = new StandaloneDocker([
'name' => 'coolify',
'uuid' => (string) new Cuid2,
'network' => 'coolify',
'server_id' => $server->id,
]);
$standaloneDocker->saveQuietly();
}
}
if (! isset($server->proxy->redirect_enabled)) {
@@ -156,6 +162,15 @@ class Server extends BaseModel
return 'server';
}
protected function isCoolifyHost(): Attribute
{
return Attribute::make(
get: function () {
return $this->id === 0;
}
);
}
public static function isReachable()
{
return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true);
@@ -188,6 +203,11 @@ class Server extends BaseModel
return $this->hasOne(ServerSetting::class);
}
public function dockerCleanupExecutions()
{
return $this->hasMany(DockerCleanupExecution::class);
}
public function proxySet()
{
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
@@ -421,10 +441,6 @@ class Server extends BaseModel
"mkdir -p $dynamic_config_path",
"echo '$base64' | base64 -d | tee $file > /dev/null",
], $this);
if (config('app.env') === 'local') {
// ray($yaml);
}
}
} elseif ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
@@ -656,9 +672,9 @@ $schema://$host {
$containers = collect([]);
$containerReplicates = collect([]);
if ($this->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
$containers = instant_remote_process_with_timeout(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = instant_remote_process(["docker service ls --format '{{json .}}'"], $this, false);
$containerReplicates = instant_remote_process_with_timeout(["docker service ls --format '{{json .}}'"], $this, false);
if ($containerReplicates) {
$containerReplicates = format_docker_command_output_to_json($containerReplicates);
foreach ($containerReplicates as $containerReplica) {
@@ -682,7 +698,7 @@ $schema://$host {
}
}
} else {
$containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = instant_remote_process_with_timeout(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false);
$containers = format_docker_command_output_to_json($containers);
$containerReplicates = collect([]);
}
@@ -693,22 +709,6 @@ $schema://$host {
];
}
public function getContainersWithSentinel(): Collection
{
$sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false);
$sentinel_found = json_decode($sentinel_found, true);
$status = data_get($sentinel_found, '0.State.Status', 'exited');
if ($status === 'running') {
$containers = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/containers"'], $this, false);
if (is_null($containers)) {
return collect([]);
}
$containers = data_get(json_decode($containers, true), 'containers', []);
return collect($containers);
}
}
public function loadAllContainers(): Collection
{
if ($this->isFunctional()) {
@@ -954,10 +954,8 @@ $schema://$host {
}
});
if ($supported->count() === 1) {
// ray('supported');
return str($supported->first());
} else {
// ray('not supported');
return false;
}
}
@@ -1026,7 +1024,7 @@ $schema://$host {
$unreachableNotificationSent = (bool) $this->unreachable_notification_sent;
$isReachable = (bool) $this->settings->is_reachable;
\Log::debug('Server reachability check', [
Log::debug('Server reachability check', [
'server_id' => $this->id,
'is_reachable' => $isReachable,
'notification_sent' => $unreachableNotificationSent,
@@ -1038,7 +1036,7 @@ $schema://$host {
$this->save();
if ($unreachableNotificationSent === true) {
\Log::debug('Server is now reachable, sending notification', [
Log::debug('Server is now reachable, sending notification', [
'server_id' => $this->id,
]);
$this->sendReachableNotification();
@@ -1048,7 +1046,7 @@ $schema://$host {
}
$this->increment('unreachable_count');
\Log::debug('Incremented unreachable count', [
Log::debug('Incremented unreachable count', [
'server_id' => $this->id,
'new_count' => $this->unreachable_count,
]);
@@ -1056,7 +1054,7 @@ $schema://$host {
if ($this->unreachable_count === 1) {
$this->settings->is_reachable = true;
$this->settings->save();
\Log::debug('First unreachable attempt, marking as reachable', [
Log::debug('First unreachable attempt, marking as reachable', [
'server_id' => $this->id,
]);
@@ -1067,7 +1065,7 @@ $schema://$host {
$failedChecks = 0;
for ($i = 0; $i < 3; $i++) {
$status = $this->serverStatus();
\Log::debug('Additional reachability check', [
Log::debug('Additional reachability check', [
'server_id' => $this->id,
'attempt' => $i + 1,
'status' => $status,
@@ -1079,7 +1077,7 @@ $schema://$host {
}
if ($failedChecks === 3 && ! $unreachableNotificationSent) {
\Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
Log::debug('Server confirmed unreachable after 3 attempts, sending notification', [
'server_id' => $this->id,
]);
$this->sendUnreachableNotification();
@@ -1325,4 +1323,11 @@ $schema://$host {
throw new \Exception('Invalid proxy type.');
}
}
public function isEmpty()
{
return $this->applications()->count() == 0 &&
$this->databases()->count() == 0 &&
$this->services()->count() == 0;
}
}

View File

@@ -1050,10 +1050,11 @@ class Service extends BaseModel
$fields->put('MySQL', $data->toArray());
break;
case $image->contains('mariadb'):
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', 'SERVICE_USER_MYSQL', 'MYSQL_USER'];
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS', 'MYSQL_PASSWORD'];
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS', 'MYSQL_ROOT_PASSWORD'];
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA', 'MYSQL_DATABASE'];
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
@@ -1102,6 +1103,23 @@ class Service extends BaseModel
break;
}
}
$fields = collect($fields)->map(function ($extraFields) {
if (is_array($extraFields)) {
$extraFields = collect($extraFields)->map(function ($field) {
if (filled($field['value']) && str($field['value'])->startsWith('$SERVICE_')) {
$searchValue = str($field['value'])->after('$')->value;
$newValue = $this->environment_variables()->where('key', $searchValue)->first();
if ($newValue) {
$field['value'] = $newValue->value;
}
}
return $field;
});
}
return $extraFields;
});
return $fields;
}
@@ -1120,7 +1138,8 @@ class Service extends BaseModel
'key' => $key,
'value' => $value,
'is_build_time' => false,
'service_id' => $this->id,
'resourceable_id' => $this->id,
'resourceable_type' => $this->getMorphClass(),
'is_preview' => false,
]);
}
@@ -1132,7 +1151,7 @@ class Service extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'service_uuid' => data_get($this, 'uuid'),
]);
}
@@ -1145,7 +1164,7 @@ class Service extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
$route = route('project.service.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'service_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid,
]);
@@ -1232,14 +1251,17 @@ class Service extends BaseModel
return $this->hasMany(ScheduledTask::class)->orderBy('name', 'asc');
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function environment_variables_preview(): HasMany
public function environment_variables_preview()
{
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->where('is_preview', true)
->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
}
public function workdir()

View File

@@ -78,11 +78,15 @@ class ServiceDatabase extends BaseModel
public function databaseType()
{
$image = str($this->image)->before(':');
if ($image->value() === 'postgres') {
$image = 'postgresql';
if ($image->contains('supabase/postgres')) {
$finalImage = 'supabase/postgres';
} elseif ($image->contains('postgres') || $image->contains('postgis')) {
$finalImage = 'postgresql';
} else {
$finalImage = $image;
}
return "standalone-$image";
return "standalone-$finalImage";
}
public function getServiceDatabaseUrl()

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneClickhouse extends BaseModel
@@ -169,7 +168,7 @@ class StandaloneClickhouse extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,15 @@ class StandaloneClickhouse extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()

View File

@@ -6,6 +6,19 @@ class StandaloneDocker extends BaseModel
{
protected $guarded = [];
protected static function boot()
{
parent::boot();
static::created(function ($newStandaloneDocker) {
$server = $newStandaloneDocker->server;
instant_remote_process([
"docker network inspect $newStandaloneDocker->network >/dev/null 2>&1 || docker network create --driver overlay --attachable $newStandaloneDocker->network >/dev/null",
], $server, false);
$connectProxyToDockerNetworks = connectProxyToNetworks($server);
instant_remote_process($connectProxyToDockerNetworks, $server, false);
});
}
public function applications()
{
return $this->morphMany(Application::class, 'destination');

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneDragonfly extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneDragonfly extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,9 @@ class StandaloneDragonfly extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -319,4 +313,10 @@ class StandaloneDragonfly extends BaseModel
{
return false;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneKeydb extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneKeydb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,9 @@ class StandaloneKeydb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -319,4 +313,10 @@ class StandaloneKeydb extends BaseModel
{
return false;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel
@@ -174,7 +173,7 @@ class StandaloneMariadb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -251,14 +250,15 @@ class StandaloneMariadb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
public function runtime_environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMongodb extends BaseModel
@@ -183,7 +182,7 @@ class StandaloneMongodb extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -271,14 +270,9 @@ class StandaloneMongodb extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -339,4 +333,10 @@ class StandaloneMongodb extends BaseModel
{
return true;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMysql extends BaseModel
@@ -175,7 +174,7 @@ class StandaloneMysql extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -252,14 +251,9 @@ class StandaloneMysql extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -320,4 +314,10 @@ class StandaloneMysql extends BaseModel
{
return true;
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandalonePostgresql extends BaseModel
@@ -170,7 +169,7 @@ class StandalonePostgresql extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -252,14 +251,9 @@ class StandalonePostgresql extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -320,4 +314,10 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray();
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneRedis extends BaseModel
@@ -39,6 +38,12 @@ class StandaloneRedis extends BaseModel
$database->forceFill(['last_online_at' => now()]);
}
});
static::retrieved(function ($database) {
if (! $database->redis_username) {
$database->redis_username = 'default';
}
});
}
protected function serverStatus(): Attribute
@@ -170,7 +175,7 @@ class StandaloneRedis extends BaseModel
if (data_get($this, 'environment.project.uuid')) {
return route('project.database.configuration', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'environment_uuid' => data_get($this, 'environment.uuid'),
'database_uuid' => data_get($this, 'uuid'),
]);
}
@@ -194,8 +199,8 @@ class StandaloneRedis extends BaseModel
{
return Attribute::make(
get: fn () => is_null($this->ports_mappings)
? []
: explode(',', $this->ports_mappings),
? []
: explode(',', $this->ports_mappings),
);
}
@@ -262,14 +267,9 @@ class StandaloneRedis extends BaseModel
return $this->morphTo();
}
public function environment_variables(): HasMany
public function runtime_environment_variables()
{
return $this->hasMany(EnvironmentVariable::class);
}
public function runtime_environment_variables(): HasMany
{
return $this->hasMany(EnvironmentVariable::class);
return $this->morphMany(EnvironmentVariable::class, 'resourceable');
}
public function persistentStorages()
@@ -352,11 +352,22 @@ class StandaloneRedis extends BaseModel
get: function () {
$username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first();
if (! $username) {
return null;
$this->runtime_environment_variables()->create([
'key' => 'REDIS_USERNAME',
'value' => 'default',
]);
return 'default';
}
return $username->value;
}
);
}
public function environment_variables()
{
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
->orderBy('key', 'asc');
}
}

View File

@@ -93,6 +93,15 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
return $servers >= $serverLimit;
}
public function subscriptionPastOverDue()
{
if (isCloud()) {
return $this->subscription?->stripe_past_due;
}
return false;
}
public function serverOverflow()
{
if ($this->serverLimit() < $this->servers->count()) {
@@ -120,22 +129,10 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
return Attribute::make(
get: function () {
if (config('constants.coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted';
} else {
$subscription = data_get($this, 'subscription');
if (is_null($subscription)) {
$subscription = 'zero';
} else {
$subscription = $subscription->type();
}
}
if ($this->custom_server_limit) {
$serverLimit = $this->custom_server_limit;
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
return 999999999999;
}
return $serverLimit ?? 2;
return $this->custom_server_limit ?? 2;
}
);
}
@@ -197,6 +194,7 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
'stripe_past_due' => false,
]);
foreach ($this->servers as $server) {
$server->settings()->update([
@@ -259,8 +257,19 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
public function sources()
{
$sources = collect([]);
$github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get();
$gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get();
$github_apps = GithubApp::where(function ($query) {
$query->where(function ($q) {
$q->where('team_id', $this->id)
->orWhere('is_system_wide', true);
})->where('is_public', false);
})->get();
$gitlab_apps = GitlabApp::where(function ($query) {
$query->where(function ($q) {
$q->where('team_id', $this->id)
->orWhere('is_system_wide', true);
})->where('is_public', false);
})->get();
return $sources->merge($github_apps)->merge($gitlab_apps);
}