@@ -6,7 +6,7 @@ APP_KEY=
 | 
				
			|||||||
APP_URL=http://localhost
 | 
					APP_URL=http://localhost
 | 
				
			||||||
APP_PORT=8000
 | 
					APP_PORT=8000
 | 
				
			||||||
APP_DEBUG=true
 | 
					APP_DEBUG=true
 | 
				
			||||||
SSH_MUX_ENABLED=false
 | 
					SSH_MUX_ENABLED=true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# PostgreSQL Database Configuration
 | 
					# PostgreSQL Database Configuration
 | 
				
			||||||
DB_DATABASE=coolify
 | 
					DB_DATABASE=coolify
 | 
				
			||||||
@@ -19,11 +19,7 @@ DB_PORT=5432
 | 
				
			|||||||
# Set to true to enable Ray
 | 
					# Set to true to enable Ray
 | 
				
			||||||
RAY_ENABLED=false
 | 
					RAY_ENABLED=false
 | 
				
			||||||
# Set custom ray port
 | 
					# Set custom ray port
 | 
				
			||||||
RAY_PORT=
 | 
					# RAY_PORT=
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Clockwork Configuration
 | 
					 | 
				
			||||||
CLOCKWORK_ENABLED=false
 | 
					 | 
				
			||||||
CLOCKWORK_QUEUE_COLLECT=true
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Enable Laravel Telescope for debugging
 | 
					# Enable Laravel Telescope for debugging
 | 
				
			||||||
TELESCOPE_ENABLED=false
 | 
					TELESCOPE_ENABLED=false
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/pull_request_template.md
									
									
									
									
										vendored
									
									
								
							@@ -1 +1,13 @@
 | 
				
			|||||||
> Always use `next` branch as destination branch for PRs, not `main`
 | 
					## Submit Checklist (REMOVE THIS SECTION BEFORE SUBMITTING)
 | 
				
			||||||
 | 
					- [ ] I have selected the `next` branch as the destination for my PR, not `main`.
 | 
				
			||||||
 | 
					- [ ] I have listed all changes in the `Changes` section.
 | 
				
			||||||
 | 
					- [ ] I have filled out the `Issues` section with the issue/discussion link(s) (if applicable).
 | 
				
			||||||
 | 
					- [ ] I have tested my changes.
 | 
				
			||||||
 | 
					- [ ] I have considered backwards compatibility.
 | 
				
			||||||
 | 
					- [ ] I have removed this checklist and any unused sections.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Changes
 | 
				
			||||||
 | 
					- 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Issues
 | 
				
			||||||
 | 
					- fix #
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,11 +65,14 @@ jobs:
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
 | 
					            if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
 | 
				
			||||||
              const { data: closedIssues } = await github.rest.search.issuesAndPullRequests({
 | 
					              const pr = context.payload.pull_request;
 | 
				
			||||||
                q: `repo:${owner}/${repo} is:issue is:closed linked:${context.payload.pull_request.number}`,
 | 
					              if (pr.body) {
 | 
				
			||||||
                per_page: 100
 | 
					                const issueReferences = pr.body.match(/#(\d+)/g);
 | 
				
			||||||
              });
 | 
					                if (issueReferences) {
 | 
				
			||||||
              for (const issue of closedIssues.items) {
 | 
					                  for (const reference of issueReferences) {
 | 
				
			||||||
                await processIssue(issue.number);
 | 
					                    const issueNumber = parseInt(reference.substring(1));
 | 
				
			||||||
 | 
					                    await processIssue(issueNumber);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Actions\Application;
 | 
					namespace App\Actions\Application;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Actions\Server\CleanupDocker;
 | 
				
			||||||
use App\Models\Application;
 | 
					use App\Models\Application;
 | 
				
			||||||
use Lorisleiva\Actions\Concerns\AsAction;
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,44 +10,35 @@ class StopApplication
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    use AsAction;
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle(Application $application, bool $previewDeployments = false)
 | 
					    public function handle(Application $application, bool $previewDeployments = false, bool $dockerCleanup = true)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if ($application->destination->server->isSwarm()) {
 | 
					        try {
 | 
				
			||||||
            instant_remote_process(["docker stack rm {$application->uuid}"], $application->destination->server);
 | 
					            $server = $application->destination->server;
 | 
				
			||||||
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $servers = collect([]);
 | 
					 | 
				
			||||||
        $servers->push($application->destination->server);
 | 
					 | 
				
			||||||
        $application->additional_servers->map(function ($server) use ($servers) {
 | 
					 | 
				
			||||||
            $servers->push($server);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        foreach ($servers as $server) {
 | 
					 | 
				
			||||||
            if (! $server->isFunctional()) {
 | 
					            if (! $server->isFunctional()) {
 | 
				
			||||||
                return 'Server is not functional';
 | 
					                return 'Server is not functional';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if ($previewDeployments) {
 | 
					            ray('Stopping application: '.$application->name);
 | 
				
			||||||
                $containers = getCurrentApplicationContainerStatus($server, $application->id, includePullrequests: true);
 | 
					
 | 
				
			||||||
            } else {
 | 
					            if ($server->isSwarm()) {
 | 
				
			||||||
                $containers = getCurrentApplicationContainerStatus($server, $application->id, 0);
 | 
					                instant_remote_process(["docker stack rm {$application->uuid}"], $server);
 | 
				
			||||||
            }
 | 
					
 | 
				
			||||||
            if ($containers->count() > 0) {
 | 
					                return;
 | 
				
			||||||
                foreach ($containers as $container) {
 | 
					 | 
				
			||||||
                    $containerName = data_get($container, 'Names');
 | 
					 | 
				
			||||||
                    if ($containerName) {
 | 
					 | 
				
			||||||
                        instant_remote_process(command: ["docker stop --time=30 $containerName"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                        instant_remote_process(command: ["docker rm $containerName"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                        instant_remote_process(command: ["docker rm -f {$containerName}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $containersToStop = $application->getContainersToStop($previewDeployments);
 | 
				
			||||||
 | 
					            $application->stopContainers($containersToStop, $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($application->build_pack === 'dockercompose') {
 | 
					            if ($application->build_pack === 'dockercompose') {
 | 
				
			||||||
                // remove network
 | 
					                $application->delete_connected_networks($application->uuid);
 | 
				
			||||||
                $uuid = $application->uuid;
 | 
					 | 
				
			||||||
                instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
 | 
					 | 
				
			||||||
                instant_remote_process(["docker network rm {$uuid}"], $server, false);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($dockerCleanup) {
 | 
				
			||||||
 | 
					                CleanupDocker::dispatch($server, true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            ray($e->getMessage());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return $e->getMessage();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ use Illuminate\Process\ProcessResult;
 | 
				
			|||||||
use Illuminate\Support\Facades\DB;
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
use Illuminate\Support\Facades\Process;
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
use Spatie\Activitylog\Models\Activity;
 | 
					use Spatie\Activitylog\Models\Activity;
 | 
				
			||||||
 | 
					use App\Helpers\SshMultiplexingHelper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RunRemoteProcess
 | 
					class RunRemoteProcess
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -137,7 +138,7 @@ class RunRemoteProcess
 | 
				
			|||||||
        $command = $this->activity->getExtraProperty('command');
 | 
					        $command = $this->activity->getExtraProperty('command');
 | 
				
			||||||
        $server = Server::whereUuid($server_uuid)->firstOrFail();
 | 
					        $server = Server::whereUuid($server_uuid)->firstOrFail();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return generateSshCommand($server, $command);
 | 
					        return SshMultiplexingHelper::generateSshCommand($server, $command);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected function handleOutput(string $type, string $output)
 | 
					    protected function handleOutput(string $type, string $output)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Actions\Database;
 | 
					namespace App\Actions\Database;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Actions\Server\CleanupDocker;
 | 
				
			||||||
use App\Models\StandaloneClickhouse;
 | 
					use App\Models\StandaloneClickhouse;
 | 
				
			||||||
use App\Models\StandaloneDragonfly;
 | 
					use App\Models\StandaloneDragonfly;
 | 
				
			||||||
use App\Models\StandaloneKeydb;
 | 
					use App\Models\StandaloneKeydb;
 | 
				
			||||||
@@ -10,25 +11,65 @@ use App\Models\StandaloneMongodb;
 | 
				
			|||||||
use App\Models\StandaloneMysql;
 | 
					use App\Models\StandaloneMysql;
 | 
				
			||||||
use App\Models\StandalonePostgresql;
 | 
					use App\Models\StandalonePostgresql;
 | 
				
			||||||
use App\Models\StandaloneRedis;
 | 
					use App\Models\StandaloneRedis;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
use Lorisleiva\Actions\Concerns\AsAction;
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StopDatabase
 | 
					class StopDatabase
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use AsAction;
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database)
 | 
					    public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|StandaloneMysql|StandaloneMariadb|StandaloneKeydb|StandaloneDragonfly|StandaloneClickhouse $database, bool $isDeleteOperation = false, bool $dockerCleanup = true)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $server = $database->destination->server;
 | 
					        $server = $database->destination->server;
 | 
				
			||||||
        if (! $server->isFunctional()) {
 | 
					        if (! $server->isFunctional()) {
 | 
				
			||||||
            return 'Server is not functional';
 | 
					            return 'Server is not functional';
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        instant_remote_process(command: ["docker stop --time=30 $database->uuid"], server: $server, throwError: false);
 | 
					        $this->stopContainer($database, $database->uuid, 300);
 | 
				
			||||||
        instant_remote_process(command: ["docker rm $database->uuid"], server: $server, throwError: false);
 | 
					        if (! $isDeleteOperation) {
 | 
				
			||||||
        instant_remote_process(command: ["docker rm -f $database->uuid"], server: $server, throwError: false);
 | 
					            if ($dockerCleanup) {
 | 
				
			||||||
 | 
					                CleanupDocker::dispatch($server, true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($database->is_public) {
 | 
					        if ($database->is_public) {
 | 
				
			||||||
            StopDatabaseProxy::run($database);
 | 
					            StopDatabaseProxy::run($database);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return 'Database stopped successfully';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function stopContainer($database, string $containerName, int $timeout = 300): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $server = $database->destination->server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $startTime = time();
 | 
				
			||||||
 | 
					        while ($process->running()) {
 | 
				
			||||||
 | 
					            if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                $this->forceStopContainer($containerName, $server);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            usleep(100000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function forceStopContainer(string $containerName, $server): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function removeContainer(string $containerName, $server): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function deleteConnectedNetworks($uuid, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network rm {$uuid}"], $server, false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,7 @@ class CheckProxy
 | 
				
			|||||||
        if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
 | 
					        if (is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        ['uptime' => $uptime, 'error' => $error] = $server->validateConnection();
 | 
					        ['uptime' => $uptime, 'error' => $error] = $server->validateConnection(false);
 | 
				
			||||||
        if (! $uptime) {
 | 
					        if (! $uptime) {
 | 
				
			||||||
            throw new \Exception($error);
 | 
					            throw new \Exception($error);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Actions\Service;
 | 
					namespace App\Actions\Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Actions\Server\CleanupDocker;
 | 
				
			||||||
use App\Models\Service;
 | 
					use App\Models\Service;
 | 
				
			||||||
use Lorisleiva\Actions\Concerns\AsAction;
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,11 +10,11 @@ class DeleteService
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    use AsAction;
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle(Service $service)
 | 
					    public function handle(Service $service, bool $deleteConfigurations, bool $deleteVolumes, bool $dockerCleanup, bool $deleteConnectedNetworks)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $server = data_get($service, 'server');
 | 
					            $server = data_get($service, 'server');
 | 
				
			||||||
            if ($server->isFunctional()) {
 | 
					            if ($deleteVolumes && $server->isFunctional()) {
 | 
				
			||||||
                $storagesToDelete = collect([]);
 | 
					                $storagesToDelete = collect([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                $service->environment_variables()->delete();
 | 
					                $service->environment_variables()->delete();
 | 
				
			||||||
@@ -33,13 +34,29 @@ class DeleteService
 | 
				
			|||||||
                foreach ($storagesToDelete as $storage) {
 | 
					                foreach ($storagesToDelete as $storage) {
 | 
				
			||||||
                    $commands[] = "docker volume rm -f $storage->name";
 | 
					                    $commands[] = "docker volume rm -f $storage->name";
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                $commands[] = "docker rm -f $service->uuid";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                instant_remote_process($commands, $server, false);
 | 
					                // Execute volume deletion first, this must be done first otherwise volumes will not be deleted.
 | 
				
			||||||
 | 
					                if (! empty($commands)) {
 | 
				
			||||||
 | 
					                    foreach ($commands as $command) {
 | 
				
			||||||
 | 
					                        $result = instant_remote_process([$command], $server, false);
 | 
				
			||||||
 | 
					                        if ($result !== 0) {
 | 
				
			||||||
 | 
					                            ray("Failed to execute: $command");
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($deleteConnectedNetworks) {
 | 
				
			||||||
 | 
					                $service->delete_connected_networks($service->uuid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            throw new \Exception($e->getMessage());
 | 
					            throw new \Exception($e->getMessage());
 | 
				
			||||||
        } finally {
 | 
					        } finally {
 | 
				
			||||||
 | 
					            if ($deleteConfigurations) {
 | 
				
			||||||
 | 
					                $service->delete_configurations();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            foreach ($service->applications()->get() as $application) {
 | 
					            foreach ($service->applications()->get() as $application) {
 | 
				
			||||||
                $application->forceDelete();
 | 
					                $application->forceDelete();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -50,6 +67,11 @@ class DeleteService
 | 
				
			|||||||
                $task->delete();
 | 
					                $task->delete();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $service->tags()->detach();
 | 
					            $service->tags()->detach();
 | 
				
			||||||
 | 
					            $service->forceDelete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($dockerCleanup) {
 | 
				
			||||||
 | 
					                CleanupDocker::dispatch($server, true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Actions\Service;
 | 
					namespace App\Actions\Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Actions\Server\CleanupDocker;
 | 
				
			||||||
use App\Models\Service;
 | 
					use App\Models\Service;
 | 
				
			||||||
use Lorisleiva\Actions\Concerns\AsAction;
 | 
					use Lorisleiva\Actions\Concerns\AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,40 +10,27 @@ class StopService
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    use AsAction;
 | 
					    use AsAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle(Service $service)
 | 
					    public function handle(Service $service, bool $isDeleteOperation = false, bool $dockerCleanup = true)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $server = $service->destination->server;
 | 
					            $server = $service->destination->server;
 | 
				
			||||||
            if (! $server->isFunctional()) {
 | 
					            if (! $server->isFunctional()) {
 | 
				
			||||||
                return 'Server is not functional';
 | 
					                return 'Server is not functional';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ray('Stopping service: '.$service->name);
 | 
					 | 
				
			||||||
            $applications = $service->applications()->get();
 | 
					 | 
				
			||||||
            foreach ($applications as $application) {
 | 
					 | 
				
			||||||
                if ($applications->count() < 6) {
 | 
					 | 
				
			||||||
                    instant_remote_process(command: ["docker stop --time=10 {$application->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                instant_remote_process(command: ["docker rm {$application->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                instant_remote_process(command: ["docker rm -f {$application->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                $application->update(['status' => 'exited']);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $dbs = $service->databases()->get();
 | 
					 | 
				
			||||||
            foreach ($dbs as $db) {
 | 
					 | 
				
			||||||
                if ($dbs->count() < 6) {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    instant_remote_process(command: ["docker stop --time=10 {$db->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					            $containersToStop = $service->getContainersToStop();
 | 
				
			||||||
 | 
					            $service->stopContainers($containersToStop, $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (! $isDeleteOperation) {
 | 
				
			||||||
 | 
					                $service->delete_connected_networks($service->uuid);
 | 
				
			||||||
 | 
					                if ($dockerCleanup) {
 | 
				
			||||||
 | 
					                    CleanupDocker::dispatch($server, true);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                instant_remote_process(command: ["docker rm {$db->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                instant_remote_process(command: ["docker rm -f {$db->name}-{$service->uuid}"], server: $server, throwError: false);
 | 
					 | 
				
			||||||
                $db->update(['status' => 'exited']);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            instant_remote_process(["docker network disconnect {$service->uuid} coolify-proxy"], $service->server);
 | 
					 | 
				
			||||||
            instant_remote_process(["docker network rm {$service->uuid}"], $service->server);
 | 
					 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            ray($e->getMessage());
 | 
					            ray($e->getMessage());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $e->getMessage();
 | 
					            return $e->getMessage();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,6 +43,8 @@ class Kernel extends ConsoleKernel
 | 
				
			|||||||
            $schedule->command('uploads:clear')->everyTwoMinutes();
 | 
					            $schedule->command('uploads:clear')->everyTwoMinutes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $schedule->command('telescope:prune')->daily();
 | 
					            $schedule->command('telescope:prune')->daily();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            // Instance Jobs
 | 
					            // Instance Jobs
 | 
				
			||||||
            $schedule->command('horizon:snapshot')->everyFiveMinutes();
 | 
					            $schedule->command('horizon:snapshot')->everyFiveMinutes();
 | 
				
			||||||
@@ -77,11 +79,11 @@ class Kernel extends ConsoleKernel
 | 
				
			|||||||
                    }
 | 
					                    }
 | 
				
			||||||
                })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
 | 
					                })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $schedule->job(new PullHelperImageJob($server))
 | 
					 | 
				
			||||||
                ->cron($settings->update_check_frequency)
 | 
					 | 
				
			||||||
                ->timezone($settings->instance_timezone)
 | 
					 | 
				
			||||||
                ->onOneServer();
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        $schedule->job(new PullHelperImageJob)
 | 
				
			||||||
 | 
					            ->cron($settings->update_check_frequency)
 | 
				
			||||||
 | 
					            ->timezone($settings->instance_timezone)
 | 
				
			||||||
 | 
					            ->onOneServer();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function schedule_updates($schedule)
 | 
					    private function schedule_updates($schedule)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										212
									
								
								app/Helpers/SshMultiplexingHelper.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								app/Helpers/SshMultiplexingHelper.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,212 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Helpers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
 | 
					use App\Models\Server;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SshMultiplexingHelper
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public static function serverSshConfiguration(Server $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $privateKey = PrivateKey::findOrFail($server->private_key_id);
 | 
				
			||||||
 | 
					        $sshKeyLocation = $privateKey->getKeyLocation();
 | 
				
			||||||
 | 
					        $muxFilename = '/var/www/html/storage/app/ssh/mux/mux_'.$server->uuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'sshKeyLocation' => $sshKeyLocation,
 | 
				
			||||||
 | 
					            'muxFilename' => $muxFilename,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function ensureMultiplexedConnection(Server $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (! self::isMultiplexingEnabled()) {
 | 
				
			||||||
 | 
					            // ray('SSH Multiplexing: DISABLED')->red();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('SSH Multiplexing: ENABLED')->green();
 | 
				
			||||||
 | 
					        // ray('Ensuring multiplexed connection for server:', $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $sshConfig = self::serverSshConfiguration($server);
 | 
				
			||||||
 | 
					        $muxSocket = $sshConfig['muxFilename'];
 | 
				
			||||||
 | 
					        $sshKeyLocation = $sshConfig['sshKeyLocation'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::validateSshKey($sshKeyLocation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
 | 
				
			||||||
 | 
					        $process = Process::run($checkCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($process->exitCode() !== 0) {
 | 
				
			||||||
 | 
					            // ray('SSH Multiplexing: Existing connection check failed or not found')->orange();
 | 
				
			||||||
 | 
					            // ray('Establishing new connection');
 | 
				
			||||||
 | 
					            self::establishNewMultiplexedConnection($server);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // ray('SSH Multiplexing: Existing connection is valid')->green();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function establishNewMultiplexedConnection(Server $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $sshConfig = self::serverSshConfiguration($server);
 | 
				
			||||||
 | 
					        $sshKeyLocation = $sshConfig['sshKeyLocation'];
 | 
				
			||||||
 | 
					        $muxSocket = $sshConfig['muxFilename'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('Establishing new multiplexed connection')->blue();
 | 
				
			||||||
 | 
					        // ray('SSH Key Location:', $sshKeyLocation);
 | 
				
			||||||
 | 
					        // ray('Mux Socket:', $muxSocket);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $connectionTimeout = config('constants.ssh.connection_timeout');
 | 
				
			||||||
 | 
					        $serverInterval = config('constants.ssh.server_interval');
 | 
				
			||||||
 | 
					        $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} "
 | 
				
			||||||
 | 
					            .self::getCommonSshOptions($server, $sshKeyLocation, $connectionTimeout, $serverInterval)
 | 
				
			||||||
 | 
					            ."{$server->user}@{$server->ip}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('Establish Command:', $establishCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $establishProcess = Process::run($establishCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('Establish Process Exit Code:', $establishProcess->exitCode());
 | 
				
			||||||
 | 
					        // ray('Establish Process Output:', $establishProcess->output());
 | 
				
			||||||
 | 
					        // ray('Establish Process Error Output:', $establishProcess->errorOutput());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($establishProcess->exitCode() !== 0) {
 | 
				
			||||||
 | 
					            // ray('Failed to establish multiplexed connection')->red();
 | 
				
			||||||
 | 
					            throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('Successfully established multiplexed connection')->green();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Check if the mux socket file was created
 | 
				
			||||||
 | 
					        if (! file_exists($muxSocket)) {
 | 
				
			||||||
 | 
					            // ray('Mux socket file not found after connection establishment')->orange();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function removeMuxFile(Server $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $sshConfig = self::serverSshConfiguration($server);
 | 
				
			||||||
 | 
					        $muxSocket = $sshConfig['muxFilename'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
 | 
				
			||||||
 | 
					        $process = Process::run($closeCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // ray('Closing multiplexed connection')->blue();
 | 
				
			||||||
 | 
					        // ray('Close command:', $closeCommand);
 | 
				
			||||||
 | 
					        // ray('Close process exit code:', $process->exitCode());
 | 
				
			||||||
 | 
					        // ray('Close process output:', $process->output());
 | 
				
			||||||
 | 
					        // ray('Close process error output:', $process->errorOutput());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($process->exitCode() !== 0) {
 | 
				
			||||||
 | 
					            // ray('Failed to close multiplexed connection')->orange();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // ray('Successfully closed multiplexed connection')->green();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function generateScpCommand(Server $server, string $source, string $dest)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $sshConfig = self::serverSshConfiguration($server);
 | 
				
			||||||
 | 
					        $sshKeyLocation = $sshConfig['sshKeyLocation'];
 | 
				
			||||||
 | 
					        $muxSocket = $sshConfig['muxFilename'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $timeout = config('constants.ssh.command_timeout');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $scp_command = "timeout $timeout scp ";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (self::isMultiplexingEnabled()) {
 | 
				
			||||||
 | 
					            $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
				
			||||||
 | 
					            $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
 | 
				
			||||||
 | 
					            self::ensureMultiplexedConnection($server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::addCloudflareProxyCommand($scp_command, $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $scp_command .= self::getCommonSshOptions($server, $sshKeyLocation, config('constants.ssh.connection_timeout'), config('constants.ssh.server_interval'), isScp: true);
 | 
				
			||||||
 | 
					        $scp_command .= "{$source} {$server->user}@{$server->ip}:{$dest}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $scp_command;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function generateSshCommand(Server $server, string $command)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if ($server->settings->force_disabled) {
 | 
				
			||||||
 | 
					            throw new \RuntimeException('Server is disabled.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $sshConfig = self::serverSshConfiguration($server);
 | 
				
			||||||
 | 
					        $sshKeyLocation = $sshConfig['sshKeyLocation'];
 | 
				
			||||||
 | 
					        $muxSocket = $sshConfig['muxFilename'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $timeout = config('constants.ssh.command_timeout');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $ssh_command = "timeout $timeout ssh ";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (self::isMultiplexingEnabled()) {
 | 
				
			||||||
 | 
					            $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
				
			||||||
 | 
					            $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
 | 
				
			||||||
 | 
					            self::ensureMultiplexedConnection($server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self::addCloudflareProxyCommand($ssh_command, $server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $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);
 | 
				
			||||||
 | 
					        $command = str_replace($delimiter, '', $command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $ssh_command .= "{$server->user}@{$server->ip} 'bash -se' << \\$delimiter".PHP_EOL
 | 
				
			||||||
 | 
					            .$command.PHP_EOL
 | 
				
			||||||
 | 
					            .$delimiter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $ssh_command;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function isMultiplexingEnabled(): bool
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return config('constants.ssh.mux_enabled') && ! config('coolify.is_windows_docker_desktop');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function validateSshKey(string $sshKeyLocation): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $checkKeyCommand = "ls $sshKeyLocation 2>/dev/null";
 | 
				
			||||||
 | 
					        $keyCheckProcess = Process::run($checkKeyCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($keyCheckProcess->exitCode() !== 0) {
 | 
				
			||||||
 | 
					            throw new \RuntimeException("SSH key file not accessible: $sshKeyLocation");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function addCloudflareProxyCommand(string &$command, Server $server): void
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (data_get($server, 'settings.is_cloudflare_tunnel')) {
 | 
				
			||||||
 | 
					            $command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function getCommonSshOptions(Server $server, string $sshKeyLocation, int $connectionTimeout, int $serverInterval, bool $isScp = false): string
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $options = "-i {$sshKeyLocation} "
 | 
				
			||||||
 | 
					            .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
 | 
				
			||||||
 | 
					            .'-o PasswordAuthentication=no '
 | 
				
			||||||
 | 
					            ."-o ConnectTimeout=$connectionTimeout "
 | 
				
			||||||
 | 
					            ."-o ServerAliveInterval=$serverInterval "
 | 
				
			||||||
 | 
					            .'-o RequestTTY=no '
 | 
				
			||||||
 | 
					            .'-o LogLevel=ERROR ';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Bruh
 | 
				
			||||||
 | 
					        if ($isScp) {
 | 
				
			||||||
 | 
					            $options .= "-P {$server->port} ";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $options .= "-p {$server->port} ";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $options;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2529,6 +2529,131 @@ class ApplicationsController extends Controller
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[OA\Post(
 | 
				
			||||||
 | 
					        summary: 'Execute Command',
 | 
				
			||||||
 | 
					        description: "Execute a command on the application's current container.",
 | 
				
			||||||
 | 
					        path: '/applications/{uuid}/execute',
 | 
				
			||||||
 | 
					        operationId: 'execute-command-application',
 | 
				
			||||||
 | 
					        security: [
 | 
				
			||||||
 | 
					            ['bearerAuth' => []],
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        tags: ['Applications'],
 | 
				
			||||||
 | 
					        parameters: [
 | 
				
			||||||
 | 
					            new OA\Parameter(
 | 
				
			||||||
 | 
					                name: 'uuid',
 | 
				
			||||||
 | 
					                in: 'path',
 | 
				
			||||||
 | 
					                description: 'UUID of the application.',
 | 
				
			||||||
 | 
					                required: true,
 | 
				
			||||||
 | 
					                schema: new OA\Schema(
 | 
				
			||||||
 | 
					                    type: 'string',
 | 
				
			||||||
 | 
					                    format: 'uuid',
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        requestBody: new OA\RequestBody(
 | 
				
			||||||
 | 
					            required: true,
 | 
				
			||||||
 | 
					            description: 'Command to execute.',
 | 
				
			||||||
 | 
					            content: new OA\MediaType(
 | 
				
			||||||
 | 
					                mediaType: 'application/json',
 | 
				
			||||||
 | 
					                schema: new OA\Schema(
 | 
				
			||||||
 | 
					                    type: 'object',
 | 
				
			||||||
 | 
					                    properties: [
 | 
				
			||||||
 | 
					                        'command' => ['type' => 'string', 'description' => 'Command to execute.'],
 | 
				
			||||||
 | 
					                    ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        responses: [
 | 
				
			||||||
 | 
					            new OA\Response(
 | 
				
			||||||
 | 
					                response: 200,
 | 
				
			||||||
 | 
					                description: "Execute a command on the application's current container.",
 | 
				
			||||||
 | 
					                content: [
 | 
				
			||||||
 | 
					                    new OA\MediaType(
 | 
				
			||||||
 | 
					                        mediaType: 'application/json',
 | 
				
			||||||
 | 
					                        schema: new OA\Schema(
 | 
				
			||||||
 | 
					                            type: 'object',
 | 
				
			||||||
 | 
					                            properties: [
 | 
				
			||||||
 | 
					                                'message' => ['type' => 'string', 'example' => 'Command executed.'],
 | 
				
			||||||
 | 
					                                'response' => ['type' => 'string'],
 | 
				
			||||||
 | 
					                            ]
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            new OA\Response(
 | 
				
			||||||
 | 
					                response: 401,
 | 
				
			||||||
 | 
					                ref: '#/components/responses/401',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            new OA\Response(
 | 
				
			||||||
 | 
					                response: 400,
 | 
				
			||||||
 | 
					                ref: '#/components/responses/400',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            new OA\Response(
 | 
				
			||||||
 | 
					                response: 404,
 | 
				
			||||||
 | 
					                ref: '#/components/responses/404',
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    )]
 | 
				
			||||||
 | 
					    public function execute_command_by_uuid(Request $request)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // TODO: Need to review this from security perspective, to not allow arbitrary command execution
 | 
				
			||||||
 | 
					        $allowedFields = ['command'];
 | 
				
			||||||
 | 
					        $teamId = getTeamIdFromToken();
 | 
				
			||||||
 | 
					        if (is_null($teamId)) {
 | 
				
			||||||
 | 
					            return invalidTokenResponse();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $uuid = $request->route('uuid');
 | 
				
			||||||
 | 
					        if (! $uuid) {
 | 
				
			||||||
 | 
					            return response()->json(['message' => 'UUID is required.'], 400);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
 | 
				
			||||||
 | 
					        if (! $application) {
 | 
				
			||||||
 | 
					            return response()->json(['message' => 'Application not found.'], 404);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $return = validateIncomingRequest($request);
 | 
				
			||||||
 | 
					        if ($return instanceof \Illuminate\Http\JsonResponse) {
 | 
				
			||||||
 | 
					            return $return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $validator = customApiValidator($request->all(), [
 | 
				
			||||||
 | 
					            'command' => 'string|required',
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $extraFields = array_diff(array_keys($request->all()), $allowedFields);
 | 
				
			||||||
 | 
					        if ($validator->fails() || ! empty($extraFields)) {
 | 
				
			||||||
 | 
					            $errors = $validator->errors();
 | 
				
			||||||
 | 
					            if (! empty($extraFields)) {
 | 
				
			||||||
 | 
					                foreach ($extraFields as $field) {
 | 
				
			||||||
 | 
					                    $errors->add($field, 'This field is not allowed.');
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return response()->json([
 | 
				
			||||||
 | 
					                'message' => 'Validation failed.',
 | 
				
			||||||
 | 
					                'errors' => $errors,
 | 
				
			||||||
 | 
					            ], 422);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $container = getCurrentApplicationContainerStatus($application->destination->server, $application->id)->firstOrFail();
 | 
				
			||||||
 | 
					        $status = getContainerStatus($application->destination->server, $container['Names']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($status !== 'running') {
 | 
				
			||||||
 | 
					            return response()->json([
 | 
				
			||||||
 | 
					                'message' => 'Application is not running.',
 | 
				
			||||||
 | 
					            ], 400);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $commands = collect([
 | 
				
			||||||
 | 
					            executeInDocker($container['Names'], $request->command),
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $res = instant_remote_process(command: $commands, server: $application->destination->server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return response()->json([
 | 
				
			||||||
 | 
					            'message' => 'Command executed.',
 | 
				
			||||||
 | 
					            'response' => $res,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function validateDataApplications(Request $request, Server $server)
 | 
					    private function validateDataApplications(Request $request, Server $server)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $teamId = getTeamIdFromToken();
 | 
					        $teamId = getTeamIdFromToken();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,6 +29,7 @@ use Illuminate\Queue\SerializesModels;
 | 
				
			|||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
use Illuminate\Support\Sleep;
 | 
					use Illuminate\Support\Sleep;
 | 
				
			||||||
use Illuminate\Support\Str;
 | 
					use Illuminate\Support\Str;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
use RuntimeException;
 | 
					use RuntimeException;
 | 
				
			||||||
use Symfony\Component\Yaml\Yaml;
 | 
					use Symfony\Component\Yaml\Yaml;
 | 
				
			||||||
use Throwable;
 | 
					use Throwable;
 | 
				
			||||||
@@ -210,7 +211,6 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        ray('New container name: ', $this->container_name)->green();
 | 
					        ray('New container name: ', $this->container_name)->green();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        savePrivateKeyToFs($this->server);
 | 
					 | 
				
			||||||
        $this->saved_outputs = collect();
 | 
					        $this->saved_outputs = collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Set preview fqdn
 | 
					        // Set preview fqdn
 | 
				
			||||||
@@ -1442,21 +1442,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
        if ($this->pull_request_id !== 0) {
 | 
					        if ($this->pull_request_id !== 0) {
 | 
				
			||||||
            $local_branch = "pull/{$this->pull_request_id}/head";
 | 
					            $local_branch = "pull/{$this->pull_request_id}/head";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $private_key = data_get($this->application, 'private_key.private_key');
 | 
					        $private_key = $this->application->privateKey?->getKeyLocation();
 | 
				
			||||||
        if ($private_key) {
 | 
					        if ($private_key) {
 | 
				
			||||||
            $private_key = base64_encode($private_key);
 | 
					 | 
				
			||||||
            $this->execute_remote_command(
 | 
					            $this->execute_remote_command(
 | 
				
			||||||
                [
 | 
					                [
 | 
				
			||||||
                    executeInDocker($this->deployment_uuid, 'mkdir -p /root/.ssh'),
 | 
					                    executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i {$private_key}\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                    executeInDocker($this->deployment_uuid, "echo '{$private_key}' | base64 -d | tee /root/.ssh/id_rsa > /dev/null"),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                    executeInDocker($this->deployment_uuid, 'chmod 600 /root/.ssh/id_rsa'),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                [
 | 
					 | 
				
			||||||
                    executeInDocker($this->deployment_uuid, "GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$this->customPort} -o Port={$this->customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git ls-remote {$this->fullRepoUrl} {$local_branch}"),
 | 
					 | 
				
			||||||
                    'hidden' => true,
 | 
					                    'hidden' => true,
 | 
				
			||||||
                    'save' => 'git_commit_sha',
 | 
					                    'save' => 'git_commit_sha',
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
@@ -2211,20 +2201,40 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
 | 
				
			|||||||
        $this->application_deployment_queue->addLogEntry('Building docker image completed.');
 | 
					        $this->application_deployment_queue->addLogEntry('Building docker image completed.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    private function graceful_shutdown_container(string $containerName, int $timeout = 300)
 | 
				
			||||||
     * @param  int  $timeout  in seconds
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    private function graceful_shutdown_container(string $containerName, int $timeout = 30)
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->execute_remote_command(
 | 
					            $process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
                ["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
 | 
					
 | 
				
			||||||
                ["docker rm $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
					            $startTime = time();
 | 
				
			||||||
            );
 | 
					            while ($process->running()) {
 | 
				
			||||||
 | 
					                if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                    $this->execute_remote_command(
 | 
				
			||||||
 | 
					                        ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                usleep(100000);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $isRunning = $this->execute_remote_command(
 | 
				
			||||||
 | 
					                ["docker inspect -f '{{.State.Running}}' $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
				
			||||||
 | 
					            ) === 'true';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($isRunning) {
 | 
				
			||||||
 | 
					                $this->execute_remote_command(
 | 
				
			||||||
 | 
					                    ["docker kill $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } catch (\Exception $error) {
 | 
					        } catch (\Exception $error) {
 | 
				
			||||||
            // report error if needed
 | 
					            $this->application_deployment_queue->addLogEntry("Error stopping container $containerName: " . $error->getMessage(), 'stderr');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $this->remove_container($containerName);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function remove_container(string $containerName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
        $this->execute_remote_command(
 | 
					        $this->execute_remote_command(
 | 
				
			||||||
            ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
					            ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
@@ -2240,7 +2250,7 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
 | 
				
			|||||||
                $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
 | 
					                $containers = getCurrentApplicationContainerStatus($this->server, $this->application->id, $this->pull_request_id);
 | 
				
			||||||
                if ($this->pull_request_id === 0) {
 | 
					                if ($this->pull_request_id === 0) {
 | 
				
			||||||
                    $containers = $containers->filter(function ($container) {
 | 
					                    $containers = $containers->filter(function ($container) {
 | 
				
			||||||
                        return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name.'-pr-'.$this->pull_request_id;
 | 
					                        return data_get($container, 'Names') !== $this->container_name && data_get($container, 'Names') !== $this->container_name . '-pr-' . $this->pull_request_id;
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                $containers->each(function ($container) {
 | 
					                $containers->each(function ($container) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,11 +21,10 @@ class CleanupHelperContainersJob implements ShouldBeEncrypted, ShouldBeUnique, S
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            ray('Cleaning up helper containers on '.$this->server->name);
 | 
					            ray('Cleaning up helper containers on '.$this->server->name);
 | 
				
			||||||
            $containers = instant_remote_process(['docker container ps --filter "ancestor=ghcr.io/coollabsio/coolify-helper:next" --filter "ancestor=ghcr.io/coollabsio/coolify-helper:latest" --format \'{{json .}}\''], $this->server, false);
 | 
					            $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false);
 | 
				
			||||||
            $containers = format_docker_command_output_to_json($containers);
 | 
					            $containerIds = collect(json_decode($containers))->pluck('ID');
 | 
				
			||||||
            if ($containers->count() > 0) {
 | 
					            if ($containerIds->count() > 0) {
 | 
				
			||||||
                foreach ($containers as $container) {
 | 
					                foreach ($containerIds as $containerId) {
 | 
				
			||||||
                    $containerId = data_get($container, 'ID');
 | 
					 | 
				
			||||||
                    ray('Removing container '.$containerId);
 | 
					                    ray('Removing container '.$containerId);
 | 
				
			||||||
                    instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
 | 
					                    instant_remote_process(['docker container rm -f '.$containerId], $this->server, false);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ use Illuminate\Foundation\Bus\Dispatchable;
 | 
				
			|||||||
use Illuminate\Queue\InteractsWithQueue;
 | 
					use Illuminate\Queue\InteractsWithQueue;
 | 
				
			||||||
use Illuminate\Queue\SerializesModels;
 | 
					use Illuminate\Queue\SerializesModels;
 | 
				
			||||||
use Illuminate\Support\Facades\Process;
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
 | 
					use Carbon\Carbon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CleanupStaleMultiplexedConnections implements ShouldQueue
 | 
					class CleanupStaleMultiplexedConnections implements ShouldQueue
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -16,22 +18,64 @@ class CleanupStaleMultiplexedConnections implements ShouldQueue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function handle()
 | 
					    public function handle()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        Server::chunk(100, function ($servers) {
 | 
					        $this->cleanupStaleConnections();
 | 
				
			||||||
            foreach ($servers as $server) {
 | 
					        $this->cleanupNonExistentServerConnections();
 | 
				
			||||||
                $this->cleanupStaleConnection($server);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private function cleanupStaleConnection(Server $server)
 | 
					    private function cleanupStaleConnections()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $muxSocket = "/tmp/mux_{$server->id}";
 | 
					        $muxFiles = Storage::disk('ssh-mux')->files();
 | 
				
			||||||
        $checkCommand = "ssh -O check -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
 | 
					 | 
				
			||||||
        $checkProcess = Process::run($checkCommand);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($checkProcess->exitCode() !== 0) {
 | 
					        foreach ($muxFiles as $muxFile) {
 | 
				
			||||||
            $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip} 2>/dev/null";
 | 
					            $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
 | 
				
			||||||
            Process::run($closeCommand);
 | 
					            $server = Server::where('uuid', $serverUuid)->first();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!$server) {
 | 
				
			||||||
 | 
					                $this->removeMultiplexFile($muxFile);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
 | 
				
			||||||
 | 
					            $checkCommand = "ssh -O check -o ControlPath={$muxSocket} {$server->user}@{$server->ip} 2>/dev/null";
 | 
				
			||||||
 | 
					            $checkProcess = Process::run($checkCommand);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($checkProcess->exitCode() !== 0) {
 | 
				
			||||||
 | 
					                $this->removeMultiplexFile($muxFile);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $muxContent = Storage::disk('ssh-mux')->get($muxFile);
 | 
				
			||||||
 | 
					                $establishedAt = Carbon::parse(substr($muxContent, 37));
 | 
				
			||||||
 | 
					                $expirationTime = $establishedAt->addSeconds(config('constants.ssh.mux_persist_time'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (Carbon::now()->isAfter($expirationTime)) {
 | 
				
			||||||
 | 
					                    $this->removeMultiplexFile($muxFile);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function cleanupNonExistentServerConnections()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $muxFiles = Storage::disk('ssh-mux')->files();
 | 
				
			||||||
 | 
					        $existingServerUuids = Server::pluck('uuid')->toArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($muxFiles as $muxFile) {
 | 
				
			||||||
 | 
					            $serverUuid = $this->extractServerUuidFromMuxFile($muxFile);
 | 
				
			||||||
 | 
					            if (!in_array($serverUuid, $existingServerUuids)) {
 | 
				
			||||||
 | 
					                $this->removeMultiplexFile($muxFile);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function extractServerUuidFromMuxFile($muxFile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return substr($muxFile, 4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function removeMultiplexFile($muxFile)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $muxSocket = "/var/www/html/storage/app/ssh/mux/{$muxFile}";
 | 
				
			||||||
 | 
					        $closeCommand = "ssh -O exit -o ControlPath={$muxSocket} localhost 2>/dev/null";
 | 
				
			||||||
 | 
					        Process::run($closeCommand);
 | 
				
			||||||
 | 
					        Storage::disk('ssh-mux')->delete($muxFile);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ namespace App\Jobs;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use App\Actions\Application\StopApplication;
 | 
					use App\Actions\Application\StopApplication;
 | 
				
			||||||
use App\Actions\Database\StopDatabase;
 | 
					use App\Actions\Database\StopDatabase;
 | 
				
			||||||
 | 
					use App\Actions\Server\CleanupDocker;
 | 
				
			||||||
use App\Actions\Service\DeleteService;
 | 
					use App\Actions\Service\DeleteService;
 | 
				
			||||||
use App\Actions\Service\StopService;
 | 
					use App\Actions\Service\StopService;
 | 
				
			||||||
use App\Models\Application;
 | 
					use App\Models\Application;
 | 
				
			||||||
@@ -30,8 +31,11 @@ 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 = false,
 | 
					        public bool $deleteConfigurations,
 | 
				
			||||||
        public bool $deleteVolumes = false) {}
 | 
					        public bool $deleteVolumes,
 | 
				
			||||||
 | 
					        public bool $dockerCleanup,
 | 
				
			||||||
 | 
					        public bool $deleteConnectedNetworks
 | 
				
			||||||
 | 
					    ) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle()
 | 
					    public function handle()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -51,11 +55,11 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
                case 'standalone-dragonfly':
 | 
					                case 'standalone-dragonfly':
 | 
				
			||||||
                case 'standalone-clickhouse':
 | 
					                case 'standalone-clickhouse':
 | 
				
			||||||
                    $persistentStorages = $this->resource?->persistentStorages()?->get();
 | 
					                    $persistentStorages = $this->resource?->persistentStorages()?->get();
 | 
				
			||||||
                    StopDatabase::run($this->resource);
 | 
					                    StopDatabase::run($this->resource, true);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                case 'service':
 | 
					                case 'service':
 | 
				
			||||||
                    StopService::run($this->resource);
 | 
					                    StopService::run($this->resource, true);
 | 
				
			||||||
                    DeleteService::run($this->resource);
 | 
					                    DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -65,12 +69,31 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
            if ($this->deleteConfigurations) {
 | 
					            if ($this->deleteConfigurations) {
 | 
				
			||||||
                $this->resource?->delete_configurations();
 | 
					                $this->resource?->delete_configurations();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $isDatabase = $this->resource instanceof StandalonePostgresql
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneRedis
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneMongodb
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneMysql
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneMariadb
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneKeydb
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneDragonfly
 | 
				
			||||||
 | 
					                || $this->resource instanceof StandaloneClickhouse;
 | 
				
			||||||
 | 
					            $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
 | 
				
			||||||
 | 
					            if (($this->dockerCleanup || $isDatabase) && $server) {
 | 
				
			||||||
 | 
					                CleanupDocker::dispatch($server, true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($this->deleteConnectedNetworks && ! $isDatabase) {
 | 
				
			||||||
 | 
					                $this->resource?->delete_connected_networks($this->resource->uuid);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            ray($e->getMessage());
 | 
					 | 
				
			||||||
            send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
 | 
					            send_internal_notification('ContainerStoppingJob failed with: '.$e->getMessage());
 | 
				
			||||||
            throw $e;
 | 
					            throw $e;
 | 
				
			||||||
        } finally {
 | 
					        } finally {
 | 
				
			||||||
            $this->resource->forceDelete();
 | 
					            $this->resource->forceDelete();
 | 
				
			||||||
 | 
					            if ($this->dockerCleanup) {
 | 
				
			||||||
 | 
					                CleanupDocker::dispatch($server, true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            Artisan::queue('cleanup:stucked-resources');
 | 
					            Artisan::queue('cleanup:stucked-resources');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 | 
				
			|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
					use Illuminate\Contracts\Queue\ShouldQueue;
 | 
				
			||||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
					use Illuminate\Foundation\Bus\Dispatchable;
 | 
				
			||||||
use Illuminate\Queue\InteractsWithQueue;
 | 
					use Illuminate\Queue\InteractsWithQueue;
 | 
				
			||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
 | 
					 | 
				
			||||||
use Illuminate\Queue\SerializesModels;
 | 
					use Illuminate\Queue\SerializesModels;
 | 
				
			||||||
use Illuminate\Support\Facades\Log;
 | 
					use Illuminate\Support\Facades\Log;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,16 +25,6 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function __construct(public Server $server) {}
 | 
					    public function __construct(public Server $server) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function middleware(): array
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return [new WithoutOverlapping($this->server->id)];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function uniqueId(): int
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->server->id;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function handle(): void
 | 
					    public function handle(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@ use Illuminate\Contracts\Queue\ShouldBeEncrypted;
 | 
				
			|||||||
use Illuminate\Contracts\Queue\ShouldQueue;
 | 
					use Illuminate\Contracts\Queue\ShouldQueue;
 | 
				
			||||||
use Illuminate\Foundation\Bus\Dispatchable;
 | 
					use Illuminate\Foundation\Bus\Dispatchable;
 | 
				
			||||||
use Illuminate\Queue\InteractsWithQueue;
 | 
					use Illuminate\Queue\InteractsWithQueue;
 | 
				
			||||||
use Illuminate\Queue\Middleware\WithoutOverlapping;
 | 
					 | 
				
			||||||
use Illuminate\Queue\SerializesModels;
 | 
					use Illuminate\Queue\SerializesModels;
 | 
				
			||||||
use Illuminate\Support\Facades\Http;
 | 
					use Illuminate\Support\Facades\Http;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -19,17 +18,7 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $timeout = 1000;
 | 
					    public $timeout = 1000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function middleware(): array
 | 
					    public function __construct() {}
 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return [(new WithoutOverlapping($this->server->uuid))];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function uniqueId(): string
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return $this->server->uuid;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function __construct(public Server $server) {}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function handle(): void
 | 
					    public function handle(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,7 +93,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    private function serverStatus()
 | 
					    private function serverStatus()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        ['uptime' => $uptime] = $this->server->validateConnection();
 | 
					        ['uptime' => $uptime] = $this->server->validateConnection(false);
 | 
				
			||||||
        if ($uptime) {
 | 
					        if ($uptime) {
 | 
				
			||||||
            if ($this->server->unreachable_notification_sent === true) {
 | 
					            if ($this->server->unreachable_notification_sent === true) {
 | 
				
			||||||
                $this->server->update(['unreachable_notification_sent' => false]);
 | 
					                $this->server->update(['unreachable_notification_sent' => false]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -141,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
				
			|||||||
            if (! $this->createdServer) {
 | 
					            if (! $this->createdServer) {
 | 
				
			||||||
                return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
 | 
					                return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
 | 
					            $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $this->validateServer('localhost');
 | 
					            return $this->validateServer('localhost');
 | 
				
			||||||
        } elseif ($this->selectedServerType === 'remote') {
 | 
					        } elseif ($this->selectedServerType === 'remote') {
 | 
				
			||||||
@@ -175,7 +175,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
 | 
					        $this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
 | 
				
			||||||
        $this->serverPublicKey = $this->createdServer->privateKey->publicKey();
 | 
					        $this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
 | 
				
			||||||
        $this->updateServerDetails();
 | 
					        $this->updateServerDetails();
 | 
				
			||||||
        $this->currentState = 'validate-server';
 | 
					        $this->currentState = 'validate-server';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
				
			|||||||
    public function savePrivateKey()
 | 
					    public function savePrivateKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->validate([
 | 
					        $this->validate([
 | 
				
			||||||
            'privateKeyName' => 'required',
 | 
					            'privateKeyName' => 'required|string|max:255',
 | 
				
			||||||
            'privateKey' => 'required',
 | 
					            'privateKeyDescription' => 'nullable|string|max:255',
 | 
				
			||||||
 | 
					            'privateKey' => 'required|string',
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        $this->createdPrivateKey = PrivateKey::create([
 | 
					
 | 
				
			||||||
            'name' => $this->privateKeyName,
 | 
					        try {
 | 
				
			||||||
            'description' => $this->privateKeyDescription,
 | 
					            $privateKey = PrivateKey::createAndStore([
 | 
				
			||||||
            'private_key' => $this->privateKey,
 | 
					                'name' => $this->privateKeyName,
 | 
				
			||||||
            'team_id' => currentTeam()->id,
 | 
					                'description' => $this->privateKeyDescription,
 | 
				
			||||||
        ]);
 | 
					                'private_key' => $this->privateKey,
 | 
				
			||||||
        $this->createdPrivateKey->save();
 | 
					                'team_id' => currentTeam()->id,
 | 
				
			||||||
        $this->currentState = 'create-server';
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $this->createdPrivateKey = $privateKey;
 | 
				
			||||||
 | 
					            $this->currentState = 'create-server';
 | 
				
			||||||
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
 | 
					            $this->addError('privateKey', 'Failed to save private key: ' . $e->getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function saveServer()
 | 
					    public function saveServer()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,6 @@ class Dashboard extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function cleanup_queue()
 | 
					    public function cleanup_queue()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->dispatch('success', 'Cleanup started.');
 | 
					 | 
				
			||||||
        Artisan::queue('cleanup:application-deployment-queue', [
 | 
					        Artisan::queue('cleanup:application-deployment-queue', [
 | 
				
			||||||
            '--team-id' => currentTeam()->id,
 | 
					            '--team-id' => currentTeam()->id,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace App\Livewire\Destination;
 | 
					namespace App\Livewire\Destination;
 | 
				
			||||||
 | 
					 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Form extends Component
 | 
					class Form extends Component
 | 
				
			||||||
@@ -38,7 +37,7 @@ class Form extends Component
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            $this->destination->delete();
 | 
					            $this->destination->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return redirect()->route('dashboard');
 | 
					            return redirect()->route('destination.all');
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,11 +4,25 @@ namespace App\Livewire;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use Illuminate\Support\Facades\DB;
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class NavbarDeleteTeam extends Component
 | 
					class NavbarDeleteTeam extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function delete()
 | 
					    public $team;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function mount()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        $this->team = currentTeam()->name;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function delete($password)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $currentTeam = currentTeam();
 | 
					        $currentTeam = currentTeam();
 | 
				
			||||||
        $currentTeam->delete();
 | 
					        $currentTeam->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,8 @@ class Heading extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected string $deploymentUuid;
 | 
					    protected string $deploymentUuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool $docker_cleanup = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getListeners()
 | 
					    public function getListeners()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $teamId = auth()->user()->currentTeam()->id;
 | 
					        $teamId = auth()->user()->currentTeam()->id;
 | 
				
			||||||
@@ -102,7 +104,7 @@ class Heading extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function stop()
 | 
					    public function stop()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        StopApplication::run($this->application);
 | 
					        StopApplication::run($this->application, false, $this->docker_cleanup);
 | 
				
			||||||
        $this->application->status = 'exited';
 | 
					        $this->application->status = 'exited';
 | 
				
			||||||
        $this->application->save();
 | 
					        $this->application->save();
 | 
				
			||||||
        if ($this->application->additional_servers->count() > 0) {
 | 
					        if ($this->application->additional_servers->count() > 0) {
 | 
				
			||||||
@@ -135,4 +137,13 @@ class Heading extends Component
 | 
				
			|||||||
            'environment_name' => $this->parameters['environment_name'],
 | 
					            'environment_name' => $this->parameters['environment_name'],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.application.heading', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ use Illuminate\Support\Collection;
 | 
				
			|||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
use Spatie\Url\Url;
 | 
					use Spatie\Url\Url;
 | 
				
			||||||
use Visus\Cuid2\Cuid2;
 | 
					use Visus\Cuid2\Cuid2;
 | 
				
			||||||
 | 
					use Illuminate\Process\InvokedProcess;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Previews extends Component
 | 
					class Previews extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -184,17 +186,20 @@ class Previews extends Component
 | 
				
			|||||||
    public function stop(int $pull_request_id)
 | 
					    public function stop(int $pull_request_id)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					            $server = $this->application->destination->server;
 | 
				
			||||||
 | 
					            $timeout = 300;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($this->application->destination->server->isSwarm()) {
 | 
					            if ($this->application->destination->server->isSwarm()) {
 | 
				
			||||||
                instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
 | 
					                instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
 | 
					                $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
 | 
				
			||||||
                foreach ($containers as $container) {
 | 
					                $this->stopContainers($containers, $server, $timeout);
 | 
				
			||||||
                    $name = str_replace('/', '', $container['Names']);
 | 
					 | 
				
			||||||
                    instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
 | 
					            
 | 
				
			||||||
            $this->dispatch('reloadWindow');
 | 
					            GetContainersStatus::run($server);
 | 
				
			||||||
 | 
					            $this->application->refresh();
 | 
				
			||||||
 | 
					            $this->dispatch('containerStatusUpdated');
 | 
				
			||||||
 | 
					            $this->dispatch('success', 'Preview Deployment stopped.');
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -203,16 +208,21 @@ class Previews extends Component
 | 
				
			|||||||
    public function delete(int $pull_request_id)
 | 
					    public function delete(int $pull_request_id)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					            $server = $this->application->destination->server;
 | 
				
			||||||
 | 
					            $timeout = 300;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($this->application->destination->server->isSwarm()) {
 | 
					            if ($this->application->destination->server->isSwarm()) {
 | 
				
			||||||
                instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $this->application->destination->server);
 | 
					                instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                $containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
 | 
					                $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
 | 
				
			||||||
                foreach ($containers as $container) {
 | 
					                $this->stopContainers($containers, $server, $timeout);
 | 
				
			||||||
                    $name = str_replace('/', '', $container['Names']);
 | 
					 | 
				
			||||||
                    instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
 | 
					
 | 
				
			||||||
 | 
					            ApplicationPreview::where('application_id', $this->application->id)
 | 
				
			||||||
 | 
					                ->where('pull_request_id', $pull_request_id)
 | 
				
			||||||
 | 
					                ->first()
 | 
				
			||||||
 | 
					                ->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $this->application->refresh();
 | 
					            $this->application->refresh();
 | 
				
			||||||
            $this->dispatch('update_links');
 | 
					            $this->dispatch('update_links');
 | 
				
			||||||
            $this->dispatch('success', 'Preview deleted.');
 | 
					            $this->dispatch('success', 'Preview deleted.');
 | 
				
			||||||
@@ -220,4 +230,49 @@ class Previews extends Component
 | 
				
			|||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function stopContainers(array $containers, $server, int $timeout)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $processes = [];
 | 
				
			||||||
 | 
					        foreach ($containers as $container) {
 | 
				
			||||||
 | 
					            $containerName = str_replace('/', '', $container['Names']);
 | 
				
			||||||
 | 
					            $processes[$containerName] = $this->stopContainer($containerName, $timeout);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $startTime = time();
 | 
				
			||||||
 | 
					        while (count($processes) > 0) {
 | 
				
			||||||
 | 
					            $finishedProcesses = array_filter($processes, function ($process) {
 | 
				
			||||||
 | 
					                return !$process->running();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            foreach (array_keys($finishedProcesses) as $containerName) {
 | 
				
			||||||
 | 
					                unset($processes[$containerName]);
 | 
				
			||||||
 | 
					                $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                $this->forceStopRemainingContainers(array_keys($processes), $server);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            usleep(100000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function stopContainer(string $containerName, int $timeout): InvokedProcess
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function removeContainer(string $containerName, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function forceStopRemainingContainers(array $containerNames, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach ($containerNames as $containerName) {
 | 
				
			||||||
 | 
					            instant_remote_process(["docker kill $containerName"], $server, throwError: false);
 | 
				
			||||||
 | 
					            $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ namespace App\Livewire\Project\Database;
 | 
				
			|||||||
use App\Models\ScheduledDatabaseBackup;
 | 
					use App\Models\ScheduledDatabaseBackup;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
use Spatie\Url\Url;
 | 
					use Spatie\Url\Url;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BackupEdit extends Component
 | 
					class BackupEdit extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -12,6 +14,10 @@ class BackupEdit extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $s3s;
 | 
					    public $s3s;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool $delete_associated_backups_locally = false;
 | 
				
			||||||
 | 
					    public bool $delete_associated_backups_s3 = false;
 | 
				
			||||||
 | 
					    public bool $delete_associated_backups_sftp = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ?string $status = null;
 | 
					    public ?string $status = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public array $parameters;
 | 
					    public array $parameters;
 | 
				
			||||||
@@ -46,16 +52,29 @@ class BackupEdit extends Component
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
 | 
					            if ($this->delete_associated_backups_locally) {
 | 
				
			||||||
 | 
					                $this->deleteAssociatedBackupsLocally();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if ($this->delete_associated_backups_s3) {
 | 
				
			||||||
 | 
					                $this->deleteAssociatedBackupsS3();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $this->backup->delete();
 | 
					            $this->backup->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
 | 
					            if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
 | 
				
			||||||
                $previousUrl = url()->previous();
 | 
					                $previousUrl = url()->previous();
 | 
				
			||||||
                $url = Url::fromString($previousUrl);
 | 
					                $url = Url::fromString($previousUrl);
 | 
				
			||||||
                $url = $url->withoutQueryParameter('selectedBackupId');
 | 
					                $url = $url->withoutQueryParameter('selectedBackupId');
 | 
				
			||||||
                $url = $url->withFragment('backups');
 | 
					                $url = $url->withFragment('backups');
 | 
				
			||||||
                $url = $url->getPath()."#{$url->getFragment()}";
 | 
					                $url = $url->getPath() . "#{$url->getFragment()}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return redirect($url);
 | 
					                return redirect($url);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
@@ -104,4 +123,66 @@ class BackupEdit extends Component
 | 
				
			|||||||
            $this->dispatch('error', $e->getMessage());
 | 
					            $this->dispatch('error', $e->getMessage());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function deleteAssociatedBackupsLocally()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $executions = $this->backup->executions;
 | 
				
			||||||
 | 
					        $backupFolder = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        foreach ($executions as $execution) {
 | 
				
			||||||
 | 
					            if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
 | 
				
			||||||
 | 
					                $server = $this->backup->database->service->destination->server;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $server = $this->backup->database->destination->server;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (!$backupFolder) {
 | 
				
			||||||
 | 
					                $backupFolder = dirname($execution->filename);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            delete_backup_locally($execution->filename, $server);
 | 
				
			||||||
 | 
					            $execution->delete();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($backupFolder) {
 | 
				
			||||||
 | 
					            $this->deleteEmptyBackupFolder($backupFolder, $server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function deleteAssociatedBackupsS3()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        //Add function to delete backups from S3
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function deleteAssociatedBackupsSftp()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        //Add function to delete backups from SFTP
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function deleteEmptyBackupFolder($folderPath, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        if (trim($checkEmpty) === 'empty') {
 | 
				
			||||||
 | 
					            instant_remote_process(["rmdir '$folderPath'"], $server);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            $parentFolder = dirname($folderPath);
 | 
				
			||||||
 | 
					            $checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (trim($checkParentEmpty) === 'empty') {
 | 
				
			||||||
 | 
					                instant_remote_process(["rmdir '$parentFolder'"], $server);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.database.backup-edit', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,9 @@ namespace App\Livewire\Project\Database;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use App\Models\ScheduledDatabaseBackup;
 | 
					use App\Models\ScheduledDatabaseBackup;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					use Livewire\Attributes\On;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BackupExecutions extends Component
 | 
					class BackupExecutions extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -12,9 +15,12 @@ class BackupExecutions extends Component
 | 
				
			|||||||
    public $executions = [];
 | 
					    public $executions = [];
 | 
				
			||||||
    public $setDeletableBackup;
 | 
					    public $setDeletableBackup;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $delete_backup_s3 = true;
 | 
				
			||||||
 | 
					    public $delete_backup_sftp = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getListeners()
 | 
					    public function getListeners()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $userId = auth()->user()->id;
 | 
					        $userId = Auth::id();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
 | 
					            "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
 | 
				
			||||||
@@ -31,19 +37,34 @@ class BackupExecutions extends Component
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function deleteBackup($exeuctionId)
 | 
					    #[On('deleteBackup')]
 | 
				
			||||||
 | 
					    public function deleteBackup($executionId, $password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $execution = $this->backup->executions()->where('id', $exeuctionId)->first();
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
        if (is_null($execution)) {
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
            $this->dispatch('error', 'Backup execution not found.');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $execution = $this->backup->executions()->where('id', $executionId)->first();
 | 
				
			||||||
 | 
					        if (is_null($execution)) {
 | 
				
			||||||
 | 
					            $this->dispatch('error', 'Backup execution not found.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
 | 
					        if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
 | 
				
			||||||
            delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
 | 
					            delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
 | 
					            delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->delete_backup_s3) {
 | 
				
			||||||
 | 
					            // Add logic to delete from S3
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->delete_backup_sftp) {
 | 
				
			||||||
 | 
					            // Add logic to delete from SFTP
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $execution->delete();
 | 
					        $execution->delete();
 | 
				
			||||||
        $this->dispatch('success', 'Backup deleted.');
 | 
					        $this->dispatch('success', 'Backup deleted.');
 | 
				
			||||||
        $this->refreshBackupExecutions();
 | 
					        $this->refreshBackupExecutions();
 | 
				
			||||||
@@ -106,4 +127,14 @@ class BackupExecutions extends Component
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        return $dateObj->format('Y-m-d H:i:s T');
 | 
					        return $dateObj->format('Y-m-d H:i:s T');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.database.backup-executions', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
 | 
				
			||||||
 | 
					                ['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,8 @@ class Heading extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public array $parameters;
 | 
					    public array $parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $docker_cleanup = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getListeners()
 | 
					    public function getListeners()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $userId = auth()->user()->id;
 | 
					        $userId = auth()->user()->id;
 | 
				
			||||||
@@ -54,7 +56,7 @@ class Heading extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function stop()
 | 
					    public function stop()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        StopDatabase::run($this->database);
 | 
					        StopDatabase::run($this->database, false, $this->docker_cleanup);
 | 
				
			||||||
        $this->database->status = 'exited';
 | 
					        $this->database->status = 'exited';
 | 
				
			||||||
        $this->database->save();
 | 
					        $this->database->save();
 | 
				
			||||||
        $this->check_status();
 | 
					        $this->check_status();
 | 
				
			||||||
@@ -71,4 +73,13 @@ class Heading extends Component
 | 
				
			|||||||
        $activity = StartDatabase::run($this->database);
 | 
					        $activity = StartDatabase::run($this->database);
 | 
				
			||||||
        $this->dispatch('activityMonitor', $activity->id);
 | 
					        $this->dispatch('activityMonitor', $activity->id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.database.heading', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public bool $disabled = false;
 | 
					    public bool $disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $environmentName = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function mount()
 | 
					    public function mount()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->parameters = get_route_parameters();
 | 
					        $this->parameters = get_route_parameters();
 | 
				
			||||||
 | 
					        $this->environmentName = Environment::findOrFail($this->environment_id)->name;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,9 +13,12 @@ class DeleteProject extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public bool $disabled = false;
 | 
					    public bool $disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $projectName = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function mount()
 | 
					    public function mount()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->parameters = get_route_parameters();
 | 
					        $this->parameters = get_route_parameters();
 | 
				
			||||||
 | 
					        $this->projectName = Project::findOrFail($this->project_id)->name;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,7 @@ class Configuration extends Component
 | 
				
			|||||||
            $application = $this->service->applications->find($id);
 | 
					            $application = $this->service->applications->find($id);
 | 
				
			||||||
            if ($application) {
 | 
					            if ($application) {
 | 
				
			||||||
                $application->restart();
 | 
					                $application->restart();
 | 
				
			||||||
                $this->dispatch('success', 'Application restarted successfully.');
 | 
					                $this->dispatch('success', 'Service application restarted successfully.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
@@ -65,7 +65,7 @@ class Configuration extends Component
 | 
				
			|||||||
            $database = $this->service->databases->find($id);
 | 
					            $database = $this->service->databases->find($id);
 | 
				
			||||||
            if ($database) {
 | 
					            if ($database) {
 | 
				
			||||||
                $database->restart();
 | 
					                $database->restart();
 | 
				
			||||||
                $this->dispatch('success', 'Database restarted successfully.');
 | 
					                $this->dispatch('success', 'Service database restarted successfully.');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,6 +15,8 @@ use App\Models\StandaloneMysql;
 | 
				
			|||||||
use App\Models\StandalonePostgresql;
 | 
					use App\Models\StandalonePostgresql;
 | 
				
			||||||
use App\Models\StandaloneRedis;
 | 
					use App\Models\StandaloneRedis;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FileStorage extends Component
 | 
					class FileStorage extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -83,8 +85,13 @@ class FileStorage extends Component
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $message = 'File deleted.';
 | 
					            $message = 'File deleted.';
 | 
				
			||||||
            if ($this->fileStorage->is_directory) {
 | 
					            if ($this->fileStorage->is_directory) {
 | 
				
			||||||
@@ -127,8 +134,15 @@ class FileStorage extends Component
 | 
				
			|||||||
        $this->submit();
 | 
					        $this->submit();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function render()
 | 
					       public function render()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return view('livewire.project.service.file-storage');
 | 
					        return view('livewire.project.service.file-storage', [
 | 
				
			||||||
 | 
					            'directoryDeletionCheckboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'fileDeletionCheckboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,8 @@ class Navbar extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $isDeploymentProgress = false;
 | 
					    public $isDeploymentProgress = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $docker_cleanup = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $title = 'Configuration';
 | 
					    public $title = 'Configuration';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function mount()
 | 
					    public function mount()
 | 
				
			||||||
@@ -42,7 +44,7 @@ class Navbar extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function serviceStarted()
 | 
					    public function serviceStarted()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->dispatch('success', 'Service status changed.');
 | 
					        // $this->dispatch('success', 'Service status changed.');
 | 
				
			||||||
        if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
 | 
					        if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
 | 
				
			||||||
            $this->service->isConfigurationChanged(true);
 | 
					            $this->service->isConfigurationChanged(true);
 | 
				
			||||||
            $this->dispatch('configurationChanged');
 | 
					            $this->dispatch('configurationChanged');
 | 
				
			||||||
@@ -62,11 +64,6 @@ class Navbar extends Component
 | 
				
			|||||||
        $this->dispatch('success', 'Service status updated.');
 | 
					        $this->dispatch('success', 'Service status updated.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function render()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return view('livewire.project.service.navbar');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function checkDeployments()
 | 
					    public function checkDeployments()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
@@ -97,14 +94,9 @@ class Navbar extends Component
 | 
				
			|||||||
        $this->dispatch('activityMonitor', $activity->id);
 | 
					        $this->dispatch('activityMonitor', $activity->id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function stop(bool $forceCleanup = false)
 | 
					    public function stop()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        StopService::run($this->service);
 | 
					        StopService::run($this->service, false, $this->docker_cleanup);
 | 
				
			||||||
        if ($forceCleanup) {
 | 
					 | 
				
			||||||
            $this->dispatch('success', 'Containers cleaned up.');
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            $this->dispatch('success', 'Service stopped.');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        ServiceStatusChanged::dispatch();
 | 
					        ServiceStatusChanged::dispatch();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -123,4 +115,13 @@ class Navbar extends Component
 | 
				
			|||||||
        $activity = StartService::run($this->service);
 | 
					        $activity = StartService::run($this->service);
 | 
				
			||||||
        $this->dispatch('activityMonitor', $activity->id);
 | 
					        $this->dispatch('activityMonitor', $activity->id);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.service.navbar', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@
 | 
				
			|||||||
namespace App\Livewire\Project\Service;
 | 
					namespace App\Livewire\Project\Service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use App\Models\ServiceApplication;
 | 
					use App\Models\ServiceApplication;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ServiceApplicationView extends Component
 | 
					class ServiceApplicationView extends Component
 | 
				
			||||||
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $parameters;
 | 
					    public $parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $docker_cleanup = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $delete_volumes = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $rules = [
 | 
					    protected $rules = [
 | 
				
			||||||
        'application.human_name' => 'nullable',
 | 
					        'application.human_name' => 'nullable',
 | 
				
			||||||
        'application.description' => 'nullable',
 | 
					        'application.description' => 'nullable',
 | 
				
			||||||
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
 | 
				
			|||||||
        'application.is_stripprefix_enabled' => 'nullable|boolean',
 | 
					        'application.is_stripprefix_enabled' => 'nullable|boolean',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function render()
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        return view('livewire.project.service.service-application-view');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function updatedApplicationFqdn()
 | 
					    public function updatedApplicationFqdn()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
 | 
					        $this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
 | 
				
			||||||
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
 | 
				
			|||||||
        $this->dispatch('success', 'You need to restart the service for the changes to take effect.');
 | 
					        $this->dispatch('success', 'You need to restart the service for the changes to take effect.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (! Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->application->delete();
 | 
					            $this->application->delete();
 | 
				
			||||||
            $this->dispatch('success', 'Application deleted.');
 | 
					            $this->dispatch('success', 'Application deleted.');
 | 
				
			||||||
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
 | 
				
			|||||||
            $this->dispatch('generateDockerCompose');
 | 
					            $this->dispatch('generateDockerCompose');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.service.service-application-view', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
 | 
				
			||||||
 | 
					                ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,11 @@
 | 
				
			|||||||
namespace App\Livewire\Project\Shared;
 | 
					namespace App\Livewire\Project\Shared;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use App\Jobs\DeleteResourceJob;
 | 
					use App\Jobs\DeleteResourceJob;
 | 
				
			||||||
 | 
					use App\Models\Service;
 | 
				
			||||||
 | 
					use App\Models\ServiceApplication;
 | 
				
			||||||
 | 
					use App\Models\ServiceDatabase;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
use Visus\Cuid2\Cuid2;
 | 
					use Visus\Cuid2\Cuid2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,6 +15,8 @@ class Danger extends Component
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public $resource;
 | 
					    public $resource;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public $resourceName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $projectUuid;
 | 
					    public $projectUuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public $environmentName;
 | 
					    public $environmentName;
 | 
				
			||||||
@@ -18,22 +25,93 @@ class Danger extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public bool $delete_volumes = true;
 | 
					    public bool $delete_volumes = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool $docker_cleanup = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public bool $delete_connected_networks = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ?string $modalId = null;
 | 
					    public ?string $modalId = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $resourceDomain = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function mount()
 | 
					    public function mount()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->modalId = new Cuid2;
 | 
					 | 
				
			||||||
        $parameters = get_route_parameters();
 | 
					        $parameters = get_route_parameters();
 | 
				
			||||||
 | 
					        $this->modalId = new Cuid2;
 | 
				
			||||||
        $this->projectUuid = data_get($parameters, 'project_uuid');
 | 
					        $this->projectUuid = data_get($parameters, 'project_uuid');
 | 
				
			||||||
        $this->environmentName = data_get($parameters, 'environment_name');
 | 
					        $this->environmentName = data_get($parameters, 'environment_name');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->resource === null) {
 | 
				
			||||||
 | 
					            if (isset($parameters['service_uuid'])) {
 | 
				
			||||||
 | 
					                $this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
 | 
				
			||||||
 | 
					            } elseif (isset($parameters['stack_service_uuid'])) {
 | 
				
			||||||
 | 
					                $this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
 | 
				
			||||||
 | 
					                    ?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if ($this->resource === null) {
 | 
				
			||||||
 | 
					            $this->resourceName = 'Unknown Resource';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (! method_exists($this->resource, 'type')) {
 | 
				
			||||||
 | 
					            $this->resourceName = 'Unknown Resource';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        switch ($this->resource->type()) {
 | 
				
			||||||
 | 
					            case 'application':
 | 
				
			||||||
 | 
					                $this->resourceName = $this->resource->name ?? 'Application';
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'standalone-postgresql':
 | 
				
			||||||
 | 
					            case 'standalone-redis':
 | 
				
			||||||
 | 
					            case 'standalone-mongodb':
 | 
				
			||||||
 | 
					            case 'standalone-mysql':
 | 
				
			||||||
 | 
					            case 'standalone-mariadb':
 | 
				
			||||||
 | 
					            case 'standalone-keydb':
 | 
				
			||||||
 | 
					            case 'standalone-dragonfly':
 | 
				
			||||||
 | 
					            case 'standalone-clickhouse':
 | 
				
			||||||
 | 
					                $this->resourceName = $this->resource->name ?? 'Database';
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'service':
 | 
				
			||||||
 | 
					                $this->resourceName = $this->resource->name ?? 'Service';
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'service-application':
 | 
				
			||||||
 | 
					                $this->resourceName = $this->resource->name ?? 'Service Application';
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            case 'service-database':
 | 
				
			||||||
 | 
					                $this->resourceName = $this->resource->name ?? 'Service Database';
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					                $this->resourceName = 'Unknown Resource';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (! Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (! $this->resource) {
 | 
				
			||||||
 | 
					            $this->addError('resource', 'Resource not found.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            // $this->authorize('delete', $this->resource);
 | 
					 | 
				
			||||||
            $this->resource->delete();
 | 
					            $this->resource->delete();
 | 
				
			||||||
            DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
 | 
					            DeleteResourceJob::dispatch(
 | 
				
			||||||
 | 
					                $this->resource,
 | 
				
			||||||
 | 
					                $this->delete_configurations,
 | 
				
			||||||
 | 
					                $this->delete_volumes,
 | 
				
			||||||
 | 
					                $this->docker_cleanup,
 | 
				
			||||||
 | 
					                $this->delete_connected_networks
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return redirect()->route('project.resource.index', [
 | 
					            return redirect()->route('project.resource.index', [
 | 
				
			||||||
                'project_uuid' => $this->projectUuid,
 | 
					                'project_uuid' => $this->projectUuid,
 | 
				
			||||||
@@ -43,4 +121,19 @@ class Danger extends Component
 | 
				
			|||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return view('livewire.project.shared.danger', [
 | 
				
			||||||
 | 
					            'checkboxes' => [
 | 
				
			||||||
 | 
					                ['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
 | 
				
			||||||
 | 
					                ['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
 | 
				
			||||||
 | 
					                ['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
 | 
				
			||||||
 | 
					                ['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
 | 
				
			||||||
 | 
					                // ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,8 @@ use App\Models\Server;
 | 
				
			|||||||
use App\Models\StandaloneDocker;
 | 
					use App\Models\StandaloneDocker;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
use Visus\Cuid2\Cuid2;
 | 
					use Visus\Cuid2\Cuid2;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Destination extends Component
 | 
					class Destination extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -115,8 +117,13 @@ class Destination extends Component
 | 
				
			|||||||
        ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
 | 
					        ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function removeServer(int $network_id, int $server_id)
 | 
					    public function removeServer(int $network_id, int $server_id, $password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
 | 
					        if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
 | 
				
			||||||
            $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
 | 
					            $this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,7 @@ use App\Models\StandalonePostgresql;
 | 
				
			|||||||
use App\Models\StandaloneRedis;
 | 
					use App\Models\StandaloneRedis;
 | 
				
			||||||
use Illuminate\Support\Facades\Process;
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use App\Helpers\SshMultiplexingHelper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GetLogs extends Component
 | 
					class GetLogs extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -108,14 +109,14 @@ class GetLogs extends Component
 | 
				
			|||||||
                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
					                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
				
			||||||
                        $command = $command[0];
 | 
					                        $command = $command[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $sshCommand = generateSshCommand($this->server, $command);
 | 
					                    $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    $command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
 | 
					                    $command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
 | 
				
			||||||
                    if ($this->server->isNonRoot()) {
 | 
					                    if ($this->server->isNonRoot()) {
 | 
				
			||||||
                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
					                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
				
			||||||
                        $command = $command[0];
 | 
					                        $command = $command[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $sshCommand = generateSshCommand($this->server, $command);
 | 
					                    $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                if ($this->server->isSwarm()) {
 | 
					                if ($this->server->isSwarm()) {
 | 
				
			||||||
@@ -124,14 +125,14 @@ class GetLogs extends Component
 | 
				
			|||||||
                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
					                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
				
			||||||
                        $command = $command[0];
 | 
					                        $command = $command[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $sshCommand = generateSshCommand($this->server, $command);
 | 
					                    $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    $command = "docker logs -n {$this->numberOfLines} {$this->container}";
 | 
					                    $command = "docker logs -n {$this->numberOfLines} {$this->container}";
 | 
				
			||||||
                    if ($this->server->isNonRoot()) {
 | 
					                    if ($this->server->isNonRoot()) {
 | 
				
			||||||
                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
					                        $command = parseCommandsByLineForSudo(collect($command), $this->server);
 | 
				
			||||||
                        $command = $command[0];
 | 
					                        $command = $command[0];
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $sshCommand = generateSshCommand($this->server, $command);
 | 
					                    $sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if ($refresh) {
 | 
					            if ($refresh) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,8 @@ class Show extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public string $type;
 | 
					    public string $type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public string $scheduledTaskName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $rules = [
 | 
					    protected $rules = [
 | 
				
			||||||
        'task.enabled' => 'required|boolean',
 | 
					        'task.enabled' => 'required|boolean',
 | 
				
			||||||
        'task.name' => 'required|string',
 | 
					        'task.name' => 'required|string',
 | 
				
			||||||
@@ -49,6 +51,7 @@ class Show extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $this->modalId = new Cuid2;
 | 
					        $this->modalId = new Cuid2;
 | 
				
			||||||
        $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
 | 
					        $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
 | 
				
			||||||
 | 
					        $this->scheduledTaskName = $this->task->name;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function instantSave()
 | 
					    public function instantSave()
 | 
				
			||||||
@@ -75,9 +78,9 @@ class Show extends Component
 | 
				
			|||||||
            $this->task->delete();
 | 
					            $this->task->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($this->type == 'application') {
 | 
					            if ($this->type == 'application') {
 | 
				
			||||||
                return redirect()->route('project.application.configuration', $this->parameters);
 | 
					                return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                return redirect()->route('project.service.configuration', $this->parameters);
 | 
					                return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } catch (\Exception $e) {
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            return handleError($e);
 | 
					            return handleError($e);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@
 | 
				
			|||||||
namespace App\Livewire\Project\Shared\Storages;
 | 
					namespace App\Livewire\Project\Shared\Storages;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use App\Models\LocalPersistentVolume;
 | 
					use App\Models\LocalPersistentVolume;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Show extends Component
 | 
					class Show extends Component
 | 
				
			||||||
@@ -36,8 +38,13 @@ class Show extends Component
 | 
				
			|||||||
        $this->dispatch('success', 'Storage updated successfully');
 | 
					        $this->dispatch('success', 'Storage updated successfully');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->storage->delete();
 | 
					        $this->storage->delete();
 | 
				
			||||||
        $this->dispatch('refreshStorages');
 | 
					        $this->dispatch('refreshStorages');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Livewire\Project\Shared;
 | 
					namespace App\Livewire\Project\Shared;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Helpers\SshMultiplexingHelper;
 | 
				
			||||||
use App\Models\Server;
 | 
					use App\Models\Server;
 | 
				
			||||||
use Livewire\Attributes\On;
 | 
					use Livewire\Attributes\On;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
@@ -19,9 +20,9 @@ class Terminal extends Component
 | 
				
			|||||||
            if ($status !== 'running') {
 | 
					            if ($status !== 'running') {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $command = 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 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            $command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
 | 
					            $command = SshMultiplexingHelper::generateSshCommand($server, "sh -c '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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,22 +3,14 @@
 | 
				
			|||||||
namespace App\Livewire\Security\PrivateKey;
 | 
					namespace App\Livewire\Security\PrivateKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use App\Models\PrivateKey;
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
 | 
					 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
use phpseclib3\Crypt\PublicKeyLoader;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Create extends Component
 | 
					class Create extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    use WithRateLimiting;
 | 
					    public string $name = '';
 | 
				
			||||||
 | 
					    public string $value = '';
 | 
				
			||||||
    public string $name;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public string $value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ?string $from = null;
 | 
					    public ?string $from = null;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ?string $description = null;
 | 
					    public ?string $description = null;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public ?string $publicKey = null;
 | 
					    public ?string $publicKey = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $rules = [
 | 
					    protected $rules = [
 | 
				
			||||||
@@ -26,72 +18,69 @@ class Create extends Component
 | 
				
			|||||||
        'value' => 'required|string',
 | 
					        'value' => 'required|string',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $validationAttributes = [
 | 
					 | 
				
			||||||
        'name' => 'name',
 | 
					 | 
				
			||||||
        'value' => 'private Key',
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public function generateNewRSAKey()
 | 
					    public function generateNewRSAKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        $this->generateNewKey('rsa');
 | 
				
			||||||
            $this->rateLimit(10);
 | 
					 | 
				
			||||||
            $this->name = generate_random_name();
 | 
					 | 
				
			||||||
            $this->description = 'Created by Coolify';
 | 
					 | 
				
			||||||
            ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
 | 
					 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
            return handleError($e, $this);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function generateNewEDKey()
 | 
					    public function generateNewEDKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        $this->generateNewKey('ed25519');
 | 
				
			||||||
            $this->rateLimit(10);
 | 
					 | 
				
			||||||
            $this->name = generate_random_name();
 | 
					 | 
				
			||||||
            $this->description = 'Created by Coolify';
 | 
					 | 
				
			||||||
            ['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
 | 
					 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					 | 
				
			||||||
            return handleError($e, $this);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function updated($updateProperty)
 | 
					    private function generateNewKey($type)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if ($updateProperty === 'value') {
 | 
					        $keyData = PrivateKey::generateNewKeyPair($type);
 | 
				
			||||||
            try {
 | 
					        $this->setKeyData($keyData);
 | 
				
			||||||
                $this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
 | 
					    }
 | 
				
			||||||
            } catch (\Throwable $e) {
 | 
					
 | 
				
			||||||
                if ($this->$updateProperty === '') {
 | 
					    public function updated($property)
 | 
				
			||||||
                    $this->publicKey = '';
 | 
					    {
 | 
				
			||||||
                } else {
 | 
					        if ($property === 'value') {
 | 
				
			||||||
                    $this->publicKey = 'Invalid private key';
 | 
					            $this->validatePrivateKey();
 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $this->validateOnly($updateProperty);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function createPrivateKey()
 | 
					    public function createPrivateKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->validate();
 | 
					        $this->validate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->value = trim($this->value);
 | 
					            $privateKey = PrivateKey::createAndStore([
 | 
				
			||||||
            if (! str_ends_with($this->value, "\n")) {
 | 
					 | 
				
			||||||
                $this->value .= "\n";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            $private_key = PrivateKey::create([
 | 
					 | 
				
			||||||
                'name' => $this->name,
 | 
					                'name' => $this->name,
 | 
				
			||||||
                'description' => $this->description,
 | 
					                'description' => $this->description,
 | 
				
			||||||
                'private_key' => $this->value,
 | 
					                'private_key' => trim($this->value) . "\n",
 | 
				
			||||||
                'team_id' => currentTeam()->id,
 | 
					                'team_id' => currentTeam()->id,
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
            if ($this->from === 'server') {
 | 
					 | 
				
			||||||
                return redirect()->route('dashboard');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
 | 
					            return $this->redirectAfterCreation($privateKey);
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function setKeyData(array $keyData)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->name = $keyData['name'];
 | 
				
			||||||
 | 
					        $this->description = $keyData['description'];
 | 
				
			||||||
 | 
					        $this->value = $keyData['private_key'];
 | 
				
			||||||
 | 
					        $this->publicKey = $keyData['public_key'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function validatePrivateKey()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
 | 
				
			||||||
 | 
					        $this->publicKey = $validationResult['publicKey'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!$validationResult['isValid']) {
 | 
				
			||||||
 | 
					            $this->addError('value', 'Invalid private key');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function redirectAfterCreation(PrivateKey $privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->from === 'server'
 | 
				
			||||||
 | 
					            ? redirect()->route('dashboard')
 | 
				
			||||||
 | 
					            : redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								app/Livewire/Security/PrivateKey/Index.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/Livewire/Security/PrivateKey/Index.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace App\Livewire\Security\PrivateKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Index extends Component
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function render()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return view('livewire.security.private-key.index', [
 | 
				
			||||||
 | 
					            'privateKeys' => $privateKeys,
 | 
				
			||||||
 | 
					        ])->layout('components.layout');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function cleanupUnusedKeys()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        PrivateKey::cleanupUnusedKeys();
 | 
				
			||||||
 | 
					        $this->dispatch('success', 'Unused keys have been cleaned up.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -29,25 +29,27 @@ class Show extends Component
 | 
				
			|||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
 | 
					            $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            abort(404);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function loadPublicKey()
 | 
					    public function loadPublicKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->public_key = $this->private_key->publicKey();
 | 
					        $this->public_key = $this->private_key->getPublicKey();
 | 
				
			||||||
 | 
					        if ($this->public_key === 'Error loading private key') {
 | 
				
			||||||
 | 
					            $this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            if ($this->private_key->isEmpty()) {
 | 
					            $this->private_key->safeDelete();
 | 
				
			||||||
                $this->private_key->delete();
 | 
					            currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
 | 
				
			||||||
                currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return redirect()->route('security.private-key.index');
 | 
					            return redirect()->route('security.private-key.index');
 | 
				
			||||||
            }
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
            $this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
 | 
					            $this->dispatch('error', $e->getMessage());
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -56,8 +58,9 @@ class Show extends Component
 | 
				
			|||||||
    public function changePrivateKey()
 | 
					    public function changePrivateKey()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
 | 
					            $this->private_key->updatePrivateKey([
 | 
				
			||||||
            $this->private_key->save();
 | 
					                'private_key' => formatPrivateKey($this->private_key->private_key),
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
            refresh_server_connection($this->private_key);
 | 
					            refresh_server_connection($this->private_key);
 | 
				
			||||||
            $this->dispatch('success', 'Private key updated.');
 | 
					            $this->dispatch('success', 'Private key updated.');
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,8 @@ namespace App\Livewire\Server;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 | 
					use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Delete extends Component
 | 
					class Delete extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -11,8 +13,12 @@ class Delete extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $server;
 | 
					    public $server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete()
 | 
					    public function delete($password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $this->authorize('delete', $this->server);
 | 
					            $this->authorize('delete', $this->server);
 | 
				
			||||||
            if ($this->server->hasDefinedResources()) {
 | 
					            if ($this->server->hasDefinedResources()) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,7 @@ class Proxy extends Component
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->server->proxy = null;
 | 
					        $this->server->proxy = null;
 | 
				
			||||||
        $this->server->save();
 | 
					        $this->server->save();
 | 
				
			||||||
 | 
					        $this->dispatch('proxyChanged');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function selectProxy($proxy_type)
 | 
					    public function selectProxy($proxy_type)
 | 
				
			||||||
@@ -47,7 +48,7 @@ class Proxy extends Component
 | 
				
			|||||||
        $this->server->proxy->set('type', $proxy_type);
 | 
					        $this->server->proxy->set('type', $proxy_type);
 | 
				
			||||||
        $this->server->save();
 | 
					        $this->server->save();
 | 
				
			||||||
        $this->selectedProxy = $this->server->proxy->type;
 | 
					        $this->selectedProxy = $this->server->proxy->type;
 | 
				
			||||||
        if ($this->selectedProxy !== 'NONE') {
 | 
					        if ($this->server->proxySet()) {
 | 
				
			||||||
            StartProxy::run($this->server, false);
 | 
					            StartProxy::run($this->server, false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $this->dispatch('proxyStatusUpdated');
 | 
					        $this->dispatch('proxyStatusUpdated');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ use App\Actions\Proxy\StartProxy;
 | 
				
			|||||||
use App\Events\ProxyStatusChanged;
 | 
					use App\Events\ProxyStatusChanged;
 | 
				
			||||||
use App\Models\Server;
 | 
					use App\Models\Server;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use Illuminate\Process\InvokedProcess;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Deploy extends Component
 | 
					class Deploy extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -29,6 +31,7 @@ class Deploy extends Component
 | 
				
			|||||||
            'serverRefresh' => 'proxyStatusUpdated',
 | 
					            'serverRefresh' => 'proxyStatusUpdated',
 | 
				
			||||||
            'checkProxy',
 | 
					            'checkProxy',
 | 
				
			||||||
            'startProxy',
 | 
					            'startProxy',
 | 
				
			||||||
 | 
					            'proxyChanged' => 'proxyStatusUpdated',
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,21 +97,43 @@ class Deploy extends Component
 | 
				
			|||||||
    public function stop(bool $forceStop = true)
 | 
					    public function stop(bool $forceStop = true)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            if ($this->server->isSwarm()) {
 | 
					            $containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
 | 
				
			||||||
                instant_remote_process([
 | 
					            $timeout = 30;
 | 
				
			||||||
                    'docker service rm coolify-proxy_traefik',
 | 
					
 | 
				
			||||||
                ], $this->server);
 | 
					            $process = $this->stopContainer($containerName, $timeout);
 | 
				
			||||||
            } else {
 | 
					
 | 
				
			||||||
                instant_remote_process([
 | 
					            $startTime = time();
 | 
				
			||||||
                    'docker rm -f coolify-proxy',
 | 
					            while ($process->running()) {
 | 
				
			||||||
                ], $this->server);
 | 
					                if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                    $this->forceStopContainer($containerName);
 | 
				
			||||||
 | 
					                    break;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                usleep(100000);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $this->server->proxy->status = 'exited';
 | 
					
 | 
				
			||||||
            $this->server->proxy->force_stop = $forceStop;
 | 
					            $this->removeContainer($containerName);
 | 
				
			||||||
            $this->server->save();
 | 
					 | 
				
			||||||
            $this->dispatch('proxyStatusUpdated');
 | 
					 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return handleError($e, $this);
 | 
					            return handleError($e, $this);
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            $this->server->proxy->force_stop = $forceStop;
 | 
				
			||||||
 | 
					            $this->server->proxy->status = 'exited';
 | 
				
			||||||
 | 
					            $this->server->save();
 | 
				
			||||||
 | 
					            $this->dispatch('proxyStatusUpdated');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function stopContainer(string $containerName, int $timeout): InvokedProcess
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function forceStopContainer(string $containerName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private function removeContainer(string $containerName)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ class Show extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $parameters = [];
 | 
					    public $parameters = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $listeners = ['proxyStatusUpdated'];
 | 
					    protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function proxyStatusUpdated()
 | 
					    public function proxyStatusUpdated()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Livewire\Server;
 | 
					namespace App\Livewire\Server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
use App\Models\Server;
 | 
					use App\Models\Server;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public $parameters;
 | 
					    public $parameters;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function setPrivateKey($newPrivateKeyId)
 | 
					    public function setPrivateKey($privateKeyId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            $oldPrivateKeyId = $this->server->private_key_id;
 | 
					            $privateKey = PrivateKey::findOrFail($privateKeyId);
 | 
				
			||||||
            refresh_server_connection($this->server->privateKey);
 | 
					            $this->server->update(['private_key_id' => $privateKey->id]);
 | 
				
			||||||
            $this->server->update([
 | 
					 | 
				
			||||||
                'private_key_id' => $newPrivateKeyId,
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
            $this->server->refresh();
 | 
					            $this->server->refresh();
 | 
				
			||||||
            refresh_server_connection($this->server->privateKey);
 | 
					            $this->dispatch('success', 'Private key updated successfully.');
 | 
				
			||||||
            $this->checkConnection();
 | 
					        } catch (\Exception $e) {
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					            $this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
 | 
				
			||||||
            $this->server->update([
 | 
					 | 
				
			||||||
                'private_key_id' => $oldPrivateKeyId,
 | 
					 | 
				
			||||||
            ]);
 | 
					 | 
				
			||||||
            $this->server->refresh();
 | 
					 | 
				
			||||||
            refresh_server_connection($this->server->privateKey);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return handleError($e, $this);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
 | 
				
			|||||||
                $this->dispatch('success', 'Server is reachable.');
 | 
					                $this->dispatch('success', 'Server is reachable.');
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                ray($error);
 | 
					                ray($error);
 | 
				
			||||||
                $this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.');
 | 
					                $this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ namespace App\Livewire\Team;
 | 
				
			|||||||
use App\Models\Team;
 | 
					use App\Models\Team;
 | 
				
			||||||
use App\Models\User;
 | 
					use App\Models\User;
 | 
				
			||||||
use Livewire\Component;
 | 
					use Livewire\Component;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Hash;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AdminView extends Component
 | 
					class AdminView extends Component
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -73,8 +75,12 @@ class AdminView extends Component
 | 
				
			|||||||
        $team->delete();
 | 
					        $team->delete();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete($id)
 | 
					    public function delete($id, $password)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        if (!Hash::check($password, Auth::user()->password)) {
 | 
				
			||||||
 | 
					            $this->addError('password', 'The provided password is incorrect.');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        if (! auth()->user()->isInstanceAdmin()) {
 | 
					        if (! auth()->user()->isInstanceAdmin()) {
 | 
				
			||||||
            return $this->dispatch('error', 'You are not authorized to delete users');
 | 
					            return $this->dispatch('error', 'You are not authorized to delete users');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
				
			|||||||
use Illuminate\Database\Eloquent\SoftDeletes;
 | 
					use Illuminate\Database\Eloquent\SoftDeletes;
 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
use Illuminate\Support\Str;
 | 
					use Illuminate\Support\Str;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use Illuminate\Process\InvokedProcess;
 | 
				
			||||||
use OpenApi\Attributes as OA;
 | 
					use OpenApi\Attributes as OA;
 | 
				
			||||||
use RuntimeException;
 | 
					use RuntimeException;
 | 
				
			||||||
use Spatie\Activitylog\Models\Activity;
 | 
					use Spatie\Activitylog\Models\Activity;
 | 
				
			||||||
@@ -149,13 +151,65 @@ class Application extends BaseModel
 | 
				
			|||||||
        return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
 | 
					        return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getContainersToStop(bool $previewDeployments = false): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $containers = $previewDeployments
 | 
				
			||||||
 | 
					            ? getCurrentApplicationContainerStatus($this->destination->server, $this->id, includePullrequests: true)
 | 
				
			||||||
 | 
					            : getCurrentApplicationContainerStatus($this->destination->server, $this->id, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $containers->pluck('Names')->toArray();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function stopContainers(array $containerNames, $server, int $timeout = 600)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $processes = [];
 | 
				
			||||||
 | 
					        foreach ($containerNames as $containerName) {
 | 
				
			||||||
 | 
					            $processes[$containerName] = $this->stopContainer($containerName, $server, $timeout);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $startTime = time();
 | 
				
			||||||
 | 
					        while (count($processes) > 0) {
 | 
				
			||||||
 | 
					            $finishedProcesses = array_filter($processes, function ($process) {
 | 
				
			||||||
 | 
					                return !$process->running();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            foreach ($finishedProcesses as $containerName => $process) {
 | 
				
			||||||
 | 
					                unset($processes[$containerName]);
 | 
				
			||||||
 | 
					                $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                $this->forceStopRemainingContainers(array_keys($processes), $server);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            usleep(100000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function stopContainer(string $containerName, $server, int $timeout): InvokedProcess
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function removeContainer(string $containerName, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function forceStopRemainingContainers(array $containerNames, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach ($containerNames as $containerName) {
 | 
				
			||||||
 | 
					            instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					            $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete_configurations()
 | 
					    public function delete_configurations()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $server = data_get($this, 'destination.server');
 | 
					        $server = data_get($this, 'destination.server');
 | 
				
			||||||
        $workdir = $this->workdir();
 | 
					        $workdir = $this->workdir();
 | 
				
			||||||
        if (str($workdir)->endsWith($this->uuid)) {
 | 
					        if (str($workdir)->endsWith($this->uuid)) {
 | 
				
			||||||
            ray('Deleting workdir');
 | 
					            instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
 | 
				
			||||||
            instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -176,6 +230,14 @@ class Application extends BaseModel
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function delete_connected_networks($uuid)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $server = data_get($this, 'destination.server');
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network rm {$uuid}"], $server, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function additional_servers()
 | 
					    public function additional_servers()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->belongsToMany(Server::class, 'additional_destinations')
 | 
					        return $this->belongsToMany(Server::class, 'additional_destinations')
 | 
				
			||||||
@@ -283,7 +345,7 @@ class Application extends BaseModel
 | 
				
			|||||||
    public function publishDirectory(): Attribute
 | 
					    public function publishDirectory(): Attribute
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
            set: fn ($value) => $value ? '/'.ltrim($value, '/') : null,
 | 
					            set: fn ($value) => $value ? '/' . ltrim($value, '/') : null,
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -291,7 +353,7 @@ class Application extends BaseModel
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
            get: function () {
 | 
					            get: function () {
 | 
				
			||||||
                if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
 | 
					                if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
 | 
				
			||||||
                    if (str($this->git_repository)->contains('bitbucket')) {
 | 
					                    if (str($this->git_repository)->contains('bitbucket')) {
 | 
				
			||||||
                        return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
 | 
					                        return "{$this->source->html_url}/{$this->git_repository}/src/{$this->git_branch}";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
@@ -318,7 +380,7 @@ class Application extends BaseModel
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
            get: function () {
 | 
					            get: function () {
 | 
				
			||||||
                if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
 | 
					                if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
 | 
				
			||||||
                    return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
 | 
					                    return "{$this->source->html_url}/{$this->git_repository}/settings/hooks";
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                // Convert the SSH URL to HTTPS URL
 | 
					                // Convert the SSH URL to HTTPS URL
 | 
				
			||||||
@@ -337,7 +399,7 @@ class Application extends BaseModel
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
            get: function () {
 | 
					            get: function () {
 | 
				
			||||||
                if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) {
 | 
					                if (!is_null($this->source?->html_url) && !is_null($this->git_repository) && !is_null($this->git_branch)) {
 | 
				
			||||||
                    return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
 | 
					                    return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}";
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                // Convert the SSH URL to HTTPS URL
 | 
					                // Convert the SSH URL to HTTPS URL
 | 
				
			||||||
@@ -354,7 +416,7 @@ class Application extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function gitCommitLink($link): string
 | 
					    public function gitCommitLink($link): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (! is_null(data_get($this, 'source.html_url')) && ! is_null(data_get($this, 'git_repository')) && ! is_null(data_get($this, 'git_branch'))) {
 | 
					        if (!is_null(data_get($this, 'source.html_url')) && !is_null(data_get($this, 'git_repository')) && !is_null(data_get($this, 'git_branch'))) {
 | 
				
			||||||
            if (str($this->source->html_url)->contains('bitbucket')) {
 | 
					            if (str($this->source->html_url)->contains('bitbucket')) {
 | 
				
			||||||
                return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
 | 
					                return "{$this->source->html_url}/{$this->git_repository}/commits/{$link}";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -365,7 +427,7 @@ class Application extends BaseModel
 | 
				
			|||||||
            $git_repository = str_replace('.git', '', $this->git_repository);
 | 
					            $git_repository = str_replace('.git', '', $this->git_repository);
 | 
				
			||||||
            $url = Url::fromString($git_repository);
 | 
					            $url = Url::fromString($git_repository);
 | 
				
			||||||
            $url = $url->withUserInfo('');
 | 
					            $url = $url->withUserInfo('');
 | 
				
			||||||
            $url = $url->withPath($url->getPath().'/commits/'.$link);
 | 
					            $url = $url->withPath($url->getPath() . '/commits/' . $link);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $url->__toString();
 | 
					            return $url->__toString();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -418,7 +480,7 @@ class Application extends BaseModel
 | 
				
			|||||||
    public function baseDirectory(): Attribute
 | 
					    public function baseDirectory(): Attribute
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
            set: fn ($value) => '/'.ltrim($value, '/'),
 | 
					            set: fn ($value) => '/' . ltrim($value, '/'),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -761,7 +823,7 @@ class Application extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function workdir()
 | 
					    public function workdir()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return application_configuration_dir()."/{$this->uuid}";
 | 
					        return application_configuration_dir() . "/{$this->uuid}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function isLogDrainEnabled()
 | 
					    public function isLogDrainEnabled()
 | 
				
			||||||
@@ -771,7 +833,7 @@ class Application extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function isConfigurationChanged(bool $save = false)
 | 
					    public function isConfigurationChanged(bool $save = false)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $newConfigHash = $this->fqdn.$this->git_repository.$this->git_branch.$this->git_commit_sha.$this->build_pack.$this->static_image.$this->install_command.$this->build_command.$this->start_command.$this->ports_exposes.$this->ports_mappings.$this->base_directory.$this->publish_directory.$this->dockerfile.$this->dockerfile_location.$this->custom_labels.$this->custom_docker_run_options.$this->dockerfile_target_build.$this->redirect;
 | 
					        $newConfigHash = $this->fqdn . $this->git_repository . $this->git_branch . $this->git_commit_sha . $this->build_pack . $this->static_image . $this->install_command . $this->build_command . $this->start_command . $this->ports_exposes . $this->ports_mappings . $this->base_directory . $this->publish_directory . $this->dockerfile . $this->dockerfile_location . $this->custom_labels . $this->custom_docker_run_options . $this->dockerfile_target_build . $this->redirect;
 | 
				
			||||||
        if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
 | 
					        if ($this->pull_request_id === 0 || $this->pull_request_id === null) {
 | 
				
			||||||
            $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
 | 
					            $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -825,7 +887,7 @@ class Application extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function dirOnServer()
 | 
					    public function dirOnServer()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return application_configuration_dir()."/{$this->uuid}";
 | 
					        return application_configuration_dir() . "/{$this->uuid}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
 | 
					    public function setGitImportSettings(string $deployment_uuid, string $git_clone_command, bool $public = false)
 | 
				
			||||||
@@ -871,7 +933,7 @@ class Application extends BaseModel
 | 
				
			|||||||
                if ($this->source->is_public) {
 | 
					                if ($this->source->is_public) {
 | 
				
			||||||
                    $fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
 | 
					                    $fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
 | 
					                    $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}";
 | 
				
			||||||
                    if (! $only_checkout) {
 | 
					                    if (!$only_checkout) {
 | 
				
			||||||
                        $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
 | 
					                        $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: true);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
@@ -888,7 +950,7 @@ class Application extends BaseModel
 | 
				
			|||||||
                        $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
 | 
					                        $git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository} {$baseDir}";
 | 
				
			||||||
                        $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
 | 
					                        $fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if (! $only_checkout) {
 | 
					                    if (!$only_checkout) {
 | 
				
			||||||
                        $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
 | 
					                        $git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command, public: false);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
@@ -949,7 +1011,7 @@ class Application extends BaseModel
 | 
				
			|||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
 | 
				
			||||||
                } elseif ($git_type === 'github' || $git_type === 'gitea') {
 | 
					                } elseif ($git_type === 'github' || $git_type === 'gitea') {
 | 
				
			||||||
                    $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
 | 
					                    $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
@@ -957,14 +1019,14 @@ class Application extends BaseModel
 | 
				
			|||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
 | 
				
			||||||
                } elseif ($git_type === 'bitbucket') {
 | 
					                } elseif ($git_type === 'bitbucket') {
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
                        $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
 | 
					                        $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -993,7 +1055,7 @@ class Application extends BaseModel
 | 
				
			|||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
 | 
				
			||||||
                } elseif ($git_type === 'github' || $git_type === 'gitea') {
 | 
					                } elseif ($git_type === 'github' || $git_type === 'gitea') {
 | 
				
			||||||
                    $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
 | 
					                    $branch = "pull/{$pull_request_id}/head:$pr_branch_name";
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
@@ -1001,14 +1063,14 @@ class Application extends BaseModel
 | 
				
			|||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && ".$this->buildGitCheckoutCommand($pr_branch_name);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" git fetch origin $branch && " . $this->buildGitCheckoutCommand($pr_branch_name);
 | 
				
			||||||
                } elseif ($git_type === 'bitbucket') {
 | 
					                } elseif ($git_type === 'bitbucket') {
 | 
				
			||||||
                    if ($exec_in_docker) {
 | 
					                    if ($exec_in_docker) {
 | 
				
			||||||
                        $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
 | 
					                        $commands->push(executeInDocker($deployment_uuid, "echo 'Checking out $branch'"));
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $commands->push("echo 'Checking out $branch'");
 | 
					                        $commands->push("echo 'Checking out $branch'");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" ".$this->buildGitCheckoutCommand($commit);
 | 
					                    $git_clone_command = "{$git_clone_command} && cd {$baseDir} && GIT_SSH_COMMAND=\"ssh -o ConnectTimeout=30 -p {$customPort} -o Port={$customPort} -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i /root/.ssh/id_rsa\" " . $this->buildGitCheckoutCommand($commit);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1060,20 +1122,20 @@ class Application extends BaseModel
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        if ($source->startsWith('.')) {
 | 
					                        if ($source->startsWith('.')) {
 | 
				
			||||||
                            $source = $source->after('.');
 | 
					                            $source = $source->after('.');
 | 
				
			||||||
                            $source = $workdir.$source;
 | 
					                            $source = $workdir . $source;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        $commands->push("mkdir -p $source > /dev/null 2>&1 || true");
 | 
					                        $commands->push("mkdir -p $source > /dev/null 2>&1 || true");
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $labels = collect(data_get($service, 'labels', []));
 | 
					            $labels = collect(data_get($service, 'labels', []));
 | 
				
			||||||
            if (! $labels->contains('coolify.managed')) {
 | 
					            if (!$labels->contains('coolify.managed')) {
 | 
				
			||||||
                $labels->push('coolify.managed=true');
 | 
					                $labels->push('coolify.managed=true');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (! $labels->contains('coolify.applicationId')) {
 | 
					            if (!$labels->contains('coolify.applicationId')) {
 | 
				
			||||||
                $labels->push('coolify.applicationId='.$this->id);
 | 
					                $labels->push('coolify.applicationId=' . $this->id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (! $labels->contains('coolify.type')) {
 | 
					            if (!$labels->contains('coolify.type')) {
 | 
				
			||||||
                $labels->push('coolify.type=application');
 | 
					                $labels->push('coolify.type=application');
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            data_set($service, 'labels', $labels->toArray());
 | 
					            data_set($service, 'labels', $labels->toArray());
 | 
				
			||||||
@@ -1149,7 +1211,7 @@ class Application extends BaseModel
 | 
				
			|||||||
                $jsonNames = $json->keys()->toArray();
 | 
					                $jsonNames = $json->keys()->toArray();
 | 
				
			||||||
                $diff = array_diff($jsonNames, $names);
 | 
					                $diff = array_diff($jsonNames, $names);
 | 
				
			||||||
                $json = $json->filter(function ($value, $key) use ($diff) {
 | 
					                $json = $json->filter(function ($value, $key) use ($diff) {
 | 
				
			||||||
                    return ! in_array($key, $diff);
 | 
					                    return !in_array($key, $diff);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                if ($json) {
 | 
					                if ($json) {
 | 
				
			||||||
                    $this->docker_compose_domains = json_encode($json);
 | 
					                    $this->docker_compose_domains = json_encode($json);
 | 
				
			||||||
@@ -1166,13 +1228,12 @@ class Application extends BaseModel
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
 | 
					            throw new \RuntimeException("Docker Compose file not found at: $workdir$composeFile<br><br>Check if you used the right extension (.yaml or .yml) in the compose file name.");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function parseContainerLabels(?ApplicationPreview $preview = null)
 | 
					    public function parseContainerLabels(?ApplicationPreview $preview = null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $customLabels = data_get($this, 'custom_labels');
 | 
					        $customLabels = data_get($this, 'custom_labels');
 | 
				
			||||||
        if (! $customLabels) {
 | 
					        if (!$customLabels) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
 | 
					        if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) {
 | 
				
			||||||
@@ -1255,10 +1316,10 @@ class Application extends BaseModel
 | 
				
			|||||||
                    continue;
 | 
					                    continue;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
 | 
					                if (isset($healthcheckCommand) && str_contains($trimmedLine, '\\')) {
 | 
				
			||||||
                    $healthcheckCommand .= ' '.trim($trimmedLine, '\\ ');
 | 
					                    $healthcheckCommand .= ' ' . trim($trimmedLine, '\\ ');
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                if (isset($healthcheckCommand) && ! str_contains($trimmedLine, '\\') && ! empty($healthcheckCommand)) {
 | 
					                if (isset($healthcheckCommand) && !str_contains($trimmedLine, '\\') && !empty($healthcheckCommand)) {
 | 
				
			||||||
                    $healthcheckCommand .= ' '.$trimmedLine;
 | 
					                    $healthcheckCommand .= ' ' . $trimmedLine;
 | 
				
			||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace App\Models;
 | 
					namespace App\Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use DanHarrin\LivewireRateLimiting\WithRateLimiting;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
 | 
					use Illuminate\Validation\ValidationException;
 | 
				
			||||||
use OpenApi\Attributes as OA;
 | 
					use OpenApi\Attributes as OA;
 | 
				
			||||||
use phpseclib3\Crypt\PublicKeyLoader;
 | 
					use phpseclib3\Crypt\PublicKeyLoader;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -22,48 +25,144 @@ use phpseclib3\Crypt\PublicKeyLoader;
 | 
				
			|||||||
)]
 | 
					)]
 | 
				
			||||||
class PrivateKey extends BaseModel
 | 
					class PrivateKey extends BaseModel
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    use WithRateLimiting;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected $fillable = [
 | 
					    protected $fillable = [
 | 
				
			||||||
        'name',
 | 
					        'name',
 | 
				
			||||||
        'description',
 | 
					        'description',
 | 
				
			||||||
        'private_key',
 | 
					        'private_key',
 | 
				
			||||||
        'is_git_related',
 | 
					        'is_git_related',
 | 
				
			||||||
        'team_id',
 | 
					        'team_id',
 | 
				
			||||||
 | 
					        'fingerprint',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    protected $casts = [
 | 
				
			||||||
 | 
					        'private_key' => 'encrypted',
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected static function booted()
 | 
					    protected static function booted()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        static::saving(function ($key) {
 | 
					        static::saving(function ($key) {
 | 
				
			||||||
            $privateKey = data_get($key, 'private_key');
 | 
					            $key->private_key = formatPrivateKey($key->private_key);
 | 
				
			||||||
            if (substr($privateKey, -1) !== "\n") {
 | 
					
 | 
				
			||||||
                $key->private_key = $privateKey."\n";
 | 
					            if (! self::validatePrivateKey($key->private_key)) {
 | 
				
			||||||
 | 
					                throw ValidationException::withMessages([
 | 
				
			||||||
 | 
					                    'private_key' => ['The private key is invalid.'],
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $key->fingerprint = self::generateFingerprint($key->private_key);
 | 
				
			||||||
 | 
					            if (self::fingerprintExists($key->fingerprint, $key->id)) {
 | 
				
			||||||
 | 
					                throw ValidationException::withMessages([
 | 
				
			||||||
 | 
					                    'private_key' => ['This private key already exists.'],
 | 
				
			||||||
 | 
					                ]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        static::deleted(function ($key) {
 | 
				
			||||||
 | 
					            self::deleteFromStorage($key);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getPublicKey()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return self::extractPublicKeyFromPrivate($this->private_key) ?? 'Error loading private key';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static function ownedByCurrentTeam(array $select = ['*'])
 | 
					    public static function ownedByCurrentTeam(array $select = ['*'])
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $selectArray = collect($select)->concat(['id']);
 | 
					        $selectArray = collect($select)->concat(['id']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return PrivateKey::whereTeamId(currentTeam()->id)->select($selectArray->all());
 | 
					        return self::whereTeamId(currentTeam()->id)->select($selectArray->all());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function publicKey()
 | 
					    public static function validatePrivateKey($privateKey)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            return PublicKeyLoader::load($this->private_key)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
 | 
					            PublicKeyLoader::load($privateKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
        } catch (\Throwable $e) {
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
            return 'Error loading private key';
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function isEmpty()
 | 
					    public static function createAndStore(array $data)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if ($this->servers()->count() === 0 && $this->applications()->count() === 0 && $this->githubApps()->count() === 0 && $this->gitlabApps()->count() === 0) {
 | 
					        $privateKey = new self($data);
 | 
				
			||||||
            return true;
 | 
					        $privateKey->save();
 | 
				
			||||||
        }
 | 
					        $privateKey->storeInFileSystem();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return false;
 | 
					        return $privateKey;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function generateNewKeyPair($type = 'rsa')
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $instance = new self;
 | 
				
			||||||
 | 
					            $instance->rateLimit(10);
 | 
				
			||||||
 | 
					            $name = generate_random_name();
 | 
				
			||||||
 | 
					            $description = 'Created by Coolify';
 | 
				
			||||||
 | 
					            $keyPair = generateSSHKey($type === 'ed25519' ? 'ed25519' : 'rsa');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return [
 | 
				
			||||||
 | 
					                'name' => $name,
 | 
				
			||||||
 | 
					                'description' => $description,
 | 
				
			||||||
 | 
					                'private_key' => $keyPair['private'],
 | 
				
			||||||
 | 
					                'public_key' => $keyPair['public'],
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 | 
					            throw new \Exception("Failed to generate new {$type} key: ".$e->getMessage());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function extractPublicKeyFromPrivate($privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $key = PublicKeyLoader::load($privateKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return $key->getPublicKey()->toString('OpenSSH', ['comment' => '']);
 | 
				
			||||||
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function validateAndExtractPublicKey($privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $isValid = self::validatePrivateKey($privateKey);
 | 
				
			||||||
 | 
					        $publicKey = $isValid ? self::extractPublicKeyFromPrivate($privateKey) : '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            'isValid' => $isValid,
 | 
				
			||||||
 | 
					            'publicKey' => $publicKey,
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function storeInFileSystem()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $filename = "ssh_key@{$this->uuid}";
 | 
				
			||||||
 | 
					        Storage::disk('ssh-keys')->put($filename, $this->private_key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return "/var/www/html/storage/app/ssh/keys/{$filename}";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function deleteFromStorage(self $privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $filename = "ssh_key@{$privateKey->uuid}";
 | 
				
			||||||
 | 
					        Storage::disk('ssh-keys')->delete($filename);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getKeyLocation()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return "/var/www/html/storage/app/ssh/keys/ssh_key@{$this->uuid}";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function updatePrivateKey(array $data)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->update($data);
 | 
				
			||||||
 | 
					        $this->storeInFileSystem();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function servers()
 | 
					    public function servers()
 | 
				
			||||||
@@ -85,4 +184,53 @@ class PrivateKey extends BaseModel
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->hasMany(GitlabApp::class);
 | 
					        return $this->hasMany(GitlabApp::class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function isInUse()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->servers()->exists()
 | 
				
			||||||
 | 
					            || $this->applications()->exists()
 | 
				
			||||||
 | 
					            || $this->githubApps()->exists()
 | 
				
			||||||
 | 
					            || $this->gitlabApps()->exists();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function safeDelete()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if (! $this->isInUse()) {
 | 
				
			||||||
 | 
					            $this->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function generateFingerprint($privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            $key = PublicKeyLoader::load($privateKey);
 | 
				
			||||||
 | 
					            $publicKey = $key->getPublicKey();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return $publicKey->getFingerprint('sha256');
 | 
				
			||||||
 | 
					        } catch (\Throwable $e) {
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static function fingerprintExists($fingerprint, $excludeId = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $query = self::where('fingerprint', $fingerprint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (! is_null($excludeId)) {
 | 
				
			||||||
 | 
					            $query->where('id', '!=', $excludeId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $query->exists();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function cleanupUnusedKeys()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        self::ownedByCurrentTeam()->each(function ($privateKey) {
 | 
				
			||||||
 | 
					            $privateKey->safeDelete();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,6 @@ namespace App\Models;
 | 
				
			|||||||
use App\Actions\Server\InstallDocker;
 | 
					use App\Actions\Server\InstallDocker;
 | 
				
			||||||
use App\Enums\ProxyTypes;
 | 
					use App\Enums\ProxyTypes;
 | 
				
			||||||
use App\Jobs\PullSentinelImageJob;
 | 
					use App\Jobs\PullSentinelImageJob;
 | 
				
			||||||
use App\Notifications\Server\Revived;
 | 
					 | 
				
			||||||
use Illuminate\Database\Eloquent\Builder;
 | 
					use Illuminate\Database\Eloquent\Builder;
 | 
				
			||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
 | 
					use Illuminate\Database\Eloquent\Casts\Attribute;
 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
@@ -156,11 +155,17 @@ class Server extends BaseModel
 | 
				
			|||||||
        return $this->hasOne(ServerSetting::class);
 | 
					        return $this->hasOne(ServerSetting::class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function proxySet()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return $this->proxyType() && $this->proxyType() !== 'NONE' && $this->isFunctional() && ! $this->isSwarmWorker() && ! $this->settings->is_build_server;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function setupDefault404Redirect()
 | 
					    public function setupDefault404Redirect()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $dynamic_conf_path = $this->proxyPath().'/dynamic';
 | 
					        $dynamic_conf_path = $this->proxyPath().'/dynamic';
 | 
				
			||||||
        $proxy_type = $this->proxyType();
 | 
					        $proxy_type = $this->proxyType();
 | 
				
			||||||
        $redirect_url = $this->proxy->redirect_url;
 | 
					        $redirect_url = $this->proxy->redirect_url;
 | 
				
			||||||
 | 
					        ray($proxy_type);
 | 
				
			||||||
        if ($proxy_type === ProxyTypes::TRAEFIK->value) {
 | 
					        if ($proxy_type === ProxyTypes::TRAEFIK->value) {
 | 
				
			||||||
            $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
 | 
					            $default_redirect_file = "$dynamic_conf_path/default_redirect_404.yaml";
 | 
				
			||||||
        } elseif ($proxy_type === 'CADDY') {
 | 
					        } elseif ($proxy_type === 'CADDY') {
 | 
				
			||||||
@@ -833,9 +838,9 @@ $schema://$host {
 | 
				
			|||||||
            $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
 | 
					            $clickhouses = data_get($standaloneDocker, 'clickhouses', collect([]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
 | 
					            return $postgresqls->concat($redis)->concat($mongodbs)->concat($mysqls)->concat($mariadbs)->concat($keydbs)->concat($dragonflies)->concat($clickhouses);
 | 
				
			||||||
        })->filter(function ($item) {
 | 
					        })->flatten()->filter(function ($item) {
 | 
				
			||||||
            return data_get($item, 'name') !== 'coolify-db';
 | 
					            return data_get($item, 'name') !== 'coolify-db';
 | 
				
			||||||
        })->flatten();
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function applications()
 | 
					    public function applications()
 | 
				
			||||||
@@ -879,6 +884,35 @@ $schema://$host {
 | 
				
			|||||||
        return $this->hasMany(Service::class);
 | 
					        return $this->hasMany(Service::class);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function port(): Attribute
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Attribute::make(
 | 
				
			||||||
 | 
					            get: function ($value) {
 | 
				
			||||||
 | 
					                return preg_replace('/[^0-9]/', '', $value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function user(): Attribute
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Attribute::make(
 | 
				
			||||||
 | 
					            get: function ($value) {
 | 
				
			||||||
 | 
					                $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return $sanitizedValue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function ip(): Attribute
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Attribute::make(
 | 
				
			||||||
 | 
					            get: function ($value) {
 | 
				
			||||||
 | 
					                return preg_replace('/[^0-9a-zA-Z.-]/', '', $value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function getIp(): Attribute
 | 
					    public function getIp(): Attribute
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return Attribute::make(
 | 
					        return Attribute::make(
 | 
				
			||||||
@@ -951,10 +985,9 @@ $schema://$host {
 | 
				
			|||||||
    public function isFunctional()
 | 
					    public function isFunctional()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
 | 
					        $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled;
 | 
				
			||||||
        ['private_key_filename' => $private_key_filename, 'mux_filename' => $mux_filename] = server_ssh_configuration($this);
 | 
					
 | 
				
			||||||
        if (! $isFunctional) {
 | 
					        if (! $isFunctional) {
 | 
				
			||||||
            Storage::disk('ssh-keys')->delete($private_key_filename);
 | 
					            Storage::disk('ssh-mux')->delete($this->muxFilename());
 | 
				
			||||||
            Storage::disk('ssh-mux')->delete($mux_filename);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $isFunctional;
 | 
					        return $isFunctional;
 | 
				
			||||||
@@ -1006,9 +1039,10 @@ $schema://$host {
 | 
				
			|||||||
        return data_get($this, 'settings.is_swarm_worker');
 | 
					        return data_get($this, 'settings.is_swarm_worker');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function validateConnection()
 | 
					    public function validateConnection($isManualCheck = true)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        config()->set('constants.ssh.mux_enabled', false);
 | 
					        config()->set('constants.ssh.mux_enabled', ! $isManualCheck);
 | 
				
			||||||
 | 
					        // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $server = Server::find($this->id);
 | 
					        $server = Server::find($this->id);
 | 
				
			||||||
        if (! $server) {
 | 
					        if (! $server) {
 | 
				
			||||||
@@ -1018,7 +1052,6 @@ $schema://$host {
 | 
				
			|||||||
            return ['uptime' => false, 'error' => 'Server skipped.'];
 | 
					            return ['uptime' => false, 'error' => 'Server skipped.'];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            // EC2 does not have `uptime` command, lol
 | 
					 | 
				
			||||||
            instant_remote_process(['ls /'], $server);
 | 
					            instant_remote_process(['ls /'], $server);
 | 
				
			||||||
            $server->settings()->update([
 | 
					            $server->settings()->update([
 | 
				
			||||||
                'is_reachable' => true,
 | 
					                'is_reachable' => true,
 | 
				
			||||||
@@ -1027,7 +1060,6 @@ $schema://$host {
 | 
				
			|||||||
                'unreachable_count' => 0,
 | 
					                'unreachable_count' => 0,
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
            if (data_get($server, 'unreachable_notification_sent') === true) {
 | 
					            if (data_get($server, 'unreachable_notification_sent') === true) {
 | 
				
			||||||
                // $server->team?->notify(new Revived($server));
 | 
					 | 
				
			||||||
                $server->update(['unreachable_notification_sent' => false]);
 | 
					                $server->update(['unreachable_notification_sent' => false]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1156,4 +1188,24 @@ $schema://$host {
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        return $this->settings->is_build_server;
 | 
					        return $this->settings->is_build_server;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static function createWithPrivateKey(array $data, PrivateKey $privateKey)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $server = new self($data);
 | 
				
			||||||
 | 
					        $server->privateKey()->associate($privateKey);
 | 
				
			||||||
 | 
					        $server->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $server;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function updateWithPrivateKey(array $data, ?PrivateKey $privateKey = null)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $this->update($data);
 | 
				
			||||||
 | 
					        if ($privateKey) {
 | 
				
			||||||
 | 
					            $this->privateKey()->associate($privateKey);
 | 
				
			||||||
 | 
					            $this->save();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $this;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
 | 
				
			|||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
					use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
				
			||||||
use Illuminate\Database\Eloquent\SoftDeletes;
 | 
					use Illuminate\Database\Eloquent\SoftDeletes;
 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use Illuminate\Process\InvokedProcess;
 | 
				
			||||||
use Illuminate\Support\Facades\Storage;
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
use OpenApi\Attributes as OA;
 | 
					use OpenApi\Attributes as OA;
 | 
				
			||||||
use Spatie\Url\Url;
 | 
					use Spatie\Url\Url;
 | 
				
			||||||
@@ -68,7 +70,7 @@ class Service extends BaseModel
 | 
				
			|||||||
        $databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten()->sortBy('id');
 | 
					        $databaseStorages = $this->databases()->get()->pluck('persistentStorages')->flatten()->sortBy('id');
 | 
				
			||||||
        $storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
 | 
					        $storages = $applicationStorages->merge($databaseStorages)->implode('updated_at');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $newConfigHash = $images.$domains.$images.$storages;
 | 
					        $newConfigHash = $images . $domains . $images . $storages;
 | 
				
			||||||
        $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
 | 
					        $newConfigHash .= json_encode($this->environment_variables()->get('value')->sort());
 | 
				
			||||||
        $newConfigHash = md5($newConfigHash);
 | 
					        $newConfigHash = md5($newConfigHash);
 | 
				
			||||||
        $oldConfigHash = data_get($this, 'config_hash');
 | 
					        $oldConfigHash = data_get($this, 'config_hash');
 | 
				
			||||||
@@ -131,15 +133,80 @@ class Service extends BaseModel
 | 
				
			|||||||
        return $this->morphToMany(Tag::class, 'taggable');
 | 
					        return $this->morphToMany(Tag::class, 'taggable');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function getContainersToStop(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $containersToStop = [];
 | 
				
			||||||
 | 
					        $applications = $this->applications()->get();
 | 
				
			||||||
 | 
					        foreach ($applications as $application) {
 | 
				
			||||||
 | 
					            $containersToStop[] = "{$application->name}-{$this->uuid}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        $dbs = $this->databases()->get();
 | 
				
			||||||
 | 
					        foreach ($dbs as $db) {
 | 
				
			||||||
 | 
					            $containersToStop[] = "{$db->name}-{$this->uuid}";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return $containersToStop;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function stopContainers(array $containerNames, $server, int $timeout = 300)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $processes = [];
 | 
				
			||||||
 | 
					        foreach ($containerNames as $containerName) {
 | 
				
			||||||
 | 
					            $processes[$containerName] = $this->stopContainer($containerName, $timeout);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $startTime = time();
 | 
				
			||||||
 | 
					        while (count($processes) > 0) {
 | 
				
			||||||
 | 
					            $finishedProcesses = array_filter($processes, function ($process) {
 | 
				
			||||||
 | 
					                return !$process->running();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            foreach (array_keys($finishedProcesses) as $containerName) {
 | 
				
			||||||
 | 
					                unset($processes[$containerName]);
 | 
				
			||||||
 | 
					                $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (time() - $startTime >= $timeout) {
 | 
				
			||||||
 | 
					                $this->forceStopRemainingContainers(array_keys($processes), $server);
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            usleep(100000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function stopContainer(string $containerName, int $timeout): InvokedProcess
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function removeContainer(string $containerName, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        instant_remote_process(command: ["docker rm -f $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function forceStopRemainingContainers(array $containerNames, $server)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        foreach ($containerNames as $containerName) {
 | 
				
			||||||
 | 
					            instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
 | 
				
			||||||
 | 
					            $this->removeContainer($containerName, $server);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function delete_configurations()
 | 
					    public function delete_configurations()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $server = data_get($this, 'server');
 | 
					        $server = data_get($this, 'destination.server');
 | 
				
			||||||
        $workdir = $this->workdir();
 | 
					        $workdir = $this->workdir();
 | 
				
			||||||
        if (str($workdir)->endsWith($this->uuid)) {
 | 
					        if (str($workdir)->endsWith($this->uuid)) {
 | 
				
			||||||
            instant_remote_process(['rm -rf '.$this->workdir()], $server, false);
 | 
					            instant_remote_process(['rm -rf ' . $this->workdir()], $server, false);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function delete_connected_networks($uuid)
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $server = data_get($this, 'destination.server');
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
 | 
				
			||||||
 | 
					        instant_remote_process(["docker network rm {$uuid}"], $server, false);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function status()
 | 
					    public function status()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $applications = $this->applications;
 | 
					        $applications = $this->applications;
 | 
				
			||||||
@@ -994,7 +1061,7 @@ class Service extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function workdir()
 | 
					    public function workdir()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return service_configuration_dir()."/{$this->uuid}";
 | 
					        return service_configuration_dir() . "/{$this->uuid}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function saveComposeConfigs()
 | 
					    public function saveComposeConfigs()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ class ServiceApplication extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function restart()
 | 
					    public function restart()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $container_id = $this->name.'-'.$this->service->uuid;
 | 
					        $container_id = $this->name . '-' . $this->service->uuid;
 | 
				
			||||||
        instant_remote_process(["docker restart {$container_id}"], $this->service->server);
 | 
					        instant_remote_process(["docker restart {$container_id}"], $this->service->server);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -69,7 +69,7 @@ class ServiceApplication extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function workdir()
 | 
					    public function workdir()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return service_configuration_dir()."/{$this->service->uuid}";
 | 
					        return service_configuration_dir() . "/{$this->service->uuid}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function serviceType()
 | 
					    public function serviceType()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ class ServiceDatabase extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function restart()
 | 
					    public function restart()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $container_id = $this->name.'-'.$this->service->uuid;
 | 
					        $container_id = $this->name . '-' . $this->service->uuid;
 | 
				
			||||||
        remote_process(["docker restart {$container_id}"], $this->service->server);
 | 
					        remote_process(["docker restart {$container_id}"], $this->service->server);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -88,7 +88,7 @@ class ServiceDatabase extends BaseModel
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function workdir()
 | 
					    public function workdir()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return service_configuration_dir()."/{$this->service->uuid}";
 | 
					        return service_configuration_dir() . "/{$this->service->uuid}";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public function service()
 | 
					    public function service()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -52,7 +52,7 @@ class ForceDisabled extends Notification implements ShouldQueue
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public function toDiscord(): string
 | 
					    public function toDiscord(): string
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).";
 | 
					        $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $message;
 | 
					        return $message;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -60,7 +60,7 @@ class ForceDisabled extends Notification implements ShouldQueue
 | 
				
			|||||||
    public function toTelegram(): array
 | 
					    public function toTelegram(): array
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        return [
 | 
					        return [
 | 
				
			||||||
            'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).",
 | 
					            'message' => "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).",
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ use App\Models\Server;
 | 
				
			|||||||
use Carbon\Carbon;
 | 
					use Carbon\Carbon;
 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
use Illuminate\Support\Facades\Process;
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use App\Helpers\SshMultiplexingHelper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
trait ExecuteRemoteCommand
 | 
					trait ExecuteRemoteCommand
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -42,7 +43,7 @@ trait ExecuteRemoteCommand
 | 
				
			|||||||
                    $command = parseLineForSudo($command, $this->server);
 | 
					                    $command = parseLineForSudo($command, $this->server);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            $remote_command = generateSshCommand($this->server, $command);
 | 
					            $remote_command = SshMultiplexingHelper::generateSshCommand($this->server, $command);
 | 
				
			||||||
            $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
 | 
					            $process = Process::timeout(3600)->idleTimeout(3600)->start($remote_command, function (string $type, string $output) use ($command, $hidden, $customType, $append) {
 | 
				
			||||||
                $output = str($output)->trim();
 | 
					                $output = str($output)->trim();
 | 
				
			||||||
                if ($output->startsWith('╔')) {
 | 
					                if ($output->startsWith('╔')) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@
 | 
				
			|||||||
use App\Actions\CoolifyTask\PrepareCoolifyTask;
 | 
					use App\Actions\CoolifyTask\PrepareCoolifyTask;
 | 
				
			||||||
use App\Data\CoolifyTaskArgs;
 | 
					use App\Data\CoolifyTaskArgs;
 | 
				
			||||||
use App\Enums\ActivityTypes;
 | 
					use App\Enums\ActivityTypes;
 | 
				
			||||||
 | 
					use App\Helpers\SshMultiplexingHelper;
 | 
				
			||||||
use App\Models\Application;
 | 
					use App\Models\Application;
 | 
				
			||||||
use App\Models\ApplicationDeploymentQueue;
 | 
					use App\Models\ApplicationDeploymentQueue;
 | 
				
			||||||
use App\Models\PrivateKey;
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
@@ -10,9 +11,8 @@ use App\Models\Server;
 | 
				
			|||||||
use Carbon\Carbon;
 | 
					use Carbon\Carbon;
 | 
				
			||||||
use Illuminate\Database\Eloquent\Model;
 | 
					use Illuminate\Database\Eloquent\Model;
 | 
				
			||||||
use Illuminate\Support\Collection;
 | 
					use Illuminate\Support\Collection;
 | 
				
			||||||
use Illuminate\Support\Facades\Hash;
 | 
					use Illuminate\Support\Facades\Auth;
 | 
				
			||||||
use Illuminate\Support\Facades\Process;
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
use Illuminate\Support\Facades\Storage;
 | 
					 | 
				
			||||||
use Illuminate\Support\Str;
 | 
					use Illuminate\Support\Str;
 | 
				
			||||||
use Spatie\Activitylog\Contracts\Activity;
 | 
					use Spatie\Activitylog\Contracts\Activity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,29 +26,28 @@ function remote_process(
 | 
				
			|||||||
    $callEventOnFinish = null,
 | 
					    $callEventOnFinish = null,
 | 
				
			||||||
    $callEventData = null
 | 
					    $callEventData = null
 | 
				
			||||||
): Activity {
 | 
					): Activity {
 | 
				
			||||||
    if (is_null($type)) {
 | 
					    $type = $type ?? ActivityTypes::INLINE->value;
 | 
				
			||||||
        $type = ActivityTypes::INLINE->value;
 | 
					    $command = $command instanceof Collection ? $command->toArray() : $command;
 | 
				
			||||||
    }
 | 
					
 | 
				
			||||||
    if ($command instanceof Collection) {
 | 
					 | 
				
			||||||
        $command = $command->toArray();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ($server->isNonRoot()) {
 | 
					    if ($server->isNonRoot()) {
 | 
				
			||||||
        $command = parseCommandsByLineForSudo(collect($command), $server);
 | 
					        $command = parseCommandsByLineForSudo(collect($command), $server);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $command_string = implode("\n", $command);
 | 
					    $command_string = implode("\n", $command);
 | 
				
			||||||
    if (auth()->user()) {
 | 
					
 | 
				
			||||||
        $teams = auth()->user()->teams->pluck('id');
 | 
					    if (Auth::check()) {
 | 
				
			||||||
 | 
					        $teams = Auth::user()->teams->pluck('id');
 | 
				
			||||||
        if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
 | 
					        if (! $teams->contains($server->team_id) && ! $teams->contains(0)) {
 | 
				
			||||||
            throw new \Exception('User is not part of the team that owns this server');
 | 
					            throw new \Exception('User is not part of the team that owns this server');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    SshMultiplexingHelper::ensureMultiplexedConnection($server);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return resolve(PrepareCoolifyTask::class, [
 | 
					    return resolve(PrepareCoolifyTask::class, [
 | 
				
			||||||
        'remoteProcessArgs' => new CoolifyTaskArgs(
 | 
					        'remoteProcessArgs' => new CoolifyTaskArgs(
 | 
				
			||||||
            server_uuid: $server->uuid,
 | 
					            server_uuid: $server->uuid,
 | 
				
			||||||
            command: <<<EOT
 | 
					            command: $command_string,
 | 
				
			||||||
                {$command_string}
 | 
					 | 
				
			||||||
                EOT,
 | 
					 | 
				
			||||||
            type: $type,
 | 
					            type: $type,
 | 
				
			||||||
            type_uuid: $type_uuid,
 | 
					            type_uuid: $type_uuid,
 | 
				
			||||||
            model: $model,
 | 
					            model: $model,
 | 
				
			||||||
@@ -58,313 +57,67 @@ function remote_process(
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ])();
 | 
					    ])();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function server_ssh_configuration(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    $uuid = data_get($server, 'uuid');
 | 
					 | 
				
			||||||
    if (is_null($uuid)) {
 | 
					 | 
				
			||||||
        throw new \Exception('Server does not have a uuid');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $private_key_filename = "id.root@{$server->uuid}";
 | 
					 | 
				
			||||||
    $location = '/var/www/html/storage/app/ssh/keys/'.$private_key_filename;
 | 
					 | 
				
			||||||
    $mux_filename = '/var/www/html/storage/app/ssh/mux/'.$server->muxFilename();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
        'location' => $location,
 | 
					 | 
				
			||||||
        'mux_filename' => $mux_filename,
 | 
					 | 
				
			||||||
        'private_key_filename' => $private_key_filename,
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
function savePrivateKeyToFs(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    if (data_get($server, 'privateKey.private_key') === null) {
 | 
					 | 
				
			||||||
        throw new \Exception("Server {$server->name} does not have a private key");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    ['location' => $location, 'private_key_filename' => $private_key_filename] = server_ssh_configuration($server);
 | 
					 | 
				
			||||||
    Storage::disk('ssh-keys')->makeDirectory('.');
 | 
					 | 
				
			||||||
    Storage::disk('ssh-mux')->makeDirectory('.');
 | 
					 | 
				
			||||||
    Storage::disk('ssh-keys')->put($private_key_filename, $server->privateKey->private_key);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return $location;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function generateScpCommand(Server $server, string $source, string $dest)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    $user = $server->user;
 | 
					 | 
				
			||||||
    $port = $server->port;
 | 
					 | 
				
			||||||
    $privateKeyLocation = savePrivateKeyToFs($server);
 | 
					 | 
				
			||||||
    $timeout = config('constants.ssh.command_timeout');
 | 
					 | 
				
			||||||
    $connectionTimeout = config('constants.ssh.connection_timeout');
 | 
					 | 
				
			||||||
    $serverInterval = config('constants.ssh.server_interval');
 | 
					 | 
				
			||||||
    $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $scp_command = "timeout $timeout scp ";
 | 
					 | 
				
			||||||
    $muxEnabled = config('constants.ssh.mux_enabled', true) && config('coolify.is_windows_docker_desktop') == false;
 | 
					 | 
				
			||||||
    // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if ($muxEnabled) {
 | 
					 | 
				
			||||||
        $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
 | 
					 | 
				
			||||||
        $scp_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
 | 
					 | 
				
			||||||
        ensureMultiplexedConnection($server);
 | 
					 | 
				
			||||||
        // ray('Using SSH Multiplexing')->green();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        // ray('Not using SSH Multiplexing')->red();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (data_get($server, 'settings.is_cloudflare_tunnel')) {
 | 
					 | 
				
			||||||
        $scp_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $scp_command .= "-i {$privateKeyLocation} "
 | 
					 | 
				
			||||||
        .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
 | 
					 | 
				
			||||||
        .'-o PasswordAuthentication=no '
 | 
					 | 
				
			||||||
        ."-o ConnectTimeout=$connectionTimeout "
 | 
					 | 
				
			||||||
        ."-o ServerAliveInterval=$serverInterval "
 | 
					 | 
				
			||||||
        .'-o RequestTTY=no '
 | 
					 | 
				
			||||||
        .'-o LogLevel=ERROR '
 | 
					 | 
				
			||||||
        ."-P {$port} "
 | 
					 | 
				
			||||||
        ."{$source} "
 | 
					 | 
				
			||||||
        ."{$user}@{$server->ip}:{$dest}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return $scp_command;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
function instant_scp(string $source, string $dest, Server $server, $throwError = true)
 | 
					function instant_scp(string $source, string $dest, Server $server, $throwError = true)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $timeout = config('constants.ssh.command_timeout');
 | 
					    $scp_command = SshMultiplexingHelper::generateScpCommand($server, $source, $dest);
 | 
				
			||||||
    $scp_command = generateScpCommand($server, $source, $dest);
 | 
					    $process = Process::timeout(config('constants.ssh.command_timeout'))->run($scp_command);
 | 
				
			||||||
    $process = Process::timeout($timeout)->run($scp_command);
 | 
					 | 
				
			||||||
    $output = trim($process->output());
 | 
					    $output = trim($process->output());
 | 
				
			||||||
    $exitCode = $process->exitCode();
 | 
					    $exitCode = $process->exitCode();
 | 
				
			||||||
    if ($exitCode !== 0) {
 | 
					    if ($exitCode !== 0) {
 | 
				
			||||||
        if (! $throwError) {
 | 
					        return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
 | 
				
			||||||
            return null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return excludeCertainErrors($process->errorOutput(), $exitCode);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ($output === 'null') {
 | 
					 | 
				
			||||||
        $output = null;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return $output;
 | 
					    return $output === 'null' ? null : $output;
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
function generateSshCommand(Server $server, string $command)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    if ($server->settings->force_disabled) {
 | 
					 | 
				
			||||||
        throw new \RuntimeException('Server is disabled.');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $user = $server->user;
 | 
					 | 
				
			||||||
    $port = $server->port;
 | 
					 | 
				
			||||||
    $privateKeyLocation = savePrivateKeyToFs($server);
 | 
					 | 
				
			||||||
    $timeout = config('constants.ssh.command_timeout');
 | 
					 | 
				
			||||||
    $connectionTimeout = config('constants.ssh.connection_timeout');
 | 
					 | 
				
			||||||
    $serverInterval = config('constants.ssh.server_interval');
 | 
					 | 
				
			||||||
    $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $ssh_command = "timeout $timeout ssh ";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $muxEnabled = config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false;
 | 
					 | 
				
			||||||
    // ray('SSH Multiplexing Enabled:', $muxEnabled)->blue();
 | 
					 | 
				
			||||||
    if ($muxEnabled) {
 | 
					 | 
				
			||||||
        // Always use multiplexing when enabled
 | 
					 | 
				
			||||||
        $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
 | 
					 | 
				
			||||||
        $ssh_command .= "-o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
 | 
					 | 
				
			||||||
        ensureMultiplexedConnection($server);
 | 
					 | 
				
			||||||
        // ray('Using SSH Multiplexing')->green();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
        // ray('Not using SSH Multiplexing')->red();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (data_get($server, 'settings.is_cloudflare_tunnel')) {
 | 
					 | 
				
			||||||
        $ssh_command .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $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);
 | 
					 | 
				
			||||||
    $command = str_replace($delimiter, '', $command);
 | 
					 | 
				
			||||||
    $ssh_command .= "-i {$privateKeyLocation} "
 | 
					 | 
				
			||||||
        .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
 | 
					 | 
				
			||||||
        .'-o PasswordAuthentication=no '
 | 
					 | 
				
			||||||
        ."-o ConnectTimeout=$connectionTimeout "
 | 
					 | 
				
			||||||
        ."-o ServerAliveInterval=$serverInterval "
 | 
					 | 
				
			||||||
        .'-o RequestTTY=no '
 | 
					 | 
				
			||||||
        .'-o LogLevel=ERROR '
 | 
					 | 
				
			||||||
        ."-p {$port} "
 | 
					 | 
				
			||||||
        ."{$user}@{$server->ip} "
 | 
					 | 
				
			||||||
        ." 'bash -se' << \\$delimiter".PHP_EOL
 | 
					 | 
				
			||||||
        .$command.PHP_EOL
 | 
					 | 
				
			||||||
        .$delimiter;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return $ssh_command;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function ensureMultiplexedConnection(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static $ensuredConnections = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isset($ensuredConnections[$server->id])) {
 | 
					 | 
				
			||||||
        if (! shouldResetMultiplexedConnection($server)) {
 | 
					 | 
				
			||||||
            // ray('Using Existing Multiplexed Connection')->green();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $muxSocket = "/var/www/html/storage/app/ssh/mux/{$server->muxFilename()}";
 | 
					 | 
				
			||||||
    $checkCommand = "ssh -O check -o ControlPath=$muxSocket ";
 | 
					 | 
				
			||||||
    if (data_get($server, 'settings.is_cloudflare_tunnel')) {
 | 
					 | 
				
			||||||
        $checkCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $checkCommand .= " {$server->user}@{$server->ip}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $process = Process::run($checkCommand);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if ($process->exitCode() === 0) {
 | 
					 | 
				
			||||||
        // ray('Existing Multiplexed Connection is Valid')->green();
 | 
					 | 
				
			||||||
        $ensuredConnections[$server->id] = [
 | 
					 | 
				
			||||||
            'timestamp' => now(),
 | 
					 | 
				
			||||||
            'muxSocket' => $muxSocket,
 | 
					 | 
				
			||||||
        ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ray('Establishing New Multiplexed Connection')->orange();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $privateKeyLocation = savePrivateKeyToFs($server);
 | 
					 | 
				
			||||||
    $connectionTimeout = config('constants.ssh.connection_timeout');
 | 
					 | 
				
			||||||
    $serverInterval = config('constants.ssh.server_interval');
 | 
					 | 
				
			||||||
    $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $establishCommand = "ssh -fNM -o ControlMaster=auto -o ControlPath=$muxSocket -o ControlPersist={$muxPersistTime} ";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (data_get($server, 'settings.is_cloudflare_tunnel')) {
 | 
					 | 
				
			||||||
        $establishCommand .= '-o ProxyCommand="/usr/local/bin/cloudflared access ssh --hostname %h" ';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $establishCommand .= "-i {$privateKeyLocation} "
 | 
					 | 
				
			||||||
        .'-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null '
 | 
					 | 
				
			||||||
        .'-o PasswordAuthentication=no '
 | 
					 | 
				
			||||||
        ."-o ConnectTimeout=$connectionTimeout "
 | 
					 | 
				
			||||||
        ."-o ServerAliveInterval=$serverInterval "
 | 
					 | 
				
			||||||
        .'-o RequestTTY=no '
 | 
					 | 
				
			||||||
        .'-o LogLevel=ERROR '
 | 
					 | 
				
			||||||
        ."-p {$server->port} "
 | 
					 | 
				
			||||||
        ."{$server->user}@{$server->ip}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $establishProcess = Process::run($establishCommand);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if ($establishProcess->exitCode() !== 0) {
 | 
					 | 
				
			||||||
        throw new \RuntimeException('Failed to establish multiplexed connection: '.$establishProcess->errorOutput());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $ensuredConnections[$server->id] = [
 | 
					 | 
				
			||||||
        'timestamp' => now(),
 | 
					 | 
				
			||||||
        'muxSocket' => $muxSocket,
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // ray('Established New Multiplexed Connection')->green();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function shouldResetMultiplexedConnection(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static $ensuredConnections = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (! isset($ensuredConnections[$server->id])) {
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $lastEnsured = $ensuredConnections[$server->id]['timestamp'];
 | 
					 | 
				
			||||||
    $muxPersistTime = config('constants.ssh.mux_persist_time');
 | 
					 | 
				
			||||||
    $resetInterval = strtotime($muxPersistTime) - time();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return $lastEnsured->addSeconds($resetInterval)->isPast();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function resetMultiplexedConnection(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    if (! (config('constants.ssh.mux_enabled') && config('coolify.is_windows_docker_desktop') == false)) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    static $ensuredConnections = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (isset($ensuredConnections[$server->id])) {
 | 
					 | 
				
			||||||
        $muxSocket = $ensuredConnections[$server->id]['muxSocket'];
 | 
					 | 
				
			||||||
        $closeCommand = "ssh -O exit -o ControlPath=$muxSocket {$server->user}@{$server->ip}";
 | 
					 | 
				
			||||||
        Process::run($closeCommand);
 | 
					 | 
				
			||||||
        unset($ensuredConnections[$server->id]);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
 | 
					function instant_remote_process(Collection|array $command, Server $server, bool $throwError = true, bool $no_sudo = false): ?string
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    static $processCount = 0;
 | 
					    $command = $command instanceof Collection ? $command->toArray() : $command;
 | 
				
			||||||
    $processCount++;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $timeout = config('constants.ssh.command_timeout');
 | 
					 | 
				
			||||||
    if ($command instanceof Collection) {
 | 
					 | 
				
			||||||
        $command = $command->toArray();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ($server->isNonRoot() && ! $no_sudo) {
 | 
					    if ($server->isNonRoot() && ! $no_sudo) {
 | 
				
			||||||
        $command = parseCommandsByLineForSudo(collect($command), $server);
 | 
					        $command = parseCommandsByLineForSudo(collect($command), $server);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    $command_string = implode("\n", $command);
 | 
					    $command_string = implode("\n", $command);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $start_time = microtime(true);
 | 
					    // $start_time = microtime(true);
 | 
				
			||||||
    $sshCommand = generateSshCommand($server, $command_string);
 | 
					    $sshCommand = SshMultiplexingHelper::generateSshCommand($server, $command_string);
 | 
				
			||||||
    $process = Process::timeout($timeout)->run($sshCommand);
 | 
					    $process = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand);
 | 
				
			||||||
    $end_time = microtime(true);
 | 
					    // $end_time = microtime(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
 | 
					    // $execution_time = ($end_time - $start_time) * 1000; // Convert to milliseconds
 | 
				
			||||||
    // ray('SSH command execution time:', $execution_time.' ms')->orange();
 | 
					    // ray('SSH command execution time:', $execution_time.' ms')->orange();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $output = trim($process->output());
 | 
					    $output = trim($process->output());
 | 
				
			||||||
    $exitCode = $process->exitCode();
 | 
					    $exitCode = $process->exitCode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if ($exitCode !== 0) {
 | 
					    if ($exitCode !== 0) {
 | 
				
			||||||
        if (! $throwError) {
 | 
					        return $throwError ? excludeCertainErrors($process->errorOutput(), $exitCode) : null;
 | 
				
			||||||
            return null;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return excludeCertainErrors($process->errorOutput(), $exitCode);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ($output === 'null') {
 | 
					 | 
				
			||||||
        $output = null;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return $output;
 | 
					    return $output === 'null' ? null : $output;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
 | 
					function excludeCertainErrors(string $errorOutput, ?int $exitCode = null)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $ignoredErrors = collect([
 | 
					    $ignoredErrors = collect([
 | 
				
			||||||
        'Permission denied (publickey',
 | 
					        'Permission denied (publickey',
 | 
				
			||||||
        'Could not resolve hostname',
 | 
					        'Could not resolve hostname',
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
    $ignored = false;
 | 
					    $ignored = $ignoredErrors->contains(fn ($error) => Str::contains($errorOutput, $error));
 | 
				
			||||||
    foreach ($ignoredErrors as $ignoredError) {
 | 
					 | 
				
			||||||
        if (Str::contains($errorOutput, $ignoredError)) {
 | 
					 | 
				
			||||||
            $ignored = true;
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if ($ignored) {
 | 
					    if ($ignored) {
 | 
				
			||||||
        // TODO: Create new exception and disable in sentry
 | 
					        // TODO: Create new exception and disable in sentry
 | 
				
			||||||
        throw new \RuntimeException($errorOutput, $exitCode);
 | 
					        throw new \RuntimeException($errorOutput, $exitCode);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    throw new \RuntimeException($errorOutput, $exitCode);
 | 
					    throw new \RuntimeException($errorOutput, $exitCode);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
 | 
					function decode_remote_command_output(?ApplicationDeploymentQueue $application_deployment_queue = null): Collection
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $application = Application::find(data_get($application_deployment_queue, 'application_id'));
 | 
					 | 
				
			||||||
    $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
 | 
					 | 
				
			||||||
    if (is_null($application_deployment_queue)) {
 | 
					    if (is_null($application_deployment_queue)) {
 | 
				
			||||||
        return collect([]);
 | 
					        return collect([]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    $application = Application::find(data_get($application_deployment_queue, 'application_id'));
 | 
				
			||||||
 | 
					    $is_debug_enabled = data_get($application, 'settings.is_debug_enabled');
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        $decoded = json_decode(
 | 
					        $decoded = json_decode(
 | 
				
			||||||
            data_get($application_deployment_queue, 'logs'),
 | 
					            data_get($application_deployment_queue, 'logs'),
 | 
				
			||||||
@@ -379,7 +132,8 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
 | 
				
			|||||||
    if (! $is_debug_enabled) {
 | 
					    if (! $is_debug_enabled) {
 | 
				
			||||||
        $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
 | 
					        $formatted = $formatted->filter(fn ($i) => $i['hidden'] === false ?? false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    $formatted = $formatted
 | 
					
 | 
				
			||||||
 | 
					    return $formatted
 | 
				
			||||||
        ->sortBy(fn ($i) => data_get($i, 'order'))
 | 
					        ->sortBy(fn ($i) => data_get($i, 'order'))
 | 
				
			||||||
        ->map(function ($i) {
 | 
					        ->map(function ($i) {
 | 
				
			||||||
            data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
 | 
					            data_set($i, 'timestamp', Carbon::parse(data_get($i, 'timestamp'))->format('Y-M-d H:i:s.u'));
 | 
				
			||||||
@@ -421,36 +175,22 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            return $deploymentLogLines;
 | 
					            return $deploymentLogLines;
 | 
				
			||||||
        }, collect());
 | 
					        }, collect());
 | 
				
			||||||
 | 
					 | 
				
			||||||
    return $formatted;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function remove_iip($text)
 | 
					function remove_iip($text)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
 | 
					    $text = preg_replace('/x-access-token:.*?(?=@)/', 'x-access-token:'.REDACTED, $text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
 | 
					    return preg_replace('/\x1b\[[0-9;]*m/', '', $text);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function remove_mux_and_private_key(Server $server)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    $muxFilename = $server->muxFilename();
 | 
					 | 
				
			||||||
    $privateKeyLocation = savePrivateKeyToFs($server);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
 | 
					 | 
				
			||||||
    Process::run($closeCommand);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Storage::disk('ssh-mux')->delete($muxFilename);
 | 
					 | 
				
			||||||
    Storage::disk('ssh-keys')->delete($privateKeyLocation);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
function refresh_server_connection(?PrivateKey $private_key = null)
 | 
					function refresh_server_connection(?PrivateKey $private_key = null)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    if (is_null($private_key)) {
 | 
					    if (is_null($private_key)) {
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    foreach ($private_key->servers as $server) {
 | 
					    foreach ($private_key->servers as $server) {
 | 
				
			||||||
        $muxFilename = $server->muxFilename();
 | 
					        SshMultiplexingHelper::removeMuxFile($server);
 | 
				
			||||||
        $closeCommand = "ssh -O exit -o ControlPath=/var/www/html/storage/app/ssh/mux/{$muxFilename} {$server->user}@{$server->ip}";
 | 
					 | 
				
			||||||
        Process::run($closeCommand);
 | 
					 | 
				
			||||||
        Storage::disk('ssh-mux')->delete($muxFilename);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -468,9 +208,8 @@ function checkRequiredCommands(Server $server)
 | 
				
			|||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
 | 
					        $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false);
 | 
				
			||||||
        if ($commandFound) {
 | 
					        if (! $commandFound) {
 | 
				
			||||||
            continue;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2928,6 +2928,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    $allMagicEnvironments = collect([]);
 | 
					    $allMagicEnvironments = collect([]);
 | 
				
			||||||
    foreach ($services as $serviceName => $service) {
 | 
					    foreach ($services as $serviceName => $service) {
 | 
				
			||||||
 | 
					        $predefinedPort = null;
 | 
				
			||||||
        $magicEnvironments = collect([]);
 | 
					        $magicEnvironments = collect([]);
 | 
				
			||||||
        $image = data_get_str($service, 'image');
 | 
					        $image = data_get_str($service, 'image');
 | 
				
			||||||
        $environment = collect(data_get($service, 'environment', []));
 | 
					        $environment = collect(data_get($service, 'environment', []));
 | 
				
			||||||
@@ -2936,6 +2937,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
        $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
 | 
					        $isDatabase = isDatabaseImage(data_get_str($service, 'image'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($isService) {
 | 
					        if ($isService) {
 | 
				
			||||||
 | 
					            $containerName = "$serviceName-{$resource->uuid}";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if ($serviceName === 'registry') {
 | 
				
			||||||
 | 
					                $tempServiceName = 'docker-registry';
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                $tempServiceName = $serviceName;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (str(data_get($service, 'image'))->contains('glitchtip')) {
 | 
				
			||||||
 | 
					                $tempServiceName = 'glitchtip';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if ($serviceName === 'supabase-kong') {
 | 
				
			||||||
 | 
					                $tempServiceName = 'supabase';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            $serviceDefinition = data_get($allServices, $tempServiceName);
 | 
				
			||||||
 | 
					            $predefinedPort = data_get($serviceDefinition, 'port');
 | 
				
			||||||
 | 
					            if ($serviceName === 'plausible') {
 | 
				
			||||||
 | 
					                $predefinedPort = '8000';
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
            if ($isDatabase) {
 | 
					            if ($isDatabase) {
 | 
				
			||||||
                $savedService = ServiceDatabase::firstOrCreate([
 | 
					                $savedService = ServiceDatabase::firstOrCreate([
 | 
				
			||||||
                    'name' => $serviceName,
 | 
					                    'name' => $serviceName,
 | 
				
			||||||
@@ -2987,8 +3006,10 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
                    // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
 | 
					                    // SERVICE_FQDN_APP or SERVICE_FQDN_APP_3000
 | 
				
			||||||
                    if (substr_count(str($key)->value(), '_') === 3) {
 | 
					                    if (substr_count(str($key)->value(), '_') === 3) {
 | 
				
			||||||
                        $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
 | 
					                        $fqdnFor = $key->after('SERVICE_FQDN_')->beforeLast('_')->lower()->value();
 | 
				
			||||||
 | 
					                        $port = $key->afterLast('_')->value();
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
 | 
					                        $fqdnFor = $key->after('SERVICE_FQDN_')->lower()->value();
 | 
				
			||||||
 | 
					                        $port = null;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    if ($isApplication) {
 | 
					                    if ($isApplication) {
 | 
				
			||||||
                        $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
 | 
					                        $fqdn = generateFqdn($server, "{$resource->name}-$uuid");
 | 
				
			||||||
@@ -2999,19 +3020,24 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
                            $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
 | 
					                            $fqdn = generateFqdn($server, "{$savedService->name}-$uuid");
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
 | 
					                    if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) {
 | 
				
			||||||
                        $path = $value->value();
 | 
					                        $path = $value->value();
 | 
				
			||||||
                        if ($path !== '/') {
 | 
					                        if ($path !== '/') {
 | 
				
			||||||
                            $fqdn = "$fqdn$path";
 | 
					                            $fqdn = "$fqdn$path";
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                    $fqdnWithPort = $fqdn;
 | 
				
			||||||
 | 
					                    if ($port) {
 | 
				
			||||||
 | 
					                        $fqdnWithPort = "$fqdn:$port";
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    if ($isApplication && is_null($resource->fqdn)) {
 | 
					                    if ($isApplication && is_null($resource->fqdn)) {
 | 
				
			||||||
                        data_forget($resource, 'environment_variables');
 | 
					                        data_forget($resource, 'environment_variables');
 | 
				
			||||||
                        data_forget($resource, 'environment_variables_preview');
 | 
					                        data_forget($resource, 'environment_variables_preview');
 | 
				
			||||||
                        $resource->fqdn = $fqdn;
 | 
					                        $resource->fqdn = $fqdnWithPort;
 | 
				
			||||||
                        $resource->save();
 | 
					                        $resource->save();
 | 
				
			||||||
                    } elseif ($isService && is_null($savedService->fqdn)) {
 | 
					                    } elseif ($isService && is_null($savedService->fqdn)) {
 | 
				
			||||||
                        $savedService->fqdn = $fqdn;
 | 
					                        $savedService->fqdn = $fqdnWithPort;
 | 
				
			||||||
                        $savedService->save();
 | 
					                        $savedService->save();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3040,7 +3066,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
 | 
					            $allMagicEnvironments = $allMagicEnvironments->merge($magicEnvironments);
 | 
				
			||||||
 | 
					 | 
				
			||||||
            if ($magicEnvironments->count() > 0) {
 | 
					            if ($magicEnvironments->count() > 0) {
 | 
				
			||||||
                foreach ($magicEnvironments as $key => $value) {
 | 
					                foreach ($magicEnvironments as $key => $value) {
 | 
				
			||||||
                    $key = str($key);
 | 
					                    $key = str($key);
 | 
				
			||||||
@@ -3631,6 +3656,14 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
        data_forget($service, 'volumes.*.is_directory');
 | 
					        data_forget($service, 'volumes.*.is_directory');
 | 
				
			||||||
        data_forget($service, 'exclude_from_hc');
 | 
					        data_forget($service, 'exclude_from_hc');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $volumesParsed = $volumesParsed->map(function ($volume) {
 | 
				
			||||||
 | 
					            data_forget($volume, 'content');
 | 
				
			||||||
 | 
					            data_forget($volume, 'is_directory');
 | 
				
			||||||
 | 
					            data_forget($volume, 'isDirectory');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return $volume;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $payload = collect($service)->merge([
 | 
					        $payload = collect($service)->merge([
 | 
				
			||||||
            'container_name' => $containerName,
 | 
					            'container_name' => $containerName,
 | 
				
			||||||
            'restart' => $restart->value(),
 | 
					            'restart' => $restart->value(),
 | 
				
			||||||
@@ -3661,6 +3694,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
 | 
				
			|||||||
        $parsedServices->put($serviceName, $payload);
 | 
					        $parsedServices->put($serviceName, $payload);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    $topLevel->put('services', $parsedServices);
 | 
					    $topLevel->put('services', $parsedServices);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
 | 
					    $customOrder = ['services', 'volumes', 'networks', 'configs', 'secrets'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {
 | 
					    $topLevel = $topLevel->sortBy(function ($value, $key) use ($customOrder) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,424 +0,0 @@
 | 
				
			|||||||
<?php
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
return [
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Enable Clockwork
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork is enabled by default only when your application is in debug mode. Here you can explicitly enable or
 | 
					 | 
				
			||||||
	| disable Clockwork. When disabled, no data is collected and the api and web ui are inactive.
 | 
					 | 
				
			||||||
	| Unless explicitly enabled, Clockwork only runs on localhost, *.local, *.test and *.wip domains.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'enable' => env('CLOCKWORK_ENABLE', null),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Features
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| You can enable or disable various Clockwork features here. Some features have additional settings (eg. slow query
 | 
					 | 
				
			||||||
	| threshold for database queries).
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'features' => [
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Cache usage stats and cache queries including results
 | 
					 | 
				
			||||||
		'cache' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_CACHE_ENABLED', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect cache queries
 | 
					 | 
				
			||||||
			'collect_queries' => env('CLOCKWORK_CACHE_QUERIES', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect values from cache queries (high performance impact with a very high number of queries)
 | 
					 | 
				
			||||||
			'collect_values' => env('CLOCKWORK_CACHE_COLLECT_VALUES', false)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Database usage stats and queries
 | 
					 | 
				
			||||||
		'database' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_DATABASE_ENABLED', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect database queries (high performance impact with a very high number of queries)
 | 
					 | 
				
			||||||
			'collect_queries' => env('CLOCKWORK_DATABASE_COLLECT_QUERIES', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect details of models updates (high performance impact with a lot of model updates)
 | 
					 | 
				
			||||||
			'collect_models_actions' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_ACTIONS', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect details of retrieved models (very high performance impact with a lot of models retrieved)
 | 
					 | 
				
			||||||
			'collect_models_retrieved' => env('CLOCKWORK_DATABASE_COLLECT_MODELS_RETRIEVED', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Query execution time threshold in milliseconds after which the query will be marked as slow
 | 
					 | 
				
			||||||
			'slow_threshold' => env('CLOCKWORK_DATABASE_SLOW_THRESHOLD'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect only slow database queries
 | 
					 | 
				
			||||||
			'slow_only' => env('CLOCKWORK_DATABASE_SLOW_ONLY', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Detect and report duplicate queries
 | 
					 | 
				
			||||||
			'detect_duplicate_queries' => env('CLOCKWORK_DATABASE_DETECT_DUPLICATE_QUERIES', false)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Dispatched events
 | 
					 | 
				
			||||||
		'events' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_EVENTS_ENABLED', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Ignored events (framework events are ignored by default)
 | 
					 | 
				
			||||||
			'ignored_events' => [
 | 
					 | 
				
			||||||
				// App\Events\UserRegistered::class,
 | 
					 | 
				
			||||||
				// 'user.registered'
 | 
					 | 
				
			||||||
			],
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Laravel log (you can still log directly to Clockwork with laravel log disabled)
 | 
					 | 
				
			||||||
		'log' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_LOG_ENABLED', true)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Sent notifications
 | 
					 | 
				
			||||||
		'notifications' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_NOTIFICATIONS_ENABLED', true),
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Performance metrics
 | 
					 | 
				
			||||||
		'performance' => [
 | 
					 | 
				
			||||||
			// Allow collecting of client metrics. Requires separate clockwork-browser npm package.
 | 
					 | 
				
			||||||
			'client_metrics' => env('CLOCKWORK_PERFORMANCE_CLIENT_METRICS', true)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Dispatched queue jobs
 | 
					 | 
				
			||||||
		'queue' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_QUEUE_ENABLED', true)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Redis commands
 | 
					 | 
				
			||||||
		'redis' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_REDIS_ENABLED', true)
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Routes list
 | 
					 | 
				
			||||||
		'routes' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_ROUTES_ENABLED', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect only routes from particular namespaces (only application routes by default)
 | 
					 | 
				
			||||||
			'only_namespaces' => [ 'App' ]
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Rendered views
 | 
					 | 
				
			||||||
		'views' => [
 | 
					 | 
				
			||||||
			'enabled' => env('CLOCKWORK_VIEWS_ENABLED', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Collect views including view data (high performance impact with a high number of views)
 | 
					 | 
				
			||||||
			'collect_data' => env('CLOCKWORK_VIEWS_COLLECT_DATA', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Use Twig profiler instead of Laravel events for apps using laravel-twigbridge (more precise, but does
 | 
					 | 
				
			||||||
			// not support collecting view data)
 | 
					 | 
				
			||||||
			'use_twig_profiler' => env('CLOCKWORK_VIEWS_USE_TWIG_PROFILER', false)
 | 
					 | 
				
			||||||
		]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Enable web UI
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork comes with a web UI accessible via http://your.app/clockwork. Here you can enable or disable this
 | 
					 | 
				
			||||||
	| feature. You can also set a custom path for the web UI.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'web' => env('CLOCKWORK_WEB', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Enable toolbar
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can show a toolbar with basic metrics on all responses. Here you can enable or disable this feature.
 | 
					 | 
				
			||||||
	| Requires a separate clockwork-browser npm library.
 | 
					 | 
				
			||||||
	| For installation instructions see https://underground.works/clockwork/#docs-viewing-data
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'toolbar' => env('CLOCKWORK_TOOLBAR', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| HTTP requests collection
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork collects data about HTTP requests to your app. Here you can choose which requests should be collected.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'requests' => [
 | 
					 | 
				
			||||||
		// With on-demand mode enabled, Clockwork will only profile requests when the browser extension is open or you
 | 
					 | 
				
			||||||
		// manually pass a "clockwork-profile" cookie or get/post data key.
 | 
					 | 
				
			||||||
		// Optionally you can specify a "secret" that has to be passed as the value to enable profiling.
 | 
					 | 
				
			||||||
		'on_demand' => env('CLOCKWORK_REQUESTS_ON_DEMAND', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Collect only errors (requests with HTTP 4xx and 5xx responses)
 | 
					 | 
				
			||||||
		'errors_only' => env('CLOCKWORK_REQUESTS_ERRORS_ONLY', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Response time threshold in milliseconds after which the request will be marked as slow
 | 
					 | 
				
			||||||
		'slow_threshold' => env('CLOCKWORK_REQUESTS_SLOW_THRESHOLD'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Collect only slow requests
 | 
					 | 
				
			||||||
		'slow_only' => env('CLOCKWORK_REQUESTS_SLOW_ONLY', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Sample the collected requests (e.g. set to 100 to collect only 1 in 100 requests)
 | 
					 | 
				
			||||||
		'sample' => env('CLOCKWORK_REQUESTS_SAMPLE', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of URIs that should not be collected
 | 
					 | 
				
			||||||
		'except' => [
 | 
					 | 
				
			||||||
			'/horizon/.*', // Laravel Horizon requests
 | 
					 | 
				
			||||||
			'/telescope/.*', // Laravel Telescope requests
 | 
					 | 
				
			||||||
			'/_tt/.*', // Laravel Telescope toolbar
 | 
					 | 
				
			||||||
			'/_debugbar/.*', // Laravel DebugBar requests
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of URIs that should be collected, any other URI will not be collected if not empty
 | 
					 | 
				
			||||||
		'only' => [
 | 
					 | 
				
			||||||
			// '/api/.*'
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Don't collect OPTIONS requests, mostly used in the CSRF pre-flight requests and are rarely of interest
 | 
					 | 
				
			||||||
		'except_preflight' => env('CLOCKWORK_REQUESTS_EXCEPT_PREFLIGHT', true)
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Artisan commands collection
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can collect data about executed artisan commands. Here you can enable and configure which commands
 | 
					 | 
				
			||||||
	| should be collected.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'artisan' => [
 | 
					 | 
				
			||||||
		// Enable or disable collection of executed Artisan commands
 | 
					 | 
				
			||||||
		'collect' => env('CLOCKWORK_ARTISAN_COLLECT', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of commands that should not be collected (built-in commands are not collected by default)
 | 
					 | 
				
			||||||
		'except' => [
 | 
					 | 
				
			||||||
			// 'inspire'
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of commands that should be collected, any other command will not be collected if not empty
 | 
					 | 
				
			||||||
		'only' => [
 | 
					 | 
				
			||||||
			// 'inspire'
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Enable or disable collection of command output
 | 
					 | 
				
			||||||
		'collect_output' => env('CLOCKWORK_ARTISAN_COLLECT_OUTPUT', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Enable or disable collection of built-in Laravel commands
 | 
					 | 
				
			||||||
		'except_laravel_commands' => env('CLOCKWORK_ARTISAN_EXCEPT_LARAVEL_COMMANDS', true)
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Queue jobs collection
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can collect data about executed queue jobs. Here you can enable and configure which queue jobs should
 | 
					 | 
				
			||||||
	| be collected.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'queue' => [
 | 
					 | 
				
			||||||
		// Enable or disable collection of executed queue jobs
 | 
					 | 
				
			||||||
		'collect' => env('CLOCKWORK_QUEUE_COLLECT', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of queue jobs that should not be collected
 | 
					 | 
				
			||||||
		'except' => [
 | 
					 | 
				
			||||||
			// App\Jobs\ExpensiveJob::class
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of queue jobs that should be collected, any other queue job will not be collected if not empty
 | 
					 | 
				
			||||||
		'only' => [
 | 
					 | 
				
			||||||
			// App\Jobs\BuggyJob::class
 | 
					 | 
				
			||||||
		]
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Tests collection
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can collect data about executed tests. Here you can enable and configure which tests should be
 | 
					 | 
				
			||||||
	| collected.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'tests' => [
 | 
					 | 
				
			||||||
		// Enable or disable collection of ran tests
 | 
					 | 
				
			||||||
		'collect' => env('CLOCKWORK_TESTS_COLLECT', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of tests that should not be collected
 | 
					 | 
				
			||||||
		'except' => [
 | 
					 | 
				
			||||||
			// Tests\Unit\ExampleTest::class
 | 
					 | 
				
			||||||
		]
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Enable data collection when Clockwork is disabled
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| You can enable this setting to collect data even when Clockwork is disabled, e.g. for future analysis.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'collect_data_always' => env('CLOCKWORK_COLLECT_DATA_ALWAYS', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Metadata storage
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Configure how is the metadata collected by Clockwork stored. Three options are available:
 | 
					 | 
				
			||||||
	|   - files - A simple fast storage implementation storing data in one-per-request files.
 | 
					 | 
				
			||||||
	|   - sql - Stores requests in a sql database. Supports MySQL, PostgreSQL and SQLite. Requires PDO.
 | 
					 | 
				
			||||||
	|   - redis - Stores requests in redis. Requires phpredis.
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'storage' => env('CLOCKWORK_STORAGE', 'files'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Path where the Clockwork metadata is stored
 | 
					 | 
				
			||||||
	'storage_files_path' => env('CLOCKWORK_STORAGE_FILES_PATH', storage_path('clockwork')),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Compress the metadata files using gzip, trading a little bit of performance for lower disk usage
 | 
					 | 
				
			||||||
	'storage_files_compress' => env('CLOCKWORK_STORAGE_FILES_COMPRESS', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// SQL database to use, can be a name of database configured in database.php or a path to a SQLite file
 | 
					 | 
				
			||||||
	'storage_sql_database' => env('CLOCKWORK_STORAGE_SQL_DATABASE', storage_path('clockwork.sqlite')),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// SQL table name to use, the table is automatically created and updated when needed
 | 
					 | 
				
			||||||
	'storage_sql_table' => env('CLOCKWORK_STORAGE_SQL_TABLE', 'clockwork'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Redis connection, name of redis connection or cluster configured in database.php
 | 
					 | 
				
			||||||
	'storage_redis' => env('CLOCKWORK_STORAGE_REDIS', 'default'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Redis prefix for Clockwork keys ("clockwork" if not set)
 | 
					 | 
				
			||||||
	'storage_redis_prefix' => env('CLOCKWORK_STORAGE_REDIS_PREFIX', 'clockwork'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Maximum lifetime of collected metadata in minutes, older requests will automatically be deleted, false to disable
 | 
					 | 
				
			||||||
	'storage_expiration' => env('CLOCKWORK_STORAGE_EXPIRATION', 60 * 24 * 7),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Authentication
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can be configured to require authentication before allowing access to the collected data. This might be
 | 
					 | 
				
			||||||
	| useful when the application is publicly accessible. Setting to true will enable a simple authentication with a
 | 
					 | 
				
			||||||
	| pre-configured password. You can also pass a class name of a custom implementation.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'authentication' => env('CLOCKWORK_AUTHENTICATION', false),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Password for the simple authentication
 | 
					 | 
				
			||||||
	'authentication_password' => env('CLOCKWORK_AUTHENTICATION_PASSWORD', 'VerySecretPassword'),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Stack traces collection
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork can collect stack traces for log messages and certain data like database queries. Here you can set
 | 
					 | 
				
			||||||
	| whether to collect stack traces, limit the number of collected frames and set further configuration. Collecting
 | 
					 | 
				
			||||||
	| long stack traces considerably increases metadata size.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'stack_traces' => [
 | 
					 | 
				
			||||||
		// Enable or disable collecting of stack traces
 | 
					 | 
				
			||||||
		'enabled' => env('CLOCKWORK_STACK_TRACES_ENABLED', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Limit the number of frames to be collected
 | 
					 | 
				
			||||||
		'limit' => env('CLOCKWORK_STACK_TRACES_LIMIT', 10),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of vendor names to skip when determining caller, common vendors are automatically added
 | 
					 | 
				
			||||||
		'skip_vendors' => [
 | 
					 | 
				
			||||||
			// 'phpunit'
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of namespaces to skip when determining caller
 | 
					 | 
				
			||||||
		'skip_namespaces' => [
 | 
					 | 
				
			||||||
			// 'Laravel'
 | 
					 | 
				
			||||||
		],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// List of class names to skip when determining caller
 | 
					 | 
				
			||||||
		'skip_classes' => [
 | 
					 | 
				
			||||||
			// App\CustomLog::class
 | 
					 | 
				
			||||||
		]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Serialization
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork serializes the collected data to json for storage and transfer. Here you can configure certain aspects
 | 
					 | 
				
			||||||
	| of serialization. Serialization has a large effect on the cpu time and memory usage.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Maximum depth of serialized multi-level arrays and objects
 | 
					 | 
				
			||||||
	'serialization_depth' => env('CLOCKWORK_SERIALIZATION_DEPTH', 10),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// A list of classes that will never be serialized (e.g. a common service container class)
 | 
					 | 
				
			||||||
	'serialization_blackbox' => [
 | 
					 | 
				
			||||||
		\Illuminate\Container\Container::class,
 | 
					 | 
				
			||||||
		\Illuminate\Foundation\Application::class,
 | 
					 | 
				
			||||||
		\Laravel\Lumen\Application::class
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Register helpers
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork comes with a "clock" global helper function. You can use this helper to quickly log something and to
 | 
					 | 
				
			||||||
	| access the Clockwork instance.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'register_helpers' => env('CLOCKWORK_REGISTER_HELPERS', true),
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Send headers for AJAX request
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| When trying to collect data, the AJAX method can sometimes fail if it is missing required headers. For example, an
 | 
					 | 
				
			||||||
	| API might require a version number using Accept headers to route the HTTP request to the correct codebase.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'headers' => [
 | 
					 | 
				
			||||||
		// 'Accept' => 'application/vnd.com.whatever.v1+json',
 | 
					 | 
				
			||||||
	],
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	/*
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	| Server timing
 | 
					 | 
				
			||||||
	|------------------------------------------------------------------------------------------------------------------
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	| Clockwork supports the W3C Server Timing specification, which allows for collecting a simple performance metrics
 | 
					 | 
				
			||||||
	| in a cross-browser way. E.g. in Chrome, your app, database and timeline event timings will be shown in the Dev
 | 
					 | 
				
			||||||
	| Tools network tab. This setting specifies the max number of timeline events that will be sent. Setting to false
 | 
					 | 
				
			||||||
	| will disable the feature.
 | 
					 | 
				
			||||||
	|
 | 
					 | 
				
			||||||
	*/
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	'server_timing' => env('CLOCKWORK_SERVER_TIMING', 10)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
@@ -6,9 +6,8 @@ return [
 | 
				
			|||||||
        'contact' => 'https://coolify.io/docs/contact',
 | 
					        'contact' => 'https://coolify.io/docs/contact',
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    'ssh' => [
 | 
					    'ssh' => [
 | 
				
			||||||
        // Using MUX
 | 
					        'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true)),
 | 
				
			||||||
        'mux_enabled' => env('MUX_ENABLED', env('SSH_MUX_ENABLED', true), true),
 | 
					        'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', 3600),
 | 
				
			||||||
        'mux_persist_time' => env('SSH_MUX_PERSIST_TIME', '1h'),
 | 
					 | 
				
			||||||
        'connection_timeout' => 10,
 | 
					        'connection_timeout' => 10,
 | 
				
			||||||
        'server_interval' => 20,
 | 
					        'server_interval' => 20,
 | 
				
			||||||
        'command_timeout' => 7200,
 | 
					        'command_timeout' => 7200,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.341',
 | 
					    'release' => '4.0.0-beta.342',
 | 
				
			||||||
    // 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'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,3 @@
 | 
				
			|||||||
<?php
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
return '4.0.0-beta.341';
 | 
					return '4.0.0-beta.342';
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\DB;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Crypt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EncryptExistingPrivateKeys extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        DB::table('private_keys')->chunkById(100, function ($keys) {
 | 
				
			||||||
 | 
					            foreach ($keys as $key) {
 | 
				
			||||||
 | 
					                DB::table('private_keys')
 | 
				
			||||||
 | 
					                    ->where('id', $key->id)
 | 
				
			||||||
 | 
					                    ->update(['private_key' => Crypt::encryptString($key->private_key)]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PopulateSshKeysAndClearMuxDirectory extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        // Storage::disk('ssh-keys')->deleteDirectory('');
 | 
				
			||||||
 | 
					        // Storage::disk('ssh-keys')->makeDirectory('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Storage::disk('ssh-mux')->deleteDirectory('');
 | 
				
			||||||
 | 
					        // Storage::disk('ssh-mux')->makeDirectory('');
 | 
				
			||||||
 | 
					        // PrivateKey::chunk(100, function ($keys) {
 | 
				
			||||||
 | 
					        //     foreach ($keys as $key) {
 | 
				
			||||||
 | 
					        //         $key->storeInFileSystem();
 | 
				
			||||||
 | 
					        //         if ($key->id === 0) {
 | 
				
			||||||
 | 
					        //             Storage::disk('ssh-keys')->put('id.root@host.docker.internal', $key->private_key);
 | 
				
			||||||
 | 
					        //         }
 | 
				
			||||||
 | 
					        //     }
 | 
				
			||||||
 | 
					        // });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
 | 
					use Illuminate\Database\Migrations\Migration;
 | 
				
			||||||
 | 
					use Illuminate\Database\Schema\Blueprint;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class AddSshKeyFingerprintToPrivateKeysTable extends Migration
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function up()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::table('private_keys', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->string('fingerprint')->after('private_key')->nullable();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        PrivateKey::whereNull('fingerprint')->each(function ($key) {
 | 
				
			||||||
 | 
					            $fingerprint = PrivateKey::generateFingerprint($key->private_key);
 | 
				
			||||||
 | 
					            if ($fingerprint) {
 | 
				
			||||||
 | 
					                $key->fingerprint = $fingerprint;
 | 
				
			||||||
 | 
					                $key->save();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public function down()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Schema::table('private_keys', function (Blueprint $table) {
 | 
				
			||||||
 | 
					            $table->dropColumn('fingerprint');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder
 | 
				
			|||||||
            UserSeeder::class,
 | 
					            UserSeeder::class,
 | 
				
			||||||
            TeamSeeder::class,
 | 
					            TeamSeeder::class,
 | 
				
			||||||
            PrivateKeySeeder::class,
 | 
					            PrivateKeySeeder::class,
 | 
				
			||||||
 | 
					            PopulateSshKeysDirectorySeeder::class,
 | 
				
			||||||
            ServerSeeder::class,
 | 
					            ServerSeeder::class,
 | 
				
			||||||
            ServerSettingSeeder::class,
 | 
					            ServerSettingSeeder::class,
 | 
				
			||||||
            ProjectSeeder::class,
 | 
					            ProjectSeeder::class,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,7 @@ class GithubAppSeeder extends Seeder
 | 
				
			|||||||
            'client_id' => 'Iv1.220e564d2b0abd8c',
 | 
					            'client_id' => 'Iv1.220e564d2b0abd8c',
 | 
				
			||||||
            'client_secret' => '116d1d80289f378410dd70ab4e4b81dd8d2c52b6',
 | 
					            'client_secret' => '116d1d80289f378410dd70ab4e4b81dd8d2c52b6',
 | 
				
			||||||
            'webhook_secret' => '326a47b49054f03288f800d81247ec9414d0abf3',
 | 
					            'webhook_secret' => '326a47b49054f03288f800d81247ec9414d0abf3',
 | 
				
			||||||
            'private_key_id' => 1,
 | 
					            'private_key_id' => 2,
 | 
				
			||||||
            'team_id' => 0,
 | 
					            'team_id' => 0,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,19 +20,5 @@ class GitlabAppSeeder extends Seeder
 | 
				
			|||||||
            'is_public' => true,
 | 
					            'is_public' => true,
 | 
				
			||||||
            'team_id' => 0,
 | 
					            'team_id' => 0,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        GitlabApp::create([
 | 
					 | 
				
			||||||
            'id' => 2,
 | 
					 | 
				
			||||||
            'name' => 'coolify-laravel-development-private-gitlab',
 | 
					 | 
				
			||||||
            'api_url' => 'https://gitlab.com/api/v4',
 | 
					 | 
				
			||||||
            'html_url' => 'https://gitlab.com',
 | 
					 | 
				
			||||||
            'app_id' => 1234,
 | 
					 | 
				
			||||||
            'app_secret' => '1234',
 | 
					 | 
				
			||||||
            'oauth_id' => 1234,
 | 
					 | 
				
			||||||
            'deploy_key_id' => '1234',
 | 
					 | 
				
			||||||
            'public_key' => 'dfjasiourj',
 | 
					 | 
				
			||||||
            'webhook_token' => '4u3928u4y392',
 | 
					 | 
				
			||||||
            'private_key_id' => 2,
 | 
					 | 
				
			||||||
            'team_id' => 0,
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										35
									
								
								database/seeders/PopulateSshKeysDirectorySeeder.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								database/seeders/PopulateSshKeysDirectorySeeder.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace Database\Seeders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Models\PrivateKey;
 | 
				
			||||||
 | 
					use Illuminate\Database\Seeder;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Process;
 | 
				
			||||||
 | 
					use Illuminate\Support\Facades\Storage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PopulateSshKeysDirectorySeeder extends Seeder
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function run()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Storage::disk('ssh-keys')->deleteDirectory('');
 | 
				
			||||||
 | 
					        Storage::disk('ssh-keys')->makeDirectory('');
 | 
				
			||||||
 | 
					        Storage::disk('ssh-mux')->deleteDirectory('');
 | 
				
			||||||
 | 
					        Storage::disk('ssh-mux')->makeDirectory('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        PrivateKey::chunk(100, function ($keys) {
 | 
				
			||||||
 | 
					            foreach ($keys as $key) {
 | 
				
			||||||
 | 
					                echo 'Storing key: '.$key->name."\n";
 | 
				
			||||||
 | 
					                $key->storeInFileSystem();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isDev()) {
 | 
				
			||||||
 | 
					            $user = env('PUID').':'.env('PGID');
 | 
				
			||||||
 | 
					            Process::run("chown -R $user ".storage_path('app/ssh/keys'));
 | 
				
			||||||
 | 
					            Process::run("chown -R $user ".storage_path('app/ssh/mux'));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Process::run('chown -R 9999:9999 '.storage_path('app/ssh/keys'));
 | 
				
			||||||
 | 
					            Process::run('chown -R 9999:9999 '.storage_path('app/ssh/mux'));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -13,9 +13,8 @@ class PrivateKeySeeder extends Seeder
 | 
				
			|||||||
    public function run(): void
 | 
					    public function run(): void
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        PrivateKey::create([
 | 
					        PrivateKey::create([
 | 
				
			||||||
            'id' => 0,
 | 
					 | 
				
			||||||
            'team_id' => 0,
 | 
					            'team_id' => 0,
 | 
				
			||||||
            'name' => 'Testing-host',
 | 
					            'name' => 'Testing Host Key',
 | 
				
			||||||
            'description' => 'This is a test docker container',
 | 
					            'description' => 'This is a test docker container',
 | 
				
			||||||
            'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
 | 
					            'private_key' => '-----BEGIN OPENSSH PRIVATE KEY-----
 | 
				
			||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
					b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
 | 
				
			||||||
@@ -25,10 +24,9 @@ AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
 | 
				
			|||||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
					uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
				
			||||||
-----END OPENSSH PRIVATE KEY-----
 | 
					-----END OPENSSH PRIVATE KEY-----
 | 
				
			||||||
',
 | 
					',
 | 
				
			||||||
 | 
					 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        PrivateKey::create([
 | 
					        PrivateKey::create([
 | 
				
			||||||
            'id' => 1,
 | 
					 | 
				
			||||||
            'team_id' => 0,
 | 
					            'team_id' => 0,
 | 
				
			||||||
            'name' => 'development-github-app',
 | 
					            'name' => 'development-github-app',
 | 
				
			||||||
            'description' => 'This is the key for using the development GitHub app',
 | 
					            'description' => 'This is the key for using the development GitHub app',
 | 
				
			||||||
@@ -61,12 +59,5 @@ a1C8EDKapCw5hAhizEFOUQKOygL8Ipn+tmEUkORYdZ8Q8cWFCv9nIw==
 | 
				
			|||||||
-----END RSA PRIVATE KEY-----',
 | 
					-----END RSA PRIVATE KEY-----',
 | 
				
			||||||
            'is_git_related' => true,
 | 
					            'is_git_related' => true,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        PrivateKey::create([
 | 
					 | 
				
			||||||
            'id' => 2,
 | 
					 | 
				
			||||||
            'team_id' => 0,
 | 
					 | 
				
			||||||
            'name' => 'development-gitlab-app',
 | 
					 | 
				
			||||||
            'description' => 'This is the key for using the development Gitlab app',
 | 
					 | 
				
			||||||
            'private_key' => 'asdf',
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,64 +64,82 @@ class ProductionSeeder extends Seeder
 | 
				
			|||||||
                'team_id' => 0,
 | 
					                'team_id' => 0,
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        // Add Coolify host (localhost) as Server if it doesn't exist
 | 
				
			||||||
 | 
					        if (Server::find(0) == null) {
 | 
				
			||||||
 | 
					            $server_details = [
 | 
				
			||||||
 | 
					                'id' => 0,
 | 
				
			||||||
 | 
					                'name' => 'localhost',
 | 
				
			||||||
 | 
					                'description' => "This is the server where Coolify is running on. Don't delete this!",
 | 
				
			||||||
 | 
					                'user' => 'root',
 | 
				
			||||||
 | 
					                'ip' => 'host.docker.internal',
 | 
				
			||||||
 | 
					                'team_id' => 0,
 | 
				
			||||||
 | 
					                'private_key_id' => 0,
 | 
				
			||||||
 | 
					            ];
 | 
				
			||||||
 | 
					            $server_details['proxy'] = ServerMetadata::from([
 | 
				
			||||||
 | 
					                'type' => ProxyTypes::TRAEFIK->value,
 | 
				
			||||||
 | 
					                'status' => ProxyStatus::EXITED->value,
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					            $server = Server::create($server_details);
 | 
				
			||||||
 | 
					            $server->settings->is_reachable = true;
 | 
				
			||||||
 | 
					            $server->settings->is_usable = true;
 | 
				
			||||||
 | 
					            $server->settings->save();
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            $server = Server::find(0);
 | 
				
			||||||
 | 
					            $server->settings->is_reachable = true;
 | 
				
			||||||
 | 
					            $server->settings->is_usable = true;
 | 
				
			||||||
 | 
					            $server->settings->save();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (StandaloneDocker::find(0) == null) {
 | 
				
			||||||
 | 
					            StandaloneDocker::create([
 | 
				
			||||||
 | 
					                'id' => 0,
 | 
				
			||||||
 | 
					                'name' => 'localhost-coolify',
 | 
				
			||||||
 | 
					                'network' => 'coolify',
 | 
				
			||||||
 | 
					                'server_id' => 0,
 | 
				
			||||||
 | 
					            ]);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) {
 | 
					        if (! isCloud() && config('coolify.is_windows_docker_desktop') == false) {
 | 
				
			||||||
            echo "Checking localhost key.\n";
 | 
					            echo "Checking localhost key.\n";
 | 
				
			||||||
            // Save SSH Keys for the Coolify Host
 | 
					            $coolify_key_name = '@host.docker.internal';
 | 
				
			||||||
            $coolify_key_name = 'id.root@host.docker.internal';
 | 
					            $ssh_keys_directory = Storage::disk('ssh-keys')->files();
 | 
				
			||||||
            $coolify_key = Storage::disk('ssh-keys')->get("{$coolify_key_name}");
 | 
					            $coolify_key = collect($ssh_keys_directory)->firstWhere(fn ($item) => str($item)->contains($coolify_key_name));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if ($coolify_key) {
 | 
					            $found = PrivateKey::find(0);
 | 
				
			||||||
                PrivateKey::updateOrCreate(
 | 
					            if ($found) {
 | 
				
			||||||
                    [
 | 
					                echo 'Private Key found in database.\n';
 | 
				
			||||||
 | 
					                if ($coolify_key) {
 | 
				
			||||||
 | 
					                    echo "SSH key found for the Coolify host machine (localhost).\n";
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                if ($coolify_key) {
 | 
				
			||||||
 | 
					                    $coolify_key = Storage::disk('ssh-keys')->get($coolify_key);
 | 
				
			||||||
 | 
					                    $user = str($coolify_key)->before('@')->after('id.');
 | 
				
			||||||
 | 
					                    PrivateKey::create([
 | 
				
			||||||
                        'id' => 0,
 | 
					                        'id' => 0,
 | 
				
			||||||
                        'team_id' => 0,
 | 
					                        'team_id' => 0,
 | 
				
			||||||
                    ],
 | 
					 | 
				
			||||||
                    [
 | 
					 | 
				
			||||||
                        'name' => 'localhost\'s key',
 | 
					                        'name' => 'localhost\'s key',
 | 
				
			||||||
                        'description' => 'The private key for the Coolify host machine (localhost).',
 | 
					                        'description' => 'The private key for the Coolify host machine (localhost).',
 | 
				
			||||||
                        'private_key' => $coolify_key,
 | 
					                        'private_key' => $coolify_key,
 | 
				
			||||||
                    ]
 | 
					                    ]);
 | 
				
			||||||
                );
 | 
					                    $server->update(['user' => $user]);
 | 
				
			||||||
            } else {
 | 
					                    echo "SSH key found for the Coolify host machine (localhost).\n";
 | 
				
			||||||
                echo "No SSH key found for the Coolify host machine (localhost).\n";
 | 
					
 | 
				
			||||||
                echo "Please generate one and save it in /data/coolify/ssh/keys/{$coolify_key_name}\n";
 | 
					                } else {
 | 
				
			||||||
                echo "Then try to install again.\n";
 | 
					                    PrivateKey::create(
 | 
				
			||||||
                exit(1);
 | 
					                        [
 | 
				
			||||||
            }
 | 
					                            'id' => 0,
 | 
				
			||||||
            // Add Coolify host (localhost) as Server if it doesn't exist
 | 
					                            'team_id' => 0,
 | 
				
			||||||
            if (Server::find(0) == null) {
 | 
					                            'name' => 'localhost\'s key',
 | 
				
			||||||
                $server_details = [
 | 
					                            'description' => 'The private key for the Coolify host machine (localhost).',
 | 
				
			||||||
                    'id' => 0,
 | 
					                            'private_key' => 'Paste here you private key!!',
 | 
				
			||||||
                    'name' => 'localhost',
 | 
					                        ]
 | 
				
			||||||
                    'description' => "This is the server where Coolify is running on. Don't delete this!",
 | 
					                    );
 | 
				
			||||||
                    'user' => 'root',
 | 
					                    echo "No SSH key found for the Coolify host machine (localhost).\n";
 | 
				
			||||||
                    'ip' => 'host.docker.internal',
 | 
					                    echo "Please read the following documentation (point 3) to fix it: https://coolify.io/docs/knowledge-base/server/openssh/\n";
 | 
				
			||||||
                    'team_id' => 0,
 | 
					                    echo "Your localhost connection won't work until then.";
 | 
				
			||||||
                    'private_key_id' => 0,
 | 
					                }
 | 
				
			||||||
                ];
 | 
					 | 
				
			||||||
                $server_details['proxy'] = ServerMetadata::from([
 | 
					 | 
				
			||||||
                    'type' => ProxyTypes::TRAEFIK->value,
 | 
					 | 
				
			||||||
                    'status' => ProxyStatus::EXITED->value,
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
                $server = Server::create($server_details);
 | 
					 | 
				
			||||||
                $server->settings->is_reachable = true;
 | 
					 | 
				
			||||||
                $server->settings->is_usable = true;
 | 
					 | 
				
			||||||
                $server->settings->save();
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                $server = Server::find(0);
 | 
					 | 
				
			||||||
                $server->settings->is_reachable = true;
 | 
					 | 
				
			||||||
                $server->settings->is_usable = true;
 | 
					 | 
				
			||||||
                $server->settings->save();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            if (StandaloneDocker::find(0) == null) {
 | 
					 | 
				
			||||||
                StandaloneDocker::create([
 | 
					 | 
				
			||||||
                    'id' => 0,
 | 
					 | 
				
			||||||
                    'name' => 'localhost-coolify',
 | 
					 | 
				
			||||||
                    'network' => 'coolify',
 | 
					 | 
				
			||||||
                    'server_id' => 0,
 | 
					 | 
				
			||||||
                ]);
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (config('coolify.is_windows_docker_desktop')) {
 | 
					        if (config('coolify.is_windows_docker_desktop')) {
 | 
				
			||||||
            PrivateKey::updateOrCreate(
 | 
					            PrivateKey::updateOrCreate(
 | 
				
			||||||
@@ -179,8 +197,8 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        get_public_ips();
 | 
					        get_public_ips();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $oauth_settings_seeder = new OauthSettingSeeder;
 | 
					        $this->call(OauthSettingSeeder::class);
 | 
				
			||||||
        $oauth_settings_seeder->run();
 | 
					        $this->call(PopulateSshKeysDirectorySeeder::class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace Database\Seeders;
 | 
					namespace Database\Seeders;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use App\Enums\ProxyStatus;
 | 
				
			||||||
 | 
					use App\Enums\ProxyTypes;
 | 
				
			||||||
use App\Models\Server;
 | 
					use App\Models\Server;
 | 
				
			||||||
use Illuminate\Database\Seeder;
 | 
					use Illuminate\Database\Seeder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -15,7 +17,11 @@ class ServerSeeder extends Seeder
 | 
				
			|||||||
            'description' => 'This is a test docker container in development mode',
 | 
					            'description' => 'This is a test docker container in development mode',
 | 
				
			||||||
            'ip' => 'coolify-testing-host',
 | 
					            'ip' => 'coolify-testing-host',
 | 
				
			||||||
            'team_id' => 0,
 | 
					            'team_id' => 0,
 | 
				
			||||||
            'private_key_id' => 0,
 | 
					            'private_key_id' => 1,
 | 
				
			||||||
 | 
					            'proxy' => [
 | 
				
			||||||
 | 
					                'type' => ProxyTypes::TRAEFIK->value,
 | 
				
			||||||
 | 
					                'status' => ProxyStatus::EXITED->value,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,5 +26,11 @@
 | 
				
			|||||||
    "input.code": "One-time code",
 | 
					    "input.code": "One-time code",
 | 
				
			||||||
    "input.recovery_code": "Recovery code",
 | 
					    "input.recovery_code": "Recovery code",
 | 
				
			||||||
    "button.save": "Save",
 | 
					    "button.save": "Save",
 | 
				
			||||||
    "repository.url": "<span class='text-helper'>Examples</span><br>For Public repositories, use <span class='text-helper'>https://...</span>.<br>For Private repositories, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch will be selected.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch will be selected.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch will be selected."
 | 
					    "repository.url": "<span class='text-helper'>Examples</span><br>For Public repositories, use <span class='text-helper'>https://...</span>.<br>For Private repositories, use <span class='text-helper'>git@...</span>.<br><br>https://github.com/coollabsio/coolify-examples <span class='text-helper'>main</span> branch will be selected<br>https://github.com/coollabsio/coolify-examples/tree/nodejs-fastify <span class='text-helper'>nodejs-fastify</span> branch will be selected.<br>https://gitea.com/sedlav/expressjs.git <span class='text-helper'>main</span> branch will be selected.<br>https://gitlab.com/andrasbacsai/nodejs-example.git <span class='text-helper'>main</span> branch will be selected.",
 | 
				
			||||||
 | 
					    "service.stop": "This service will be stopped.",
 | 
				
			||||||
 | 
					    "resource.docker_cleanup": "Run Docker Cleanup (remove unused images and builder cache).",
 | 
				
			||||||
 | 
					    "resource.non_persistent": "All non-persistent data will be deleted.",
 | 
				
			||||||
 | 
					    "resource.delete_volumes": "All volumes associated with this resource will be permanently deleted.",
 | 
				
			||||||
 | 
					    "resource.delete_connected_networks": "All non-predefined networks associated with this resource will be permanently deleted.",
 | 
				
			||||||
 | 
					    "resource.delete_configurations": "All configuration files will be permanently deleted from the server."
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,11 +19,7 @@ DB_PORT=5432
 | 
				
			|||||||
# Set to true to enable Ray
 | 
					# Set to true to enable Ray
 | 
				
			||||||
RAY_ENABLED=false
 | 
					RAY_ENABLED=false
 | 
				
			||||||
# Set custom ray port
 | 
					# Set custom ray port
 | 
				
			||||||
RAY_PORT=
 | 
					# RAY_PORT=
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Clockwork Configuration
 | 
					 | 
				
			||||||
CLOCKWORK_ENABLED=false
 | 
					 | 
				
			||||||
CLOCKWORK_QUEUE_COLLECT=true
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Enable Laravel Telescope for debugging
 | 
					# Enable Laravel Telescope for debugging
 | 
				
			||||||
TELESCOPE_ENABLED=false
 | 
					TELESCOPE_ENABLED=false
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,8 @@ DATE=$(date +"%Y%m%d-%H%M%S")
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
VERSION="1.5"
 | 
					VERSION="1.5"
 | 
				
			||||||
DOCKER_VERSION="26.0"
 | 
					DOCKER_VERSION="26.0"
 | 
				
			||||||
 | 
					# TODO: Ask for a user
 | 
				
			||||||
 | 
					CURRENT_USER=$USER
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
 | 
					mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs}
 | 
				
			||||||
mkdir -p /data/coolify/ssh/{keys,mux}
 | 
					mkdir -p /data/coolify/ssh/{keys,mux}
 | 
				
			||||||
@@ -23,7 +25,7 @@ INSTALLATION_LOG_WITH_DATE="/data/coolify/source/installation-${DATE}.log"
 | 
				
			|||||||
exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
 | 
					exec > >(tee -a $INSTALLATION_LOG_WITH_DATE) 2>&1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
getAJoke() {
 | 
					getAJoke() {
 | 
				
			||||||
    JOKES=$(curl -s --max-time 2 https://v2.jokeapi.dev/joke/Programming?format=txt&type=single&amount=1 || true)
 | 
					    JOKES=$(curl -s --max-time 2 "https://v2.jokeapi.dev/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist,explicit&format=txt&type=single" || true)
 | 
				
			||||||
    if [ "$JOKES" != "" ]; then
 | 
					    if [ "$JOKES" != "" ]; then
 | 
				
			||||||
        echo -e " - Until then, here's a joke for you:\n"
 | 
					        echo -e " - Until then, here's a joke for you:\n"
 | 
				
			||||||
        echo -e "$JOKES\n"
 | 
					        echo -e "$JOKES\n"
 | 
				
			||||||
@@ -477,7 +479,17 @@ syncSshKeys() {
 | 
				
			|||||||
    fi
 | 
					    fi
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
syncSshKeys || true
 | 
					set +e
 | 
				
			||||||
 | 
					IS_COOLIFY_VOLUME_EXISTS=$(docker volume ls | grep coolify-db | wc -l)
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					if [ "$IS_COOLIFY_VOLUME_EXISTS" -eq 0 ]; then
 | 
				
			||||||
 | 
					    echo " - Generating SSH key."
 | 
				
			||||||
 | 
					    ssh-keygen -t ed25519 -a 100 -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal -q -N "" -C coolify
 | 
				
			||||||
 | 
					    chown 9999 /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal
 | 
				
			||||||
 | 
					    sed -i "/coolify/d" ~/.ssh/authorized_keys
 | 
				
			||||||
 | 
					    cat /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub >> ~/.ssh/authorized_keys
 | 
				
			||||||
 | 
					    rm -f /data/coolify/ssh/keys/id.$CURRENT_USER@host.docker.internal.pub
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
chown -R 9999:root /data/coolify
 | 
					chown -R 9999:root /data/coolify
 | 
				
			||||||
chmod -R 700 /data/coolify
 | 
					chmod -R 700 /data/coolify
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,15 @@
 | 
				
			|||||||
 | 
					@props([
 | 
				
			||||||
 | 
					    'id',
 | 
				
			||||||
 | 
					    'label' => null,
 | 
				
			||||||
 | 
					    'helper' => null,
 | 
				
			||||||
 | 
					    'disabled' => false,
 | 
				
			||||||
 | 
					    'instantSave' => false,
 | 
				
			||||||
 | 
					    'value' => null,
 | 
				
			||||||
 | 
					    'hideLabel' => false,
 | 
				
			||||||
 | 
					])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
 | 
					<div class="flex flex-row items-center gap-4 px-2 py-1 form-control min-w-fit dark:hover:bg-coolgray-100">
 | 
				
			||||||
 | 
					    @if(!$hideLabel)
 | 
				
			||||||
    <label class="flex gap-4 px-0 min-w-fit label">
 | 
					    <label class="flex gap-4 px-0 min-w-fit label">
 | 
				
			||||||
        <span class="flex gap-2">
 | 
					        <span class="flex gap-2">
 | 
				
			||||||
            @if ($label)
 | 
					            @if ($label)
 | 
				
			||||||
@@ -11,6 +22,7 @@
 | 
				
			|||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
 | 
					    @endif
 | 
				
			||||||
    <span class="flex-grow"></span>
 | 
					    <span class="flex-grow"></span>
 | 
				
			||||||
    <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
 | 
					    <input @disabled($disabled) type="checkbox" {{ $attributes->merge(['class' => $defaultClass]) }}
 | 
				
			||||||
        @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
 | 
					        @if ($instantSave) wire:loading.attr="disabled" wire:click='{{ $instantSave === 'instantSave' || $instantSave == '1' ? 'instantSave' : $instantSave }}'
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,105 @@
 | 
				
			|||||||
@props([
 | 
					@props([
 | 
				
			||||||
    'title' => 'Are you sure?',
 | 
					    'title' => 'Are you sure?',
 | 
				
			||||||
    'isErrorButton' => false,
 | 
					    'isErrorButton' => false,
 | 
				
			||||||
    'buttonTitle' => 'REWRITE THIS BUTTON TITLE PLEASSSSEEEE',
 | 
					    'buttonTitle' => 'Confirm Action',
 | 
				
			||||||
    'buttonFullWidth' => false,
 | 
					    'buttonFullWidth' => false,
 | 
				
			||||||
    'customButton' => null,
 | 
					    'customButton' => null,
 | 
				
			||||||
    'disabled' => false,
 | 
					    'disabled' => false,
 | 
				
			||||||
    'action' => 'delete',
 | 
					    'submitAction' => 'delete',
 | 
				
			||||||
    'content' => null,
 | 
					    'content' => null,
 | 
				
			||||||
 | 
					    'checkboxes' => [],
 | 
				
			||||||
 | 
					    'actions' => [],
 | 
				
			||||||
 | 
					    'confirmWithText' => true,
 | 
				
			||||||
 | 
					    'confirmationText' => 'Confirm Deletion',
 | 
				
			||||||
 | 
					    'confirmationLabel' => 'Please confirm the execution of the actions by entering the Name below',
 | 
				
			||||||
 | 
					    'shortConfirmationLabel' => 'Name',
 | 
				
			||||||
 | 
					    'confirmWithPassword' => true,
 | 
				
			||||||
 | 
					    'step1ButtonText' => 'Continue',
 | 
				
			||||||
 | 
					    'step2ButtonText' => 'Continue',
 | 
				
			||||||
 | 
					    'step3ButtonText' => 'Confirm',
 | 
				
			||||||
 | 
					    'dispatchEvent' => false,
 | 
				
			||||||
 | 
					    'dispatchEventType' => 'success',
 | 
				
			||||||
 | 
					    'dispatchEventMessage' => '',
 | 
				
			||||||
])
 | 
					])
 | 
				
			||||||
<div x-data="{ modalOpen: false }" @keydown.escape.window="modalOpen = false" :class="{ 'z-40': modalOpen }"
 | 
					
 | 
				
			||||||
 | 
					<div x-data="{
 | 
				
			||||||
 | 
					    modalOpen: false,
 | 
				
			||||||
 | 
					    step: {{ empty($checkboxes) ? 2 : 1 }},
 | 
				
			||||||
 | 
					    initialStep: {{ empty($checkboxes) ? 2 : 1 }},
 | 
				
			||||||
 | 
					    finalStep: {{ $confirmWithPassword ? 3 : 2 }},
 | 
				
			||||||
 | 
					    deleteText: '',
 | 
				
			||||||
 | 
					    password: '',
 | 
				
			||||||
 | 
					    actions: @js($actions),
 | 
				
			||||||
 | 
					    confirmationText: @js($confirmationText),
 | 
				
			||||||
 | 
					    userConfirmationText: '',
 | 
				
			||||||
 | 
					    confirmWithText: @js($confirmWithText),
 | 
				
			||||||
 | 
					    confirmWithPassword: @js($confirmWithPassword),
 | 
				
			||||||
 | 
					    copied: false,
 | 
				
			||||||
 | 
					    submitAction: @js($submitAction),
 | 
				
			||||||
 | 
					    passwordError: '',
 | 
				
			||||||
 | 
					    selectedActions: @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all()),
 | 
				
			||||||
 | 
					    dispatchEvent: @js($dispatchEvent),
 | 
				
			||||||
 | 
					    dispatchEventType: @js($dispatchEventType),
 | 
				
			||||||
 | 
					    dispatchEventMessage: @js($dispatchEventMessage),
 | 
				
			||||||
 | 
					    resetModal() {
 | 
				
			||||||
 | 
					        this.step = this.initialStep;
 | 
				
			||||||
 | 
					        this.deleteText = '';
 | 
				
			||||||
 | 
					        this.password = '';
 | 
				
			||||||
 | 
					        this.userConfirmationText = '';
 | 
				
			||||||
 | 
					        this.selectedActions = @js(collect($checkboxes)->pluck('id')->filter(fn($id) => $this->$id)->values()->all());
 | 
				
			||||||
 | 
					        $wire.$refresh();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    step1ButtonText: @js($step1ButtonText),
 | 
				
			||||||
 | 
					    step2ButtonText: @js($step2ButtonText),
 | 
				
			||||||
 | 
					    step3ButtonText: @js($step3ButtonText),
 | 
				
			||||||
 | 
					    validatePassword() {
 | 
				
			||||||
 | 
					        if (this.confirmWithPassword && !this.password) {
 | 
				
			||||||
 | 
					            return 'Password is required.';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return '';
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    submitForm() {
 | 
				
			||||||
 | 
					        if (this.confirmWithPassword) {
 | 
				
			||||||
 | 
					            this.passwordError = this.validatePassword();
 | 
				
			||||||
 | 
					            if (this.passwordError) {
 | 
				
			||||||
 | 
					                return Promise.resolve(this.passwordError);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const methodName = this.submitAction.split('(')[0];
 | 
				
			||||||
 | 
					        const paramsMatch = this.submitAction.match(/\((.*?)\)/);
 | 
				
			||||||
 | 
					        const params = paramsMatch ? paramsMatch[1].split(',').map(param => param.trim()) : [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.confirmWithPassword) {
 | 
				
			||||||
 | 
					            params.push(this.password);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        params.push(this.selectedActions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $wire[methodName](...params)
 | 
				
			||||||
 | 
					            .then(result => {
 | 
				
			||||||
 | 
					                if (result === true) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                } else if (typeof result === 'string') {
 | 
				
			||||||
 | 
					                    return result;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    copyConfirmationText() {
 | 
				
			||||||
 | 
					        navigator.clipboard.writeText(this.confirmationText);
 | 
				
			||||||
 | 
					        this.copied = true;
 | 
				
			||||||
 | 
					        setTimeout(() => {
 | 
				
			||||||
 | 
					            this.copied = false;
 | 
				
			||||||
 | 
					        }, 2000);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    toggleAction(id) {
 | 
				
			||||||
 | 
					        const index = this.selectedActions.indexOf(id);
 | 
				
			||||||
 | 
					        if (index > -1) {
 | 
				
			||||||
 | 
					            this.selectedActions.splice(index, 1);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            this.selectedActions.push(id);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}" @keydown.escape.window="modalOpen = false; resetModal()" :class="{ 'z-40': modalOpen }"
 | 
				
			||||||
    class="relative w-auto h-auto">
 | 
					    class="relative w-auto h-auto">
 | 
				
			||||||
    @if ($customButton)
 | 
					    @if ($customButton)
 | 
				
			||||||
        @if ($buttonFullWidth)
 | 
					        @if ($buttonFullWidth)
 | 
				
			||||||
@@ -60,11 +151,9 @@
 | 
				
			|||||||
        @endif
 | 
					        @endif
 | 
				
			||||||
    @endif
 | 
					    @endif
 | 
				
			||||||
    <template x-teleport="body">
 | 
					    <template x-teleport="body">
 | 
				
			||||||
        <div x-show="modalOpen"
 | 
					        <div x-show="modalOpen" @click.away="modalOpen = false; resetModal()"
 | 
				
			||||||
            class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>
 | 
					            class="fixed top-0 lg:pt-10 left-0 z-[99] flex items-start justify-center w-screen h-screen" x-cloak>
 | 
				
			||||||
            <div x-show="modalOpen" x-transition:enter="ease-out duration-100" x-transition:enter-start="opacity-0"
 | 
					            <div x-show="modalOpen" @click="modalOpen = false; resetModal()"
 | 
				
			||||||
                x-transition:enter-end="opacity-100" x-transition:leave="ease-in duration-100"
 | 
					 | 
				
			||||||
                x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0" @click="modalOpen=false"
 | 
					 | 
				
			||||||
                class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
 | 
					                class="absolute inset-0 w-full h-full bg-black bg-opacity-20 backdrop-blur-sm"></div>
 | 
				
			||||||
            <div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
 | 
					            <div x-show="modalOpen" x-trap.inert.noscroll="modalOpen" x-transition:enter="ease-out duration-100"
 | 
				
			||||||
                x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
 | 
					                x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
 | 
				
			||||||
@@ -72,56 +161,186 @@
 | 
				
			|||||||
                x-transition:leave="ease-in duration-100"
 | 
					                x-transition:leave="ease-in duration-100"
 | 
				
			||||||
                x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
 | 
					                x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
 | 
				
			||||||
                x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
 | 
					                x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
 | 
				
			||||||
                class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-fit bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
 | 
					                class="relative w-full py-6 border rounded min-w-full lg:min-w-[36rem] max-w-[48rem] bg-neutral-100 border-neutral-400 dark:bg-base px-7 dark:border-coolgray-300">
 | 
				
			||||||
                <div class="flex items-center justify-between pb-3">
 | 
					                <div class="flex items-center justify-between pb-3">
 | 
				
			||||||
                    <h3 class="text-2xl font-bold">{{ $title }}</h3>
 | 
					                    <h3 class="text-2xl font-bold pr-8">{{ $title }}</h3>
 | 
				
			||||||
                    {{-- <button @click="modalOpen=false"
 | 
					                    <button @click="modalOpen = false; resetModal()"
 | 
				
			||||||
                        class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-coolgray-300">
 | 
					                        class="absolute top-2 right-2 flex items-center justify-center w-8 h-8 rounded-full dark:text-white hover:bg-coolgray-300">
 | 
				
			||||||
                        <svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
					                        <svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
 | 
				
			||||||
                            stroke-width="1.5" stroke="currentColor">
 | 
					                            stroke-width="1.5" stroke="currentColor">
 | 
				
			||||||
                            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
 | 
					                            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
 | 
				
			||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                    </button> --}}
 | 
					                    </button>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="relative w-auto pb-8">
 | 
					                <div class="relative w-auto pb-8">
 | 
				
			||||||
                    {{ $slot }}
 | 
					                    @if (!empty($checkboxes))
 | 
				
			||||||
                </div>
 | 
					                        <!-- Step 1: Select actions -->
 | 
				
			||||||
                <div class="flex flex-row justify-end space-x-2">
 | 
					                        <div x-show="step === 1">
 | 
				
			||||||
                    <x-forms.button @click="modalOpen=false"
 | 
					                            <div class="flex justify-between items-center">
 | 
				
			||||||
                        class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">Cancel
 | 
					                                <h4>Actions</h4>
 | 
				
			||||||
                    </x-forms.button>
 | 
					                            </div>
 | 
				
			||||||
                    <div class="flex-1"></div>
 | 
					                            @foreach ($checkboxes as $index => $checkbox)
 | 
				
			||||||
                    @if ($attributes->whereStartsWith('wire:click')->first())
 | 
					                                <div class="flex items-center justify-between mb-2">
 | 
				
			||||||
                        @if ($isErrorButton)
 | 
					                                    <label for="{{ $checkbox['id'] }}"
 | 
				
			||||||
                            <x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
 | 
					                                        class="text-sm leading-5 text-gray-700 dark:text-gray-300 flex-grow pr-4">
 | 
				
			||||||
                                wire:click.prevent="{{ $attributes->get('wire:click') }}">Continue
 | 
					                                        {{ $checkbox['label'] }}
 | 
				
			||||||
                            </x-forms.button>
 | 
					                                    </label>
 | 
				
			||||||
                        @else
 | 
					                                    <x-forms.checkbox :id="$checkbox['id']" :wire:model="$checkbox['id']"
 | 
				
			||||||
                            <x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
 | 
					                                        x-on:change="toggleAction('{{ $checkbox['id'] }}')" :checked="$this->{$checkbox['id']}"
 | 
				
			||||||
                                wire:click.prevent="{{ $attributes->get('wire:click') }}">Continue
 | 
					                                        x-bind:checked="selectedActions.includes('{{ $checkbox['id'] }}')"
 | 
				
			||||||
                            </x-forms.button>
 | 
					                                        class="flex-shrink-0" :hideLabel="true" />
 | 
				
			||||||
                        @endif
 | 
					                                </div>
 | 
				
			||||||
                    @elseif ($attributes->whereStartsWith('@click')->first())
 | 
					                            @endforeach
 | 
				
			||||||
                        @if ($isErrorButton)
 | 
					                        </div>
 | 
				
			||||||
                            <x-forms.button class="w-24" isError type="button"
 | 
					 | 
				
			||||||
                                @click="modalOpen=false;{{ $attributes->get('@click') }}">Continue
 | 
					 | 
				
			||||||
                            </x-forms.button>
 | 
					 | 
				
			||||||
                        @else
 | 
					 | 
				
			||||||
                            <x-forms.button class="w-24" isHighlighted type="button"
 | 
					 | 
				
			||||||
                                @click="modalOpen=false;{{ $attributes->get('@click') }}">Continue
 | 
					 | 
				
			||||||
                            </x-forms.button>
 | 
					 | 
				
			||||||
                        @endif
 | 
					 | 
				
			||||||
                    @elseif ($action)
 | 
					 | 
				
			||||||
                        @if ($isErrorButton)
 | 
					 | 
				
			||||||
                            <x-forms.button @click="modalOpen=false" class="w-24" isError type="button"
 | 
					 | 
				
			||||||
                                wire:click.prevent="{{ $action }}">Continue
 | 
					 | 
				
			||||||
                            </x-forms.button>
 | 
					 | 
				
			||||||
                        @else
 | 
					 | 
				
			||||||
                            <x-forms.button @click="modalOpen=false" class="w-24" isHighlighted type="button"
 | 
					 | 
				
			||||||
                                wire:click.prevent="{{ $action }}">Continue
 | 
					 | 
				
			||||||
                            </x-forms.button>
 | 
					 | 
				
			||||||
                        @endif
 | 
					 | 
				
			||||||
                    @endif
 | 
					                    @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <!-- Step 2: Confirm deletion -->
 | 
				
			||||||
 | 
					                    <div x-show="step === 2">
 | 
				
			||||||
 | 
					                        <div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
 | 
				
			||||||
 | 
					                            <p class="font-bold">Warning</p>
 | 
				
			||||||
 | 
					                            <p>This operation is permanent and cannot be undone. Please think again before proceeding!
 | 
				
			||||||
 | 
					                            </p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="mb-4">The following actions will be performed:</div>
 | 
				
			||||||
 | 
					                        <ul class="mb-4 space-y-2">
 | 
				
			||||||
 | 
					                            @foreach ($actions as $action)
 | 
				
			||||||
 | 
					                                <li class="flex items-center text-red-500">
 | 
				
			||||||
 | 
					                                    <svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
 | 
				
			||||||
 | 
					                                        viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
				
			||||||
 | 
					                                            d="M6 18L18 6M6 6l12 12"></path>
 | 
				
			||||||
 | 
					                                    </svg>
 | 
				
			||||||
 | 
					                                    <span>{{ $action }}</span>
 | 
				
			||||||
 | 
					                                </li>
 | 
				
			||||||
 | 
					                            @endforeach
 | 
				
			||||||
 | 
					                            @foreach ($checkboxes as $checkbox)
 | 
				
			||||||
 | 
					                                <template x-if="selectedActions.includes('{{ $checkbox['id'] }}')">
 | 
				
			||||||
 | 
					                                    <li class="flex items-center text-red-500">
 | 
				
			||||||
 | 
					                                        <svg class="w-5 h-5 mr-2 flex-shrink-0" fill="none" stroke="currentColor"
 | 
				
			||||||
 | 
					                                            viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					                                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
 | 
				
			||||||
 | 
					                                                d="M6 18L18 6M6 6l12 12"></path>
 | 
				
			||||||
 | 
					                                        </svg>
 | 
				
			||||||
 | 
					                                        <span>{{ $checkbox['label'] }}</span>
 | 
				
			||||||
 | 
					                                    </li>
 | 
				
			||||||
 | 
					                                </template>
 | 
				
			||||||
 | 
					                            @endforeach
 | 
				
			||||||
 | 
					                        </ul>
 | 
				
			||||||
 | 
					                        @if ($confirmWithText)
 | 
				
			||||||
 | 
					                            <div class="mb-4">
 | 
				
			||||||
 | 
					                                <h4 class="text-lg font-semibold mb-2">Confirm Actions</h4>
 | 
				
			||||||
 | 
					                                <p class="text-sm mb-2">{{ $confirmationLabel }}</p>
 | 
				
			||||||
 | 
					                                <div class="relative mb-2">
 | 
				
			||||||
 | 
					                                    <input type="text" x-model="confirmationText"
 | 
				
			||||||
 | 
					                                        class="w-full p-2 pr-10 rounded text-black input cursor-text" readonly>
 | 
				
			||||||
 | 
					                                    <button @click="copyConfirmationText()"
 | 
				
			||||||
 | 
					                                        class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
 | 
				
			||||||
 | 
					                                        title="Copy confirmation text" x-ref="copyButton">
 | 
				
			||||||
 | 
					                                        <template x-if="!copied">
 | 
				
			||||||
 | 
					                                            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
 | 
				
			||||||
 | 
					                                                fill="currentColor">
 | 
				
			||||||
 | 
					                                                <path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
 | 
				
			||||||
 | 
					                                                <path
 | 
				
			||||||
 | 
					                                                    d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
 | 
				
			||||||
 | 
					                                            </svg>
 | 
				
			||||||
 | 
					                                        </template>
 | 
				
			||||||
 | 
					                                        <template x-if="copied">
 | 
				
			||||||
 | 
					                                            <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500"
 | 
				
			||||||
 | 
					                                                viewBox="0 0 20 20" fill="currentColor">
 | 
				
			||||||
 | 
					                                                <path fill-rule="evenodd"
 | 
				
			||||||
 | 
					                                                    d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
 | 
				
			||||||
 | 
					                                                    clip-rule="evenodd" />
 | 
				
			||||||
 | 
					                                            </svg>
 | 
				
			||||||
 | 
					                                        </template>
 | 
				
			||||||
 | 
					                                    </button>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                <label for="userConfirmationText"
 | 
				
			||||||
 | 
					                                    class="block text-sm font-medium text-gray-700 dark:text-gray-300 mt-4">
 | 
				
			||||||
 | 
					                                    {{ $shortConfirmationLabel }}
 | 
				
			||||||
 | 
					                                </label>
 | 
				
			||||||
 | 
					                                <input type="text" x-model="userConfirmationText"
 | 
				
			||||||
 | 
					                                    class="w-full p-2 rounded text-black input mt-1">
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        @endif
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <!-- Step 3: Password confirmation -->
 | 
				
			||||||
 | 
					                    <div x-show="step === 3 && confirmWithPassword">
 | 
				
			||||||
 | 
					                        <div class="bg-error border-l-4 border-red-500 text-white p-4 mb-4" role="alert">
 | 
				
			||||||
 | 
					                            <p class="font-bold">Final Confirmation</p>
 | 
				
			||||||
 | 
					                            <p>Please enter your password to confirm this destructive action.</p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <div class="flex flex-col gap-2 mb-4">
 | 
				
			||||||
 | 
					                            <label for="password-confirm"
 | 
				
			||||||
 | 
					                                class="block text-sm font-medium text-gray-700 dark:text-gray-300">
 | 
				
			||||||
 | 
					                                Your Password
 | 
				
			||||||
 | 
					                            </label>
 | 
				
			||||||
 | 
					                            <input type="password" id="password-confirm" x-model="password" class="input w-full"
 | 
				
			||||||
 | 
					                                placeholder="Enter your password">
 | 
				
			||||||
 | 
					                            <p x-show="passwordError" x-text="passwordError" class="text-red-500 text-sm mt-1"></p>
 | 
				
			||||||
 | 
					                            @error('password')
 | 
				
			||||||
 | 
					                                <p class="text-red-500 text-sm mt-1">{{ $message }}</p>
 | 
				
			||||||
 | 
					                            @enderror
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                <!-- Navigation buttons -->
 | 
				
			||||||
 | 
					                <div class="flex flex-wrap gap-2 justify-between mt-4">
 | 
				
			||||||
 | 
					                    <template x-if="step > initialStep">
 | 
				
			||||||
 | 
					                        <x-forms.button @click="step--" class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
 | 
				
			||||||
 | 
					                            Back
 | 
				
			||||||
 | 
					                        </x-forms.button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					                    <template x-if="step === initialStep">
 | 
				
			||||||
 | 
					                        <x-forms.button @click="modalOpen = false; resetModal()"
 | 
				
			||||||
 | 
					                            class="w-24 dark:bg-coolgray-200 dark:hover:bg-coolgray-300">
 | 
				
			||||||
 | 
					                            Cancel
 | 
				
			||||||
 | 
					                        </x-forms.button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <template x-if="step === 1">
 | 
				
			||||||
 | 
					                        <x-forms.button @click="step++" class="w-auto" isError>
 | 
				
			||||||
 | 
					                            <span x-text="step1ButtonText"></span>
 | 
				
			||||||
 | 
					                        </x-forms.button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <template x-if="step === 2">
 | 
				
			||||||
 | 
					                        <x-forms.button x-bind:disabled="confirmWithText && userConfirmationText !== confirmationText"
 | 
				
			||||||
 | 
					                            class="w-auto" isError
 | 
				
			||||||
 | 
					                            @click="
 | 
				
			||||||
 | 
					                            if (dispatchEvent) {
 | 
				
			||||||
 | 
					                                $wire.dispatch(dispatchEventType, dispatchEventMessage);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            if (confirmWithPassword) {
 | 
				
			||||||
 | 
					                                step++;
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                modalOpen = false;
 | 
				
			||||||
 | 
					                                resetModal();
 | 
				
			||||||
 | 
					                                submitForm();
 | 
				
			||||||
 | 
					                            }">
 | 
				
			||||||
 | 
					                            <span x-text="step2ButtonText"></span>
 | 
				
			||||||
 | 
					                        </x-forms.button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    <template x-if="step === 3 && confirmWithPassword">
 | 
				
			||||||
 | 
					                        <x-forms.button x-bind:disabled="!password" class="w-auto" isError
 | 
				
			||||||
 | 
					                            @click="
 | 
				
			||||||
 | 
					                            if (dispatchEvent) {
 | 
				
			||||||
 | 
					                                $wire.dispatch(dispatchEventType, dispatchEventMessage);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            submitForm().then((result) => {
 | 
				
			||||||
 | 
					                                if (result === true) {
 | 
				
			||||||
 | 
					                                    modalOpen = false;
 | 
				
			||||||
 | 
					                                    resetModal();
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    passwordError = result;
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            });
 | 
				
			||||||
 | 
					                        ">
 | 
				
			||||||
 | 
					                            <span x-text="step3ButtonText"></span>
 | 
				
			||||||
 | 
					                        </x-forms.button>
 | 
				
			||||||
 | 
					                    </template>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,9 @@
 | 
				
			|||||||
    <livewire:server.proxy.modal :server="$server" />
 | 
					    <livewire:server.proxy.modal :server="$server" />
 | 
				
			||||||
    <div class="flex items-center gap-2">
 | 
					    <div class="flex items-center gap-2">
 | 
				
			||||||
        <h1>Server</h1>
 | 
					        <h1>Server</h1>
 | 
				
			||||||
        <livewire:server.proxy.status :server="$server" />
 | 
					        @if ($server->proxySet())
 | 
				
			||||||
 | 
					            <livewire:server.proxy.status :server="$server" />
 | 
				
			||||||
 | 
					        @endif
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="subtitle">{{ data_get($server, 'name') }}.</div>
 | 
					    <div class="subtitle">{{ data_get($server, 'name') }}.</div>
 | 
				
			||||||
    <div class="navbar-main">
 | 
					    <div class="navbar-main">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,18 @@
 | 
				
			|||||||
<div class="flex h-full pr-4">
 | 
					@if ($server->proxySet())
 | 
				
			||||||
    <div class="flex flex-col w-48 gap-4 min-w-fit">
 | 
					    <div class="flex h-full pr-4">
 | 
				
			||||||
        <a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
 | 
					        <div class="flex flex-col w-48 gap-4 min-w-fit">
 | 
				
			||||||
            href="{{ route('server.proxy', $parameters) }}">
 | 
					            <a class="{{ request()->routeIs('server.proxy') ? 'dark:text-white' : '' }}"
 | 
				
			||||||
            <button>Configuration</button>
 | 
					                href="{{ route('server.proxy', $parameters) }}">
 | 
				
			||||||
        </a>
 | 
					                <button>Configuration</button>
 | 
				
			||||||
        @if ($server->proxyType() !== 'NONE')
 | 
					            </a>
 | 
				
			||||||
            {{-- @if ($server->proxyType() === 'TRAEFIK') --}}
 | 
					 | 
				
			||||||
            <a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'dark:text-white' : '' }}"
 | 
					            <a class="{{ request()->routeIs('server.proxy.dynamic-confs') ? 'dark:text-white' : '' }}"
 | 
				
			||||||
                href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
 | 
					                href="{{ route('server.proxy.dynamic-confs', $parameters) }}">
 | 
				
			||||||
                <button>Dynamic Configurations</button>
 | 
					                <button>Dynamic Configurations</button>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
            {{-- @endif --}}
 | 
					 | 
				
			||||||
            <a class="{{ request()->routeIs('server.proxy.logs') ? 'dark:text-white' : '' }}"
 | 
					            <a class="{{ request()->routeIs('server.proxy.logs') ? 'dark:text-white' : '' }}"
 | 
				
			||||||
                href="{{ route('server.proxy.logs', $parameters) }}">
 | 
					                href="{{ route('server.proxy.logs', $parameters) }}">
 | 
				
			||||||
                <button>Logs</button>
 | 
					                <button>Logs</button>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
        @endif
 | 
					        </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					@endif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
<x-emails.layout>
 | 
					<x-emails.layout>
 | 
				
			||||||
Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
 | 
					    Your server ({{ $name }}) disabled because it is not paid! All automations and integrations are stopped.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Please update your subscription to enable the server again [here](https://app.coolify.io/subsciprtions).
 | 
					    Please update your subscription to enable the server again [here](https://app.coolify.io/subscriptions).
 | 
				
			||||||
</x-emails.layout>
 | 
					</x-emails.layout>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -122,9 +122,19 @@
 | 
				
			|||||||
            @if (count($deployments_per_server) > 0)
 | 
					            @if (count($deployments_per_server) > 0)
 | 
				
			||||||
                <x-loading />
 | 
					                <x-loading />
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
            <x-modal-confirmation isErrorButton action="cleanup_queue" buttonTitle="Cleanup Queues">
 | 
					            <x-modal-confirmation 
 | 
				
			||||||
                This will clean up the deployment queue. <br>Please think again.
 | 
					                title="Confirm Cleanup Queues?"
 | 
				
			||||||
            </x-modal-confirmation>
 | 
					                buttonTitle="Cleanup Queues"
 | 
				
			||||||
 | 
					                isErrorButton
 | 
				
			||||||
 | 
					                submitAction="cleanup_queue"
 | 
				
			||||||
 | 
					                :actions="['All running Deployment Queues will be cleaned up.']"
 | 
				
			||||||
 | 
					                :confirmWithText="false"
 | 
				
			||||||
 | 
					                :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                step2ButtonText="Permanently Cleanup Deployment Queues"
 | 
				
			||||||
 | 
					                :dispatchEvent="true"
 | 
				
			||||||
 | 
					                dispatchEventType="success"
 | 
				
			||||||
 | 
					                dispatchEventMessage="Deployment Queues cleanup started."
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div wire:poll.3000ms="get_deployments" class="grid grid-cols-1">
 | 
					        <div wire:poll.3000ms="get_deployments" class="grid grid-cols-1">
 | 
				
			||||||
            @forelse ($deployments_per_server as $server_name => $deployments)
 | 
					            @forelse ($deployments_per_server as $server_name => $deployments)
 | 
				
			||||||
@@ -170,4 +180,6 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
    {{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
 | 
					    {{-- <x-forms.button wire:click='getIptables'>Get IPTABLES</x-forms.button> --}}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,9 +6,10 @@
 | 
				
			|||||||
                Save
 | 
					                Save
 | 
				
			||||||
            </x-forms.button>
 | 
					            </x-forms.button>
 | 
				
			||||||
            @if ($destination->network !== 'coolify')
 | 
					            @if ($destination->network !== 'coolify')
 | 
				
			||||||
                <x-modal-confirmation isErrorButton buttonTitle="Delete Destination">
 | 
					                <x-modal-confirmation title="Confirm Destination Deletion?" buttonTitle="Delete Destination" isErrorButton
 | 
				
			||||||
                    This destination will be deleted. It is not reversible. <br>Please think again.
 | 
					                    submitAction="delete" :actions="['This will delete the selected destination/network.']" confirmationText="{{ $destination->name }}"
 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                    confirmationLabel="Please confirm the execution of the actions by entering the Destination Name below"
 | 
				
			||||||
 | 
					                    shortConfirmationLabel="Destination Name" :confirmWithPassword="false" step2ButtonText="Permanently Delete" />
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,13 @@
 | 
				
			|||||||
<div>
 | 
					<div>
 | 
				
			||||||
    <x-modal-confirmation buttonFullWidth isErrorButton buttonTitle="Delete Team">
 | 
					    <x-modal-confirmation
 | 
				
			||||||
        This team be deleted. It is not reversible. <br>Please think again.
 | 
					        title="Confirm Team Deletion?"
 | 
				
			||||||
    </x-modal-confirmation>
 | 
					        buttonTitle="Delete Team"
 | 
				
			||||||
 | 
					        isErrorButton
 | 
				
			||||||
 | 
					        submitAction="delete"
 | 
				
			||||||
 | 
					        :actions="['The current Team will be permanently deleted.']"
 | 
				
			||||||
 | 
					        confirmationText="{{ $team }}"
 | 
				
			||||||
 | 
					        confirmationLabel="Please confirm the execution of the actions by entering the Team Name below"
 | 
				
			||||||
 | 
					        shortConfirmationLabel="Team Name"
 | 
				
			||||||
 | 
					        step3ButtonText="Permanently Delete"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,11 +68,20 @@
 | 
				
			|||||||
                        <option value="www">Redirect to www.</option>
 | 
					                        <option value="www">Redirect to www.</option>
 | 
				
			||||||
                        <option value="non-www">Redirect to non-www.</option>
 | 
					                        <option value="non-www">Redirect to non-www.</option>
 | 
				
			||||||
                    </x-forms.select>
 | 
					                    </x-forms.select>
 | 
				
			||||||
                    <x-modal-confirmation action="set_redirect">
 | 
					                    <x-modal-confirmation 
 | 
				
			||||||
 | 
					                    title="Confirm Redirection Setting?"
 | 
				
			||||||
 | 
					                    buttonTitle="Set Direction"
 | 
				
			||||||
 | 
					                    submitAction="set_redirect"
 | 
				
			||||||
 | 
					                    :actions="['All traffic will be redirected to the selected direction.']"
 | 
				
			||||||
 | 
					                    confirmationText="{{ $application->fqdn . '/' }}"
 | 
				
			||||||
 | 
					                    confirmationLabel="Please confirm the execution of the action by entering the Application URL below"
 | 
				
			||||||
 | 
					                    shortConfirmationLabel="Application URL"
 | 
				
			||||||
 | 
					                    :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                    step2ButtonText="Set Direction"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
                        <x-slot:customButton>
 | 
					                        <x-slot:customButton>
 | 
				
			||||||
                            <div class="w-[7.2rem]">Set Direction</div>
 | 
					                            <div class="w-[7.2rem]">Set Direction</div>
 | 
				
			||||||
                        </x-slot:customButton>
 | 
					                        </x-slot:customButton>
 | 
				
			||||||
                        This will reset the container labels. Are you sure?
 | 
					 | 
				
			||||||
                    </x-modal-confirmation>
 | 
					                    </x-modal-confirmation>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
@@ -302,12 +311,18 @@
 | 
				
			|||||||
                        helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically. <br><br>Be careful, it could break the proxy configuration after you restart the container."
 | 
					                        helper="If you know what are you doing, you can enable this to edit the labels directly. Coolify won't update labels automatically. <br><br>Be careful, it could break the proxy configuration after you restart the container."
 | 
				
			||||||
                        id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox>
 | 
					                        id="application.settings.is_container_label_readonly_enabled" instantSave></x-forms.checkbox>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <x-modal-confirmation buttonFullWidth action="resetDefaultLabels"
 | 
					                <x-modal-confirmation 
 | 
				
			||||||
                    buttonTitle="Reset to Coolify Generated Labels">
 | 
					                title="Confirm Labels Reset to Coolify Defaults?"
 | 
				
			||||||
                    Are you sure you want to reset the labels to Coolify generated labels? <br>It could break the proxy
 | 
					                buttonTitle="Reset Labels to Coolify Defaults"
 | 
				
			||||||
                    configuration after you restart the container.
 | 
					                buttonFullWidth
 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                submitAction="resetDefaultLabels"
 | 
				
			||||||
 | 
					                :actions="['All your custom proxy labels will be lost.', 'Proxy labels (traefik, caddy, etc) will be reset to the coolify defaults.']"
 | 
				
			||||||
 | 
					                confirmationText="{{ $application->fqdn . '/' }}"
 | 
				
			||||||
 | 
					                confirmationLabel="Please confirm the execution of the actions by entering the Application URL below"
 | 
				
			||||||
 | 
					                shortConfirmationLabel="Application URL"
 | 
				
			||||||
 | 
					                :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                step2ButtonText="Permanently Reset Labels"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <h3 class="pt-8">Pre/Post Deployment Commands</h3>
 | 
					            <h3 class="pt-8">Pre/Post Deployment Commands</h3>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,7 +72,13 @@
 | 
				
			|||||||
                                </x-forms.button>
 | 
					                                </x-forms.button>
 | 
				
			||||||
                            @endif
 | 
					                            @endif
 | 
				
			||||||
                        @endif
 | 
					                        @endif
 | 
				
			||||||
                        <x-modal-confirmation @click="$wire.dispatch('stopEvent')">
 | 
					                        <x-modal-confirmation title="Confirm Application Stopping?" buttonTitle="Stop"
 | 
				
			||||||
 | 
					                            submitAction="stop" :checkboxes="$checkboxes" :actions="[
 | 
				
			||||||
 | 
					                                'This application will be stopped.',
 | 
				
			||||||
 | 
					                                'All non-persistent data of this application will be deleted.',
 | 
				
			||||||
 | 
					                            ]" :confirmWithText="false" :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                            step1ButtonText="Continue" step2ButtonText="Confirm" :dispatchEvent="true"
 | 
				
			||||||
 | 
					                            dispatchEventType="stopEvent">
 | 
				
			||||||
                            <x-slot:button-title>
 | 
					                            <x-slot:button-title>
 | 
				
			||||||
                                <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
					                                <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
				
			||||||
                                    stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
					                                    stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
				
			||||||
@@ -87,7 +93,6 @@
 | 
				
			|||||||
                                </svg>
 | 
					                                </svg>
 | 
				
			||||||
                                Stop
 | 
					                                Stop
 | 
				
			||||||
                            </x-slot:button-title>
 | 
					                            </x-slot:button-title>
 | 
				
			||||||
                            This application will be stopped. <br>Please think again.
 | 
					 | 
				
			||||||
                        </x-modal-confirmation>
 | 
					                        </x-modal-confirmation>
 | 
				
			||||||
                    @else
 | 
					                    @else
 | 
				
			||||||
                        <x-forms.button wire:click='deploy'>
 | 
					                        <x-forms.button wire:click='deploy'>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -152,8 +152,15 @@
 | 
				
			|||||||
                            @endif
 | 
					                            @endif
 | 
				
			||||||
                        </x-forms.button>
 | 
					                        </x-forms.button>
 | 
				
			||||||
                        @if (data_get($preview, 'status') !== 'exited')
 | 
					                        @if (data_get($preview, 'status') !== 'exited')
 | 
				
			||||||
                            <x-modal-confirmation isErrorButton
 | 
					                            <x-modal-confirmation 
 | 
				
			||||||
                                action="stop({{ data_get($preview, 'pull_request_id') }})">
 | 
					                            title="Confirm Preview Deployment Stopping?"
 | 
				
			||||||
 | 
					                            buttonTitle="Stop"
 | 
				
			||||||
 | 
					                            submitAction="stop({{ data_get($preview, 'pull_request_id') }})"
 | 
				
			||||||
 | 
					                            :actions="['This preview deployment will be stopped.', 'If the preview deployment is currently in use data could be lost.', 'All non-persistent data of this preview deployment (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the preview deployment again).']"
 | 
				
			||||||
 | 
					                            :confirmWithText="false"
 | 
				
			||||||
 | 
					                            :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                            step2ButtonText="Stop Preview Deployment"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
                                <x-slot:customButton>
 | 
					                                <x-slot:customButton>
 | 
				
			||||||
                                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
 | 
					                                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error"
 | 
				
			||||||
                                        viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
 | 
					                                        viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none"
 | 
				
			||||||
@@ -168,14 +175,19 @@
 | 
				
			|||||||
                                    </svg>
 | 
					                                    </svg>
 | 
				
			||||||
                                    Stop
 | 
					                                    Stop
 | 
				
			||||||
                                </x-slot:customButton>
 | 
					                                </x-slot:customButton>
 | 
				
			||||||
                                This will stop the preview deployment. <br>Please think again.
 | 
					 | 
				
			||||||
                            </x-modal-confirmation>
 | 
					                            </x-modal-confirmation>
 | 
				
			||||||
                        @endif
 | 
					                        @endif
 | 
				
			||||||
                        <x-modal-confirmation isErrorButton
 | 
					                        <x-modal-confirmation 
 | 
				
			||||||
                            action="delete({{ data_get($preview, 'pull_request_id') }})" buttonTitle="Delete">
 | 
					                        title="Confirm Preview Deployment Deletion?"
 | 
				
			||||||
                            This will delete the preview deployment. <br>Please think again.
 | 
					                        buttonTitle="Delete"
 | 
				
			||||||
                        </x-modal-confirmation>
 | 
					                        isErrorButton
 | 
				
			||||||
 | 
					                        submitAction="delete({{ data_get($preview, 'pull_request_id') }})"
 | 
				
			||||||
 | 
					                        :actions="['All containers of this preview deployment will be stopped and permanently deleted.']"
 | 
				
			||||||
 | 
					                        confirmationText="{{ data_get($preview, 'fqdn'). '/' }}"
 | 
				
			||||||
 | 
					                        confirmationLabel="Please confirm the execution of the actions by entering the Preview Deployment name below"
 | 
				
			||||||
 | 
					                        shortConfirmationLabel="Preview Deployment Name"
 | 
				
			||||||
 | 
					                        :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            @endforeach
 | 
					            @endforeach
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,12 +8,17 @@
 | 
				
			|||||||
            <livewire:project.database.backup-now :backup="$backup" />
 | 
					            <livewire:project.database.backup-now :backup="$backup" />
 | 
				
			||||||
        @endif
 | 
					        @endif
 | 
				
			||||||
        @if ($backup->database_id !== 0)
 | 
					        @if ($backup->database_id !== 0)
 | 
				
			||||||
            <x-modal-confirmation isErrorButton>
 | 
					            <x-modal-confirmation 
 | 
				
			||||||
                <x-slot:button-title>
 | 
					            title="Confirm Backup Schedule Deletion?"
 | 
				
			||||||
                    Delete
 | 
					            buttonTitle="Delete Backups and Schedule"
 | 
				
			||||||
                </x-slot:button-title>
 | 
					            isErrorButton
 | 
				
			||||||
                This will stop the scheduled backup for this database.<br>Please think again.
 | 
					            submitAction="delete"
 | 
				
			||||||
            </x-modal-confirmation>
 | 
					            :checkboxes="$checkboxes" 
 | 
				
			||||||
 | 
					            :actions="['The selected backup schedule will be deleted.', 'Scheduled backups for this database will be stopped (if this is the only backup schedule for this database).']"
 | 
				
			||||||
 | 
					            confirmationText="{{ $backup->database->name }}"
 | 
				
			||||||
 | 
					            confirmationLabel="Please confirm the execution of the actions by entering the Database Name of the scheduled backups below"
 | 
				
			||||||
 | 
					            shortConfirmationLabel="Database Name"
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
        @endif
 | 
					        @endif
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="w-48 pb-2">
 | 
					    <div class="w-48 pb-2">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,12 +45,20 @@
 | 
				
			|||||||
                            <x-forms.button class="dark:hover:bg-coolgray-400"
 | 
					                            <x-forms.button class="dark:hover:bg-coolgray-400"
 | 
				
			||||||
                                x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
 | 
					                                x-on:click="download_file('{{ data_get($execution, 'id') }}')">Download</x-forms.button>
 | 
				
			||||||
                        @endif
 | 
					                        @endif
 | 
				
			||||||
                        <x-modal-confirmation isErrorButton action="deleteBackup({{ data_get($execution, 'id') }})">
 | 
					                        <x-modal-confirmation
 | 
				
			||||||
                            <x-slot:button-title>
 | 
					                        title="Confirm Backup Deletion?"
 | 
				
			||||||
                                Delete
 | 
					                        buttonTitle="Delete"
 | 
				
			||||||
                            </x-slot:button-title>
 | 
					                        isErrorButton
 | 
				
			||||||
                            This will delete this backup. It is not reversible.<br>Please think again.
 | 
					                        submitAction="deleteBackup({{ data_get($execution, 'id') }})"
 | 
				
			||||||
                        </x-modal-confirmation>
 | 
					                        {{-- :checkboxes="$checkboxes"  --}}
 | 
				
			||||||
 | 
					                        :actions="[
 | 
				
			||||||
 | 
					                            'This backup will be permanently deleted from local storage.'
 | 
				
			||||||
 | 
					                        ]"
 | 
				
			||||||
 | 
					                        confirmationText="{{ data_get($execution, 'filename') }}"
 | 
				
			||||||
 | 
					                        confirmationLabel="Please confirm the execution of the actions by entering the Backup Filename below"
 | 
				
			||||||
 | 
					                        shortConfirmationLabel="Backup Filename"
 | 
				
			||||||
 | 
					                        step3ButtonText="Permanently Delete"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            @empty
 | 
					            @empty
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,12 @@
 | 
				
			|||||||
        </nav>
 | 
					        </nav>
 | 
				
			||||||
        <div class="flex flex-wrap gap-2 items-center">
 | 
					        <div class="flex flex-wrap gap-2 items-center">
 | 
				
			||||||
            @if (!str($database->status)->startsWith('exited'))
 | 
					            @if (!str($database->status)->startsWith('exited'))
 | 
				
			||||||
                <x-modal-confirmation @click="$wire.dispatch('restartEvent')">
 | 
					                <x-modal-confirmation title="Confirm Database Restart?" buttonTitle="Restart" submitAction="restart"
 | 
				
			||||||
 | 
					                    :actions="[
 | 
				
			||||||
 | 
					                        'This database will be unavailable during the restart.',
 | 
				
			||||||
 | 
					                        'If the database is currently in use data could be lost.',
 | 
				
			||||||
 | 
					                    ]" :confirmWithText="false" :confirmWithPassword="false" step2ButtonText="Restart Database"
 | 
				
			||||||
 | 
					                    :dispatchEvent="true" dispatchEventType="restartEvent">
 | 
				
			||||||
                    <x-slot:button-title>
 | 
					                    <x-slot:button-title>
 | 
				
			||||||
                        <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | 
					                        <svg class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
                            <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
					                            <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
 | 
				
			||||||
@@ -46,9 +51,14 @@
 | 
				
			|||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                        Restart
 | 
					                        Restart
 | 
				
			||||||
                    </x-slot:button-title>
 | 
					                    </x-slot:button-title>
 | 
				
			||||||
                    This database will be restarted. <br>Please think again.
 | 
					 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                </x-modal-confirmation>
 | 
				
			||||||
                <x-modal-confirmation @click="$wire.dispatch('stopEvent')">
 | 
					                <x-modal-confirmation title="Confirm Database Stopping?" buttonTitle="Stop" submitAction="stop"
 | 
				
			||||||
 | 
					                    :checkboxes="$checkboxes" :actions="[
 | 
				
			||||||
 | 
					                        'This database will be stopped.',
 | 
				
			||||||
 | 
					                        'If the database is currently in use data could be lost.',
 | 
				
			||||||
 | 
					                        'All non-persistent data of this database (containers, networks, unused images) will be deleted (don\'t worry, no data is lost and you can start the database again).',
 | 
				
			||||||
 | 
					                    ]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
 | 
				
			||||||
 | 
					                    step2ButtonText="Stop Database" :dispatchEvent="true" dispatchEventType="stopEvent">
 | 
				
			||||||
                    <x-slot:button-title>
 | 
					                    <x-slot:button-title>
 | 
				
			||||||
                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
					                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
				
			||||||
                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
					                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
				
			||||||
@@ -61,7 +71,6 @@
 | 
				
			|||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                        Stop
 | 
					                        Stop
 | 
				
			||||||
                    </x-slot:button-title>
 | 
					                    </x-slot:button-title>
 | 
				
			||||||
                    This database will be stopped. <br>Please think again.
 | 
					 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                </x-modal-confirmation>
 | 
				
			||||||
            @else
 | 
					            @else
 | 
				
			||||||
                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
					                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,9 +2,13 @@
 | 
				
			|||||||
    <div class="flex items-end gap-2">
 | 
					    <div class="flex items-end gap-2">
 | 
				
			||||||
        <x-forms.input id="filename" label="Filename" />
 | 
					        <x-forms.input id="filename" label="Filename" />
 | 
				
			||||||
        <x-forms.button type="submit">Save</x-forms.button>
 | 
					        <x-forms.button type="submit">Save</x-forms.button>
 | 
				
			||||||
        <x-modal-confirmation isErrorButton buttonTitle="Delete">
 | 
					        <x-modal-confirmation title="Confirm init-script deletion?" buttonTitle="Delete" isErrorButton
 | 
				
			||||||
            This script will be deleted. It is not reversible. <br>Please think again.
 | 
					            submitAction="delete" :actions="[
 | 
				
			||||||
        </x-modal-confirmation>
 | 
					                'The init-script of this database will be permanently deleted.',
 | 
				
			||||||
 | 
					                'If you are actively using this init-script, it could cause errors on redeployment.',
 | 
				
			||||||
 | 
					            ]" confirmationText="{{ $filename }}"
 | 
				
			||||||
 | 
					            confirmationLabel="Please confirm the execution of the actions by entering the init-script name below"
 | 
				
			||||||
 | 
					            shortConfirmationLabel="Init-script Name" :confirmWithPassword=false step2ButtonText="Permanently Delete" />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <x-forms.textarea id="content" label="Content" />
 | 
					    <x-forms.textarea id="content" label="Content" />
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,5 @@
 | 
				
			|||||||
<x-modal-confirmation isErrorButton buttonTitle="Delete Environment" disabled="{{ $disabled }}">
 | 
					<x-modal-confirmation title="Confirm Environment Deletion?" buttonTitle="Delete Environment" isErrorButton
 | 
				
			||||||
    This environment will be deleted. It is not reversible. <br>Please think again.
 | 
					    submitAction="delete" :actions="['This will delete the selected environment.']"
 | 
				
			||||||
</x-modal-confirmation>
 | 
					    confirmationLabel="Please confirm the execution of the actions by entering the Environment Name below"
 | 
				
			||||||
 | 
					    shortConfirmationLabel="Environment Name" confirmationText="{{ $environmentName }}" :confirmWithPassword="false"
 | 
				
			||||||
 | 
					    step2ButtonText="Permanently Delete" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,7 @@
 | 
				
			|||||||
<x-modal-confirmation isErrorButton buttonTitle="Delete Project" disabled="{{ $disabled }}">
 | 
					<x-modal-confirmation title="Confirm Project Deletion?" buttonTitle="Delete Project" isErrorButton submitAction="delete"
 | 
				
			||||||
    This project will be deleted. It is not reversible. <br>Please think again.
 | 
					    :actions="[
 | 
				
			||||||
</x-modal-confirmation>
 | 
					        'This will delete the selected project',
 | 
				
			||||||
 | 
					        'All Environments inside the project will be deleted as well.',
 | 
				
			||||||
 | 
					    ]" confirmationLabel="Please confirm the execution of the actions by entering the Project Name below"
 | 
				
			||||||
 | 
					    shortConfirmationLabel="Project Name" confirmationText="{{ $projectName }}" :confirmWithPassword="false"
 | 
				
			||||||
 | 
					    step2ButtonText="Permanently Delete" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -491,7 +491,9 @@
 | 
				
			|||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="pb-4 text-xs">Trademarks Policy: The respective trademarks mentioned here are owned by the
 | 
					            <div class="pb-4 text-xs">Trademarks Policy: The respective trademarks mentioned here are owned by the
 | 
				
			||||||
                respective
 | 
					                respective
 | 
				
			||||||
                companies, and use of them does not imply any affiliation or endorsement.</div>
 | 
					                companies, and use of them does not imply any affiliation or endorsement.<br>Find more services <a
 | 
				
			||||||
 | 
					                    class="dark:text-white underline" target="_blank"
 | 
				
			||||||
 | 
					                    href="https://coolify.io/docs/services">here</a>.</div>
 | 
				
			||||||
            <input class="input" autofocus wire:model.live.debounce.200ms="search" autofocus
 | 
					            <input class="input" autofocus wire:model.live.debounce.200ms="search" autofocus
 | 
				
			||||||
                placeholder="Search...">
 | 
					                placeholder="Search...">
 | 
				
			||||||
            @if ($loadingServices)
 | 
					            @if ($loadingServices)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -107,11 +107,15 @@
 | 
				
			|||||||
                                        Settings
 | 
					                                        Settings
 | 
				
			||||||
                                    </a>
 | 
					                                    </a>
 | 
				
			||||||
                                    @if (str($application->status)->contains('running'))
 | 
					                                    @if (str($application->status)->contains('running'))
 | 
				
			||||||
                                        <x-modal-confirmation action="restartApplication({{ $application->id }})"
 | 
					                                        <x-modal-confirmation 
 | 
				
			||||||
                                            isErrorButton buttonTitle="Restart">
 | 
					                                        title="Confirm Service Application Restart?"
 | 
				
			||||||
                                            This application will be unavailable during the restart. <br>Please think
 | 
					                                        buttonTitle="Restart"
 | 
				
			||||||
                                            again.
 | 
					                                        submitAction="restartApplication({{ $application->id }})"
 | 
				
			||||||
                                        </x-modal-confirmation>
 | 
					                                        :actions="['The selected service application will be unavailable during the restart.', 'If the service application is currently in use data could be lost.']"
 | 
				
			||||||
 | 
					                                        :confirmWithText="false"
 | 
				
			||||||
 | 
					                                        :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                                        step2ButtonText="Restart Service Container"
 | 
				
			||||||
 | 
					                                        />
 | 
				
			||||||
                                    @endif
 | 
					                                    @endif
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
@@ -151,11 +155,15 @@
 | 
				
			|||||||
                                        Settings
 | 
					                                        Settings
 | 
				
			||||||
                                    </a>
 | 
					                                    </a>
 | 
				
			||||||
                                    @if (str($database->status)->contains('running'))
 | 
					                                    @if (str($database->status)->contains('running'))
 | 
				
			||||||
                                        <x-modal-confirmation action="restartDatabase({{ $database->id }})"
 | 
					                                        <x-modal-confirmation
 | 
				
			||||||
                                            isErrorButton buttonTitle="Restart">
 | 
					                                        title="Confirm Service Database Restart?"
 | 
				
			||||||
                                            This database will be unavailable during the restart. <br>Please think
 | 
					                                        buttonTitle="Restart"
 | 
				
			||||||
                                            again.
 | 
					                                        submitAction="restartDatabase({{ $database->id }})"
 | 
				
			||||||
                                        </x-modal-confirmation>
 | 
					                                        :actions="['This service database will be unavailable during the restart.', 'If the service database is currently in use data could be lost.']"
 | 
				
			||||||
 | 
					                                        :confirmWithText="false"
 | 
				
			||||||
 | 
					                                        :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                                        step2ButtonText="Restart Database"
 | 
				
			||||||
 | 
					                                        />
 | 
				
			||||||
                                    @endif
 | 
					                                    @endif
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,18 +15,32 @@
 | 
				
			|||||||
    <form wire:submit='submit' class="flex flex-col gap-2">
 | 
					    <form wire:submit='submit' class="flex flex-col gap-2">
 | 
				
			||||||
        <div class="flex gap-2">
 | 
					        <div class="flex gap-2">
 | 
				
			||||||
            @if ($fileStorage->is_directory)
 | 
					            @if ($fileStorage->is_directory)
 | 
				
			||||||
                <x-modal-confirmation action="convertToFile" buttonTitle="Convert to file">
 | 
					                <x-modal-confirmation title="Confirm Directory Conversion to File?" buttonTitle="Convert to file"
 | 
				
			||||||
                    <div>This will delete all files in this directory. It is not reversible. <strong
 | 
					                    submitAction="convertToFile" :actions="[
 | 
				
			||||||
                            class="text-error">Please think
 | 
					                        'All files in this directory will be permanently deleted and an empty file will be created in its place.',
 | 
				
			||||||
                            again.</strong><br><br></div>
 | 
					                    ]" confirmationText="{{ $fs_path }}"
 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                    confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
 | 
				
			||||||
 | 
					                    shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to file" />
 | 
				
			||||||
            @else
 | 
					            @else
 | 
				
			||||||
                <x-modal-confirmation action="convertToDirectory" buttonTitle="Convert to directory">
 | 
					                <x-modal-confirmation title="Confirm File Conversion to Directory?" buttonTitle="Convert to directory"
 | 
				
			||||||
                    <div>This will delete the file and make a directory instead. It is not reversible.
 | 
					                    submitAction="convertToDirectory" :actions="[
 | 
				
			||||||
                        <strong class="text-error">Please think
 | 
					                        'The selected file will be permanently deleted and an empty directory will be created in its place.',
 | 
				
			||||||
                            again.</strong><br><br>
 | 
					                    ]" confirmationText="{{ $fs_path }}"
 | 
				
			||||||
                    </div>
 | 
					                    confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                    shortConfirmationLabel="Filepath" :confirmWithPassword="false" step2ButtonText="Convert to directory" />
 | 
				
			||||||
 | 
					            @endif
 | 
				
			||||||
 | 
					            @if ($fileStorage->is_directory)
 | 
				
			||||||
 | 
					                <x-modal-confirmation title="Confirm Directory Deletion?" buttonTitle="Delete Directory" isErrorButton
 | 
				
			||||||
 | 
					                    submitAction="delete" :checkboxes="$directoryDeletionCheckboxes" :actions="[
 | 
				
			||||||
 | 
					                        'The selected directory and all its contents will be permanently deleted from the container.',
 | 
				
			||||||
 | 
					                    ]" confirmationText="{{ $fs_path }}"
 | 
				
			||||||
 | 
					                    confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
 | 
				
			||||||
 | 
					                    shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
 | 
				
			||||||
 | 
					            @else
 | 
				
			||||||
 | 
					                <x-modal-confirmation title="Confirm File Deletion?" buttonTitle="Delete File" isErrorButton
 | 
				
			||||||
 | 
					                    submitAction="delete" :checkboxes="$fileDeletionCheckboxes" :actions="['The selected file will be permanently deleted from the container.']" confirmationText="{{ $fs_path }}"
 | 
				
			||||||
 | 
					                    confirmationLabel="Please confirm the execution of the actions by entering the Filepath below"
 | 
				
			||||||
 | 
					                    shortConfirmationLabel="Filepath" step3ButtonText="Permanently Delete" />
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            @if (!$fileStorage->is_based_on_git)
 | 
					            @if (!$fileStorage->is_based_on_git)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,7 +32,9 @@
 | 
				
			|||||||
                    </svg>
 | 
					                    </svg>
 | 
				
			||||||
                    Pull Latest Images & Restart
 | 
					                    Pull Latest Images & Restart
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
                <x-modal-confirmation @click="$wire.dispatch('stopEvent')">
 | 
					                <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
 | 
				
			||||||
 | 
					                    :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false" step1ButtonText="Continue"
 | 
				
			||||||
 | 
					                    step2ButtonText="Stop Service" :dispatchEvent="true" dispatchEventType="stopEvent">
 | 
				
			||||||
                    <x-slot:button-title>
 | 
					                    <x-slot:button-title>
 | 
				
			||||||
                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
					                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
				
			||||||
                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
					                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
				
			||||||
@@ -45,7 +47,6 @@
 | 
				
			|||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                        Stop
 | 
					                        Stop
 | 
				
			||||||
                    </x-slot:button-title>
 | 
					                    </x-slot:button-title>
 | 
				
			||||||
                    This service will be stopped. <br>Please think again.
 | 
					 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                </x-modal-confirmation>
 | 
				
			||||||
            @elseif (str($service->status())->contains('degraded'))
 | 
					            @elseif (str($service->status())->contains('degraded'))
 | 
				
			||||||
                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
					                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
				
			||||||
@@ -58,7 +59,10 @@
 | 
				
			|||||||
                    </svg>
 | 
					                    </svg>
 | 
				
			||||||
                    Restart Degraded Services
 | 
					                    Restart Degraded Services
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
                <x-modal-confirmation @click="$wire.dispatch('stopEvent')">
 | 
					                <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
 | 
				
			||||||
 | 
					                    :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                    step1ButtonText="Continue" step2ButtonText="Stop Service" :dispatchEvent="true"
 | 
				
			||||||
 | 
					                    dispatchEventType="stopEvent">
 | 
				
			||||||
                    <x-slot:button-title>
 | 
					                    <x-slot:button-title>
 | 
				
			||||||
                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
					                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
				
			||||||
                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
					                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
				
			||||||
@@ -71,7 +75,6 @@
 | 
				
			|||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                        Stop
 | 
					                        Stop
 | 
				
			||||||
                    </x-slot:button-title>
 | 
					                    </x-slot:button-title>
 | 
				
			||||||
                    This service will be stopped. <br>Please think again.
 | 
					 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                </x-modal-confirmation>
 | 
				
			||||||
            @elseif (str($service->status())->contains('exited'))
 | 
					            @elseif (str($service->status())->contains('exited'))
 | 
				
			||||||
                <button wire:click='stop(true)' class="gap-2 button">
 | 
					                <button wire:click='stop(true)' class="gap-2 button">
 | 
				
			||||||
@@ -92,7 +95,10 @@
 | 
				
			|||||||
                    Deploy
 | 
					                    Deploy
 | 
				
			||||||
                </button>
 | 
					                </button>
 | 
				
			||||||
            @else
 | 
					            @else
 | 
				
			||||||
                <x-modal-confirmation @click="$wire.dispatch('stopEvent')">
 | 
					                <x-modal-confirmation title="Confirm Service Stopping?" buttonTitle="Stop" submitAction="stop"
 | 
				
			||||||
 | 
					                    :checkboxes="$checkboxes" :actions="[__('service.stop'), __('resource.non_persistent')]" :confirmWithText="false" :confirmWithPassword="false"
 | 
				
			||||||
 | 
					                    step1ButtonText="Continue" step2ButtonText="Stop Service" :dispatchEvent="true"
 | 
				
			||||||
 | 
					                    dispatchEventType="stopEvent">
 | 
				
			||||||
                    <x-slot:button-title>
 | 
					                    <x-slot:button-title>
 | 
				
			||||||
                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
					                        <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 text-error" viewBox="0 0 24 24"
 | 
				
			||||||
                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
					                            stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round"
 | 
				
			||||||
@@ -105,7 +111,6 @@
 | 
				
			|||||||
                        </svg>
 | 
					                        </svg>
 | 
				
			||||||
                        Stop
 | 
					                        Stop
 | 
				
			||||||
                    </x-slot:button-title>
 | 
					                    </x-slot:button-title>
 | 
				
			||||||
                    This service will be stopped. <br>Please think again.
 | 
					 | 
				
			||||||
                </x-modal-confirmation>
 | 
					                </x-modal-confirmation>
 | 
				
			||||||
                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
					                <button @click="$wire.dispatch('startEvent')" class="gap-2 button">
 | 
				
			||||||
                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
 | 
					                    <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:text-warning" viewBox="0 0 24 24"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,12 +7,11 @@
 | 
				
			|||||||
                <h2>{{ Str::headline($application->name) }}</h2>
 | 
					                <h2>{{ Str::headline($application->name) }}</h2>
 | 
				
			||||||
            @endif
 | 
					            @endif
 | 
				
			||||||
            <x-forms.button type="submit">Save</x-forms.button>
 | 
					            <x-forms.button type="submit">Save</x-forms.button>
 | 
				
			||||||
            <x-modal-confirmation isErrorButton>
 | 
					            <x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
 | 
				
			||||||
                <x-slot:button-title>
 | 
					                submitAction="delete" {{-- :checkboxes="$checkboxes"  --}} :actions="['The selected service application container will be stopped and permanently deleted.']"
 | 
				
			||||||
                    Delete
 | 
					                confirmationText="{{ Str::headline($application->name) }}"
 | 
				
			||||||
                </x-slot:button-title>
 | 
					                confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
 | 
				
			||||||
                This will delete this service application. It is not reversible.<br>Please think again.
 | 
					                shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
 | 
				
			||||||
            </x-modal-confirmation>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="flex flex-col gap-2">
 | 
					        <div class="flex flex-col gap-2">
 | 
				
			||||||
            <div class="flex gap-2">
 | 
					            <div class="flex gap-2">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,15 +2,10 @@
 | 
				
			|||||||
    <h2>Danger Zone</h2>
 | 
					    <h2>Danger Zone</h2>
 | 
				
			||||||
    <div class="">Woah. I hope you know what are you doing.</div>
 | 
					    <div class="">Woah. I hope you know what are you doing.</div>
 | 
				
			||||||
    <h4 class="pt-4">Delete Resource</h4>
 | 
					    <h4 class="pt-4">Delete Resource</h4>
 | 
				
			||||||
    <div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming
 | 
					    <div class="pb-4">This will stop your containers, delete all related data, etc. Beware! There is no coming back!
 | 
				
			||||||
        back!
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <x-modal-confirmation isErrorButton buttonTitle="Delete" confirm={{ $confirm }}>
 | 
					    <x-modal-confirmation title="Confirm Resource Deletion?" buttonTitle="Delete" isErrorButton submitAction="delete"
 | 
				
			||||||
        <div class="px-2">This resource will be deleted. It is not reversible. <strong class="text-error">Please think
 | 
					        buttonTitle="Delete" :checkboxes="$checkboxes" :actions="['All containers of this resource will be stopped and permanently deleted.']" confirmationText="{{ $resourceName }}"
 | 
				
			||||||
                again.</strong><br><br></div>
 | 
					        confirmationLabel="Please confirm the execution of the actions by entering the NAME of the resource below"
 | 
				
			||||||
        <h4>Actions</h4>
 | 
					        shortConfirmationLabel="Resource Name" step3ButtonText="Delete Permanently" />
 | 
				
			||||||
        <x-forms.checkbox id="delete_configurations"
 | 
					 | 
				
			||||||
            label="Permanently delete configuration files from the server?"></x-forms.checkbox>
 | 
					 | 
				
			||||||
        <x-forms.checkbox id="delete_volumes" label="Permanently delete associated volumes?"></x-forms.checkbox>
 | 
					 | 
				
			||||||
    </x-modal-confirmation>
 | 
					 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,11 +63,16 @@
 | 
				
			|||||||
                                wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button>
 | 
					                                wire:click="stop('{{ data_get($destination, 'server.id') }}')">Stop</x-forms.button>
 | 
				
			||||||
                        @endif
 | 
					                        @endif
 | 
				
			||||||
                        <x-modal-confirmation
 | 
					                        <x-modal-confirmation
 | 
				
			||||||
                            action="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})"
 | 
					                            title="Confirm server removal?"
 | 
				
			||||||
                            isErrorButton buttonTitle="Remove Server">
 | 
					                            isErrorButton
 | 
				
			||||||
                            This will stop the running application in this server and remove it as a deployment
 | 
					                            buttonTitle="Remove Server"
 | 
				
			||||||
                            destination.<br><br>Please think again.
 | 
					                            submitAction="removeServer({{ data_get($destination, 'id') }},{{ data_get($destination, 'server.id') }})"
 | 
				
			||||||
                        </x-modal-confirmation>
 | 
					                            :actions="['This will stop the all running applications on this server and remove it as a deployment destination.']"
 | 
				
			||||||
 | 
					                            confirmationText="{{ data_get($destination, 'server.name') }}"
 | 
				
			||||||
 | 
					                            confirmationLabel="Please confirm the execution of the actions by entering the Server Name below"
 | 
				
			||||||
 | 
					                            shortConfirmationLabel="Server Name"
 | 
				
			||||||
 | 
					                            step3ButtonText="Permanently Remove Server"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
            @endforeach
 | 
					            @endforeach
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user