Merge branch 'next' into feat/disable-default-redirect

This commit is contained in:
Kael
2024-11-18 21:02:20 +11:00
committed by GitHub
307 changed files with 18023 additions and 5041 deletions

View File

@@ -98,6 +98,7 @@ 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.'],
]
)]
@@ -114,11 +115,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 +140,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 +178,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 +638,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 +876,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 +906,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 +939,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,6 +1316,11 @@ class Application extends BaseModel
$workdir = rtrim($this->base_directory, '/');
$composeFile = $this->docker_compose_location;
$fileList = collect([".$workdir$composeFile"]);
$gitRemoteStatus = $this->getGitRemoteStatus(deployment_uuid: $uuid);
if (! $gitRemoteStatus['is_accessible']) {
throw new \RuntimeException("Failed to read Git source:\n\n{$gitRemoteStatus['error']}");
}
$commands = collect([
"rm -rf /tmp/{$uuid}",
"mkdir -p /tmp/{$uuid}",

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;
@@ -26,22 +27,23 @@ 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.'],
'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.'],
]
)]
@@ -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);
}
});
@@ -457,7 +459,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?
@@ -969,10 +971,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;
}
@@ -981,9 +983,6 @@ $schema://$host {
public function status(): bool
{
if ($this->isFunctional() === false) {
return false;
}
['uptime' => $uptime] = $this->validateConnection(false);
if ($uptime === false) {
foreach ($this->applications() as $application) {
@@ -1227,7 +1226,7 @@ $schema://$host {
return str($this->ip)->contains(':');
}
public function restartSentinel(bool $async = true): void
public function restartSentinel(bool $async = true)
{
try {
if ($async) {
@@ -1236,7 +1235,7 @@ $schema://$host {
StartSentinel::run($this, true);
}
} catch (\Throwable $e) {
loggy('Error restarting Sentinel: '.$e->getMessage());
return handleError($e);
}
}
@@ -1249,4 +1248,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(
@@ -63,13 +64,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 +89,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 +98,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 +123,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

@@ -165,14 +165,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 +187,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 +257,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 +274,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');
}