Merge branch 'next' into feat/deployment-token

This commit is contained in:
Andras Bacsai
2024-12-09 09:16:59 +01:00
449 changed files with 21219 additions and 5891 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Enums\ApplicationDeploymentStatus;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Process\InvokedProcess;
@@ -98,12 +99,13 @@ use Visus\Cuid2\Cuid2;
'updated_at' => ['type' => 'string', 'format' => 'date-time', 'description' => 'The date and time when the application was last updated.'],
'deleted_at' => ['type' => 'string', 'format' => 'date-time', 'nullable' => true, 'description' => 'The date and time when the application was deleted.'],
'compose_parsing_version' => ['type' => 'string', 'description' => 'How Coolify parse the compose file.'],
'custom_nginx_configuration' => ['type' => 'string', 'nullable' => true, 'description' => 'Custom Nginx configuration base64 encoded.'],
]
)]
class Application extends BaseModel
{
use SoftDeletes;
use HasFactory, SoftDeletes;
private static $parserVersion = '4';
@@ -114,11 +116,11 @@ class Application extends BaseModel
protected static function booted()
{
static::saving(function ($application) {
if ($application->fqdn === '') {
$application->fqdn = null;
}
$payload = [];
if ($application->isDirty('fqdn')) {
if ($application->fqdn === '') {
$application->fqdn = null;
}
$payload['fqdn'] = $application->fqdn;
}
if ($application->isDirty('install_command')) {
@@ -139,6 +141,11 @@ class Application extends BaseModel
if ($application->isDirty('status')) {
$payload['last_online_at'] = now();
}
if ($application->isDirty('custom_nginx_configuration')) {
if ($application->custom_nginx_configuration === '') {
$payload['custom_nginx_configuration'] = null;
}
}
if (count($payload) > 0) {
$application->forceFill($payload);
}
@@ -172,6 +179,11 @@ class Application extends BaseModel
return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
}
public static function ownedByCurrentTeam()
{
return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function getContainersToStop(bool $previewDeployments = false): array
{
$containers = $previewDeployments
@@ -627,6 +639,14 @@ class Application extends BaseModel
);
}
public function customNginxConfiguration(): Attribute
{
return Attribute::make(
set: fn ($value) => base64_encode($value),
get: fn ($value) => base64_decode($value),
);
}
public function portsExposesArray(): Attribute
{
return Attribute::make(
@@ -857,7 +877,7 @@ class Application extends BaseModel
public function isConfigurationChanged(bool $save = false)
{
$newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
$newConfigHash = base64_encode($this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect.$this->custom_nginx_configuration);
if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
$newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
} else {
@@ -887,21 +907,7 @@ class Application extends BaseModel
public function customRepository()
{
preg_match('/(?<=:)\d+(?=\/)/', $this->git_repository, $matches);
$port = 22;
if (count($matches) === 1) {
$port = $matches[0];
$gitHost = str($this->git_repository)->before(':');
$gitRepo = str($this->git_repository)->after('/');
$repository = "$gitHost:$gitRepo";
} else {
$repository = $this->git_repository;
}
return [
'repository' => $repository,
'port' => $port,
];
return convertGitUrl($this->git_repository, $this->deploymentType(), $this->source);
}
public function generateBaseDir(string $uuid)
@@ -934,6 +940,122 @@ class Application extends BaseModel
return $git_clone_command;
}
public function getGitRemoteStatus(string $deployment_uuid)
{
try {
['commands' => $lsRemoteCommand] = $this->generateGitLsRemoteCommands(deployment_uuid: $deployment_uuid, exec_in_docker: false);
instant_remote_process([$lsRemoteCommand], $this->destination->server, true);
return [
'is_accessible' => true,
'error' => null,
];
} catch (\RuntimeException $ex) {
return [
'is_accessible' => false,
'error' => $ex->getMessage(),
];
}
}
public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_in_docker = true)
{
$branch = $this->git_branch;
['repository' => $customRepository, 'port' => $customPort] = $this->customRepository();
$commands = collect([]);
$base_command = 'git ls-remote';
if ($this->deploymentType() === 'source') {
$source_html_url = data_get($this, 'source.html_url');
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
$source_html_url_host = $url['host'];
$source_html_url_scheme = $url['scheme'];
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
if ($this->source->is_public) {
$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);
if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
} else {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
}
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
if ($this->deploymentType() === 'deploy_key') {
$fullRepoUrl = $customRepository;
$private_key = data_get($this, 'private_key.private_key');
if (is_null($private_key)) {
throw new RuntimeException('Private key not found. Please add a private key to the application and try again.');
}
$private_key = base64_encode($private_key);
$base_comamnd = "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" {$base_command} {$customRepository}";
if ($exec_in_docker) {
$commands = collect([
executeInDocker($deployment_uuid, 'mkdir -p /root/.ssh'),
executeInDocker($deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
executeInDocker($deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
]);
} else {
$commands = collect([
'mkdir -p /root/.ssh',
"echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null",
'chmod 600 /root/.ssh/id_rsa',
]);
}
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_comamnd));
} else {
$commands->push($base_comamnd);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
if ($this->deploymentType() === 'other') {
$fullRepoUrl = $customRepository;
$base_command = "{$base_command} {$customRepository}";
$base_command = $this->setGitImportSettings($deployment_uuid, $base_command, public: true);
if ($exec_in_docker) {
$commands->push(executeInDocker($deployment_uuid, $base_command));
} else {
$commands->push($base_command);
}
return [
'commands' => $commands->implode(' && '),
'branch' => $branch,
'fullRepoUrl' => $fullRepoUrl,
];
}
}
public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null)
{
$branch = $this->git_branch;
@@ -1195,16 +1317,47 @@ class Application extends BaseModel
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
$fileList = collect([".$workdir$composeFile"]);
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$getGitVersion = instant_remote_process(['git --version'], $this->destination->server, false);
$gitVersion = str($getGitVersion)->explode(' ')->last();
if (version_compare($gitVersion, '2.35.1', '<')) {
$fileList = $fileList->map(function ($file) {
$parts = explode('/', trim($file, '.'));
$paths = collect();
$currentPath = '';
foreach ($parts as $part) {
$currentPath .= ($currentPath ? '/' : '').$part;
$paths->push($currentPath);
}
return $paths;
})->flatten()->unique()->values();
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
} else {
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",
"cd /tmp/{$uuid}",
$cloneCommand,
'git sparse-checkout init --cone',
"git sparse-checkout set {$fileList->implode(' ')}",
'git read-tree -mu HEAD',
"cat .$workdir$composeFile",
]);
}
try {
$composeFileContent = instant_remote_process($commands, $this->destination->server);
} catch (\Exception $e) {

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Visus\Cuid2\Cuid2;
@@ -18,4 +19,18 @@ abstract class BaseModel extends Model
}
});
}
public function name(): Attribute
{
return new Attribute(
get: fn () => sanitize_string($this->getRawOriginal('name')),
);
}
public function image(): Attribute
{
return new Attribute(
get: fn () => sanitize_string($this->getRawOriginal('image')),
);
}
}

View File

@@ -71,7 +71,7 @@ class EnvironmentVariable extends Model
}
}
$environment_variable->update([
'version' => config('version'),
'version' => config('constants.coolify.version'),
]);
});
static::saving(function (EnvironmentVariable $environmentVariable) {

View File

@@ -218,10 +218,12 @@ class PrivateKey extends BaseModel
private static function fingerprintExists($fingerprint, $excludeId = null)
{
$query = self::where('fingerprint', $fingerprint);
$query = self::query()
->where('fingerprint', $fingerprint)
->where('id', '!=', $excludeId);
if (! is_null($excludeId)) {
$query->where('id', '!=', $excludeId);
if (currentTeam()) {
$query->where('team_id', currentTeam()->id);
}
return $query->exists();

View File

@@ -122,9 +122,18 @@ class Project extends BaseModel
return $this->hasManyThrough(StandaloneMariadb::class, Environment::class);
}
public function resource_count()
public function isEmpty()
{
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->clickhouses()->count() + $this->services()->count();
return $this->applications()->count() == 0 &&
$this->redis()->count() == 0 &&
$this->postgresqls()->count() == 0 &&
$this->mysqls()->count() == 0 &&
$this->keydbs()->count() == 0 &&
$this->dragonflies()->count() == 0 &&
$this->clickhouses()->count() == 0 &&
$this->mariadbs()->count() == 0 &&
$this->mongodbs()->count() == 0 &&
$this->services()->count() == 0;
}
public function databases()

View File

@@ -2,6 +2,7 @@
namespace App\Models;
use App\Actions\Proxy\StartProxy;
use App\Actions\Server\InstallDocker;
use App\Actions\Server\StartSentinel;
use App\Enums\ProxyTypes;
@@ -10,6 +11,7 @@ use App\Notifications\Server\Reachable;
use App\Notifications\Server\Unreachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
@@ -26,28 +28,28 @@ use Symfony\Component\Yaml\Yaml;
description: 'Server model',
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'uuid' => ['type' => 'string'],
'name' => ['type' => 'string'],
'description' => ['type' => 'string'],
'ip' => ['type' => 'string'],
'user' => ['type' => 'string'],
'port' => ['type' => 'integer'],
'proxy' => ['type' => 'object'],
'high_disk_usage_notification_sent' => ['type' => 'boolean'],
'unreachable_notification_sent' => ['type' => 'boolean'],
'unreachable_count' => ['type' => 'integer'],
'validation_logs' => ['type' => 'string'],
'log_drain_notification_sent' => ['type' => 'boolean'],
'swarm_cluster' => ['type' => 'string'],
'delete_unused_volumes' => ['type' => 'boolean'],
'delete_unused_networks' => ['type' => 'boolean'],
'id' => ['type' => 'integer', 'description' => 'The server ID.'],
'uuid' => ['type' => 'string', 'description' => 'The server UUID.'],
'name' => ['type' => 'string', 'description' => 'The server name.'],
'description' => ['type' => 'string', 'description' => 'The server description.'],
'ip' => ['type' => 'string', 'description' => 'The IP address.'],
'user' => ['type' => 'string', 'description' => 'The user.'],
'port' => ['type' => 'integer', 'description' => 'The port number.'],
'proxy' => ['type' => 'object', 'description' => 'The proxy configuration.'],
'proxy_type' => ['type' => 'string', 'enum' => ['traefik', 'caddy', 'none'], 'description' => 'The proxy type.'],
'high_disk_usage_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the high disk usage notification has been sent.'],
'unreachable_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unreachable notification has been sent.'],
'unreachable_count' => ['type' => 'integer', 'description' => 'The unreachable count for your server.'],
'validation_logs' => ['type' => 'string', 'description' => 'The validation logs.'],
'log_drain_notification_sent' => ['type' => 'boolean', 'description' => 'The flag to indicate if the log drain notification has been sent.'],
'swarm_cluster' => ['type' => 'string', 'description' => 'The swarm cluster configuration.'],
'settings' => ['$ref' => '#/components/schemas/ServerSetting'],
]
)]
class Server extends BaseModel
{
use SchemalessAttributesTrait, SoftDeletes;
use HasFactory, SchemalessAttributesTrait, SoftDeletes;
public static $batch_counter = 0;
@@ -64,7 +66,7 @@ class Server extends BaseModel
$server->forceFill($payload);
});
static::saved(function ($server) {
if ($server->privateKey->isDirty()) {
if ($server->privateKey?->isDirty()) {
refresh_server_connection($server->privateKey);
}
});
@@ -103,6 +105,14 @@ class Server extends BaseModel
]);
}
}
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::retrieved(function ($server) {
if (! isset($server->proxy->redirect_enabled)) {
$server->proxy->redirect_enabled = true;
}
});
static::forceDeleting(function ($server) {
@@ -182,73 +192,80 @@ class Server extends BaseModel
return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
}
public function setupDefault404Redirect()
public function setupDefaultRedirect()
{
$banner =
"# This file is generated by Coolify, do not edit it manually.\n".
"# Disable the default redirect to customize (only if you know what are you doing).\n\n";
$dynamic_conf_path = $this->proxyPath().'/dynamic';
$proxy_type = $this->proxyType();
$redirect_enabled = $this->proxy->redirect_enabled ?? true;
$redirect_url = $this->proxy->redirect_url;
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_404.caddy";
}
if (empty($redirect_url)) {
if (isDev()) {
if ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ':80, :443 {
respond 404
}';
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
$this->reloadCaddy();
return;
$dynamic_conf_path = '/data/coolify/proxy/caddy/dynamic';
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $default_redirect_file",
], $this);
return;
}
if ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'HostRegexp(`.+`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => 1,
'middlewares' => [
0 => 'redirect-regexp',
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.yaml";
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$default_redirect_file = "$dynamic_conf_path/default_redirect_503.caddy";
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"rm -f $dynamic_conf_path/default_redirect_404.yaml",
"rm -f $dynamic_conf_path/default_redirect_404.caddy",
], $this);
if ($redirect_enabled === false) {
instant_remote_process(["rm -f $default_redirect_file"], $this);
} else {
if ($proxy_type === ProxyTypes::CADDY->value) {
if (filled($redirect_url)) {
$conf = ":80, :443 {
redir $redirect_url
}";
} else {
$conf = ':80, :443 {
respond 503
}';
}
} elseif ($proxy_type === ProxyTypes::TRAEFIK->value) {
$dynamic_conf = [
'http' => [
'routers' => [
'catchall' => [
'entryPoints' => [
0 => 'http',
1 => 'https',
],
'service' => 'noop',
'rule' => 'PathPrefix(`/`)',
'tls' => [
'certResolver' => 'letsencrypt',
],
'priority' => -1000,
],
],
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [
0 => [
'url' => '',
],
'services' => [
'noop' => [
'loadBalancer' => [
'servers' => [],
],
],
],
],
'middlewares' => [
];
if (filled($redirect_url)) {
$dynamic_conf['http']['routers']['catchall']['middlewares'] = [
0 => 'redirect-regexp',
];
$dynamic_conf['http']['services']['noop']['loadBalancer']['servers'][0] = [
'url' => '',
];
$dynamic_conf['http']['middlewares'] = [
'redirect-regexp' => [
'redirectRegex' => [
'regex' => '(.*)',
@@ -256,32 +273,17 @@ respond 404
'permanent' => false,
],
],
],
],
];
$conf = Yaml::dump($dynamic_conf, 12, 2);
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
$base64 = base64_encode($conf);
} elseif ($proxy_type === ProxyTypes::CADDY->value) {
$conf = ":80, :443 {
redir $redirect_url
}";
$conf =
"# This file is automatically generated by Coolify.\n".
"# Do not edit it manually (only if you know what are you doing).\n\n".
$conf;
];
}
$conf = Yaml::dump($dynamic_conf, 12, 2);
}
$conf = $banner.$conf;
$base64 = base64_encode($conf);
instant_remote_process([
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
}
instant_remote_process([
"mkdir -p $dynamic_conf_path",
"echo '$base64' | base64 -d | tee $default_redirect_file > /dev/null",
], $this);
if ($proxy_type === 'CADDY') {
$this->reloadCaddy();
}
@@ -462,7 +464,7 @@ $schema://$host {
public function proxyPath()
{
$base_path = config('coolify.base_config_path');
$base_path = config('constants.coolify.base_config_path');
$proxyType = $this->proxyType();
$proxy_path = "$base_path/proxy";
// TODO: should use /traefik for already exisiting configurations?
@@ -609,7 +611,9 @@ $schema://$host {
}
$memory = json_decode($memory, true);
$parsedCollection = collect($memory)->map(function ($metric) {
return [(int) $metric['time'], (float) $metric['usedPercent']];
$usedPercent = $metric['usedPercent'] ?? 0.0;
return [(int) $metric['time'], (float) $usedPercent];
});
return $parsedCollection->toArray();
@@ -812,7 +816,7 @@ $schema://$host {
{
return Attribute::make(
get: function ($value) {
return preg_replace('/[^0-9]/', '', $value);
return (int) preg_replace('/[^0-9]/', '', $value);
}
);
}
@@ -974,10 +978,10 @@ $schema://$host {
public function serverStatus(): bool
{
if ($this->isFunctional() === false) {
if ($this->status() === false) {
return false;
}
if ($this->status() === false) {
if ($this->isFunctional() === false) {
return false;
}
@@ -986,10 +990,7 @@ $schema://$host {
public function status(): bool
{
if ($this->isFunctional() === false) {
return false;
}
['uptime' => $uptime] = $this->validateConnection(false);
['uptime' => $uptime] = $this->validateConnection();
if ($uptime === false) {
foreach ($this->applications() as $application) {
$application->status = 'exited';
@@ -1041,7 +1042,7 @@ $schema://$host {
$this->unreachable_notification_sent = false;
$this->save();
$this->refresh();
$this->team->notify(new Reachable($this));
// $this->team->notify(new Reachable($this));
}
public function sendUnreachableNotification()
@@ -1049,21 +1050,17 @@ $schema://$host {
$this->unreachable_notification_sent = true;
$this->save();
$this->refresh();
$this->team->notify(new Unreachable($this));
// $this->team->notify(new Unreachable($this));
}
public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false)
public function validateConnection(bool $justCheckingNewKey = false)
{
config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
config()->set('constants.ssh.mux_enabled', false);
if ($this->skipServer()) {
return ['uptime' => false, 'error' => 'Server skipped.'];
}
try {
// Make sure the private key is stored
if ($this->privateKey) {
$this->privateKey->storeInFileSystem();
}
instant_remote_process(['ls /'], $this);
if ($this->settings->is_reachable === false) {
$this->settings->is_reachable = true;
@@ -1232,7 +1229,7 @@ $schema://$host {
return str($this->ip)->contains(':');
}
public function restartSentinel(bool $async = true): void
public function restartSentinel(bool $async = true)
{
try {
if ($async) {
@@ -1241,7 +1238,7 @@ $schema://$host {
StartSentinel::run($this, true);
}
} catch (\Throwable $e) {
loggy('Error restarting Sentinel: '.$e->getMessage());
return handleError($e);
}
}
@@ -1254,4 +1251,25 @@ $schema://$host {
{
return instant_remote_process(['docker restart '.$containerName], $this, false);
}
public function changeProxy(string $proxyType, bool $async = true)
{
$validProxyTypes = collect(ProxyTypes::cases())->map(function ($proxyType) {
return str($proxyType->value)->lower();
});
if ($validProxyTypes->contains(str($proxyType)->lower())) {
$this->proxy->set('type', str($proxyType)->upper());
$this->proxy->set('status', 'exited');
$this->save();
if ($this->proxySet()) {
if ($async) {
StartProxy::dispatch($this);
} else {
StartProxy::run($this);
}
}
} else {
throw new \Exception('Invalid proxy type.');
}
}
}

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Log;
use OpenApi\Attributes as OA;
#[OA\Schema(
@@ -44,6 +45,8 @@ use OpenApi\Attributes as OA;
'wildcard_domain' => ['type' => 'string'],
'created_at' => ['type' => 'string'],
'updated_at' => ['type' => 'string'],
'delete_unused_volumes' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused volumes should be deleted.'],
'delete_unused_networks' => ['type' => 'boolean', 'description' => 'The flag to indicate if the unused networks should be deleted.'],
]
)]
class ServerSetting extends Model
@@ -63,13 +66,13 @@ class ServerSetting extends Model
static::creating(function ($setting) {
try {
if (str($setting->sentinel_token)->isEmpty()) {
$setting->generateSentinelToken(save: false);
$setting->generateSentinelToken(save: false, ignoreEvent: true);
}
if (str($setting->sentinel_custom_url)->isEmpty()) {
$setting->generateSentinelUrl(save: false);
$setting->generateSentinelUrl(save: false, ignoreEvent: true);
}
} catch (\Throwable $e) {
loggy('Error creating server setting: '.$e->getMessage());
Log::error('Error creating server setting: '.$e->getMessage());
}
});
static::updated(function ($settings) {
@@ -88,7 +91,7 @@ class ServerSetting extends Model
});
}
public function generateSentinelToken(bool $save = true)
public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false)
{
$data = [
'server_uuid' => $this->server->uuid,
@@ -97,13 +100,17 @@ class ServerSetting extends Model
$encrypted = encrypt($token);
$this->sentinel_token = $encrypted;
if ($save) {
$this->save();
if ($ignoreEvent) {
$this->saveQuietly();
} else {
$this->save();
}
}
return $token;
}
public function generateSentinelUrl(bool $save = true)
public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false)
{
$domain = null;
$settings = InstanceSettings::get();
@@ -118,7 +125,11 @@ class ServerSetting extends Model
}
$this->sentinel_custom_url = $domain;
if ($save) {
$this->save();
if ($ignoreEvent) {
$this->saveQuietly();
} else {
$this->save();
}
}
return $domain;

View File

@@ -133,6 +133,11 @@ class Service extends BaseModel
return $this->morphToMany(Tag::class, 'taggable');
}
public static function ownedByCurrentTeam()
{
return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function getContainersToStop(): array
{
$containersToStop = [];
@@ -1166,7 +1171,7 @@ class Service extends BaseModel
$services = get_service_templates();
$service = data_get($services, str($this->name)->beforeLast('-')->value, []);
return data_get($service, 'documentation', config('constants.docs.base_url'));
return data_get($service, 'documentation', config('constants.urls.docs'));
}
public function applications()

View File

@@ -37,6 +37,11 @@ class ServiceApplication extends BaseModel
return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
public static function ownedByCurrentTeam()
{
return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function isRunning()
{
return str($this->status)->contains('running');

View File

@@ -24,6 +24,16 @@ class ServiceDatabase extends BaseModel
});
}
public static function ownedByCurrentTeamAPI(int $teamId)
{
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name');
}
public static function ownedByCurrentTeam()
{
return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name');
}
public function restart()
{
$container_id = $this->name.'-'.$this->service->uuid;

View File

@@ -4,6 +4,7 @@ namespace App\Models;
use App\Notifications\Channels\SendsDiscord;
use App\Notifications\Channels\SendsEmail;
use App\Notifications\Channels\SendsSlack;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Notifications\Notifiable;
@@ -70,7 +71,7 @@ use OpenApi\Attributes as OA;
),
]
)]
class Team extends Model implements SendsDiscord, SendsEmail
class Team extends Model implements SendsDiscord, SendsEmail, SendsSlack
{
use Notifiable;
@@ -127,6 +128,11 @@ class Team extends Model implements SendsDiscord, SendsEmail
];
}
public function routeNotificationForSlack()
{
return data_get($this, 'slack_webhook_url', null);
}
public function getRecepients($notification)
{
$recipients = data_get($notification, 'emails', null);
@@ -165,14 +171,14 @@ class Team extends Model implements SendsDiscord, SendsEmail
return 0;
}
return data_get($team, 'limits.serverLimit', 0);
return data_get($team, 'limits', 0);
}
public function limits(): Attribute
{
return Attribute::make(
get: function () {
if (config('coolify.self_hosted') || $this->id === 0) {
if (config('constants.coolify.self_hosted') || $this->id === 0) {
$subscription = 'self-hosted';
} else {
$subscription = data_get($this, 'subscription');
@@ -187,9 +193,8 @@ class Team extends Model implements SendsDiscord, SendsEmail
} else {
$serverLimit = config('constants.limits.server')[strtolower($subscription)];
}
$sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)];
return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled];
return $serverLimit ?? 2;
}
);
@@ -258,8 +263,15 @@ class Team extends Model implements SendsDiscord, SendsEmail
return $this->hasMany(S3Storage::class)->where('is_usable', true);
}
public function trialEnded()
public function subscriptionEnded()
{
$this->subscription->update([
'stripe_subscription_id' => null,
'stripe_plan_id' => null,
'stripe_cancel_at_period_end' => false,
'stripe_invoice_paid' => false,
'stripe_trial_already_ended' => false,
]);
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => false,
@@ -268,16 +280,6 @@ class Team extends Model implements SendsDiscord, SendsEmail
}
}
public function trialEndedButSubscribed()
{
foreach ($this->servers as $server) {
$server->settings()->update([
'is_usable' => true,
'is_reachable' => true,
]);
}
}
public function isAnyNotificationEnabled()
{
if (isCloud()) {

View File

@@ -28,8 +28,8 @@ class TeamInvitation extends Model
public function isValid()
{
$createdAt = $this->created_at;
$diff = $createdAt->diffInMinutes(now());
if ($diff <= config('constants.invitation.link.expiration')) {
$diff = $createdAt->diffInDays(now());
if ($diff <= config('constants.invitation.link.expiration_days')) {
return true;
} else {
$this->delete();

View File

@@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\URL;
@@ -158,7 +159,7 @@ class User extends Authenticatable implements SendsEmail
public function isAdminFromSession()
{
if (auth()->user()->id === 0) {
if (Auth::id() === 0) {
return true;
}
$teams = $this->teams()->get();
@@ -178,9 +179,9 @@ class User extends Authenticatable implements SendsEmail
public function isInstanceAdmin()
{
$found_root_team = auth()->user()->teams->filter(function ($team) {
$found_root_team = Auth::user()->teams->filter(function ($team) {
if ($team->id == 0) {
if (! auth()->user()->isAdmin()) {
if (! Auth::user()->isAdmin()) {
return false;
}
@@ -195,9 +196,9 @@ class User extends Authenticatable implements SendsEmail
public function currentTeam()
{
return Cache::remember('team:'.auth()->user()->id, 3600, function () {
if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) {
return auth()->user()->teams[0];
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];
}
return Team::find(session('currentTeam')->id);
@@ -206,7 +207,7 @@ class User extends Authenticatable implements SendsEmail
public function otherTeams()
{
return auth()->user()->teams->filter(function ($team) {
return Auth::user()->teams->filter(function ($team) {
return $team->id != currentTeam()->id;
});
}
@@ -216,7 +217,7 @@ class User extends Authenticatable implements SendsEmail
if (data_get($this, 'pivot')) {
return $this->pivot->role;
}
$user = auth()->user()->teams->where('id', currentTeam()->id)->first();
$user = Auth::user()->teams->where('id', currentTeam()->id)->first();
return data_get($user, 'pivot.role');
}