Merge branch 'next' into next

This commit is contained in:
Leonardo Cabeza
2024-10-06 14:13:10 -05:00
committed by GitHub
181 changed files with 3632 additions and 1219 deletions

View File

@@ -0,0 +1,103 @@
name: Coolify Realtime Development (v4)
on:
push:
branches: [ "next" ]
paths:
- .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile
- docker/coolify-realtime/terminal-server.js
- docker/coolify-realtime/package.json
- docker/coolify-realtime/soketi-entrypoint.sh
env:
REGISTRY: ghcr.io
IMAGE_NAME: "coollabsio/coolify-realtime"
jobs:
amd64:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
labels: |
coolify.managed=true
aarch64:
runs-on: [ self-hosted, arm64 ]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Build image and push to registry
uses: docker/build-push-action@v5
with:
no-cache: true
context: .
file: docker/coolify-realtime/Dockerfile
platforms: linux/aarch64
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64
labels: |
coolify.managed=true
merge-manifest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
needs: [ amd64, aarch64 ]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to ghcr.io
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get Version
id: version
run: |
echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT
- name: Create & publish manifest
run: |
docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next
- uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }}

View File

@@ -2,7 +2,7 @@ name: Coolify Realtime (v4)
on: on:
push: push:
branches: [ "main", "next" ] branches: [ "main" ]
paths: paths:
- .github/workflows/coolify-realtime.yml - .github/workflows/coolify-realtime.yml
- docker/coolify-realtime/Dockerfile - docker/coolify-realtime/Dockerfile

View File

@@ -46,9 +46,6 @@ class StartDragonfly
'networks' => [ 'networks' => [
$this->database->destination->network, $this->database->destination->network,
], ],
'ulimits' => [
'memlock' => '-1',
],
'labels' => [ 'labels' => [
'coolify.managed' => 'true', 'coolify.managed' => 'true',
], ],

View File

@@ -2,7 +2,6 @@
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -20,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
*/ */
public function create(array $input): User public function create(array $input): User
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (! $settings->is_registration_enabled) { if (! $settings->is_registration_enabled) {
abort(403); abort(403);
} }
@@ -48,7 +47,7 @@ class CreateNewUser implements CreatesNewUsers
$team = $user->teams()->first(); $team = $user->teams()->first();
// Disable registration after first user is created // Disable registration after first user is created
$settings = InstanceSettings::get(); $settings = instanceSettings();
$settings->is_registration_enabled = false; $settings->is_registration_enabled = false;
$settings->save(); $settings->save();
} else { } else {

View File

@@ -2,7 +2,6 @@
namespace App\Actions\License; namespace App\Actions\License;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -13,7 +12,7 @@ class CheckResaleLicense
public function handle() public function handle()
{ {
try { try {
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (isDev()) { if (isDev()) {
$settings->update([ $settings->update([
'is_resale_license_active' => true, 'is_resale_license_active' => true,

View File

@@ -22,7 +22,7 @@ class CheckConfiguration
]; ];
$proxy_configuration = instant_remote_process($payload, $server, false); $proxy_configuration = instant_remote_process($payload, $server, false);
if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) { if ($reset || ! $proxy_configuration || is_null($proxy_configuration)) {
$proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value; $proxy_configuration = str(generate_default_proxy_configuration($server))->trim()->value();
} }
if (! $proxy_configuration || is_null($proxy_configuration)) { if (! $proxy_configuration || is_null($proxy_configuration)) {
throw new \Exception('Could not generate proxy configuration'); throw new \Exception('Could not generate proxy configuration');

View File

@@ -2,14 +2,17 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Enums\ProxyTypes;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
use Symfony\Component\Yaml\Yaml;
class CheckProxy class CheckProxy
{ {
use AsAction; use AsAction;
public function handle(Server $server, $fromUI = false) // It should return if the proxy should be started (true) or not (false)
public function handle(Server $server, $fromUI = false): bool
{ {
if (! $server->isFunctional()) { if (! $server->isFunctional()) {
return false; return false;
@@ -62,22 +65,42 @@ class CheckProxy
$ip = 'host.docker.internal'; $ip = 'host.docker.internal';
} }
$connection80 = @fsockopen($ip, '80'); $portsToCheck = ['80', '443'];
$connection443 = @fsockopen($ip, '443');
$port80 = is_resource($connection80) && fclose($connection80); try {
$port443 = is_resource($connection443) && fclose($connection443); if ($server->proxyType() !== ProxyTypes::NONE->value) {
if ($port80) { $proxyCompose = CheckConfiguration::run($server);
if ($fromUI) { if (isset($proxyCompose)) {
throw new \Exception("Port 80 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); $yaml = Yaml::parse($proxyCompose);
$portsToCheck = [];
if ($server->proxyType() === ProxyTypes::TRAEFIK->value) {
$ports = data_get($yaml, 'services.traefik.ports');
} elseif ($server->proxyType() === ProxyTypes::CADDY->value) {
$ports = data_get($yaml, 'services.caddy.ports');
}
if (isset($ports)) {
foreach ($ports as $port) {
$portsToCheck[] = str($port)->before(':')->value();
}
}
}
} else { } else {
return false; $portsToCheck = [];
} }
} catch (\Exception $e) {
ray($e->getMessage());
} }
if ($port443) { if (count($portsToCheck) === 0) {
if ($fromUI) { return false;
throw new \Exception("Port 443 is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>"); }
} else { foreach ($portsToCheck as $port) {
return false; $connection = @fsockopen($ip, $port);
if (is_resource($connection) && fclose($connection)) {
if ($fromUI) {
throw new \Exception("Port $port is in use.<br>You must stop the process using this port.<br>Docs: <a target='_blank' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' href='https://coollabs.io/discord'>https://coollabs.io/discord</a>");
} else {
return false;
}
} }
} }

View File

@@ -26,7 +26,7 @@ class StartProxy
} }
SaveConfiguration::run($server, $configuration); SaveConfiguration::run($server, $configuration);
$docker_compose_yml_base64 = base64_encode($configuration); $docker_compose_yml_base64 = base64_encode($configuration);
$server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value; $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value();
$server->save(); $server->save();
if ($server->isSwarm()) { if ($server->isSwarm()) {
$commands = $commands->merge([ $commands = $commands->merge([

View File

@@ -2,7 +2,6 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -12,7 +11,7 @@ class CleanupDocker
public function handle(Server $server) public function handle(Server $server)
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$helperImageVersion = data_get($settings, 'helper_version'); $helperImageVersion = data_get($settings, 'helper_version');
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$helperImageWithVersion = "$helperImage:$helperImageVersion"; $helperImageWithVersion = "$helperImage:$helperImageVersion";

View File

@@ -3,7 +3,6 @@
namespace App\Actions\Server; namespace App\Actions\Server;
use App\Jobs\PullHelperImageJob; use App\Jobs\PullHelperImageJob;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
@@ -20,7 +19,7 @@ class UpdateCoolify
public function handle($manual_update = false) public function handle($manual_update = false)
{ {
try { try {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$this->server = Server::find(0); $this->server = Server::find(0);
if (! $this->server) { if (! $this->server) {
return; return;

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Console\Command;
class CheckApplicationDeploymentQueue extends Command
{
protected $signature = 'check:deployment-queue {--force} {--seconds=3600}';
protected $description = 'Check application deployment queue.';
public function handle()
{
$seconds = $this->option('seconds');
$deployments = ApplicationDeploymentQueue::whereIn('status', [
ApplicationDeploymentStatus::IN_PROGRESS,
ApplicationDeploymentStatus::QUEUED,
])->where('created_at', '<=', now()->subSeconds($seconds))->get();
if ($deployments->isEmpty()) {
$this->info('No deployments found in the last '.$seconds.' seconds.');
return;
}
$this->info('Found '.$deployments->count().' deployments created in the last '.$seconds.' seconds.');
foreach ($deployments as $deployment) {
if ($this->option('force')) {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
$this->cancelDeployment($deployment);
} else {
$this->info('Deployment '.$deployment->id.' created at '.$deployment->created_at.' is older than '.$seconds.' seconds. Setting status to failed.');
if ($this->confirm('Do you want to cancel this deployment?', true)) {
$this->cancelDeployment($deployment);
}
}
}
}
private function cancelDeployment(ApplicationDeploymentQueue $deployment)
{
$deployment->update(['status' => ApplicationDeploymentStatus::FAILED]);
if ($deployment->server?->isFunctional()) {
remote_process(['docker rm -f '.$deployment->deployment_uuid], $deployment->server, false);
}
}
}

View File

@@ -7,9 +7,9 @@ use Illuminate\Console\Command;
class CleanupApplicationDeploymentQueue extends Command class CleanupApplicationDeploymentQueue extends Command
{ {
protected $signature = 'cleanup:application-deployment-queue {--team-id=}'; protected $signature = 'cleanup:deployment-queue {--team-id=}';
protected $description = 'CleanupApplicationDeploymentQueue'; protected $description = 'Cleanup application deployment queue.';
public function handle() public function handle()
{ {

View File

@@ -4,6 +4,7 @@ namespace App\Console\Commands;
use App\Jobs\CleanupHelperContainersJob; use App\Jobs\CleanupHelperContainersJob;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
@@ -47,6 +48,17 @@ class CleanupStuckedResources extends Command
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Error in cleaning stucked resources: {$e->getMessage()}\n"; echo "Error in cleaning stucked resources: {$e->getMessage()}\n";
} }
try {
$applicationsDeploymentQueue = ApplicationDeploymentQueue::get();
foreach ($applicationsDeploymentQueue as $applicationDeploymentQueue) {
if (is_null($applicationDeploymentQueue->application)) {
echo "Deleting stuck application deployment queue: {$applicationDeploymentQueue->id}\n";
$applicationDeploymentQueue->delete();
}
}
} catch (\Throwable $e) {
echo "Error in cleaning stuck application deployment queue: {$e->getMessage()}\n";
}
try { try {
$applications = Application::withTrashed()->whereNotNull('deleted_at')->get(); $applications = Application::withTrashed()->whereNotNull('deleted_at')->get();
foreach ($applications as $application) { foreach ($applications as $application) {

View File

@@ -48,6 +48,13 @@ class Dev extends Command
echo "Generating APP_KEY.\n"; echo "Generating APP_KEY.\n";
Artisan::call('key:generate'); Artisan::call('key:generate');
} }
// Generate STORAGE link if not exists
if (! file_exists(public_path('storage'))) {
echo "Generating STORAGE link.\n";
Artisan::call('storage:link');
}
// Seed database if it's empty // Seed database if it's empty
$settings = InstanceSettings::find(0); $settings = InstanceSettings::find(0);
if (! $settings) { if (! $settings) {

View File

@@ -7,7 +7,6 @@ use App\Enums\ActivityTypes;
use App\Enums\ApplicationDeploymentStatus; use App\Enums\ApplicationDeploymentStatus;
use App\Models\ApplicationDeploymentQueue; use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment; use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\Server; use App\Models\Server;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
@@ -69,7 +68,7 @@ class Init extends Command
} catch (\Throwable $e) { } catch (\Throwable $e) {
echo "Could not setup dynamic configuration: {$e->getMessage()}\n"; echo "Could not setup dynamic configuration: {$e->getMessage()}\n";
} }
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (! is_null(env('AUTOUPDATE', null))) { if (! is_null(env('AUTOUPDATE', null))) {
if (env('AUTOUPDATE') == true) { if (env('AUTOUPDATE') == true) {
$settings->update(['is_auto_update_enabled' => true]); $settings->update(['is_auto_update_enabled' => true]);
@@ -196,7 +195,7 @@ class Init extends Command
{ {
$id = config('app.id'); $id = config('app.id');
$version = config('version'); $version = config('version');
$settings = InstanceSettings::get(); $settings = instanceSettings();
$do_not_track = data_get($settings, 'do_not_track'); $do_not_track = data_get($settings, 'do_not_track');
if ($do_not_track == true) { if ($do_not_track == true) {
echo "Skipping alive as do_not_track is enabled\n"; echo "Skipping alive as do_not_track is enabled\n";

View File

@@ -12,8 +12,8 @@ use App\Jobs\PullSentinelImageJob;
use App\Jobs\PullTemplatesFromCDN; use App\Jobs\PullTemplatesFromCDN;
use App\Jobs\ScheduledTaskJob; use App\Jobs\ScheduledTaskJob;
use App\Jobs\ServerCheckJob; use App\Jobs\ServerCheckJob;
use App\Jobs\ServerStorageCheckJob;
use App\Jobs\UpdateCoolifyJob; use App\Jobs\UpdateCoolifyJob;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledTask; use App\Models\ScheduledTask;
use App\Models\Server; use App\Models\Server;
@@ -28,7 +28,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void protected function schedule(Schedule $schedule): void
{ {
$this->all_servers = Server::all(); $this->all_servers = Server::all();
$settings = InstanceSettings::get(); $settings = instanceSettings();
$schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly();
@@ -66,7 +66,7 @@ class Kernel extends ConsoleKernel
private function pull_images($schedule) private function pull_images($schedule)
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4');
foreach ($servers as $server) { foreach ($servers as $server) {
if ($server->isSentinelEnabled()) { if ($server->isSentinelEnabled()) {
@@ -88,7 +88,7 @@ class Kernel extends ConsoleKernel
private function schedule_updates($schedule) private function schedule_updates($schedule)
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$updateCheckFrequency = $settings->update_check_frequency; $updateCheckFrequency = $settings->update_check_frequency;
$schedule->job(new CheckForUpdatesJob) $schedule->job(new CheckForUpdatesJob)
@@ -116,6 +116,7 @@ class Kernel extends ConsoleKernel
} }
foreach ($servers as $server) { foreach ($servers as $server) {
$schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer();
// $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer();
$serverTimezone = $server->settings->server_timezone; $serverTimezone = $server->settings->server_timezone;
if ($server->settings->force_docker_cleanup) { if ($server->settings->force_docker_cleanup) {
$schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer();

View File

@@ -65,7 +65,7 @@ class Handler extends ExceptionHandler
if ($e instanceof RuntimeException) { if ($e instanceof RuntimeException) {
return; return;
} }
$this->settings = \App\Models\InstanceSettings::get(); $this->settings = instanceSettings();
if ($this->settings->do_not_track) { if ($this->settings->do_not_track) {
return; return;
} }

View File

@@ -94,7 +94,9 @@ class SshMultiplexingHelper
$muxPersistTime = config('constants.ssh.mux_persist_time'); $muxPersistTime = config('constants.ssh.mux_persist_time');
$scp_command = "timeout $timeout scp "; $scp_command = "timeout $timeout scp ";
if ($server->isIpv6()) {
$scp_command .= '-6 ';
}
if (self::isMultiplexingEnabled()) { if (self::isMultiplexingEnabled()) {
$scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "; $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
self::ensureMultiplexedConnection($server); self::ensureMultiplexedConnection($server);
@@ -136,8 +138,8 @@ class SshMultiplexingHelper
$ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval')); $ssh_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'));
$command = "PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/host/usr/local/sbin:/host/usr/local/bin:/host/usr/sbin:/host/usr/bin:/host/sbin:/host/bin && $command";
$delimiter = Hash::make($command); $delimiter = Hash::make($command);
$delimiter = base64_encode($delimiter);
$command = str_replace($delimiter, '', $command); $command = str_replace($delimiter, '', $command);
$ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL

View File

@@ -177,6 +177,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -279,6 +280,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -381,6 +383,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -468,6 +471,7 @@ class ApplicationsController extends Controller
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -552,6 +556,7 @@ class ApplicationsController extends Controller
'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'], 'manual_webhook_secret_gitea' => ['type' => 'string', 'description' => 'Manual webhook secret for Gitea.'],
'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']], 'redirect' => ['type' => 'string', 'nullable' => true, 'description' => 'How to set redirect with Traefik / Caddy. www<->non-www.', 'enum' => ['www', 'non-www', 'both']],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -602,6 +607,7 @@ class ApplicationsController extends Controller
'name' => ['type' => 'string', 'description' => 'The application name.'], 'name' => ['type' => 'string', 'description' => 'The application name.'],
'description' => ['type' => 'string', 'description' => 'The application description.'], 'description' => ['type' => 'string', 'description' => 'The application description.'],
'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'], 'instant_deploy' => ['type' => 'boolean', 'description' => 'The flag to indicate if the application should be deployed instantly.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -627,7 +633,7 @@ class ApplicationsController extends Controller
private function create_application(Request $request, $type) private function create_application(Request $request, $type)
{ {
$allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths']; $allowedFields = ['project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'type', 'name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'private_key_uuid', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'redirect', 'github_app_uuid', 'instant_deploy', 'dockerfile', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'watch_paths', 'use_build_server'];
$teamId = getTeamIdFromToken(); $teamId = getTeamIdFromToken();
if (is_null($teamId)) { if (is_null($teamId)) {
return invalidTokenResponse(); return invalidTokenResponse();
@@ -665,6 +671,7 @@ class ApplicationsController extends Controller
$fqdn = $request->domains; $fqdn = $request->domains;
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;
$githubAppUuid = $request->github_app_uuid; $githubAppUuid = $request->github_app_uuid;
$useBuildServer = $request->use_build_server;
$project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first(); $project = Project::whereTeamId($teamId)->whereUuid($request->project_uuid)->first();
if (! $project) { if (! $project) {
@@ -737,6 +744,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -833,6 +844,10 @@ class ApplicationsController extends Controller
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
$application->source_type = $githubApp->getMorphClass(); $application->source_type = $githubApp->getMorphClass();
$application->source_id = $githubApp->id; $application->source_id = $githubApp->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -925,6 +940,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->save(); $application->save();
$application->refresh(); $application->refresh();
if (! $application->settings->is_container_label_readonly_enabled) { if (! $application->settings->is_container_label_readonly_enabled) {
@@ -1004,6 +1023,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1062,6 +1085,10 @@ class ApplicationsController extends Controller
$application->destination_id = $destination->id; $application->destination_id = $destination->id;
$application->destination_type = $destination->getMorphClass(); $application->destination_type = $destination->getMorphClass();
$application->environment_id = $environment->id; $application->environment_id = $environment->id;
if (isset($useBuildServer)) {
$application->settings->is_build_server_enabled = $useBuildServer;
$application->settings->save();
}
$application->git_repository = 'coollabsio/coolify'; $application->git_repository = 'coollabsio/coolify';
$application->git_branch = 'main'; $application->git_branch = 'main';
@@ -1259,16 +1286,10 @@ class ApplicationsController extends Controller
format: 'uuid', format: 'uuid',
) )
), ),
new OA\Parameter( new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
name: 'cleanup', new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
in: 'query', new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
description: 'Delete configurations and volumes.', new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
required: false,
schema: new OA\Schema(
type: 'boolean',
default: true,
)
),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@@ -1316,10 +1337,14 @@ class ApplicationsController extends Controller
'message' => 'Application not found', 'message' => 'Application not found',
], 404); ], 404);
} }
DeleteResourceJob::dispatch( DeleteResourceJob::dispatch(
resource: $application, resource: $application,
deleteConfigurations: $cleanup, deleteConfigurations: $request->query->get('delete_configurations', true),
deleteVolumes: $cleanup); deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
);
return response()->json([ return response()->json([
'message' => 'Application deletion request queued.', 'message' => 'Application deletion request queued.',
@@ -1404,6 +1429,7 @@ class ApplicationsController extends Controller
'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'], 'docker_compose_custom_build_command' => ['type' => 'string', 'description' => 'The Docker Compose custom build command.'],
'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'], 'docker_compose_domains' => ['type' => 'array', 'description' => 'The Docker Compose domains.'],
'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'], 'watch_paths' => ['type' => 'string', 'description' => 'The watch paths.'],
'use_build_server' => ['type' => 'boolean', 'nullable' => true, 'description' => 'Use build server.'],
], ],
)), )),
]), ]),
@@ -1460,7 +1486,7 @@ class ApplicationsController extends Controller
], 404); ], 404);
} }
$server = $application->destination->server; $server = $application->destination->server;
$allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy']; $allowedFields = ['name', 'description', 'is_static', 'domains', 'git_repository', 'git_branch', 'git_commit_sha', 'docker_registry_image_name', 'docker_registry_image_tag', 'build_pack', 'static_image', 'install_command', 'build_command', 'start_command', 'ports_exposes', 'ports_mappings', 'base_directory', 'publish_directory', 'health_check_enabled', 'health_check_path', 'health_check_port', 'health_check_host', 'health_check_method', 'health_check_return_code', 'health_check_scheme', 'health_check_response_text', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'custom_labels', 'custom_docker_run_options', 'post_deployment_command', 'post_deployment_command_container', 'pre_deployment_command', 'pre_deployment_command_container', 'watch_paths', 'manual_webhook_secret_github', 'manual_webhook_secret_gitlab', 'manual_webhook_secret_bitbucket', 'manual_webhook_secret_gitea', 'docker_compose_location', 'docker_compose_raw', 'docker_compose_custom_start_command', 'docker_compose_custom_build_command', 'docker_compose_domains', 'redirect', 'instant_deploy', 'use_build_server'];
$validator = customApiValidator($request->all(), [ $validator = customApiValidator($request->all(), [
sharedDataApplications(), sharedDataApplications(),
@@ -1538,6 +1564,13 @@ class ApplicationsController extends Controller
} }
$instantDeploy = $request->instant_deploy; $instantDeploy = $request->instant_deploy;
$use_build_server = $request->use_build_server;
if (isset($use_build_server)) {
$application->settings->is_build_server_enabled = $use_build_server;
$application->settings->save();
}
removeUnnecessaryFieldsFromRequest($request); removeUnnecessaryFieldsFromRequest($request);
$data = $request->all(); $data = $request->all();

View File

@@ -1541,16 +1541,10 @@ class DatabasesController extends Controller
format: 'uuid', format: 'uuid',
) )
), ),
new OA\Parameter( new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
name: 'cleanup', new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
in: 'query', new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
description: 'Delete configurations and volumes.', new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
required: false,
schema: new OA\Schema(
type: 'boolean',
default: true,
)
),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@@ -1595,10 +1589,14 @@ class DatabasesController extends Controller
if (! $database) { if (! $database) {
return response()->json(['message' => 'Database not found.'], 404); return response()->json(['message' => 'Database not found.'], 404);
} }
DeleteResourceJob::dispatch( DeleteResourceJob::dispatch(
resource: $database, resource: $database,
deleteConfigurations: $cleanup, deleteConfigurations: $request->query->get('delete_configurations', true),
deleteVolumes: $cleanup); deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
);
return response()->json([ return response()->json([
'message' => 'Database deletion request queued.', 'message' => 'Database deletion request queued.',

View File

@@ -86,7 +86,7 @@ class OtherController extends Controller
if ($teamId !== '0') { if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to enable the API.'], 403); return response()->json(['message' => 'You are not allowed to enable the API.'], 403);
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$settings->update(['is_api_enabled' => true]); $settings->update(['is_api_enabled' => true]);
return response()->json(['message' => 'API enabled.'], 200); return response()->json(['message' => 'API enabled.'], 200);
@@ -138,7 +138,7 @@ class OtherController extends Controller
if ($teamId !== '0') { if ($teamId !== '0') {
return response()->json(['message' => 'You are not allowed to disable the API.'], 403); return response()->json(['message' => 'You are not allowed to disable the API.'], 403);
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$settings->update(['is_api_enabled' => false]); $settings->update(['is_api_enabled' => false]);
return response()->json(['message' => 'API disabled.'], 200); return response()->json(['message' => 'API disabled.'], 200);

View File

@@ -308,7 +308,7 @@ class ServersController extends Controller
$projects = Project::where('team_id', $teamId)->get(); $projects = Project::where('team_id', $teamId)->get();
$domains = collect(); $domains = collect();
$applications = $projects->pluck('applications')->flatten(); $applications = $projects->pluck('applications')->flatten();
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if ($applications->count() > 0) { if ($applications->count() > 0) {
foreach ($applications as $application) { foreach ($applications as $application) {
$ip = $application->destination->server->ip; $ip = $application->destination->server->ip;

View File

@@ -432,6 +432,10 @@ class ServicesController extends Controller
tags: ['Services'], tags: ['Services'],
parameters: [ parameters: [
new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')), new OA\Parameter(name: 'uuid', in: 'path', required: true, description: 'Service UUID', schema: new OA\Schema(type: 'string')),
new OA\Parameter(name: 'delete_configurations', in: 'query', required: false, description: 'Delete configurations.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_volumes', in: 'query', required: false, description: 'Delete volumes.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'docker_cleanup', in: 'query', required: false, description: 'Run docker cleanup.', schema: new OA\Schema(type: 'boolean', default: true)),
new OA\Parameter(name: 'delete_connected_networks', in: 'query', required: false, description: 'Delete connected networks.', schema: new OA\Schema(type: 'boolean', default: true)),
], ],
responses: [ responses: [
new OA\Response( new OA\Response(
@@ -476,7 +480,14 @@ class ServicesController extends Controller
if (! $service) { if (! $service) {
return response()->json(['message' => 'Service not found.'], 404); return response()->json(['message' => 'Service not found.'], 404);
} }
DeleteResourceJob::dispatch($service);
DeleteResourceJob::dispatch(
resource: $service,
deleteConfigurations: $request->query->get('delete_configurations', true),
deleteVolumes: $request->query->get('delete_volumes', true),
dockerCleanup: $request->query->get('docker_cleanup', true),
deleteConnectedNetworks: $request->query->get('delete_connected_networks', true)
);
return response()->json([ return response()->json([
'message' => 'Service deletion request queued.', 'message' => 'Service deletion request queued.',
@@ -516,7 +527,8 @@ class ServicesController extends Controller
items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable') items: new OA\Items(ref: '#/components/schemas/EnvironmentVariable')
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -619,7 +631,8 @@ class ServicesController extends Controller
] ]
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -738,7 +751,8 @@ class ServicesController extends Controller
] ]
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -853,7 +867,8 @@ class ServicesController extends Controller
] ]
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -953,7 +968,8 @@ class ServicesController extends Controller
] ]
) )
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1025,9 +1041,11 @@ class ServicesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Service starting request queued.'], 'message' => ['type' => 'string', 'example' => 'Service starting request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1101,9 +1119,11 @@ class ServicesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'], 'message' => ['type' => 'string', 'example' => 'Service stopping request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',
@@ -1177,9 +1197,11 @@ class ServicesController extends Controller
type: 'object', type: 'object',
properties: [ properties: [
'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'], 'message' => ['type' => 'string', 'example' => 'Service restaring request queued.'],
]) ]
)
), ),
]), ]
),
new OA\Response( new OA\Response(
response: 401, response: 401,
ref: '#/components/responses/401', ref: '#/components/responses/401',

View File

@@ -2,7 +2,6 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -22,7 +21,7 @@ class OauthController extends Controller
$oauthUser = get_socialite_provider($provider)->user(); $oauthUser = get_socialite_provider($provider)->user();
$user = User::whereEmail($oauthUser->email)->first(); $user = User::whereEmail($oauthUser->email)->first();
if (! $user) { if (! $user) {
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (! $settings->is_registration_enabled) { if (! $settings->is_registration_enabled) {
abort(403, 'Registration is disabled'); abort(403, 'Registration is disabled');
} }

View File

@@ -14,7 +14,7 @@ class ApiAllowed
if (isCloud()) { if (isCloud()) {
return $next($request); return $next($request);
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if ($settings->is_api_enabled === false) { if ($settings->is_api_enabled === false) {
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403); return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
} }

View File

@@ -12,7 +12,6 @@ use App\Models\ApplicationPreview;
use App\Models\EnvironmentVariable; use App\Models\EnvironmentVariable;
use App\Models\GithubApp; use App\Models\GithubApp;
use App\Models\GitlabApp; use App\Models\GitlabApp;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use App\Models\SwarmDocker; use App\Models\SwarmDocker;
@@ -962,7 +961,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) { if ($this->application->environment_variables->where('key', 'COOLIFY_FQDN')->isEmpty()) {
if ($this->application->compose_parsing_version === '3') { if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_URL={$this->application->fqdn}"); $envs->push("COOLIFY_URL={$this->application->fqdn}");
} else { } else {
$envs->push("COOLIFY_FQDN={$this->application->fqdn}"); $envs->push("COOLIFY_FQDN={$this->application->fqdn}");
@@ -970,7 +969,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
} }
if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) { if ($this->application->environment_variables->where('key', 'COOLIFY_URL')->isEmpty()) {
$url = str($this->application->fqdn)->replace('http://', '')->replace('https://', ''); $url = str($this->application->fqdn)->replace('http://', '')->replace('https://', '');
if ($this->application->compose_parsing_version === '3') { if ((int) $this->application->compose_parsing_version >= 3) {
$envs->push("COOLIFY_FQDN={$url}"); $envs->push("COOLIFY_FQDN={$url}");
} else { } else {
$envs->push("COOLIFY_URL={$url}"); $envs->push("COOLIFY_URL={$url}");
@@ -1334,7 +1333,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
private function prepare_builder_image() private function prepare_builder_image()
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$helperImage = "{$helperImage}:{$settings->helper_version}"; $helperImage = "{$helperImage}:{$settings->helper_version}";
// Get user home directory // Get user home directory

View File

@@ -2,7 +2,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\InstanceSettings;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Queue\ShouldQueue;
@@ -22,7 +21,7 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue
if (isDev() || isCloud()) { if (isDev() || isCloud()) {
return; return;
} }
$settings = InstanceSettings::get(); $settings = instanceSettings();
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) { if ($response->successful()) {
$versions = $response->json(); $versions = $response->json();

View File

@@ -2,9 +2,7 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Database\StopDatabase;
use App\Events\BackupCreated; use App\Events\BackupCreated;
use App\Models\InstanceSettings;
use App\Models\S3Storage; use App\Models\S3Storage;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use App\Models\ScheduledDatabaseBackupExecution; use App\Models\ScheduledDatabaseBackupExecution;
@@ -25,7 +23,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
@@ -64,32 +61,32 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
public function __construct($backup) public function __construct($backup)
{ {
$this->backup = $backup; $this->backup = $backup;
$this->team = Team::find($backup->team_id);
if (is_null($this->team)) {
return;
}
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
} }
public function handle(): void public function handle(): void
{ {
try { try {
// Check if team is exists $this->team = Team::find($this->backup->team_id);
if (is_null($this->team)) { if (! $this->team) {
$this->backup->update(['status' => 'failed']); $this->backup->delete();
StopDatabase::run($this->database);
$this->database->delete();
return; return;
} }
if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->service->server;
$this->s3 = $this->backup->s3;
} else {
$this->database = data_get($this->backup, 'database');
$this->server = $this->database->destination->server;
$this->s3 = $this->backup->s3;
}
if (is_null($this->server)) {
throw new \Exception('Server not found?!');
}
if (is_null($this->database)) {
throw new \Exception('Database not found?!');
}
BackupCreated::dispatch($this->team->id); BackupCreated::dispatch($this->team->id);
@@ -239,7 +236,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
$this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name; $this->backup_dir = backup_dir().'/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name;
if ($this->database->name === 'coolify-db') { if ($this->database->name === 'coolify-db') {
$databasesToBackup = ['coolify']; $databasesToBackup = ['coolify'];
$this->directory_name = $this->container_name = 'coolify-db'; $this->directory_name = $this->container_name = 'coolify-db';
@@ -252,6 +248,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
try { try {
if (str($databaseType)->contains('postgres')) { if (str($databaseType)->contains('postgres')) {
$this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/pg-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -280,6 +279,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mongodb($database); $this->backup_standalone_mongodb($database);
} elseif (str($databaseType)->contains('mysql')) { } elseif (str($databaseType)->contains('mysql')) {
$this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mysql-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mysql-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -289,6 +291,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->backup_standalone_mysql($database); $this->backup_standalone_mysql($database);
} elseif (str($databaseType)->contains('mariadb')) { } elseif (str($databaseType)->contains('mariadb')) {
$this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp'; $this->backup_file = "/mariadb-dump-$database-".Carbon::now()->timestamp.'.dmp';
if ($this->backup->dump_all) {
$this->backup_file = '/mariadb-dump-all-'.Carbon::now()->timestamp.'.gz';
}
$this->backup_location = $this->backup_dir.$this->backup_file; $this->backup_location = $this->backup_dir.$this->backup_file;
$this->backup_log = ScheduledDatabaseBackupExecution::create([ $this->backup_log = ScheduledDatabaseBackupExecution::create([
'database_name' => $database, 'database_name' => $database,
@@ -327,7 +332,9 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage()); send_internal_notification('DatabaseBackupJob failed with: '.$e->getMessage());
throw $e; throw $e;
} finally { } finally {
BackupCreated::dispatch($this->team->id); if ($this->team) {
BackupCreated::dispatch($this->team->id);
}
} }
} }
@@ -386,7 +393,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
if ($this->postgres_password) { if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password"; $backupCommand .= " -e PGPASSWORD=$this->postgres_password";
} }
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location"; if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
} else {
$backupCommand .= " $this->container_name pg_dump --format=custom --no-acl --no-owner --username {$this->database->postgres_user} $database > $this->backup_location";
}
$commands[] = $backupCommand; $commands[] = $backupCommand;
ray($commands); ray($commands);
@@ -407,8 +418,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
ray($commands); $commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress | gzip > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mysqldump -u root -p{$this->database->mysql_root_password} $database > $this->backup_location";
}
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
if ($this->backup_output === '') { if ($this->backup_output === '') {
@@ -426,7 +440,11 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; if ($this->backup->dump_all) {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} --all-databases --single-transaction --quick --lock-tables=false --compress > $this->backup_location";
} else {
$commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location";
}
ray($commands); ray($commands);
$this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = instant_remote_process($commands, $this->server);
$this->backup_output = trim($this->backup_output); $this->backup_output = trim($this->backup_output);
@@ -468,34 +486,6 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
} }
} }
// private function upload_to_s3(): void
// {
// try {
// if (is_null($this->s3)) {
// return;
// }
// $key = $this->s3->key;
// $secret = $this->s3->secret;
// // $region = $this->s3->region;
// $bucket = $this->s3->bucket;
// $endpoint = $this->s3->endpoint;
// $this->s3->testConnection(shouldSave: true);
// $configName = new Cuid2;
// $s3_copy_dir = str($this->backup_location)->replace(backup_dir(), '/var/www/html/storage/app/backups/');
// $commands[] = "docker exec coolify bash -c 'mc config host add {$configName} {$endpoint} $key $secret'";
// $commands[] = "docker exec coolify bash -c 'mc cp $s3_copy_dir {$configName}/{$bucket}{$this->backup_dir}/'";
// instant_remote_process($commands, $this->server);
// $this->add_to_backup_output('Uploaded to S3.');
// } catch (\Throwable $e) {
// $this->add_to_backup_output($e->getMessage());
// throw $e;
// } finally {
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc config remove {$configName}'";
// $removeConfigCommands[] = "docker exec coolify bash -c 'mc alias rm {$configName}'";
// instant_remote_process($removeConfigCommands, $this->server, false);
// }
// }
private function upload_to_s3(): void private function upload_to_s3(): void
{ {
try { try {
@@ -517,10 +507,27 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$this->ensureHelperImageAvailable(); $this->ensureHelperImageAvailable();
$fullImageName = $this->getFullImageName(); $fullImageName = $this->getFullImageName();
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret"; if (isDev()) {
if ($this->database->name === 'coolify-db') {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/coolify/coolify-db-'.$this->server->ip.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
} else {
$backup_location_from = '/var/lib/docker/volumes/coolify_dev_backups_data/_data/databases/'.str($this->team->name)->slug().'-'.$this->team->id.'/'.$this->directory_name.$this->backup_file;
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $backup_location_from:$this->backup_location:ro {$fullImageName}";
}
} else {
$commands[] = "docker run -d --network {$network} --name backup-of-{$this->backup->uuid} --rm -v $this->backup_location:$this->backup_location:ro {$fullImageName}";
}
if ($this->s3->isHetzner()) {
$endpointWithoutBucket = 'https://'.str($endpoint)->after('https://')->after('.')->value();
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc alias set --path=off --api=S3v4 temporary {$endpointWithoutBucket} $key $secret";
} else {
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc config host add temporary {$endpoint} $key $secret";
}
$commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/"; $commands[] = "docker exec backup-of-{$this->backup->uuid} mc cp $this->backup_location temporary/$bucket{$this->backup_dir}/";
instant_remote_process($commands, $this->server); instant_remote_process($commands, $this->server);
$this->add_to_backup_output('Uploaded to S3.'); $this->add_to_backup_output('Uploaded to S3.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->add_to_backup_output($e->getMessage()); $this->add_to_backup_output($e->getMessage());
@@ -562,7 +569,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
private function getFullImageName(): string private function getFullImageName(): string
{ {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$helperImage = config('coolify.helper_image'); $helperImage = config('coolify.helper_image');
$latestVersion = $settings->helper_version; $latestVersion = $settings->helper_version;

View File

@@ -31,10 +31,10 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function __construct( public function __construct(
public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource, public Application|Service|StandalonePostgresql|StandaloneRedis|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $resource,
public bool $deleteConfigurations, public bool $deleteConfigurations = true,
public bool $deleteVolumes, public bool $deleteVolumes = true,
public bool $dockerCleanup, public bool $dockerCleanup = true,
public bool $deleteConnectedNetworks public bool $deleteConnectedNetworks = true
) {} ) {}
public function handle() public function handle()

View File

@@ -2,7 +2,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -26,7 +25,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
$response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json');
if ($response->successful()) { if ($response->successful()) {
$versions = $response->json(); $versions = $response->json();
$settings = InstanceSettings::get(); $settings = instanceSettings();
$latest_version = data_get($versions, 'coolify.helper.version'); $latest_version = data_get($versions, 'coolify.helper.version');
$current_version = $settings->helper_version; $current_version = $settings->helper_version;
if (version_compare($latest_version, $current_version, '>')) { if (version_compare($latest_version, $current_version, '>')) {

View File

@@ -0,0 +1,59 @@
<?php
namespace App\Jobs;
use App\Models\Server;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 1;
public $timeout = 60;
public $containers;
public $applications;
public $databases;
public $services;
public $previews;
public function backoff(): int
{
return isDev() ? 1 : 3;
}
public function __construct(public Server $server) {}
public function handle()
{
try {
if (! $this->server->isFunctional()) {
ray('Server is not ready.');
return 'Server is not ready.';
}
$team = $this->server->team;
$percentage = $this->server->storageCheck();
if ($percentage > 1) {
ray('Server storage is at '.$percentage.'%');
}
} catch (\Throwable $e) {
ray($e->getMessage());
return handleError($e);
}
}
}

View File

@@ -3,7 +3,6 @@
namespace App\Jobs; namespace App\Jobs;
use App\Actions\Server\UpdateCoolify; use App\Actions\Server\UpdateCoolify;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Illuminate\Bus\Queueable; use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeEncrypted;
@@ -23,7 +22,7 @@ class UpdateCoolifyJob implements ShouldBeEncrypted, ShouldQueue
{ {
try { try {
CheckForUpdatesJob::dispatchSync(); CheckForUpdatesJob::dispatchSync();
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (! $settings->new_version_available) { if (! $settings->new_version_available) {
Log::info('No new version available. Skipping update.'); Log::info('No new version available. Skipping update.');

View File

@@ -30,7 +30,7 @@ class Dashboard extends Component
public function cleanup_queue() public function cleanup_queue()
{ {
Artisan::queue('cleanup:application-deployment-queue', [ Artisan::queue('cleanup:deployment-queue', [
'--team-id' => currentTeam()->id, '--team-id' => currentTeam()->id,
]); ]);
} }

View File

@@ -47,7 +47,7 @@ class Help extends Component
] ]
); );
$mail->subject("[HELP]: {$this->subject}"); $mail->subject("[HELP]: {$this->subject}");
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (! $type) { if (! $type) {
$url = 'https://app.coolify.io/api/feedback'; $url = 'https://app.coolify.io/api/feedback';
@@ -61,6 +61,7 @@ class Help extends Component
send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io'); send_user_an_email($mail, auth()->user()?->email, 'hi@coollabs.io');
} }
$this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.'); $this->dispatch('success', 'Feedback sent.', 'We will get in touch with you as soon as possible.');
$this->reset('description', 'subject');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -172,7 +172,7 @@ class Email extends Component
public function copyFromInstanceSettings() public function copyFromInstanceSettings()
{ {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if ($settings->smtp_enabled) { if ($settings->smtp_enabled) {
$team = currentTeam(); $team = currentTeam();
$team->update([ $team->update([

View File

@@ -31,6 +31,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'required|boolean', 'backup.save_s3' => 'required|boolean',
'backup.s3_storage_id' => 'nullable|integer', 'backup.s3_storage_id' => 'nullable|integer',
'backup.databases_to_backup' => 'nullable', 'backup.databases_to_backup' => 'nullable',
'backup.dump_all' => 'required|boolean',
]; ];
protected $validationAttributes = [ protected $validationAttributes = [
@@ -40,6 +41,7 @@ class BackupEdit extends Component
'backup.save_s3' => 'Save to S3', 'backup.save_s3' => 'Save to S3',
'backup.s3_storage_id' => 'S3 Storage', 'backup.s3_storage_id' => 'S3 Storage',
'backup.databases_to_backup' => 'Databases to Backup', 'backup.databases_to_backup' => 'Databases to Backup',
'backup.dump_all' => 'Backup All Databases',
]; ];
protected $messages = [ protected $messages = [

View File

@@ -26,7 +26,7 @@ class ScheduledBackups extends Component
public function mount(): void public function mount(): void
{ {
if ($this->selectedBackupId) { if ($this->selectedBackupId) {
$this->setSelectedBackup($this->selectedBackupId); $this->setSelectedBackup($this->selectedBackupId, true);
} }
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') {
@@ -37,10 +37,13 @@ class ScheduledBackups extends Component
$this->s3s = currentTeam()->s3s; $this->s3s = currentTeam()->s3s;
} }
public function setSelectedBackup($backupId) public function setSelectedBackup($backupId, $force = false)
{ {
if ($this->selectedBackupId === $backupId && ! $force) {
return;
}
$this->selectedBackupId = $backupId; $this->selectedBackupId = $backupId;
$this->selectedBackup = $this->database->scheduledBackups->find($this->selectedBackupId); $this->selectedBackup = $this->database->scheduledBackups->find($backupId);
if (is_null($this->selectedBackup)) { if (is_null($this->selectedBackup)) {
$this->selectedBackupId = null; $this->selectedBackupId = null;
} }

File diff suppressed because one or more lines are too long

View File

@@ -108,8 +108,23 @@ class Navbar extends Component
return; return;
} }
StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse();
$this->dispatch('imagePulled');
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function pullAndRestartEvent()
{
$this->checkDeployments();
if ($this->isDeploymentProgress) {
$this->dispatch('error', 'There is a deployment in progress.');
return;
}
PullImage::run($this->service); PullImage::run($this->service);
StopService::run($this->service); StopService::run(service: $this->service, dockerCleanup: false);
$this->service->parse(); $this->service->parse();
$this->dispatch('imagePulled'); $this->dispatch('imagePulled');
$activity = StartService::run($this->service); $activity = StartService::run($this->service);

View File

@@ -91,10 +91,12 @@ class Danger extends Component
public function delete($password) public function delete($password)
{ {
if (! Hash::check($password, Auth::user()->password)) { if (isProduction()) {
$this->addError('password', 'The provided password is incorrect.'); if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return; return;
}
} }
if (! $this->resource) { if (! $this->resource) {

View File

@@ -48,14 +48,6 @@ class Add extends Component
public function submit() public function submit()
{ {
$this->validate(); $this->validate();
// if (str($this->value)->startsWith('{{') && str($this->value)->endsWith('}}')) {
// $type = str($this->value)->after('{{')->before('.')->value;
// if (! collect(SHARED_VARIABLE_TYPES)->contains($type)) {
// $this->dispatch('error', 'Invalid shared variable type.', 'Valid types are: team, project, environment.');
// return;
// }
// }
$this->dispatch('saveKey', [ $this->dispatch('saveKey', [
'key' => $this->key, 'key' => $this->key,
'value' => $this->value, 'value' => $this->value,

View File

@@ -53,30 +53,16 @@ class All extends Component
public function sortEnvironmentVariables() public function sortEnvironmentVariables()
{ {
if ($this->resource->type() === 'application') { if (! data_get($this->resource, 'settings.is_env_sorting_enabled')) {
$this->resource->load(['environment_variables', 'environment_variables_preview']); if ($this->resource->environment_variables) {
} else { $this->resource->environment_variables = $this->resource->environment_variables->sortBy('order')->values();
$this->resource->load(['environment_variables']); }
if ($this->resource->environment_variables_preview) {
$this->resource->environment_variables_preview = $this->resource->environment_variables_preview->sortBy('order')->values();
}
} }
$sortBy = data_get($this->resource, 'settings.is_env_sorting_enabled') ? 'key' : 'order';
$sortFunction = function ($variables) use ($sortBy) {
if (! $variables) {
return $variables;
}
if ($sortBy === 'key') {
return $variables->sortBy(function ($item) {
return strtolower($item->key);
}, SORT_NATURAL | SORT_FLAG_CASE)->values();
} else {
return $variables->sortBy('order')->values();
}
};
$this->resource->environment_variables = $sortFunction($this->resource->environment_variables);
$this->resource->environment_variables_preview = $sortFunction($this->resource->environment_variables_preview);
$this->getDevView(); $this->getDevView();
} }
@@ -121,6 +107,8 @@ class All extends Component
$this->sortEnvironmentVariables(); $this->sortEnvironmentVariables();
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);
} finally {
$this->refreshEnvs();
} }
} }

View File

@@ -11,6 +11,8 @@ use Livewire\Component;
class ExecuteContainerCommand extends Component class ExecuteContainerCommand extends Component
{ {
public $selected_container = 'default';
public $container; public $container;
public Collection $containers; public Collection $containers;
@@ -83,11 +85,14 @@ class ExecuteContainerCommand extends Component
$containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true); $containers = getCurrentApplicationContainerStatus($server, $this->resource->id, includePullrequests: true);
} }
foreach ($containers as $container) { foreach ($containers as $container) {
$payload = [ // if container state is running
'server' => $server, if (data_get($container, 'State') === 'running') {
'container' => $container, $payload = [
]; 'server' => $server,
$this->containers = $this->containers->push($payload); 'container' => $container,
];
$this->containers = $this->containers->push($payload);
}
} }
} elseif (data_get($this->parameters, 'database_uuid')) { } elseif (data_get($this->parameters, 'database_uuid')) {
if ($this->resource->isRunning()) { if ($this->resource->isRunning()) {
@@ -100,7 +105,6 @@ class ExecuteContainerCommand extends Component
} }
} elseif (data_get($this->parameters, 'service_uuid')) { } elseif (data_get($this->parameters, 'service_uuid')) {
$this->resource->applications()->get()->each(function ($application) { $this->resource->applications()->get()->each(function ($application) {
ray($application);
if ($application->isRunning()) { if ($application->isRunning()) {
$this->containers->push([ $this->containers->push([
'server' => $this->resource->server, 'server' => $this->resource->server,
@@ -131,9 +135,14 @@ class ExecuteContainerCommand extends Component
#[On('connectToContainer')] #[On('connectToContainer')]
public function connectToContainer() public function connectToContainer()
{ {
if ($this->selected_container === 'default') {
$this->dispatch('error', 'Please select a container.');
return;
}
try { try {
$container_name = data_get($this->container, 'container.Names'); $container = collect($this->containers)->firstWhere('container.Names', $this->selected_container);
if (is_null($container_name)) { if (is_null($container)) {
throw new \RuntimeException('Container not found.'); throw new \RuntimeException('Container not found.');
} }
$server = data_get($this->container, 'server'); $server = data_get($this->container, 'server');
@@ -141,11 +150,11 @@ class ExecuteContainerCommand extends Component
if ($server->isForceDisabled()) { if ($server->isForceDisabled()) {
throw new \RuntimeException('Server is disabled.'); throw new \RuntimeException('Server is disabled.');
} }
$this->dispatch(
$this->dispatch('send-terminal-command', 'send-terminal-command',
true, isset($container),
$container_name, data_get($container, 'container.Names'),
$server->uuid, data_get($container, 'server.uuid')
); );
} catch (\Throwable $e) { } catch (\Throwable $e) {

View File

@@ -34,9 +34,9 @@ class Terminal extends Component
if ($status !== 'running') { if ($status !== 'running') {
return; return;
} }
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'PATH=\$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else { } else {
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'"); $command = SshMultiplexingHelper::generateSshCommand($server, 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && if [ -f ~/.profile ]; then . ~/.profile; fi && if [ -n "$SHELL" ]; then exec $SHELL; else sh; fi');
} }
// ssh command is sent back to frontend then to websocket // ssh command is sent back to frontend then to websocket

View File

@@ -15,6 +15,8 @@ class ApiTokens extends Component
public bool $readOnly = true; public bool $readOnly = true;
public bool $rootAccess = false;
public array $permissions = ['read-only']; public array $permissions = ['read-only'];
public $isApiEnabled; public $isApiEnabled;
@@ -35,12 +37,11 @@ class ApiTokens extends Component
if ($this->viewSensitiveData) { if ($this->viewSensitiveData) {
$this->permissions[] = 'view:sensitive'; $this->permissions[] = 'view:sensitive';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['view:sensitive']); $this->permissions = array_diff($this->permissions, ['view:sensitive']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
$this->permissions = ['*'];
}
} }
public function updatedReadOnly() public function updatedReadOnly()
@@ -48,11 +49,30 @@ class ApiTokens extends Component
if ($this->readOnly) { if ($this->readOnly) {
$this->permissions[] = 'read-only'; $this->permissions[] = 'read-only';
$this->permissions = array_diff($this->permissions, ['*']); $this->permissions = array_diff($this->permissions, ['*']);
$this->rootAccess = false;
} else { } else {
$this->permissions = array_diff($this->permissions, ['read-only']); $this->permissions = array_diff($this->permissions, ['read-only']);
} }
if (count($this->permissions) == 0) { $this->makeSureOneIsSelected();
}
public function updatedRootAccess()
{
if ($this->rootAccess) {
$this->permissions = ['*']; $this->permissions = ['*'];
$this->readOnly = false;
$this->viewSensitiveData = false;
} else {
$this->readOnly = true;
$this->permissions = ['read-only'];
}
}
public function makeSureOneIsSelected()
{
if (count($this->permissions) == 0) {
$this->permissions = ['read-only'];
$this->readOnly = true;
} }
} }
@@ -62,12 +82,6 @@ class ApiTokens extends Component
$this->validate([ $this->validate([
'description' => 'required|min:3|max:255', 'description' => 'required|min:3|max:255',
]); ]);
// if ($this->viewSensitiveData) {
// $this->permissions[] = 'view:sensitive';
// }
// if ($this->readOnly) {
// $this->permissions[] = 'read-only';
// }
$token = auth()->user()->createToken($this->description, $this->permissions); $token = auth()->user()->createToken($this->description, $this->permissions);
$this->tokens = auth()->user()->tokens; $this->tokens = auth()->user()->tokens;
session()->flash('token', $token->plainTextToken); session()->flash('token', $token->plainTextToken);

View File

@@ -30,6 +30,11 @@ class ConfigureCloudflareTunnels extends Component
public function submit() public function submit()
{ {
try { try {
if (str($this->ssh_domain)->contains('https://')) {
$this->ssh_domain = str($this->ssh_domain)->replace('https://', '')->replace('http://', '')->trim();
// remove / from the end
$this->ssh_domain = str($this->ssh_domain)->replace('/', '');
}
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail(); $server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::dispatch($server, $this->cloudflare_token); ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true; $server->settings->is_cloudflare_tunnel = true;

View File

@@ -4,7 +4,7 @@ namespace App\Livewire\Server\Proxy;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\CheckProxy;
use App\Jobs\ContainerStatusJob; use App\Actions\Proxy\StartProxy;
use App\Models\Server; use App\Models\Server;
use Livewire\Component; use Livewire\Component;
@@ -44,7 +44,10 @@ class Status extends Component
} }
$this->numberOfPolls++; $this->numberOfPolls++;
} }
CheckProxy::run($this->server, true); $shouldStart = CheckProxy::run($this->server, true);
if ($shouldStart) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated'); $this->dispatch('proxyStatusUpdated');
if ($this->server->proxy->status === 'running') { if ($this->server->proxy->status === 'running') {
$this->polling = false; $this->polling = false;

View File

@@ -60,7 +60,7 @@ class Index extends Component
public function mount() public function mount()
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
$this->settings = InstanceSettings::get(); $this->settings = instanceSettings();
$this->do_not_track = $this->settings->do_not_track; $this->do_not_track = $this->settings->do_not_track;
$this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled;
$this->is_registration_enabled = $this->settings->is_registration_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled;
@@ -162,7 +162,7 @@ class Index extends Component
{ {
CheckForUpdatesJob::dispatchSync(); CheckForUpdatesJob::dispatchSync();
$this->dispatch('updateAvailable'); $this->dispatch('updateAvailable');
$settings = InstanceSettings::get(); $settings = instanceSettings();
if ($settings->new_version_available) { if ($settings->new_version_available) {
$this->dispatch('success', 'New version available!'); $this->dispatch('success', 'New version available!');
} else { } else {

View File

@@ -29,7 +29,7 @@ class License extends Component
abort(404); abort(404);
} }
$this->instance_id = config('app.id'); $this->instance_id = config('app.id');
$this->settings = \App\Models\InstanceSettings::get(); $this->settings = instanceSettings();
} }
public function render() public function render()

View File

@@ -42,7 +42,7 @@ class SettingsBackup extends Component
public function mount() public function mount()
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
$settings = InstanceSettings::get(); $settings = instanceSettings();
$this->database = StandalonePostgresql::whereName('coolify-db')->first(); $this->database = StandalonePostgresql::whereName('coolify-db')->first();
$s3s = S3Storage::whereTeamId(0)->get() ?? []; $s3s = S3Storage::whereTeamId(0)->get() ?? [];
if ($this->database) { if ($this->database) {

View File

@@ -43,7 +43,7 @@ class SettingsEmail extends Component
public function mount() public function mount()
{ {
if (isInstanceAdmin()) { if (isInstanceAdmin()) {
$this->settings = InstanceSettings::get(); $this->settings = instanceSettings();
$this->emails = auth()->user()->email; $this->emails = auth()->user()->email;
} else { } else {
return redirect()->route('dashboard'); return redirect()->route('dashboard');

View File

@@ -99,7 +99,7 @@ class Change extends Component
return redirect()->route('source.all'); return redirect()->route('source.all');
} }
$this->applications = $this->github_app->applications; $this->applications = $this->github_app->applications;
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->name = str($this->github_app->name)->kebab(); $this->name = str($this->github_app->name)->kebab();

View File

@@ -43,15 +43,17 @@ class Create extends Component
'endpoint' => 'Endpoint', 'endpoint' => 'Endpoint',
]; ];
public function mount() public function updatedEndpoint($value)
{ {
if (isDev()) { if (! str($value)->startsWith('https://') && ! str($value)->startsWith('http://')) {
$this->name = 'Local MinIO'; $this->endpoint = 'https://'.$value;
$this->description = 'Local MinIO'; $value = $this->endpoint;
$this->key = 'minioadmin'; }
$this->secret = 'minioadmin';
$this->bucket = 'local'; if (str($value)->contains('your-objectstorage.com') && ! isset($this->bucket)) {
$this->endpoint = 'http://coolify-minio:9000'; $this->bucket = str($value)->after('//')->before('.');
} elseif (str($value)->contains('your-objectstorage.com')) {
$this->bucket = $this->bucket ?: str($value)->after('//')->before('.');
} }
} }

View File

@@ -23,7 +23,7 @@ class Index extends Component
if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) { if (data_get(currentTeam(), 'subscription') && isSubscriptionActive()) {
return redirect()->route('subscription.show'); return redirect()->route('subscription.show');
} }
$this->settings = \App\Models\InstanceSettings::get(); $this->settings = instanceSettings();
$this->alreadySubscribed = currentTeam()->subscription()->exists(); $this->alreadySubscribed = currentTeam()->subscription()->exists();
} }

View File

@@ -104,7 +104,7 @@ class Application extends BaseModel
{ {
use SoftDeletes; use SoftDeletes;
private static $parserVersion = '3'; private static $parserVersion = '4';
protected $guarded = []; protected $guarded = [];
@@ -143,6 +143,9 @@ class Application extends BaseModel
} }
$application->tags()->detach(); $application->tags()->detach();
$application->previews()->delete(); $application->previews()->delete();
foreach ($application->deployment_queue as $deployment) {
$deployment->delete();
}
}); });
} }
@@ -304,7 +307,7 @@ class Application extends BaseModel
'application_uuid' => data_get($this, 'uuid'), 'application_uuid' => data_get($this, 'uuid'),
'task_uuid' => $task_uuid, 'task_uuid' => $task_uuid,
]); ]);
$settings = InstanceSettings::get(); $settings = instanceSettings();
if (data_get($settings, 'fqdn')) { if (data_get($settings, 'fqdn')) {
$url = Url::fromString($route); $url = Url::fromString($route);
$url = $url->withPort(null); $url = $url->withPort(null);
@@ -710,6 +713,11 @@ class Application extends BaseModel
return $this->hasMany(ApplicationPreview::class); return $this->hasMany(ApplicationPreview::class);
} }
public function deployment_queue()
{
return $this->hasMany(ApplicationDeploymentQueue::class);
}
public function destination() public function destination()
{ {
return $this->morphTo(); return $this->morphTo();
@@ -1150,7 +1158,7 @@ class Application extends BaseModel
public function parse(int $pull_request_id = 0, ?int $preview_id = null) public function parse(int $pull_request_id = 0, ?int $preview_id = null)
{ {
if ($this->compose_parsing_version === '3') { if ((int) $this->compose_parsing_version >= 3) {
return newParser($this, $pull_request_id, $preview_id); return newParser($this, $pull_request_id, $preview_id);
} elseif ($this->docker_compose_raw) { } elseif ($this->docker_compose_raw) {
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id); return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);

View File

@@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -39,6 +40,20 @@ class ApplicationDeploymentQueue extends Model
{ {
protected $guarded = []; protected $guarded = [];
public function application(): Attribute
{
return Attribute::make(
get: fn () => Application::find($this->application_id),
);
}
public function server(): Attribute
{
return Attribute::make(
get: fn () => Server::find($this->server_id),
);
}
public function setStatus(string $status) public function setStatus(string $status)
{ {
$this->update([ $this->update([

View File

@@ -126,11 +126,6 @@ class EnvironmentVariable extends Model
$env = $this->get_real_environment_variables($this->value, $resource); $env = $this->get_real_environment_variables($this->value, $resource);
return data_get($env, 'value', $env); return data_get($env, 'value', $env);
if (is_string($env)) {
return $env;
}
return $env->value;
} }
); );
} }

View File

@@ -85,4 +85,17 @@ class InstanceSettings extends Model implements SendsEmail
return "[{$instanceName}]"; return "[{$instanceName}]";
} }
public function helperVersion(): Attribute
{
return Attribute::make(
get: function ($value) {
if (isDev()) {
return 'latest';
}
return $value;
}
);
}
} }

View File

@@ -24,9 +24,11 @@ class Project extends BaseModel
{ {
protected $guarded = []; protected $guarded = [];
protected $appends = ['default_environment'];
public static function ownedByCurrentTeam() public static function ownedByCurrentTeam()
{ {
return Project::whereTeamId(currentTeam()->id)->orderBy('name'); return Project::whereTeamId(currentTeam()->id)->orderByRaw('LOWER(name)');
} }
protected static function booted() protected static function booted()
@@ -131,7 +133,7 @@ class Project extends BaseModel
return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get()); return $this->postgresqls()->get()->merge($this->redis()->get())->merge($this->mongodbs()->get())->merge($this->mysqls()->get())->merge($this->mariadbs()->get())->merge($this->keydbs()->get())->merge($this->dragonflies()->get())->merge($this->clickhouses()->get());
} }
public function default_environment() public function getDefaultEnvironmentAttribute()
{ {
$default = $this->environments()->where('name', 'production')->first(); $default = $this->environments()->where('name', 'production')->first();
if ($default) { if ($default) {

View File

@@ -40,6 +40,16 @@ class S3Storage extends BaseModel
return "{$this->endpoint}/{$this->bucket}"; return "{$this->endpoint}/{$this->bucket}";
} }
public function isHetzner()
{
return str($this->endpoint)->contains('your-objectstorage.com');
}
public function isDigitalOcean()
{
return str($this->endpoint)->contains('digitaloceanspaces.com');
}
public function testConnection(bool $shouldSave = false) public function testConnection(bool $shouldSave = false)
{ {
try { try {

View File

@@ -268,7 +268,7 @@ respond 404
public function setupDynamicProxyConfiguration() public function setupDynamicProxyConfiguration()
{ {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$dynamic_config_path = $this->proxyPath().'/dynamic'; $dynamic_config_path = $this->proxyPath().'/dynamic';
if ($this->proxyType() === ProxyTypes::TRAEFIK->value) { if ($this->proxyType() === ProxyTypes::TRAEFIK->value) {
$file = "$dynamic_config_path/coolify.yaml"; $file = "$dynamic_config_path/coolify.yaml";
@@ -448,11 +448,19 @@ $schema://$host {
// Should move everything except /caddy and /nginx to /traefik // Should move everything except /caddy and /nginx to /traefik
// The code needs to be modified as well, so maybe it does not worth it // The code needs to be modified as well, so maybe it does not worth it
if ($proxyType === ProxyTypes::TRAEFIK->value) { if ($proxyType === ProxyTypes::TRAEFIK->value) {
$proxy_path = $proxy_path; // Do nothing
} elseif ($proxyType === ProxyTypes::CADDY->value) { } elseif ($proxyType === ProxyTypes::CADDY->value) {
$proxy_path = $proxy_path.'/caddy'; if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy';
} else {
$proxy_path = $proxy_path.'/caddy';
}
} elseif ($proxyType === ProxyTypes::NGINX->value) { } elseif ($proxyType === ProxyTypes::NGINX->value) {
$proxy_path = $proxy_path.'/nginx'; if (isDev()) {
$proxy_path = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/nginx';
} else {
$proxy_path = $proxy_path.'/nginx';
}
} }
return $proxy_path; return $proxy_path;
@@ -460,15 +468,6 @@ $schema://$host {
public function proxyType() public function proxyType()
{ {
// $proxyType = $this->proxy->get('type');
// if ($proxyType === ProxyTypes::NONE->value) {
// return $proxyType;
// }
// if (is_null($proxyType)) {
// $this->proxy->type = ProxyTypes::TRAEFIK->value;
// $this->proxy->status = ProxyStatus::EXITED->value;
// $this->save();
// }
return data_get($this->proxy, 'type'); return data_get($this->proxy, 'type');
} }
@@ -1212,4 +1211,18 @@ $schema://$host {
return $this; return $this;
} }
public function storageCheck(): ?string
{
$commands = [
'df / --output=pcent | tr -cd 0-9',
];
return instant_remote_process($commands, $this, false);
}
public function isIpv6(): bool
{
return str($this->ip)->contains(':');
}
} }

View File

@@ -42,7 +42,7 @@ class Service extends BaseModel
{ {
use HasFactory, SoftDeletes; use HasFactory, SoftDeletes;
private static $parserVersion = '3'; private static $parserVersion = '4';
protected $guarded = []; protected $guarded = [];
@@ -285,6 +285,65 @@ class Service extends BaseModel
foreach ($applications as $application) { foreach ($applications as $application) {
$image = str($application->image)->before(':')->value(); $image = str($application->image)->before(':')->value();
switch ($image) { switch ($image) {
case str($image)?->contains('invoiceninja'):
$data = collect([]);
$email = $this->environment_variables()->where('key', 'IN_USER_EMAIL')->first();
$data = $data->merge([
'Email' => [
'key' => 'IN_USER_EMAIL',
'value' => data_get($email, 'value'),
'rules' => 'required|email',
],
]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_INVOICENINJAUSER')->first();
$data = $data->merge([
'Password' => [
'key' => 'IN_PASSWORD',
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
$fields->put('Invoice Ninja', $data->toArray());
break;
case str($image)?->contains('argilla'):
$data = collect([]);
$api_key = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_APIKEY')->first();
$data = $data->merge([
'API Key' => [
'key' => data_get($api_key, 'key'),
'value' => data_get($api_key, 'value'),
'isPassword' => true,
'rules' => 'required',
],
]);
$data = $data->merge([
'API Key' => [
'key' => data_get($api_key, 'key'),
'value' => data_get($api_key, 'value'),
'isPassword' => true,
'rules' => 'required',
],
]);
$username = $this->environment_variables()->where('key', 'ARGILLA_USERNAME')->first();
$data = $data->merge([
'Username' => [
'key' => data_get($username, 'key'),
'value' => data_get($username, 'value'),
'rules' => 'required',
],
]);
$password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_ARGILLA')->first();
$data = $data->merge([
'Password' => [
'key' => data_get($password, 'key'),
'value' => data_get($password, 'value'),
'rules' => 'required',
'isPassword' => true,
],
]);
$fields->put('Argilla', $data->toArray());
break;
case str($image)?->contains('rabbitmq'): case str($image)?->contains('rabbitmq'):
$data = collect([]); $data = collect([]);
$host_port = $this->environment_variables()->where('key', 'PORT')->first(); $host_port = $this->environment_variables()->where('key', 'PORT')->first();
@@ -770,6 +829,30 @@ class Service extends BaseModel
} }
$fields->put('Code Server', $data->toArray()); $fields->put('Code Server', $data->toArray());
break; break;
case str($image)->contains('elestio/strapi'):
$data = collect([]);
$license = $this->environment_variables()->where('key', 'STRAPI_LICENSE')->first();
if ($license) {
$data = $data->merge([
'License' => [
'key' => data_get($license, 'key'),
'value' => data_get($license, 'value'),
],
]);
}
$nodeEnv = $this->environment_variables()->where('key', 'NODE_ENV')->first();
if ($nodeEnv) {
$data = $data->merge([
'Node Environment' => [
'key' => data_get($nodeEnv, 'key'),
'value' => data_get($nodeEnv, 'value'),
],
]);
}
$fields->put('Strapi', $data->toArray());
break;
} }
} }
$databases = $this->databases()->get(); $databases = $this->databases()->get();
@@ -1052,12 +1135,12 @@ class Service extends BaseModel
public function environment_variables(): HasMany public function environment_variables(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->orderByRaw("key LIKE 'SERVICE%' DESC, value ASC"); return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
} }
public function environment_variables_preview(): HasMany public function environment_variables_preview(): HasMany
{ {
return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderBy('key', 'asc'); return $this->hasMany(EnvironmentVariable::class)->where('is_preview', true)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC");
} }
public function workdir() public function workdir()
@@ -1095,7 +1178,21 @@ class Service extends BaseModel
return 3; return 3;
}); });
foreach ($sorted as $env) { foreach ($sorted as $env) {
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env"; if (version_compare($env->version, '4.0.0-beta.347', '<=')) {
$commands[] = "echo '{$env->key}={$env->real_value}' >> .env";
} else {
$real_value = $env->real_value;
if ($env->version === '4.0.0-beta.239') {
$real_value = $env->real_value;
} else {
if ($env->is_literal || $env->is_multiline) {
$real_value = '\''.$real_value.'\'';
} else {
$real_value = escapeEnvVariables($env->real_value);
}
}
$commands[] = "echo \"{$env->key}={$real_value}\" >> .env";
}
} }
if ($sorted->count() === 0) { if ($sorted->count() === 0) {
$commands[] = 'touch .env'; $commands[] = 'touch .env';
@@ -1105,7 +1202,7 @@ class Service extends BaseModel
public function parse(bool $isNew = false): Collection public function parse(bool $isNew = false): Collection
{ {
if ($this->compose_parsing_version === '3') { if ((int) $this->compose_parsing_version >= 3) {
return newParser($this); return newParser($this);
} elseif ($this->docker_compose_raw) { } elseif ($this->docker_compose_raw) {
return parseDockerComposeFile($this, $isNew); return parseDockerComposeFile($this, $isNew);

View File

@@ -112,4 +112,9 @@ class ServiceApplication extends BaseModel
{ {
getFilesystemVolumesFromServer($this, $isInit); getFilesystemVolumesFromServer($this, $isInit);
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -115,4 +115,13 @@ class ServiceDatabase extends BaseModel
{ {
return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); return $this->morphMany(ScheduledDatabaseBackup::class, 'database');
} }
public function isBackupSolutionAvailable()
{
return str($this->databaseType())->contains('mysql') ||
str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongodb');
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneClickhouse extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneDragonfly extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneKeydb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -294,4 +294,9 @@ class StandaloneMariadb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -314,4 +314,9 @@ class StandaloneMongodb extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -295,4 +295,9 @@ class StandaloneMysql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -296,4 +296,9 @@ class StandalonePostgresql extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return true;
}
} }

View File

@@ -290,4 +290,9 @@ class StandaloneRedis extends BaseModel
return $parsedCollection->toArray(); return $parsedCollection->toArray();
} }
} }
public function isBackupSolutionAvailable()
{
return false;
}
} }

View File

@@ -13,7 +13,7 @@ class TransactionalEmailChannel
{ {
public function send(User $notifiable, Notification $notification): void public function send(User $notifiable, Notification $notification): void
{ {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) { if (! data_get($settings, 'smtp_enabled') && ! data_get($settings, 'resend_enabled')) {
Log::info('SMTP/Resend not enabled'); Log::info('SMTP/Resend not enabled');

View File

@@ -18,7 +18,7 @@ class ResetPassword extends Notification
public function __construct($token) public function __construct($token)
{ {
$this->settings = \App\Models\InstanceSettings::get(); $this->settings = instanceSettings();
$this->token = $token; $this->token = $token;
} }

View File

@@ -2,10 +2,8 @@
namespace App\Providers; namespace App\Providers;
use App\Models\InstanceSettings;
use App\Models\PersonalAccessToken; use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Laravel\Sanctum\Sanctum; use Laravel\Sanctum\Sanctum;
@@ -30,9 +28,5 @@ class AppServiceProvider extends ServiceProvider
])->baseUrl($api_url); ])->baseUrl($api_url);
} }
}); });
// if (! env('CI')) {
// View::share('instanceSettings', InstanceSettings::get());
// }
} }
} }

View File

@@ -46,7 +46,7 @@ class FortifyServiceProvider extends ServiceProvider
Fortify::registerView(function () { Fortify::registerView(function () {
$isFirstUser = User::count() === 0; $isFirstUser = User::count() === 0;
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (! $settings->is_registration_enabled) { if (! $settings->is_registration_enabled) {
return redirect()->route('login'); return redirect()->route('login');
} }
@@ -60,7 +60,7 @@ class FortifyServiceProvider extends ServiceProvider
}); });
Fortify::loginView(function () { Fortify::loginView(function () {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$enabled_oauth_providers = OauthSetting::where('enabled', true)->get(); $enabled_oauth_providers = OauthSetting::where('enabled', true)->get();
$users = User::count(); $users = User::count();
if ($users == 0) { if ($users == 0) {

View File

@@ -175,4 +175,5 @@ function removeUnnecessaryFieldsFromRequest(Request $request)
$request->offsetUnset('instant_deploy'); $request->offsetUnset('instant_deploy');
$request->offsetUnset('github_app_uuid'); $request->offsetUnset('github_app_uuid');
$request->offsetUnset('private_key_uuid'); $request->offsetUnset('private_key_uuid');
$request->offsetUnset('use_build_server');
} }

View File

@@ -20,12 +20,16 @@ const RESTART_MODE = 'unless-stopped';
const DATABASE_DOCKER_IMAGES = [ const DATABASE_DOCKER_IMAGES = [
'bitnami/mariadb', 'bitnami/mariadb',
'bitnami/mongodb', 'bitnami/mongodb',
'bitnami/mysql',
'bitnami/postgresql',
'bitnami/redis', 'bitnami/redis',
'mysql', 'mysql',
'bitnami/mysql',
'mysql/mysql-server',
'mariadb', 'mariadb',
'postgis/postgis',
'postgres', 'postgres',
'bitnami/postgresql',
'supabase/postgres',
'elestio/postgres',
'mongo', 'mongo',
'redis', 'redis',
'memcached', 'memcached',
@@ -33,7 +37,6 @@ const DATABASE_DOCKER_IMAGES = [
'neo4j', 'neo4j',
'influxdb', 'influxdb',
'clickhouse/clickhouse-server', 'clickhouse/clickhouse-server',
'supabase/postgres',
]; ];
const SPECIFIC_SERVICES = [ const SPECIFIC_SERVICES = [
'quay.io/minio/minio', 'quay.io/minio/minio',

View File

@@ -325,38 +325,16 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push('traefik.http.middlewares.gzip.compress=true'); $labels->push('traefik.http.middlewares.gzip.compress=true');
$labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https'); $labels->push('traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https');
$basic_auth = false; $middlewares_from_labels = collect([]);
$basic_auth_middleware = null;
$redirect = false;
$redirect_middleware = null;
if ($serviceLabels) { if ($serviceLabels) {
$basic_auth = $serviceLabels->contains(function ($value) { $middlewares_from_labels = $serviceLabels->map(function ($item) {
return str_contains($value, 'basicauth'); if (preg_match('/traefik\.http\.middlewares\.(.*?)(\.|$)/', $item, $matches)) {
}); return $matches[1];
if ($basic_auth) { }
$basic_auth_middleware = $serviceLabels return null;
->map(function ($item) { })->filter()
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.basicauth\.users/', $item, $matches)) { ->unique();
return $matches[1];
}
})
->filter()
->first();
}
$redirect = $serviceLabels->contains(function ($value) {
return str_contains($value, 'redirectregex');
});
if ($redirect) {
$redirect_middleware = $serviceLabels
->map(function ($item) {
if (preg_match('/traefik\.http\.middlewares\.(.*?)\.redirectregex\.regex/', $item, $matches)) {
return $matches[1];
}
})
->filter()
->first();
}
} }
foreach ($domains as $loop => $domain) { foreach ($domains as $loop => $domain) {
try { try {
@@ -404,20 +382,15 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port"); $labels->push("traefik.http.services.{$https_label}.loadbalancer.server.port=$port");
} }
if ($path !== '/') { if ($path !== '/') {
// Middleware handling
$middlewares = collect([]); $middlewares = collect([]);
if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { if ($is_stripprefix_enabled && !str($image)->contains('ghost')) {
$labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}");
$middlewares->push("{$https_label}-stripprefix"); $middlewares->push("{$https_label}-stripprefix");
} }
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -425,10 +398,13 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_non_www); $labels = $labels->merge($redirect_to_non_www);
$middlewares->push($to_non_www_name); $middlewares->push($to_non_www_name);
} }
if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) {
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -437,13 +413,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$middlewares = collect([]); $middlewares = collect([]);
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -455,6 +425,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$https_label}.middlewares={$middlewares}");
@@ -490,12 +463,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -507,6 +474,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");
@@ -516,12 +486,6 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
if ($is_gzip_enabled) { if ($is_gzip_enabled) {
$middlewares->push('gzip'); $middlewares->push('gzip');
} }
if ($basic_auth && $basic_auth_middleware) {
$middlewares->push($basic_auth_middleware);
}
if ($redirect && $redirect_middleware) {
$middlewares->push($redirect_middleware);
}
if (str($image)->contains('ghost')) { if (str($image)->contains('ghost')) {
$middlewares->push('redir-ghost'); $middlewares->push('redir-ghost');
} }
@@ -533,6 +497,9 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
$labels = $labels->merge($redirect_to_www); $labels = $labels->merge($redirect_to_www);
$middlewares->push($to_www_name); $middlewares->push($to_www_name);
} }
$middlewares_from_labels->each(function ($middleware_name) use ($middlewares) {
$middlewares->push($middleware_name);
});
if ($middlewares->isNotEmpty()) { if ($middlewares->isNotEmpty()) {
$middlewares = $middlewares->join(','); $middlewares = $middlewares->join(',');
$labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}"); $labels->push("traefik.http.routers.{$http_label}.middlewares={$middlewares}");

View File

@@ -1,14 +1,11 @@
<?php <?php
use App\Models\S3Storage; use App\Models\S3Storage;
use Illuminate\Support\Str;
function set_s3_target(S3Storage $s3) function set_s3_target(S3Storage $s3)
{ {
$is_digital_ocean = false; $is_digital_ocean = false;
if ($s3->endpoint) {
$is_digital_ocean = Str::contains($s3->endpoint, 'digitaloceanspaces.com');
}
config()->set('filesystems.disks.custom-s3', [ config()->set('filesystems.disks.custom-s3', [
'driver' => 's3', 'driver' => 's3',
'region' => $s3['region'], 'region' => $s3['region'],
@@ -17,7 +14,7 @@ function set_s3_target(S3Storage $s3)
'bucket' => $s3['bucket'], 'bucket' => $s3['bucket'],
'endpoint' => $s3['endpoint'], 'endpoint' => $s3['endpoint'],
'use_path_style_endpoint' => true, 'use_path_style_endpoint' => true,
'bucket_endpoint' => $is_digital_ocean, 'bucket_endpoint' => $s3->isHetzner() || $s3->isDigitalOcean(),
'aws_url' => $s3->awsUrl(), 'aws_url' => $s3->awsUrl(),
]); ]);
} }

View File

@@ -247,7 +247,7 @@ function is_transactional_emails_active(): bool
function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string function set_transanctional_email_settings(?InstanceSettings $settings = null): ?string
{ {
if (! $settings) { if (! $settings) {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
} }
config()->set('mail.from.address', data_get($settings, 'smtp_from_address')); config()->set('mail.from.address', data_get($settings, 'smtp_from_address'));
config()->set('mail.from.name', data_get($settings, 'smtp_from_name')); config()->set('mail.from.name', data_get($settings, 'smtp_from_name'));
@@ -281,7 +281,7 @@ function base_ip(): string
if (isDev()) { if (isDev()) {
return 'localhost'; return 'localhost';
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if ($settings->public_ipv4) { if ($settings->public_ipv4) {
return "$settings->public_ipv4"; return "$settings->public_ipv4";
} }
@@ -309,7 +309,7 @@ function getFqdnWithoutPort(string $fqdn)
*/ */
function base_url(bool $withPort = true): string function base_url(bool $withPort = true): string
{ {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if ($settings->fqdn) { if ($settings->fqdn) {
return $settings->fqdn; return $settings->fqdn;
} }
@@ -343,6 +343,11 @@ function isSubscribed()
{ {
return isSubscriptionActive() || auth()->user()->isInstanceAdmin(); return isSubscriptionActive() || auth()->user()->isInstanceAdmin();
} }
function isProduction(): bool
{
return ! isDev();
}
function isDev(): bool function isDev(): bool
{ {
return config('app.env') === 'local'; return config('app.env') === 'local';
@@ -384,7 +389,7 @@ function send_internal_notification(string $message): void
} }
function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void function send_user_an_email(MailMessage $mail, string $email, ?string $cc = null): void
{ {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$type = set_transanctional_email_settings($settings); $type = set_transanctional_email_settings($settings);
if (! $type) { if (! $type) {
throw new Exception('No email settings found.'); throw new Exception('No email settings found.');
@@ -703,7 +708,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -753,7 +760,9 @@ function getTopLevelNetworks(Service|Application $resource)
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -819,6 +828,31 @@ function convertToArray($collection)
return $collection; return $collection;
} }
function parseCommandFromMagicEnvVariable(Str|string $key): Stringable
{
$value = str($key);
$count = substr_count($value->value(), '_');
if ($count === 2) {
if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
// SERVICE_FQDN_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
} else {
// SERVICE_BASE64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
}
}
if ($count === 3) {
if ($value->startsWith('SERVICE_FQDN') || $value->startsWith('SERVICE_URL')) {
// SERVICE_FQDN_UMAMI_1000
$command = $value->after('SERVICE_')->before('_');
} else {
// SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_');
}
}
return str($command);
}
function parseEnvVariable(Str|string $value) function parseEnvVariable(Str|string $value)
{ {
$value = str($value); $value = str($value);
@@ -850,6 +884,7 @@ function parseEnvVariable(Str|string $value)
} else { } else {
// SERVICE_BASE64_64_UMAMI // SERVICE_BASE64_64_UMAMI
$command = $value->after('SERVICE_')->beforeLast('_'); $command = $value->after('SERVICE_')->beforeLast('_');
ray($command);
} }
} }
} }
@@ -970,7 +1005,7 @@ function validate_dns_entry(string $fqdn, Server $server)
if (str($host)->contains('sslip.io')) { if (str($host)->contains('sslip.io')) {
return true; return true;
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
$is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled'); $is_dns_validation_enabled = data_get($settings, 'is_dns_validation_enabled');
if (! $is_dns_validation_enabled) { if (! $is_dns_validation_enabled) {
return true; return true;
@@ -1090,7 +1125,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId =
if ($domainFound) { if ($domainFound) {
return true; return true;
} }
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (data_get($settings, 'fqdn')) { if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn'); $domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
@@ -1162,7 +1197,7 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
} }
} }
if ($resource) { if ($resource) {
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (data_get($settings, 'fqdn')) { if (data_get($settings, 'fqdn')) {
$domain = data_get($settings, 'fqdn'); $domain = data_get($settings, 'fqdn');
if (str($domain)->endsWith('/')) { if (str($domain)->endsWith('/')) {
@@ -1179,12 +1214,26 @@ function check_domain_usage(ServiceApplication|Application|null $resource = null
function parseCommandsByLineForSudo(Collection $commands, Server $server): array function parseCommandsByLineForSudo(Collection $commands, Server $server): array
{ {
$commands = $commands->map(function ($line) { $commands = $commands->map(function ($line) {
if (! str($line)->startsWith('cd') && ! str($line)->startsWith('command') && ! str($line)->startsWith('echo') && ! str($line)->startsWith('true')) { if (
! str(trim($line))->startsWith([
'cd',
'command',
'echo',
'true',
'if',
'fi',
])
) {
return "sudo $line"; return "sudo $line";
} }
if (str(trim($line))->startsWith('if')) {
return str_replace('if', 'if sudo', $line);
}
return $line; return $line;
}); });
$commands = $commands->map(function ($line) use ($server) { $commands = $commands->map(function ($line) use ($server) {
if (Str::startsWith($line, 'sudo mkdir -p')) { if (Str::startsWith($line, 'sudo mkdir -p')) {
return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p'); return "$line && sudo chown -R $server->user:$server->user ".Str::after($line, 'sudo mkdir -p').' && sudo chmod -R o-rwx '.Str::after($line, 'sudo mkdir -p');
@@ -1192,6 +1241,7 @@ function parseCommandsByLineForSudo(Collection $commands, Server $server): array
return $line; return $line;
}); });
$commands = $commands->map(function ($line) { $commands = $commands->map(function ($line) {
$line = str($line); $line = str($line);
if (str($line)->contains('$(')) { if (str($line)->contains('$(')) {
@@ -1588,7 +1638,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2503,7 +2555,9 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
return $value == $networkName || $key == $networkName; return $value == $networkName || $key == $networkName;
}); });
if (! $networkExists) { if (! $networkExists) {
$topLevelNetworks->put($networkDetails, null); if (is_string($networkDetails) || is_int($networkDetails)) {
$topLevelNetworks->put($networkDetails, null);
}
} }
} }
} }
@@ -2964,11 +3018,22 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3078,7 +3143,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
foreach ($magicEnvironments as $key => $value) { foreach ($magicEnvironments as $key => $value) {
$key = str($key); $key = str($key);
$value = replaceVariables($value); $value = replaceVariables($value);
$command = $key->after('SERVICE_')->before('_'); $command = parseCommandFromMagicEnvVariable($key);
$found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first(); $found = $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->first();
if ($found) { if ($found) {
continue; continue;
@@ -3189,12 +3254,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
if ($serviceName === 'plausible') { if ($serviceName === 'plausible') {
$predefinedPort = '8000'; $predefinedPort = '8000';
} }
if ($isDatabase) { if ($isDatabase) {
$savedService = ServiceDatabase::firstOrCreate([ $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
'name' => $serviceName, if ($applicationFound) {
'image' => $image, $savedService = $applicationFound;
'service_id' => $resource->id, $savedService = ServiceDatabase::firstOrCreate([
]); 'name' => $applicationFound->name,
'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id,
]);
$applicationFound->delete();
} else {
$savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName,
'image' => $image,
'service_id' => $resource->id,
]);
}
} else { } else {
$savedService = ServiceApplication::firstOrCreate([ $savedService = ServiceApplication::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3264,7 +3341,15 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
} elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') { } elseif ($source->value() === '/tmp' || $source->value() === '/tmp/') {
$volume = $source->value().':'.$target->value(); $volume = $source->value().':'.$target->value();
} else { } else {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid); if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
} elseif ($isService) {
$mainDirectory = str(base_configuration_dir().'/services/'.$uuid);
}
} else {
$mainDirectory = str(base_configuration_dir().'/applications/'.$uuid);
}
$source = replaceLocalSource($source, $mainDirectory); $source = replaceLocalSource($source, $mainDirectory);
if ($isApplication && $isPullRequest) { if ($isApplication && $isPullRequest) {
$source = $source."-pr-$pullRequestId"; $source = $source."-pr-$pullRequestId";
@@ -3284,6 +3369,17 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
'resource_type' => get_class($originalResource), 'resource_type' => get_class($originalResource),
] ]
); );
if (isDev()) {
if ((int) $resource->compose_parsing_version >= 4) {
if ($isApplication) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
} elseif ($isService) {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/services/'.$uuid);
}
} else {
$source = $source->replace($mainDirectory, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/applications/'.$uuid);
}
}
$volume = "$source:$target"; $volume = "$source:$target";
} }
} elseif ($type->value() === 'volume') { } elseif ($type->value() === 'volume') {
@@ -3606,6 +3702,18 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
}); });
} }
$serviceLabels = $labels->merge($defaultLabels); $serviceLabels = $labels->merge($defaultLabels);
if ($serviceLabels->count() > 0) {
if ($isApplication) {
$isContainerLabelEscapeEnabled = data_get($resource, 'settings.is_container_label_escape_enabled');
} else {
$isContainerLabelEscapeEnabled = data_get($resource, 'is_container_label_escape_enabled');
}
if ($isContainerLabelEscapeEnabled) {
$serviceLabels = $serviceLabels->map(function ($value, $key) {
return escapeDollarSign($value);
});
}
}
if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) { if (! $isDatabase && $fqdns instanceof Collection && $fqdns->count() > 0) {
if ($isApplication) { if ($isApplication) {
$shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels; $shouldGenerateLabelsExactly = $resource->destination->server->settings->generate_exact_labels;
@@ -3826,14 +3934,37 @@ function convertComposeEnvironmentToArray($environment)
{ {
$convertedServiceVariables = collect([]); $convertedServiceVariables = collect([]);
if (isAssociativeArray($environment)) { if (isAssociativeArray($environment)) {
// Example: $environment = ['FOO' => 'bar', 'BAZ' => 'qux'];
if ($environment instanceof Collection) {
$changedEnvironment = collect([]);
$environment->each(function ($value, $key) use ($changedEnvironment) {
if (is_numeric($key)) {
$parts = explode('=', $value, 2);
if (count($parts) === 2) {
$key = $parts[0];
$realValue = $parts[1] ?? '';
$changedEnvironment->put($key, $realValue);
} else {
$changedEnvironment->put($key, $value);
}
} else {
$changedEnvironment->put($key, $value);
}
});
return $changedEnvironment;
}
$convertedServiceVariables = $environment; $convertedServiceVariables = $environment;
} else { } else {
// Example: $environment = ['FOO=bar', 'BAZ=qux'];
foreach ($environment as $value) { foreach ($environment as $value) {
$parts = explode('=', $value, 2); if (is_string($value)) {
$key = $parts[0]; $parts = explode('=', $value, 2);
$realValue = $parts[1] ?? ''; $key = $parts[0];
if ($key) { $realValue = $parts[1] ?? '';
$convertedServiceVariables->put($key, $realValue); if ($key) {
$convertedServiceVariables->put($key, $realValue);
}
} }
} }
} }
@@ -3841,3 +3972,7 @@ function convertComposeEnvironmentToArray($environment)
return $convertedServiceVariables; return $convertedServiceVariables;
} }
function instanceSettings()
{
return InstanceSettings::get();
}

View File

@@ -48,6 +48,7 @@
"zircote/swagger-php": "^4.10" "zircote/swagger-php": "^4.10"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.13",
"fakerphp/faker": "^v1.21.0", "fakerphp/faker": "^v1.21.0",
"laravel/dusk": "^v8.0", "laravel/dusk": "^v8.0",
"laravel/pint": "^1.16", "laravel/pint": "^1.16",

154
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "96f8146407d0e6e897ff097c5eccd3a4", "content-hash": "42c28ab141b70fcabf75b51afa96c670",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@@ -11823,6 +11823,90 @@
} }
], ],
"packages-dev": [ "packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.13.5",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "92d86be45ee54edff735e46856f64f14b6a8bb07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07",
"reference": "92d86be45ee54edff735e46856f64f14b6a8bb07",
"shasum": ""
},
"require": {
"illuminate/routing": "^9|^10|^11",
"illuminate/session": "^9|^10|^11",
"illuminate/support": "^9|^10|^11",
"maximebf/debugbar": "~1.22.0",
"php": "^8.0",
"symfony/finder": "^6|^7"
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"orchestra/testbench-dusk": "^5|^6|^7|^8|^9",
"phpunit/phpunit": "^9.6|^10.5",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.13-dev"
},
"laravel": {
"providers": [
"Barryvdh\\Debugbar\\ServiceProvider"
],
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar"
}
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Barryvdh\\Debugbar\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"debug",
"debugbar",
"laravel",
"profiler",
"webprofiler"
],
"support": {
"issues": "https://github.com/barryvdh/laravel-debugbar/issues",
"source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5"
},
"funding": [
{
"url": "https://fruitcake.nl",
"type": "custom"
},
{
"url": "https://github.com/barryvdh",
"type": "github"
}
],
"time": "2024-04-12T11:20:37+00:00"
},
{ {
"name": "brianium/paratest", "name": "brianium/paratest",
"version": "v7.4.3", "version": "v7.4.3",
@@ -12301,6 +12385,74 @@
}, },
"time": "2024-09-03T15:00:28+00:00" "time": "2024-09-03T15:00:28+00:00"
}, },
{
"name": "maximebf/debugbar",
"version": "v1.22.5",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
"reference": "1b5cabe0ce013134cf595bfa427bbf2f6abcd989",
"shasum": ""
},
"require": {
"php": "^7.2|^8",
"psr/log": "^1|^2|^3",
"symfony/var-dumper": "^4|^5|^6|^7"
},
"require-dev": {
"dbrekelmans/bdi": "^1",
"phpunit/phpunit": "^8|^9",
"symfony/panther": "^1|^2.1",
"twig/twig": "^1.38|^2.7|^3.0"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"monolog/monolog": "Log using Monolog",
"predis/predis": "Redis storage"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.22-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/DebugBar/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/maximebf/php-debugbar",
"keywords": [
"debug",
"debugbar"
],
"support": {
"issues": "https://github.com/maximebf/php-debugbar/issues",
"source": "https://github.com/maximebf/php-debugbar/tree/v1.22.5"
},
"time": "2024-09-09T08:05:55+00:00"
},
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
"version": "1.6.12", "version": "1.6.12",

325
config/debugbar.php Normal file
View File

@@ -0,0 +1,325 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Debugbar Settings
|--------------------------------------------------------------------------
|
| Debugbar is enabled by default, when debug is set to true in app.php.
| You can override the value by setting enable to true or false instead of null.
|
| You can provide an array of URI's that must be ignored (eg. 'api/*')
|
*/
'enabled' => env('DEBUGBAR_ENABLED', null),
'except' => [
'telescope*',
'horizon*',
],
/*
|--------------------------------------------------------------------------
| Storage settings
|--------------------------------------------------------------------------
|
| DebugBar stores data for session/ajax requests.
| You can disable this, so the debugbar stores data in headers/session,
| but this can cause problems with large data collectors.
| By default, file storage (in the storage folder) is used. Redis and PDO
| can also be used. For PDO, run the package migrations first.
|
| Warning: Enabling storage.open will allow everyone to access previous
| request, do not enable open storage in publicly available environments!
| Specify a callback if you want to limit based on IP or authentication.
| Leaving it to null will allow localhost only.
*/
'storage' => [
'enabled' => true,
'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback.
'driver' => 'file', // redis, file, pdo, socket, custom
'path' => storage_path('debugbar'), // For file driver
'connection' => null, // Leave null for default connection (Redis/PDO)
'provider' => '', // Instance of StorageInterface for custom driver
'hostname' => '127.0.0.1', // Hostname to use with the "socket" driver
'port' => 2304, // Port to use with the "socket" driver
],
/*
|--------------------------------------------------------------------------
| Editor
|--------------------------------------------------------------------------
|
| Choose your preferred editor to use when clicking file name.
|
| Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote",
| "vscode-insiders-remote", "vscodium", "textmate", "emacs",
| "sublime", "atom", "nova", "macvim", "idea", "netbeans",
| "xdebug", "espresso"
|
*/
'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'),
/*
|--------------------------------------------------------------------------
| Remote Path Mapping
|--------------------------------------------------------------------------
|
| If you are using a remote dev server, like Laravel Homestead, Docker, or
| even a remote VPS, it will be necessary to specify your path mapping.
|
| Leaving one, or both of these, empty or null will not trigger the remote
| URL changes and Debugbar will treat your editor links as local files.
|
| "remote_sites_path" is an absolute base path for your sites or projects
| in Homestead, Vagrant, Docker, or another remote development server.
|
| Example value: "/home/vagrant/Code"
|
| "local_sites_path" is an absolute base path for your sites or projects
| on your local computer where your IDE or code editor is running on.
|
| Example values: "/Users/<name>/Code", "C:\Users\<name>\Documents\Code"
|
*/
'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'),
'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')),
/*
|--------------------------------------------------------------------------
| Vendors
|--------------------------------------------------------------------------
|
| Vendor files are included by default, but can be set to false.
| This can also be set to 'js' or 'css', to only include javascript or css vendor files.
| Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
| and for js: jquery and highlight.js
| So if you want syntax highlighting, set it to true.
| jQuery is set to not conflict with existing jQuery scripts.
|
*/
'include_vendors' => true,
/*
|--------------------------------------------------------------------------
| Capture Ajax Requests
|--------------------------------------------------------------------------
|
| The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
| you can use this option to disable sending the data through the headers.
|
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
| Note for your request to be identified as ajax requests they must either send the header
| X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header.
|
| By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar.
| Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading.
*/
'capture_ajax' => true,
'add_ajax_timing' => false,
'ajax_handler_auto_show' => true,
'ajax_handler_enable_tab' => true,
/*
|--------------------------------------------------------------------------
| Custom Error Handler for Deprecated warnings
|--------------------------------------------------------------------------
|
| When enabled, the Debugbar shows deprecated warnings for Symfony components
| in the Messages tab.
|
*/
'error_handler' => false,
/*
|--------------------------------------------------------------------------
| Clockwork integration
|--------------------------------------------------------------------------
|
| The Debugbar can emulate the Clockwork headers, so you can use the Chrome
| Extension, without the server-side code. It uses Debugbar collectors instead.
|
*/
'clockwork' => false,
/*
|--------------------------------------------------------------------------
| DataCollectors
|--------------------------------------------------------------------------
|
| Enable/disable DataCollectors
|
*/
'collectors' => [
'phpinfo' => true, // Php version
'messages' => true, // Messages
'time' => true, // Time Datalogger
'memory' => true, // Memory usage
'exceptions' => true, // Exception displayer
'log' => true, // Logs from Monolog (merged in messages if enabled)
'db' => true, // Show database (PDO) queries and bindings
'views' => true, // Views with their data
'route' => true, // Current route information
'auth' => false, // Display Laravel authentication status
'gate' => true, // Display Laravel Gate checks
'session' => true, // Display session data
'symfony_request' => true, // Only one can be enabled..
'mail' => true, // Catch mail messages
'laravel' => false, // Laravel version and environment
'events' => false, // All events fired
'default_request' => false, // Regular or special Symfony request logger
'logs' => false, // Add the latest log messages
'files' => false, // Show the included files
'config' => false, // Display config settings
'cache' => false, // Display cache events
'models' => true, // Display models
'livewire' => true, // Display Livewire (when available)
'jobs' => false, // Display dispatched jobs
],
/*
|--------------------------------------------------------------------------
| Extra options
|--------------------------------------------------------------------------
|
| Configure some DataCollectors
|
*/
'options' => [
'time' => [
'memory_usage' => false, // Calculated by subtracting memory start and end, it may be inaccurate
],
'messages' => [
'trace' => true, // Trace the origin of the debug message
],
'memory' => [
'reset_peak' => false, // run memory_reset_peak_usage before collecting
'with_baseline' => false, // Set boot memory usage as memory peak baseline
'precision' => 0, // Memory rounding precision
],
'auth' => [
'show_name' => true, // Also show the users name/email in the debugbar
'show_guards' => true, // Show the guards that are used
],
'db' => [
'with_params' => true, // Render SQL with the parameters substituted
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults)
'timeline' => false, // Add the queries to the timeline
'duration_background' => true, // Show shaded background on each query relative to how long it took to execute.
'explain' => [ // Show EXPLAIN output on queries
'enabled' => false,
'types' => ['SELECT'], // Deprecated setting, is always only SELECT
],
'hints' => false, // Show hints for common mistakes
'show_copy' => false, // Show copy button next to the query,
'slow_threshold' => false, // Only track queries that last longer than this time in ms
'memory_usage' => false, // Show queries memory usage
'soft_limit' => 100, // After the soft limit, no parameters/backtrace are captured
'hard_limit' => 500, // After the hard limit, queries are ignored
],
'mail' => [
'timeline' => false, // Add mails to the timeline
'show_body' => true,
],
'views' => [
'timeline' => false, // Add the views to the timeline (Experimental)
'data' => false, //true for all data, 'keys' for only names, false for no parameters.
'group' => 50, // Group duplicate views. Pass value to auto-group, or true/false to force
'exclude_paths' => [ // Add the paths which you don't want to appear in the views
'vendor/filament', // Exclude Filament components by default
],
],
'route' => [
'label' => true, // show complete route on bar
],
'session' => [
'hiddens' => [], // hides sensitive values using array paths
],
'symfony_request' => [
'hiddens' => [], // hides sensitive values using array paths, example: request_request.password
],
'events' => [
'data' => false, // collect events data, listeners
],
'logs' => [
'file' => null,
],
'cache' => [
'values' => true, // collect cache values
],
],
/*
|--------------------------------------------------------------------------
| Inject Debugbar in Response
|--------------------------------------------------------------------------
|
| Usually, the debugbar is added just before </body>, by listening to the
| Response after the App is done. If you disable this, you have to add them
| in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
*/
'inject' => true,
/*
|--------------------------------------------------------------------------
| DebugBar route prefix
|--------------------------------------------------------------------------
|
| Sometimes you want to set route prefix to be used by DebugBar to load
| its resources from. Usually the need comes from misconfigured web server or
| from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
*/
'route_prefix' => '_debugbar',
/*
|--------------------------------------------------------------------------
| DebugBar route middleware
|--------------------------------------------------------------------------
|
| Additional middleware to run on the Debugbar routes
*/
'route_middleware' => [],
/*
|--------------------------------------------------------------------------
| DebugBar route domain
|--------------------------------------------------------------------------
|
| By default DebugBar route served from the same domain that request served.
| To override default domain, specify it as a non-empty value.
*/
'route_domain' => null,
/*
|--------------------------------------------------------------------------
| DebugBar theme
|--------------------------------------------------------------------------
|
| Switches between light and dark theme. If set to auto it will respect system preferences
| Possible values: auto, light, dark
*/
'theme' => env('DEBUGBAR_THEME', 'auto'),
/*
|--------------------------------------------------------------------------
| Backtrace stack limit
|--------------------------------------------------------------------------
|
| By default, the DebugBar limits the number of frames returned by the 'debug_backtrace()' function.
| If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit.
*/
'debug_backtrace_limit' => 50,
];

View File

@@ -7,7 +7,7 @@ return [
// The release version of your application // The release version of your application
// Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')) // Example with dynamic git hash: trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD'))
'release' => '4.0.0-beta.348', 'release' => '4.0.0-beta.356',
// When left empty or `null` the Laravel environment will be used // When left empty or `null` the Laravel environment will be used
'environment' => config('app.env'), 'environment' => config('app.env'),

View File

@@ -1,3 +1,3 @@
<?php <?php
return '4.0.0-beta.348'; return '4.0.0-beta.356';

View File

@@ -163,7 +163,7 @@ return new class extends Migration
$table->schemalessAttributes('smtp'); $table->schemalessAttributes('smtp');
}); });
$instance_setting = InstanceSettings::get(); $instance_setting = instanceSettings();
$instance_setting->smtp = [ $instance_setting->smtp = [
'enabled' => $instance_setting->smtp_enabled, 'enabled' => $instance_setting->smtp_enabled,
'from_address' => $instance_setting->smtp_from_address, 'from_address' => $instance_setting->smtp_from_address,

View File

@@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->boolean('dump_all')->default(false);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('scheduled_database_backups', function (Blueprint $table) {
$table->dropColumn('dump_all');
});
}
};

View File

@@ -27,14 +27,14 @@ class InstanceSettingsSeeder extends Seeder
$ipv4 = Process::run('curl -4s https://ifconfig.io')->output(); $ipv4 = Process::run('curl -4s https://ifconfig.io')->output();
$ipv4 = trim($ipv4); $ipv4 = trim($ipv4);
$ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP); $ipv4 = filter_var($ipv4, FILTER_VALIDATE_IP);
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (is_null($settings->public_ipv4) && $ipv4) { if (is_null($settings->public_ipv4) && $ipv4) {
$settings->update(['public_ipv4' => $ipv4]); $settings->update(['public_ipv4' => $ipv4]);
} }
$ipv6 = Process::run('curl -6s https://ifconfig.io')->output(); $ipv6 = Process::run('curl -6s https://ifconfig.io')->output();
$ipv6 = trim($ipv6); $ipv6 = trim($ipv6);
$ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP); $ipv6 = filter_var($ipv6, FILTER_VALIDATE_IP);
$settings = \App\Models\InstanceSettings::get(); $settings = instanceSettings();
if (is_null($settings->public_ipv6) && $ipv6) { if (is_null($settings->public_ipv6) && $ipv6) {
$settings->update(['public_ipv6' => $ipv6]); $settings->update(['public_ipv6' => $ipv6]);
} }

View File

@@ -58,6 +58,7 @@ services:
SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}" SOKETI_DEFAULT_APP_ID: "${PUSHER_APP_ID:-coolify}"
SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}" SOKETI_DEFAULT_APP_KEY: "${PUSHER_APP_KEY:-coolify}"
SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}" SOKETI_DEFAULT_APP_SECRET: "${PUSHER_APP_SECRET:-coolify}"
entrypoint: ["/bin/sh", "/soketi-entrypoint.sh"]
vite: vite:
image: node:20 image: node:20
pull_policy: always pull_policy: always

View File

@@ -113,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,9 +1,27 @@
FROM quay.io/soketi/soketi:1.6-16-alpine FROM quay.io/soketi/soketi:1.6-16-alpine
ARG TARGETPLATFORM
# https://github.com/cloudflare/cloudflared/releases
ARG CLOUDFLARED_VERSION=2024.4.1
WORKDIR /terminal WORKDIR /terminal
RUN apk add --no-cache openssh-client make g++ python3 RUN apk add --no-cache openssh-client make g++ python3 curl
COPY docker/coolify-realtime/package.json ./ COPY docker/coolify-realtime/package.json ./
RUN npm i RUN npm i
RUN npm rebuild node-pty --update-binary RUN npm rebuild node-pty --update-binary
COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh COPY docker/coolify-realtime/soketi-entrypoint.sh /soketi-entrypoint.sh
COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js COPY docker/coolify-realtime/terminal-server.js /terminal/terminal-server.js
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \
echo 'amd64' && \
curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
RUN /bin/sh -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \
echo 'arm64' && \
curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \
;fi"
ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"] ENTRYPOINT ["/bin/sh", "/soketi-entrypoint.sh"]

View File

@@ -1,11 +1,19 @@
#!/bin/sh #!/bin/sh
# Function to timestamp logs # Function to timestamp logs
# Check if the first argument is 'watch'
if [ "$1" = "watch" ]; then
WATCH_MODE="--watch"
else
WATCH_MODE=""
fi
timestamp() { timestamp() {
date "+%Y-%m-%d %H:%M:%S" date "+%Y-%m-%d %H:%M:%S"
} }
# Start the terminal server in the background with logging # Start the terminal server in the background with logging
node /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 & node $WATCH_MODE /terminal/terminal-server.js > >(while read line; do echo "$(timestamp) [TERMINAL] $line"; done) 2>&1 &
TERMINAL_PID=$! TERMINAL_PID=$!
# Start the Soketi process in the background with logging # Start the Soketi process in the background with logging

View File

@@ -61,9 +61,13 @@ wss.on('connection', (ws) => {
const userSession = { ws, userId, ptyProcess: null, isActive: false }; const userSession = { ws, userId, ptyProcess: null, isActive: false };
userSessions.set(userId, userSession); userSessions.set(userId, userSession);
ws.on('message', (message) => handleMessage(userSession, message)); ws.on('message', (message) => {
handleMessage(userSession, message);
});
ws.on('error', (err) => handleError(err, userId)); ws.on('error', (err) => handleError(err, userId));
ws.on('close', () => handleClose(userId)); ws.on('close', () => handleClose(userId));
}); });
const messageHandlers = { const messageHandlers = {
@@ -108,7 +112,6 @@ function parseMessage(message) {
async function handleCommand(ws, command, userId) { async function handleCommand(ws, command, userId) {
const userSession = userSessions.get(userId); const userSession = userSessions.get(userId);
if (userSession && userSession.isActive) { if (userSession && userSession.isActive) {
const result = await killPtyProcess(userId); const result = await killPtyProcess(userId);
if (!result) { if (!result) {
@@ -127,27 +130,29 @@ async function handleCommand(ws, command, userId) {
cols: 80, cols: 80,
rows: 30, rows: 30,
cwd: process.env.HOME, cwd: process.env.HOME,
env: {},
}; };
// NOTE: - Initiates a process within the Terminal container // NOTE: - Initiates a process within the Terminal container
// Establishes an SSH connection to root@coolify with RequestTTY enabled // Establishes an SSH connection to root@coolify with RequestTTY enabled
// Executes the 'docker exec' command to connect to a specific container // Executes the 'docker exec' command to connect to a specific container
// If the user types 'exit', it terminates the container connection and reverts to the server. const ptyProcess = pty.spawn('ssh', sshArgs.concat([hereDocContent]), options);
const ptyProcess = pty.spawn('ssh', sshArgs.concat(['bash']), options);
userSession.ptyProcess = ptyProcess; userSession.ptyProcess = ptyProcess;
userSession.isActive = true; userSession.isActive = true;
ptyProcess.write(hereDocContent + '\n');
// clear the terminal if the user has clear command
ptyProcess.write('command -v clear >/dev/null 2>&1 && clear\n');
ws.send('pty-ready'); ws.send('pty-ready');
ptyProcess.onData((data) => ws.send(data)); ptyProcess.onData((data) => {
ws.send(data);
});
// when parent closes // when parent closes
ptyProcess.onExit(({ exitCode, signal }) => { ptyProcess.onExit(({ exitCode, signal }) => {
console.error(`Process exited with code ${exitCode} and signal ${signal}`); console.error(`Process exited with code ${exitCode} and signal ${signal}`);
ws.send('pty-exited');
userSession.isActive = false; userSession.isActive = false;
}); });
if (timeout) { if (timeout) {
@@ -181,7 +186,7 @@ async function killPtyProcess(userId) {
// session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098 // session.ptyProcess.kill() wont work here because of https://github.com/moby/moby/issues/9098
// patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947 // patch with https://github.com/moby/moby/issues/9098#issuecomment-189743947
session.ptyProcess.write('kill -TERM -$$ && exit\n'); session.ptyProcess.write('set +o history\nkill -TERM -$$ && exit\nset -o history\n');
setTimeout(() => { setTimeout(() => {
if (!session.isActive || !session.ptyProcess) { if (!session.isActive || !session.ptyProcess) {
@@ -230,5 +235,5 @@ function extractHereDocContent(commandString) {
} }
server.listen(6002, () => { server.listen(6002, () => {
console.log('Server listening on port 6002'); console.log('Coolify realtime terminal server listening on port 6002. Let the hacking begin!');
}); });

View File

@@ -236,6 +236,10 @@ paths:
watch_paths: watch_paths:
type: string type: string
description: 'The watch paths.' description: 'The watch paths.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -457,6 +461,10 @@ paths:
watch_paths: watch_paths:
type: string type: string
description: 'The watch paths.' description: 'The watch paths.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -678,6 +686,10 @@ paths:
watch_paths: watch_paths:
type: string type: string
description: 'The watch paths.' description: 'The watch paths.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -850,6 +862,10 @@ paths:
instant_deploy: instant_deploy:
type: boolean type: boolean
description: 'The flag to indicate if the application should be deployed instantly.' description: 'The flag to indicate if the application should be deployed instantly.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -1013,6 +1029,10 @@ paths:
instant_deploy: instant_deploy:
type: boolean type: boolean
description: 'The flag to indicate if the application should be deployed instantly.' description: 'The flag to indicate if the application should be deployed instantly.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -1067,6 +1087,10 @@ paths:
instant_deploy: instant_deploy:
type: boolean type: boolean
description: 'The flag to indicate if the application should be deployed instantly.' description: 'The flag to indicate if the application should be deployed instantly.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -1126,9 +1150,33 @@ paths:
type: string type: string
format: uuid format: uuid
- -
name: cleanup name: delete_configurations
in: query in: query
description: 'Delete configurations and volumes.' description: 'Delete configurations.'
required: false
schema:
type: boolean
default: true
-
name: delete_volumes
in: query
description: 'Delete volumes.'
required: false
schema:
type: boolean
default: true
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
required: false
schema:
type: boolean
default: true
-
name: delete_connected_networks
in: query
description: 'Delete connected networks.'
required: false required: false
schema: schema:
type: boolean type: boolean
@@ -1351,6 +1399,10 @@ paths:
watch_paths: watch_paths:
type: string type: string
description: 'The watch paths.' description: 'The watch paths.'
use_build_server:
type: boolean
nullable: true
description: 'Use build server.'
type: object type: object
responses: responses:
'200': '200':
@@ -1738,6 +1790,52 @@ paths:
security: security:
- -
bearerAuth: [] bearerAuth: []
'/applications/{uuid}/execute':
post:
tags:
- Applications
summary: 'Execute Command'
description: "Execute a command on the application's current container."
operationId: execute-command-application
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
format: uuid
requestBody:
description: 'Command to execute.'
required: true
content:
application/json:
schema:
properties:
command:
type: string
description: 'Command to execute.'
type: object
responses:
'200':
description: "Execute a command on the application's current container."
content:
application/json:
schema:
properties:
message: { type: string, example: 'Command executed.' }
response: { type: string }
type: object
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
/databases: /databases:
get: get:
tags: tags:
@@ -1809,9 +1907,33 @@ paths:
type: string type: string
format: uuid format: uuid
- -
name: cleanup name: delete_configurations
in: query in: query
description: 'Delete configurations and volumes.' description: 'Delete configurations.'
required: false
schema:
type: boolean
default: true
-
name: delete_volumes
in: query
description: 'Delete volumes.'
required: false
schema:
type: boolean
default: true
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
required: false
schema:
type: boolean
default: true
-
name: delete_connected_networks
in: query
description: 'Delete connected networks.'
required: false required: false
schema: schema:
type: boolean type: boolean
@@ -3812,6 +3934,38 @@ paths:
required: true required: true
schema: schema:
type: string type: string
-
name: delete_configurations
in: query
description: 'Delete configurations.'
required: false
schema:
type: boolean
default: true
-
name: delete_volumes
in: query
description: 'Delete volumes.'
required: false
schema:
type: boolean
default: true
-
name: docker_cleanup
in: query
description: 'Run docker cleanup.'
required: false
schema:
type: boolean
default: true
-
name: delete_connected_networks
in: query
description: 'Delete connected networks.'
required: false
schema:
type: boolean
default: true
responses: responses:
'200': '200':
description: 'Delete a service by UUID' description: 'Delete a service by UUID'
@@ -4769,6 +4923,10 @@ components:
type: boolean type: boolean
swarm_cluster: swarm_cluster:
type: string type: string
delete_unused_volumes:
type: boolean
delete_unused_networks:
type: boolean
type: object type: object
ServerSetting: ServerSetting:
description: 'Server Settings model' description: 'Server Settings model'

View File

@@ -46,6 +46,9 @@ services:
- PUSHER_APP_ID - PUSHER_APP_ID
- PUSHER_APP_KEY - PUSHER_APP_KEY
- PUSHER_APP_SECRET - PUSHER_APP_SECRET
- TERMINAL_PROTOCOL
- TERMINAL_HOST
- TERMINAL_PORT
- AUTOUPDATE - AUTOUPDATE
- SELF_HOSTED - SELF_HOSTED
- SSH_MUX_ENABLED - SSH_MUX_ENABLED
@@ -110,7 +113,7 @@ services:
retries: 10 retries: 10
timeout: 2s timeout: 2s
soketi: soketi:
image: 'ghcr.io/coollabsio/coolify-realtime:1.0.1' image: 'ghcr.io/coollabsio/coolify-realtime:1.0.3'
ports: ports:
- "${SOKETI_PORT:-6001}:6001" - "${SOKETI_PORT:-6001}:6001"
- "6002:6002" - "6002:6002"

View File

@@ -1,16 +1,16 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.347" "version": "4.0.0-beta.354"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.348" "version": "4.0.0-beta.355"
}, },
"helper": { "helper": {
"version": "1.0.1" "version": "1.0.2"
}, },
"realtime": { "realtime": {
"version": "1.0.1" "version": "1.0.3"
} }
} }
} }

166
public/svgs/anythingllm.svg Normal file
View File

@@ -0,0 +1,166 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0.00 0.00 245.00 245.00">
<path stroke="#919393" stroke-width="2.00" fill="none" stroke-linecap="butt" vector-effect="non-scaling-stroke" d="
M 187.03 159.00
L 161.49 159.00
A 1.24 1.24 0.0 0 1 160.48 158.48
Q 153.96 149.33 147.89 140.81
C 145.78 137.86 141.80 138.01 139.49 140.67
C 136.43 144.17 133.65 147.56 136.44 151.42
Q 142.22 159.39 148.22 167.98
Q 152.74 174.47 160.49 174.57
Q 173.94 174.73 188.79 174.60
C 196.64 174.52 203.24 168.72 203.23 160.63
Q 203.16 123.84 203.20 83.74
C 203.21 78.41 199.97 73.56 195.17 71.35
Q 191.90 69.85 186.23 69.92
Q 173.94 70.08 161.37 69.94
C 156.39 69.89 152.10 71.63 149.08 75.49
Q 117.03 116.46 84.45 158.09
A 2.37 2.35 -70.9 0 1 82.59 159.00
L 57.21 159.00
Q 56.65 159.00 56.65 158.45
L 56.66 85.95
Q 56.66 85.31 57.30 85.31
L 83.00 85.31
A 0.80 0.79 71.0 0 1 83.63 85.62
Q 90.51 94.60 97.61 104.71
C 99.47 107.36 103.07 108.21 105.48 105.83
Q 107.60 103.72 109.70 100.67
C 110.60 99.36 110.38 97.79 110.28 96.30
Q 110.23 95.62 109.82 95.07
Q 103.03 85.94 96.91 77.34
C 93.63 72.73 89.66 69.98 84.18 70.00
Q 71.60 70.05 56.56 69.96
C 50.37 69.93 45.44 72.61 42.63 78.14
Q 41.19 80.99 41.25 87.27
Q 41.55 120.87 41.27 158.11
Q 41.23 162.77 42.32 165.57
Q 44.32 170.75 49.37 173.22
Q 52.63 174.81 60.03 174.72
Q 72.58 174.56 82.84 174.65
Q 91.13 174.73 95.47 169.18
Q 127.35 128.40 160.51 86.00
A 1.81 1.80 18.9 0 1 161.93 85.31
L 187.42 85.31
A 0.40 0.40 0.0 0 1 187.82 85.71
L 187.82 158.21
Q 187.82 159.00 187.03 159.00"
/>
<path fill="#222627" d="
M 46.00 0.00
L 198.80 0.00
Q 221.03 2.81 233.86 19.40
Q 244.84 33.61 244.80 52.00
Q 244.65 120.20 244.83 188.95
Q 244.86 199.26 243.39 205.30
Q 241.12 214.72 235.86 222.46
C 234.56 224.37 232.98 226.08 231.56 227.86
Q 229.92 229.91 227.88 231.54
C 226.00 233.04 224.26 234.66 222.24 236.01
Q 214.15 241.39 204.78 243.49
Q 198.73 244.86 188.67 244.83
Q 119.30 244.65 52.55 244.80
Q 32.84 244.84 18.81 233.38
Q 2.64 220.18 0.00 198.69
L 0.00 45.76
Q 1.42 35.66 4.33 29.79
Q 17.17 3.86 46.00 0.00
Z
M 187.03 159.00
L 161.49 159.00
A 1.24 1.24 0.0 0 1 160.48 158.48
Q 153.96 149.33 147.89 140.81
C 145.78 137.86 141.80 138.01 139.49 140.67
C 136.43 144.17 133.65 147.56 136.44 151.42
Q 142.22 159.39 148.22 167.98
Q 152.74 174.47 160.49 174.57
Q 173.94 174.73 188.79 174.60
C 196.64 174.52 203.24 168.72 203.23 160.63
Q 203.16 123.84 203.20 83.74
C 203.21 78.41 199.97 73.56 195.17 71.35
Q 191.90 69.85 186.23 69.92
Q 173.94 70.08 161.37 69.94
C 156.39 69.89 152.10 71.63 149.08 75.49
Q 117.03 116.46 84.45 158.09
A 2.37 2.35 -70.9 0 1 82.59 159.00
L 57.21 159.00
Q 56.65 159.00 56.65 158.45
L 56.66 85.95
Q 56.66 85.31 57.30 85.31
L 83.00 85.31
A 0.80 0.79 71.0 0 1 83.63 85.62
Q 90.51 94.60 97.61 104.71
C 99.47 107.36 103.07 108.21 105.48 105.83
Q 107.60 103.72 109.70 100.67
C 110.60 99.36 110.38 97.79 110.28 96.30
Q 110.23 95.62 109.82 95.07
Q 103.03 85.94 96.91 77.34
C 93.63 72.73 89.66 69.98 84.18 70.00
Q 71.60 70.05 56.56 69.96
C 50.37 69.93 45.44 72.61 42.63 78.14
Q 41.19 80.99 41.25 87.27
Q 41.55 120.87 41.27 158.11
Q 41.23 162.77 42.32 165.57
Q 44.32 170.75 49.37 173.22
Q 52.63 174.81 60.03 174.72
Q 72.58 174.56 82.84 174.65
Q 91.13 174.73 95.47 169.18
Q 127.35 128.40 160.51 86.00
A 1.81 1.80 18.9 0 1 161.93 85.31
L 187.42 85.31
A 0.40 0.40 0.0 0 1 187.82 85.71
L 187.82 158.21
Q 187.82 159.00 187.03 159.00
Z"
/>
<path fill="#ffffff" d="
M 187.82 158.21
L 187.82 85.71
A 0.40 0.40 0.0 0 0 187.42 85.31
L 161.93 85.31
A 1.81 1.80 18.9 0 0 160.51 86.00
Q 127.35 128.40 95.47 169.18
Q 91.13 174.73 82.84 174.65
Q 72.58 174.56 60.03 174.72
Q 52.63 174.81 49.37 173.22
Q 44.32 170.75 42.32 165.57
Q 41.23 162.77 41.27 158.11
Q 41.55 120.87 41.25 87.27
Q 41.19 80.99 42.63 78.14
C 45.44 72.61 50.37 69.93 56.56 69.96
Q 71.60 70.05 84.18 70.00
C 89.66 69.98 93.63 72.73 96.91 77.34
Q 103.03 85.94 109.82 95.07
Q 110.23 95.62 110.28 96.30
C 110.38 97.79 110.60 99.36 109.70 100.67
Q 107.60 103.72 105.48 105.83
C 103.07 108.21 99.47 107.36 97.61 104.71
Q 90.51 94.60 83.63 85.62
A 0.80 0.79 71.0 0 0 83.00 85.31
L 57.30 85.31
Q 56.66 85.31 56.66 85.95
L 56.65 158.45
Q 56.65 159.00 57.21 159.00
L 82.59 159.00
A 2.37 2.35 -70.9 0 0 84.45 158.09
Q 117.03 116.46 149.08 75.49
C 152.10 71.63 156.39 69.89 161.37 69.94
Q 173.94 70.08 186.23 69.92
Q 191.90 69.85 195.17 71.35
C 199.97 73.56 203.21 78.41 203.20 83.74
Q 203.16 123.84 203.23 160.63
C 203.24 168.72 196.64 174.52 188.79 174.60
Q 173.94 174.73 160.49 174.57
Q 152.74 174.47 148.22 167.98
Q 142.22 159.39 136.44 151.42
C 133.65 147.56 136.43 144.17 139.49 140.67
C 141.80 138.01 145.78 137.86 147.89 140.81
Q 153.96 149.33 160.48 158.48
A 1.24 1.24 0.0 0 0 161.49 159.00
L 187.03 159.00
Q 187.82 159.00 187.82 158.21
Z"
/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/svgs/argilla.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

15
public/svgs/bitcoin.svg Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2019 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="100%" height="100%" version="1.1" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"
viewBox="0 0 4091.27 4091.73"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_1421344023328">
<path fill="#F7931A" fill-rule="nonzero" d="M4030.06 2540.77c-273.24,1096.01 -1383.32,1763.02 -2479.46,1489.71 -1095.68,-273.24 -1762.69,-1383.39 -1489.33,-2479.31 273.12,-1096.13 1383.2,-1763.19 2479,-1489.95 1096.06,273.24 1763.03,1383.51 1489.76,2479.57l0.02 -0.02z"/>
<path fill="white" fill-rule="nonzero" d="M2947.77 1754.38c40.72,-272.26 -166.56,-418.61 -450,-516.24l91.95 -368.8 -224.5 -55.94 -89.51 359.09c-59.02,-14.72 -119.63,-28.59 -179.87,-42.34l90.16 -361.46 -224.36 -55.94 -92 368.68c-48.84,-11.12 -96.81,-22.11 -143.35,-33.69l0.26 -1.16 -309.59 -77.31 -59.72 239.78c0,0 166.56,38.18 163.05,40.53 90.91,22.69 107.35,82.87 104.62,130.57l-104.74 420.15c6.26,1.59 14.38,3.89 23.34,7.49 -7.49,-1.86 -15.46,-3.89 -23.73,-5.87l-146.81 588.57c-11.11,27.62 -39.31,69.07 -102.87,53.33 2.25,3.26 -163.17,-40.72 -163.17,-40.72l-111.46 256.98 292.15 72.83c54.35,13.63 107.61,27.89 160.06,41.3l-92.9 373.03 224.24 55.94 92 -369.07c61.26,16.63 120.71,31.97 178.91,46.43l-91.69 367.33 224.51 55.94 92.89 -372.33c382.82,72.45 670.67,43.24 791.83,-303.02 97.63,-278.78 -4.86,-439.58 -206.26,-544.44 146.69,-33.83 257.18,-130.31 286.64,-329.61l-0.07 -0.05zm-512.93 719.26c-69.38,278.78 -538.76,128.08 -690.94,90.29l123.28 -494.2c152.17,37.99 640.17,113.17 567.67,403.91zm69.43 -723.3c-63.29,253.58 -453.96,124.75 -580.69,93.16l111.77 -448.21c126.73,31.59 534.85,90.55 468.94,355.05l-0.02 0z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Some files were not shown because too many files have changed in this diff Show More