Merge branch 'next' into feat/deployment-token
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class EnvironmentVariable extends Model
|
||||
}
|
||||
}
|
||||
$environment_variable->update([
|
||||
'version' => config('version'),
|
||||
'version' => config('constants.coolify.version'),
|
||||
]);
|
||||
});
|
||||
static::saving(function (EnvironmentVariable $environmentVariable) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user