Merge branch 'coollabsio:main' into vaultwarden-fix

This commit is contained in:
Evan
2024-05-27 16:04:02 +08:00
committed by GitHub
275 changed files with 4106 additions and 1928 deletions

View File

@@ -113,6 +113,18 @@ class Application extends BaseModel
}
return null;
}
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.application.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid
]);
}
return null;
}
public function settings()
{
return $this->hasOne(ApplicationSetting::class);
@@ -146,9 +158,13 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/tree/{$this->git_branch}";
}
return $this->git_repository;
}
);
}
@@ -159,6 +175,11 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/settings/hooks";
}
return $this->git_repository;
}
);
@@ -171,10 +192,29 @@ class Application extends BaseModel
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
}
// Convert the SSH URL to HTTPS URL
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commits/{$this->git_branch}";
}
return $this->git_repository;
}
);
}
public function gitCommitLink($link): string
{
if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
if (str($this->source->html_url)->contains('bitbucket')) {
return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
}
return "{$this->source->html_url}/{$this->git_repository}/commit/{$link}";
}
if (strpos($this->git_repository, 'git@') === 0) {
$git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository);
return "https://{$git_repository}/commit/{$link}";
}
return $this->git_repository;
}
public function dockerfileLocation(): Attribute
{
return Attribute::make(
@@ -429,6 +469,10 @@ class Application extends BaseModel
}
return false;
}
public function get_last_successful_deployment()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('status', 'finished')->where('pull_request_id', 0)->orderBy('created_at', 'desc')->first();
}
public function get_last_days_deployments()
{
return ApplicationDeploymentQueue::where('application_id', $this->id)->where('created_at', '>=', now()->subDays(7))->orderBy('created_at', 'desc')->get();
@@ -847,7 +891,7 @@ class Application extends BaseModel
if (!$composeFileContent) {
$this->docker_compose_location = $initialDockerComposeLocation;
$this->save();
throw new \RuntimeException("Could not load base compose file from $workdir$composeFile");
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.");
} else {
$this->docker_compose_raw = $composeFileContent;
$this->save();
@@ -963,4 +1007,52 @@ class Application extends BaseModel
{
getFilesystemVolumesFromServer($this, $isInit);
}
public function parseHealthcheckFromDockerfile($dockerfile, bool $isInit = false)
{
if (str($dockerfile)->contains('HEALTHCHECK') && ($this->isHealthcheckDisabled() || $isInit)) {
$healthcheckCommand = null;
$lines = $dockerfile->toArray();
foreach ($lines as $line) {
$trimmedLine = trim($line);
if (str_starts_with($trimmedLine, 'HEALTHCHECK')) {
$healthcheckCommand .= trim($trimmedLine, '\\ ');
continue;
}
if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
$healthcheckCommand .= ' ' . trim($trimmedLine, '\\ ');
}
if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) {
$healthcheckCommand .= ' ' . $trimmedLine;
break;
}
}
if (str($healthcheckCommand)->isNotEmpty()) {
$interval = str($healthcheckCommand)->match('/--interval=(\d+)/');
$timeout = str($healthcheckCommand)->match('/--timeout=(\d+)/');
$start_period = str($healthcheckCommand)->match('/--start-period=(\d+)/');
$start_interval = str($healthcheckCommand)->match('/--start-interval=(\d+)/');
$retries = str($healthcheckCommand)->match('/--retries=(\d+)/');
if ($interval->isNotEmpty()) {
$this->health_check_interval = $interval->toInteger();
}
if ($timeout->isNotEmpty()) {
$this->health_check_timeout = $timeout->toInteger();
}
if ($start_period->isNotEmpty()) {
$this->health_check_start_period = $start_period->toInteger();
}
// if ($start_interval) {
// $this->health_check_start_interval = $start_interval->value();
// }
if ($retries->isNotEmpty()) {
$this->health_check_retries = $retries->toInteger();
}
if ($interval || $timeout || $start_period || $start_interval || $retries) {
$this->custom_healthcheck_found = true;
$this->save();
}
}
}
}
}

View File

@@ -9,7 +9,8 @@ class ApplicationDeploymentQueue extends Model
{
protected $guarded = [];
public function setStatus(string $status) {
public function setStatus(string $status)
{
$this->update([
'status' => $status,
]);
@@ -21,7 +22,13 @@ class ApplicationDeploymentQueue extends Model
}
return collect(json_decode($this->logs))->where('name', $name)->first()?->output ?? null;
}
public function commitMessage()
{
if (empty($this->commit_message) || is_null($this->commit_message)) {
return null;
}
return str($this->commit_message)->trim()->limit(50)->value();
}
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false)
{
if ($type === 'error') {

View File

@@ -8,6 +8,18 @@ use Illuminate\Database\Eloquent\Model;
class Environment extends Model
{
protected $guarded = [];
protected static function booted()
{
static::deleting(function ($environment) {
$shared_variables = $environment->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting environment shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
});
}
public function isEmpty()
{
return $this->applications()->count() == 0 &&

View File

@@ -25,6 +25,11 @@ class Project extends BaseModel
static::deleting(function ($project) {
$project->environments()->delete();
$project->settings()->delete();
$shared_variables = $project->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting project shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
});
}
public function environment_variables()
@@ -55,6 +60,7 @@ class Project extends BaseModel
return $this->hasManyThrough(Application::class, Environment::class);
}
public function postgresqls()
{
return $this->hasManyThrough(StandalonePostgresql::class, Environment::class);
@@ -91,4 +97,7 @@ class Project extends BaseModel
{
return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->services()->count() + $this->clickhouses()->count();
}
public function databases() {
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());
}
}

View File

@@ -3,11 +3,14 @@
namespace App\Models;
use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
use App\Jobs\PullSentinelImageJob;
use App\Notifications\Server\Revived;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Spatie\SchemalessAttributes\Casts\SchemalessAttributes;
@@ -239,7 +242,7 @@ respond 404
$dynamic_config_path = $this->proxyPath() . "/dynamic";
if ($this->proxyType() === 'TRAEFIK_V2') {
$file = "$dynamic_config_path/coolify.yaml";
if (empty($settings->fqdn)) {
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -358,7 +361,7 @@ respond 404
}
} else if ($this->proxyType() === 'CADDY') {
$file = "$dynamic_config_path/coolify.caddy";
if (empty($settings->fqdn)) {
if (empty($settings->fqdn) || (isCloud() && $this->id !== 0)) {
instant_remote_process([
"rm -f $file",
], $this);
@@ -462,6 +465,36 @@ $schema://$host {
Storage::disk('ssh-keys')->delete($sshKeyFileLocation);
Storage::disk('ssh-mux')->delete($this->muxFilename());
}
public function checkSentinel()
{
ray("Checking sentinel on server: {$this->name}");
if ($this->is_metrics_enabled) {
$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') {
ray('Sentinel is not running, starting it...');
PullSentinelImageJob::dispatch($this);
} else {
ray('Sentinel is running');
}
}
}
public function getMetrics()
{
if ($this->is_metrics_enabled) {
$from = now()->subMinutes(5)->toIso8601ZuluString();
$cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl http://localhost:8888/api/cpu/history?from=$from'"], $this, false);
$cpu = str($cpu)->explode("\n")->skip(1)->all();
$parsedCollection = collect($cpu)->flatMap(function ($item) {
return collect(explode("\n", trim($item)))->map(function ($line) {
list($time, $value) = explode(',', trim($line));
return [(int) $time, (float) $value];
});
})->toArray();
return $parsedCollection;
}
}
public function isServerReady(int $tries = 3)
{
if ($this->skipServer()) {
@@ -548,7 +581,36 @@ $schema://$host {
{
return instant_remote_process(["docker start $id"], $this);
}
public function loadUnmanagedContainers()
public function getContainers(): 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);
} else {
if ($this->isSwarm()) {
$containers = instant_remote_process(["docker service inspect $(docker service ls -q) --format '{{json .}}'"], $this, false);
} else {
$containers = instant_remote_process(["docker container ls -q"], $this, false);
if (!$containers) {
return collect([]);
}
$containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false);
}
if (is_null($containers)) {
return collect([]);
}
return format_docker_command_output_to_json($containers);
}
}
public function loadUnmanagedContainers(): Collection
{
if ($this->isFunctional()) {
$containers = instant_remote_process(["docker ps -a --format '{{json .}}'"], $this);

View File

@@ -171,7 +171,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Tolgee', $data);
$fields->put('Tolgee', $data->toArray());
break;
case str($image)?->contains('logto'):
$data = collect([]);
@@ -195,7 +195,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Logto', $data);
$fields->put('Logto', $data->toArray());
break;
case str($image)?->contains('unleash-server'):
$data = collect([]);
@@ -218,7 +218,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Unleash', $data);
$fields->put('Unleash', $data->toArray());
break;
case str($image)?->contains('grafana'):
$data = collect([]);
@@ -241,7 +241,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Grafana', $data);
$fields->put('Grafana', $data->toArray());
break;
case str($image)?->contains('directus'):
$data = collect([]);
@@ -267,7 +267,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Directus', $data);
$fields->put('Directus', $data->toArray());
break;
case str($image)?->contains('kong'):
$data = collect([]);
@@ -370,7 +370,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Weblate', $data);
$fields->put('Weblate', $data->toArray());
break;
case str($image)?->contains('meilisearch'):
$data = collect([]);
@@ -384,7 +384,7 @@ class Service extends BaseModel
],
]);
}
$fields->put('Meilisearch', $data);
$fields->put('Meilisearch', $data->toArray());
break;
case str($image)?->contains('ghost'):
$data = collect([]);
@@ -444,7 +444,33 @@ class Service extends BaseModel
]);
}
$fields->put('Ghost', $data);
$fields->put('Ghost', $data->toArray());
break;
default:
$data = collect([]);
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_ADMIN')->first();
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ADMIN')->first();
if ($admin_user) {
$data = $data->merge([
'User' => [
'key' => 'SERVICE_USER_ADMIN',
'value' => data_get($admin_user, 'value', 'admin'),
'readonly' => true,
'rules' => 'required',
],
]);
}
if ($admin_password) {
$data = $data->merge([
'Password' => [
'key' => 'SERVICE_PASSWORD_ADMIN',
'value' => data_get($admin_password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
}
$fields->put('Admin', $data->toArray());
break;
case str($image)?->contains('vaultwarden'):
$data = collect([]);
@@ -723,6 +749,18 @@ class Service extends BaseModel
}
return null;
}
public function failedTaskLink($task_uuid)
{
if (data_get($this, 'environment.project.uuid')) {
return route('project.service.scheduled-tasks', [
'project_uuid' => data_get($this, 'environment.project.uuid'),
'environment_name' => data_get($this, 'environment.name'),
'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid
]);
}
return null;
}
public function documentation()
{
$services = getServiceTemplates();
@@ -749,6 +787,17 @@ class Service extends BaseModel
{
return $this->belongsTo(Server::class);
}
public function byUuid(string $uuid) {
$app = $this->applications()->whereUuid($uuid)->first();
if ($app) {
return $app;
}
$db = $this->databases()->whereUuid($uuid)->first();
if ($db) {
return $db;
}
return null;
}
public function byName(string $name)
{
$app = $this->applications()->whereName($name)->first();

View File

@@ -207,7 +207,4 @@ class StandaloneClickhouse extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return $this->clickhouse_db;
}
}

View File

@@ -207,7 +207,4 @@ class StandaloneDragonfly extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return '0';
}
}

View File

@@ -208,7 +208,4 @@ class StandaloneKeydb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return '0';
}
}

View File

@@ -208,7 +208,4 @@ class StandaloneMariadb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return $this->mariadb_database;
}
}

View File

@@ -223,7 +223,4 @@ class StandaloneMongodb extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return $this->mongo_db;
}
}

View File

@@ -209,7 +209,4 @@ class StandaloneMysql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return $this->mysql_database;
}
}

View File

@@ -208,7 +208,4 @@ class StandalonePostgresql extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return $this->postgres_db;
}
}

View File

@@ -204,7 +204,4 @@ class StandaloneRedis extends BaseModel
{
return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
}
public function database_name() {
return '0';
}
}

View File

@@ -26,6 +26,34 @@ class Team extends Model implements SendsDiscord, SendsEmail
throw new \Exception('You are not allowed to update this team.');
}
});
static::deleting(function ($team) {
$keys = $team->privateKeys;
foreach ($keys as $key) {
ray('Deleting key: ' . $key->name);
$key->delete();
}
$sources = $team->sources();
foreach ($sources as $source) {
ray('Deleting source: ' . $source->name);
$source->delete();
}
$tags = Tag::whereTeamId($team->id)->get();
foreach ($tags as $tag) {
ray('Deleting tag: ' . $tag->name);
$tag->delete();
}
$shared_variables = $team->environment_variables();
foreach ($shared_variables as $shared_variable) {
ray('Deleting team shared variable: ' . $shared_variable->name);
$shared_variable->delete();
}
$s3s = $team->s3s;
foreach ($s3s as $s3) {
ray('Deleting s3: ' . $s3->name);
$s3->delete();
}
});
}
public function routeNotificationForDiscord()

View File

@@ -183,6 +183,7 @@ class User extends Authenticatable implements SendsEmail
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
return auth()->user()->teams->where('id', currentTeam()->id)->first()->pivot->role;
$user = auth()->user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}
}