v4.0.0-beta.416 (#5729)

* feat(README): add InterviewPal sponsorship link and corresponding SVG icon

* chore(versions): update coolify version to 4.0.0-beta.413 and nightly version to 4.0.0-beta.414 in configuration files

* fix(terminal): enhance WebSocket client verification with authorized IPs in terminal server

* chore(versions): update realtime version to 1.0.8 in versions.json

* chore(versions): update realtime version to 1.0.8 in versions.json

* chore(docker): update soketi image version to 1.0.8 in production configuration files

* chore(versions): update coolify version to 4.0.0-beta.414 and nightly version to 4.0.0-beta.415 in configuration files

* fix(ApplicationDeploymentJob): ensure source is an object before checking GitHub app properties

* fix(ui): Disable livewire navigate feature (causing spam of setInterval())

* fix(ui): Remove required attribute from image input in service application view

* fix(ui): Change application image validation to be nullable in service application view

* fix(Server): Correct proxy path formatting for Traefik proxy type

* chore(versions): update coolify version to 4.0.0-beta.416 and nightly version to 4.0.0-beta.417 in configuration files; fix links in deployment view

* feat(Service): Add functionality to convert between applications and databases in docker-compose based applications
fix(ui): Fix service layout refresh on compose change

* fix(service): graceful shutdown of old container (#5731)

* refactor(Database): streamline container shutdown process and reduce timeout duration

* fix(ServerCheck): enhance proxy container check to ensure it is running before proceeding

* chore(seeder): update git branch from 'main' to 'v4.x' for multiple examples in ApplicationSeeder

* fix(applications): include pull_request_id in deployment queue check to prevent duplicate deployments

* refactor(core): streamline container stopping process and reduce timeout duration; update related methods for consistency

* fix(database): update label for image input field to improve clarity

* feat(migration): add 'is_migrated' and 'custom_type' columns to service_applications and service_databases tables

* feat(backup): implement custom database type selection and enhance scheduled backups management

* fix(ServerCheck): set default proxy status to 'exited' to handle missing container state

* fix(database): reduce container stop timeout from 300 to 30 seconds for improved responsiveness

* refactor(database): update DB facade usage for consistency across service files

* Update app/Livewire/Project/Service/Database.php

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* refactor(database): enhance application conversion logic and add existence checks for databases and applications

* refactor(actions): standardize method naming for network and configuration deletion across application and service classes

* refactor(logdrain): consolidate log drain stopping logic to reduce redundancy

* refactor(StandaloneMariadb): add type hint for destination method to improve code clarity

* refactor(DeleteResourceJob): streamline resource deletion logic and improve conditional checks for database types

* refactor(jobs): update middleware to prevent job release after expiration for CleanupInstanceStuffsJob, RestartProxyJob, and ServerCheckJob

* fix(ui): system theming for charts (#5740)

* chore(deps-dev): bump vite from 6.2.6 to 6.3.4 (#5743)

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.6 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(dev): mount points?!

* fix(dev): proxy mount point

* fix(ui): allow adding scheduled backups for non-migrated databases

* fix(DatabaseBackupJob): escape PostgreSQL password in backup command (#5759)

* fix(ui): correct closing div tag in service index view

* Revert "fix(dev): mount points?!"

This reverts commit 365bf3cbf0.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Jérémy <jeremy.derdaele@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: busybox <29630035+busybox11@users.noreply.github.com>
This commit is contained in:
Andras Bacsai
2025-05-05 09:04:09 +02:00
committed by GitHub
parent 9921c02367
commit ba8689fb82
62 changed files with 495 additions and 437 deletions

View File

@@ -30,7 +30,7 @@ class StopApplication
$application->stopContainers($containersToStop, $server); $application->stopContainers($containersToStop, $server);
if ($application->build_pack === 'dockercompose') { if ($application->build_pack === 'dockercompose') {
$application->delete_connected_networks($application->uuid); $application->deleteConnectedNetworks();
} }
if ($dockerCleanup) { if ($dockerCleanup) {

View File

@@ -85,7 +85,6 @@ class RunRemoteProcess
]); ]);
$processResult = $process->wait(); $processResult = $process->wait();
// $processResult = Process::timeout($timeout)->run($this->getCommand(), $this->handleOutput(...));
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) { if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
$status = ProcessStatus::ERROR; $status = ProcessStatus::ERROR;
} else { } else {

View File

@@ -11,7 +11,6 @@ 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
@@ -25,7 +24,7 @@ class StopDatabase
return 'Server is not functional'; return 'Server is not functional';
} }
$this->stopContainer($database, $database->uuid, 300); $this->stopContainer($database, $database->uuid, 30);
if ($isDeleteOperation) { if ($isDeleteOperation) {
if ($dockerCleanup) { if ($dockerCleanup) {
CleanupDocker::dispatch($server, true); CleanupDocker::dispatch($server, true);
@@ -39,37 +38,12 @@ class StopDatabase
return 'Database stopped successfully'; return 'Database stopped successfully';
} }
private function stopContainer($database, string $containerName, int $timeout = 300): void private function stopContainer($database, string $containerName, int $timeout = 30): void
{ {
$server = $database->destination->server; $server = $database->destination->server;
instant_remote_process(command: [
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); "docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
$startTime = time(); ], server: $server, throwError: false);
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);
} }
} }

View File

@@ -3,54 +3,27 @@
namespace App\Actions\Proxy; namespace App\Actions\Proxy;
use App\Models\Server; use App\Models\Server;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class StopProxy class StopProxy
{ {
use AsAction; use AsAction;
public function handle(Server $server, bool $forceStop = true) public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
{ {
try { try {
$containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy'; $containerName = $server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$timeout = 30;
$process = $this->stopContainer($containerName, $timeout); instant_remote_process(command: [
"docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
$startTime = Carbon::now()->getTimestamp();
while ($process->running()) {
if (Carbon::now()->getTimestamp() - $startTime >= $timeout) {
$this->forceStopContainer($containerName, $server);
break;
}
usleep(100000);
}
$this->removeContainer($containerName, $server);
} catch (\Throwable $e) {
return handleError($e);
} finally {
$server->proxy->force_stop = $forceStop; $server->proxy->force_stop = $forceStop;
$server->proxy->status = 'exited'; $server->proxy->status = 'exited';
$server->save(); $server->save();
} catch (\Throwable $e) {
return handleError($e);
} }
} }
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName, Server $server)
{
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
}
private function removeContainer(string $containerName, Server $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
} }

View File

@@ -99,7 +99,8 @@ class ServerCheck
return data_get($value, 'Name') === '/coolify-proxy'; return data_get($value, 'Name') === '/coolify-proxy';
} }
})->first(); })->first();
if (! $foundProxyContainer) { $proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited');
if (! $foundProxyContainer || $proxyStatus !== 'running') {
try { try {
$shouldStart = CheckProxy::run($this->server); $shouldStart = CheckProxy::run($this->server);
if ($shouldStart) { if ($shouldStart) {

View File

@@ -15,19 +15,18 @@ class StartLogDrain
{ {
if ($server->settings->is_logdrain_newrelic_enabled) { if ($server->settings->is_logdrain_newrelic_enabled) {
$type = 'newrelic'; $type = 'newrelic';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_highlight_enabled) { } elseif ($server->settings->is_logdrain_highlight_enabled) {
$type = 'highlight'; $type = 'highlight';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_axiom_enabled) { } elseif ($server->settings->is_logdrain_axiom_enabled) {
$type = 'axiom'; $type = 'axiom';
StopLogDrain::run($server);
} elseif ($server->settings->is_logdrain_custom_enabled) { } elseif ($server->settings->is_logdrain_custom_enabled) {
$type = 'custom'; $type = 'custom';
StopLogDrain::run($server);
} else { } else {
$type = 'none'; $type = 'none';
} }
if ($type !== 'none') {
StopLogDrain::run($server);
}
try { try {
if ($type === 'none') { if ($type === 'none') {
return 'No log drain is enabled.'; return 'No log drain is enabled.';
@@ -186,7 +185,6 @@ Files:
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null", "echo '{$compose}' | base64 -d | tee $compose_path > /dev/null",
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null", "echo '{$readme}' | base64 -d | tee $readme_path > /dev/null",
"test -f $config_path/.env && rm $config_path/.env", "test -f $config_path/.env && rm $config_path/.env",
]; ];
if ($type === 'newrelic') { if ($type === 'newrelic') {
$add_envs_command = [ $add_envs_command = [

View File

@@ -48,7 +48,7 @@ class DeleteService
} }
if ($deleteConnectedNetworks) { if ($deleteConnectedNetworks) {
$service->delete_connected_networks($service->uuid); $service->deleteConnectedNetworks();
} }
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false); instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
@@ -56,7 +56,7 @@ class DeleteService
throw new \Exception($e->getMessage()); throw new \Exception($e->getMessage());
} finally { } finally {
if ($deleteConfigurations) { if ($deleteConfigurations) {
$service->delete_configurations(); $service->deleteConfigurations();
} }
foreach ($service->applications()->get() as $application) { foreach ($service->applications()->get() as $application) {
$application->forceDelete(); $application->forceDelete();

View File

@@ -24,7 +24,7 @@ class StopService
$service->stopContainers($containersToStop, $server); $service->stopContainers($containersToStop, $server);
if ($isDeleteOperation) { if ($isDeleteOperation) {
$service->delete_connected_networks($service->uuid); $service->deleteConnectedNetworks();
if ($dockerCleanup) { if ($dockerCleanup) {
CleanupDocker::dispatch($server, true); CleanupDocker::dispatch($server, true);
} }

View File

@@ -12,21 +12,22 @@ class ApplicationStatusChanged implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -12,21 +12,22 @@ class BackupCreated implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -12,21 +12,22 @@ class CloudflareTunnelConfigured implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -7,27 +7,27 @@ use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Auth;
class DatabaseProxyStopped implements ShouldBroadcast class DatabaseProxyStopped implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = Auth::user()?->currentTeam()?->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -13,28 +13,24 @@ class DatabaseStatusChanged implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $userId = null; public int|string|null $userId = null;
public function __construct($userId = null) public function __construct($userId = null)
{ {
if (is_null($userId)) { if (is_null($userId)) {
$userId = Auth::id() ?? null; $userId = Auth::id() ?? null;
} }
if (is_null($userId)) {
return false;
}
$this->userId = $userId; $this->userId = $userId;
} }
public function broadcastOn(): ?array public function broadcastOn(): ?array
{ {
if (! is_null($this->userId)) { if (is_null($this->userId)) {
return [ return [];
new PrivateChannel("user.{$this->userId}"),
];
} }
return null; return [
new PrivateChannel("user.{$this->userId}"),
];
} }
} }

View File

@@ -12,18 +12,22 @@ class FileStorageChanged implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
throw new \Exception('Team id is null'); $teamId = auth()->user()->currentTeam()->id;
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -12,21 +12,22 @@ class ProxyStatusChanged implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -12,21 +12,22 @@ class ScheduledTaskDone implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct($teamId = null) public function __construct($teamId = null)
{ {
if (is_null($teamId)) { if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
$teamId = auth()->user()->currentTeam()->id ?? null; $teamId = auth()->user()->currentTeam()->id;
}
if (is_null($teamId)) {
throw new \Exception('Team id is null');
} }
$this->teamId = $teamId; $this->teamId = $teamId;
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -13,27 +13,24 @@ class ServiceStatusChanged implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public ?string $userId = null; public int|string|null $userId = null;
public function __construct($userId = null) public function __construct($userId = null)
{ {
if (is_null($userId)) { if (is_null($userId)) {
$userId = Auth::id() ?? null; $userId = Auth::id() ?? null;
} }
if (is_null($userId)) {
return false;
}
$this->userId = $userId; $this->userId = $userId;
} }
public function broadcastOn(): ?array public function broadcastOn(): ?array
{ {
if (! is_null($this->userId)) { if (is_null($this->userId)) {
return [ return [];
new PrivateChannel("user.{$this->userId}"),
];
} }
return null; return [
new PrivateChannel("user.{$this->userId}"),
];
} }
} }

View File

@@ -12,15 +12,21 @@ class TestEvent implements ShouldBroadcast
{ {
use Dispatchable, InteractsWithSockets, SerializesModels; use Dispatchable, InteractsWithSockets, SerializesModels;
public $teamId; public ?int $teamId = null;
public function __construct() public function __construct()
{ {
$this->teamId = auth()->user()->currentTeam()->id; if (auth()->check() && auth()->user()->currentTeam()) {
$this->teamId = auth()->user()->currentTeam()->id;
}
} }
public function broadcastOn(): array public function broadcastOn(): array
{ {
if (is_null($this->teamId)) {
return [];
}
return [ return [
new PrivateChannel("team.{$this->teamId}"), new PrivateChannel("team.{$this->teamId}"),
]; ];

View File

@@ -27,7 +27,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Sleep; use Illuminate\Support\Sleep;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use RuntimeException; use RuntimeException;
@@ -2246,43 +2245,16 @@ 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) private function graceful_shutdown_container(string $containerName, int $timeout = 30)
{ {
try { try {
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName"); $this->execute_remote_command(
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
$startTime = time(); ["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
while ($process->running()) { );
if (time() - $startTime >= $timeout) { } catch (Exception $error) {
$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) {
$this->application_deployment_queue->addLogEntry("Error stopping container $containerName: ".$error->getMessage(), 'stderr'); $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(
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
);
} }
private function stop_running_container(bool $force = false) private function stop_running_container(bool $force = false)

View File

@@ -23,7 +23,7 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)]; return [(new WithoutOverlapping('cleanup-instance-stuffs'))->expireAfter(60)->dontRelease()];
} }
public function handle(): void public function handle(): void

View File

@@ -390,7 +390,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
$commands[] = 'mkdir -p '.$this->backup_dir; $commands[] = 'mkdir -p '.$this->backup_dir;
$backupCommand = 'docker exec'; $backupCommand = 'docker exec';
if ($this->postgres_password) { if ($this->postgres_password) {
$backupCommand .= " -e PGPASSWORD=$this->postgres_password"; $backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
} }
if ($this->backup->dump_all) { if ($this->backup->dump_all) {
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location"; $backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";

View File

@@ -42,10 +42,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
public function handle() public function handle()
{ {
try { try {
$persistentStorages = collect();
switch ($this->resource->type()) { switch ($this->resource->type()) {
case 'application': case 'application':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopApplication::run($this->resource, previewDeployments: true); StopApplication::run($this->resource, previewDeployments: true);
break; break;
case 'standalone-postgresql': case 'standalone-postgresql':
@@ -56,53 +54,52 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
case 'standalone-keydb': case 'standalone-keydb':
case 'standalone-dragonfly': case 'standalone-dragonfly':
case 'standalone-clickhouse': case 'standalone-clickhouse':
$persistentStorages = $this->resource?->persistentStorages()?->get();
StopDatabase::run($this->resource, true); StopDatabase::run($this->resource, true);
break; break;
case 'service': case 'service':
StopService::run($this->resource, true); StopService::run($this->resource, true);
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks); DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
break;
}
if ($this->deleteVolumes && $this->resource->type() !== 'service') { return;
$this->resource->delete_volumes($persistentStorages);
$this->resource->persistentStorages()->delete();
} }
$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;
if ($this->deleteConfigurations) { if ($this->deleteConfigurations) {
$this->resource->delete_configurations(); // rename to FileStorages $this->resource->deleteConfigurations();
$this->resource->fileStorages()->delete();
} }
if ($this->deleteVolumes) {
$this->resource->deleteVolumes();
$this->resource->persistentStorages()->delete();
}
$this->resource->fileStorages()->delete();
$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;
if ($isDatabase) { if ($isDatabase) {
$this->resource->sslCertificates()->delete(); $this->resource->sslCertificates()->delete();
$this->resource->scheduledBackups()->delete(); $this->resource->scheduledBackups()->delete();
$this->resource->environment_variables()->delete();
$this->resource->tags()->detach(); $this->resource->tags()->detach();
} }
$this->resource->environment_variables()->delete();
$server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server'); if ($this->deleteConnectedNetworks && $this->resource->type() === 'application') {
if (($this->dockerCleanup || $isDatabase) && $server) { $this->resource->deleteConnectedNetworks();
CleanupDocker::dispatch($server, true);
}
if ($this->deleteConnectedNetworks && ! $isDatabase) {
$this->resource?->delete_connected_networks($this->resource->uuid);
} }
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw $e; throw $e;
} finally { } finally {
$this->resource->forceDelete(); $this->resource->forceDelete();
if ($this->dockerCleanup) { if ($this->dockerCleanup) {
CleanupDocker::dispatch($server, true); $server = data_get($this->resource, 'server') ?? data_get($this->resource, 'destination.server');
if ($server) {
CleanupDocker::dispatch($server, true);
}
} }
Artisan::queue('cleanup:stucked-resources'); Artisan::queue('cleanup:stucked-resources');
} }

View File

@@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)->dontRelease()];
} }
public function __construct(public Server $server) {} public function __construct(public Server $server) {}

View File

@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
public function middleware(): array public function middleware(): array
{ {
return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)]; return [(new WithoutOverlapping($this->server->uuid))->expireAfter(60)->dontRelease()];
} }
public function __construct(public Server $server) {} public function __construct(public Server $server) {}

View File

@@ -5,10 +5,7 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Models\Application; use App\Models\Application;
use App\Models\ApplicationPreview; use App\Models\ApplicationPreview;
use Carbon\Carbon;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -193,13 +190,12 @@ class Previews extends Component
{ {
try { try {
$server = $this->application->destination->server; $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}"], $server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout); $this->stopContainers($containers, $server);
} }
GetContainersStatus::run($server); GetContainersStatus::run($server);
@@ -215,13 +211,12 @@ class Previews extends Component
{ {
try { try {
$server = $this->application->destination->server; $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}"], $server); instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else { } else {
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray(); $containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout); $this->stopContainers($containers, $server);
} }
ApplicationPreview::where('application_id', $this->application->id) ApplicationPreview::where('application_id', $this->application->id)
@@ -237,48 +232,14 @@ class Previews extends Component
} }
} }
private function stopContainers(array $containers, $server, int $timeout) private function stopContainers(array $containers, $server, int $timeout = 30)
{ {
$processes = [];
foreach ($containers as $container) { foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']); $containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout); instant_remote_process(command: [
} "docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
$startTime = Carbon::now()->getTimestamp(); ], server: $server, throwError: false);
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 (Carbon::now()->getTimestamp() - $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);
} }
} }
} }

View File

@@ -120,6 +120,8 @@ class General extends Component
try { try {
$this->database->save(); $this->database->save();
$this->dispatch('success', 'SSL configuration updated.'); $this->dispatch('success', 'SSL configuration updated.');
$this->db_url = $this->database->internal_db_url;
$this->db_url_public = $this->database->external_db_url;
} catch (Exception $e) { } catch (Exception $e) {
return handleError($e, $this); return handleError($e, $this);
} }

View File

@@ -19,6 +19,8 @@ class ScheduledBackups extends Component
public $s3s; public $s3s;
public string $custom_type = 'mysql';
protected $listeners = ['refreshScheduledBackups']; protected $listeners = ['refreshScheduledBackups'];
protected $queryString = ['selectedBackupId']; protected $queryString = ['selectedBackupId'];
@@ -49,6 +51,14 @@ class ScheduledBackups extends Component
} }
} }
public function setCustomType()
{
$this->database->custom_type = $this->custom_type;
$this->database->save();
$this->dispatch('success', 'Database type set.');
$this->refreshScheduledBackups();
}
public function delete($scheduled_backup_id): void public function delete($scheduled_backup_id): void
{ {
$this->database->scheduledBackups->find($scheduled_backup_id)->delete(); $this->database->scheduledBackups->find($scheduled_backup_id)->delete();
@@ -62,5 +72,6 @@ class ScheduledBackups extends Component
if ($id) { if ($id) {
$this->setSelectedBackup($id); $this->setSelectedBackup($id);
} }
$this->dispatch('refreshScheduledBackups');
} }
} }

View File

@@ -31,8 +31,9 @@ class Configuration extends Component
return [ return [
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
'check_status',
'refreshStatus' => '$refresh', 'refreshStatus' => '$refresh',
'check_status',
'refreshServices',
]; ];
} }
@@ -63,6 +64,13 @@ class Configuration extends Component
$this->databases = $this->service->databases->sort(); $this->databases = $this->service->databases->sort();
} }
public function refreshServices()
{
$this->service->refresh();
$this->applications = $this->service->applications->sort();
$this->databases = $this->service->databases->sort();
}
public function restartApplication($id) public function restartApplication($id)
{ {
try { try {

View File

@@ -7,6 +7,7 @@ use App\Actions\Database\StopDatabaseProxy;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
@@ -83,6 +84,42 @@ class Database 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 convertToApplication()
{
try {
$service = $this->database->service;
$serviceDatabase = $this->database;
// Check if application with same name already exists
if ($service->applications()->where('name', $serviceDatabase->name)->exists()) {
throw new \Exception('An application with this name already exists.');
}
// Create new parameters removing database_uuid
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceDatabase) {
$service->applications()->create([
'name' => $serviceDatabase->name,
'human_name' => $serviceDatabase->human_name,
'description' => $serviceDatabase->description,
'exclude_from_status' => $serviceDatabase->exclude_from_status,
'is_log_drain_enabled' => $serviceDatabase->is_log_drain_enabled,
'image' => $serviceDatabase->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceDatabase->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function instantSave() public function instantSave()
{ {
if ($this->database->is_public && ! $this->database->public_port) { if ($this->database->is_public && ! $this->database->public_port) {

View File

@@ -24,7 +24,7 @@ class Index extends Component
public $s3s; public $s3s;
protected $listeners = ['generateDockerCompose']; protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh'];
public function mount() public function mount()
{ {

View File

@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -73,6 +74,40 @@ class ServiceApplicationView extends Component
$this->parameters = get_route_parameters(); $this->parameters = get_route_parameters();
} }
public function convertToDatabase()
{
try {
$service = $this->application->service;
$serviceApplication = $this->application;
// Check if database with same name already exists
if ($service->databases()->where('name', $serviceApplication->name)->exists()) {
throw new \Exception('A database with this name already exists.');
}
$redirectParams = collect($this->parameters)
->except('database_uuid')
->all();
DB::transaction(function () use ($service, $serviceApplication) {
$service->databases()->create([
'name' => $serviceApplication->name,
'human_name' => $serviceApplication->human_name,
'description' => $serviceApplication->description,
'exclude_from_status' => $serviceApplication->exclude_from_status,
'is_log_drain_enabled' => $serviceApplication->is_log_drain_enabled,
'image' => $serviceApplication->image,
'service_id' => $service->id,
'is_migrated' => true,
]);
$serviceApplication->delete();
});
return redirect()->route('project.service.configuration', $redirectParams);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
public function submit() public function submit()
{ {
try { try {

View File

@@ -82,6 +82,7 @@ class StackForm extends Component
$this->service->refresh(); $this->service->refresh();
$this->service->saveComposeConfigs(); $this->service->saveComposeConfigs();
$this->dispatch('refreshEnvs'); $this->dispatch('refreshEnvs');
$this->dispatch('refreshServices');
$notify && $this->dispatch('success', 'Service saved.'); $notify && $this->dispatch('success', 'Service saved.');
} catch (\Throwable $e) { } catch (\Throwable $e) {
return handleError($e, $this); return handleError($e, $this);

View File

@@ -9,9 +9,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; 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\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use OpenApi\Attributes as OA; use OpenApi\Attributes as OA;
@@ -270,51 +268,17 @@ class Application extends BaseModel
return $containers->pluck('Names')->toArray(); return $containers->pluck('Names')->toArray();
} }
public function stopContainers(array $containerNames, $server, int $timeout = 600) public function stopContainers(array $containerNames, $server, int $timeout = 30)
{
$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) { foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); instant_remote_process(command: [
$this->removeContainer($containerName, $server); "docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
} }
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -323,8 +287,9 @@ class Application extends BaseModel
} }
} }
public function delete_volumes(?Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($this->build_pack === 'dockercompose') { if ($this->build_pack === 'dockercompose') {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false); instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false);
@@ -339,8 +304,9 @@ class Application extends BaseModel
} }
} }
public function delete_connected_networks($uuid) public function deleteConnectedNetworks()
{ {
$uuid = $this->uuid;
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false); instant_remote_process(["docker network rm {$uuid}"], $server, false);

View File

@@ -6,9 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; 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\Process\InvokedProcess;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
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;
@@ -158,51 +156,17 @@ class Service extends BaseModel
return $containersToStop; return $containersToStop;
} }
public function stopContainers(array $containerNames, $server, int $timeout = 300) public function stopContainers(array $containerNames, $server, int $timeout = 30)
{
$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) { foreach ($containerNames as $containerName) {
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false); instant_remote_process(command: [
$this->removeContainer($containerName, $server); "docker stop --time=$timeout $containerName",
"docker rm -f $containerName",
], server: $server, throwError: false);
} }
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -211,11 +175,11 @@ class Service extends BaseModel
} }
} }
public function delete_connected_networks($uuid) public function deleteConnectedNetworks()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false); instant_remote_process(["docker network disconnect {$this->uuid} coolify-proxy"], $server, false);
instant_remote_process(["docker network rm {$uuid}"], $server, false); instant_remote_process(["docker network rm {$this->uuid}"], $server, false);
} }
public function getStatusAttribute() public function getStatusAttribute()

View File

@@ -16,6 +16,7 @@ class ServiceDatabase extends BaseModel
static::deleting(function ($service) { static::deleting(function ($service) {
$service->persistentStorages()->delete(); $service->persistentStorages()->delete();
$service->fileStorages()->delete(); $service->fileStorages()->delete();
$service->scheduledBackups()->delete();
}); });
static::saving(function ($service) { static::saving(function ($service) {
if ($service->isDirty('status')) { if ($service->isDirty('status')) {
@@ -77,6 +78,9 @@ class ServiceDatabase extends BaseModel
public function databaseType() public function databaseType()
{ {
if (filled($this->custom_type)) {
return 'standalone-'.$this->custom_type;
}
$image = str($this->image)->before(':'); $image = str($this->image)->before(':');
if ($image->contains('supabase/postgres')) { if ($image->contains('supabase/postgres')) {
$finalImage = 'supabase/postgres'; $finalImage = 'supabase/postgres';
@@ -141,6 +145,7 @@ class ServiceDatabase extends BaseModel
str($this->databaseType())->contains('postgres') || str($this->databaseType())->contains('postgres') ||
str($this->databaseType())->contains('postgis') || str($this->databaseType())->contains('postgis') ||
str($this->databaseType())->contains('mariadb') || str($this->databaseType())->contains('mariadb') ||
str($this->databaseType())->contains('mongo'); str($this->databaseType())->contains('mongo') ||
filled($this->custom_type);
} }
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -94,7 +93,7 @@ class StandaloneClickhouse extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -103,8 +102,9 @@ class StandaloneClickhouse extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -94,7 +93,7 @@ class StandaloneDragonfly extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -103,8 +102,9 @@ class StandaloneDragonfly extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -94,7 +93,7 @@ class StandaloneKeydb extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -103,8 +102,9 @@ class StandaloneKeydb extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,8 +3,8 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
class StandaloneMariadb extends BaseModel class StandaloneMariadb extends BaseModel
@@ -94,7 +94,7 @@ class StandaloneMariadb extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -103,8 +103,9 @@ class StandaloneMariadb extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }
@@ -253,7 +254,7 @@ class StandaloneMariadb extends BaseModel
return $this->morphMany(LocalFileVolume::class, 'resource'); return $this->morphMany(LocalFileVolume::class, 'resource');
} }
public function destination() public function destination(): MorphTo
{ {
return $this->morphTo(); return $this->morphTo();
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -98,7 +97,7 @@ class StandaloneMongodb extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -107,8 +106,9 @@ class StandaloneMongodb extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -95,7 +94,7 @@ class StandaloneMysql extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -104,8 +103,9 @@ class StandaloneMysql extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -59,7 +58,7 @@ class StandalonePostgresql extends BaseModel
); );
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -68,8 +67,9 @@ class StandalonePostgresql extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@@ -96,7 +95,7 @@ class StandaloneRedis extends BaseModel
return database_configuration_dir()."/{$this->uuid}"; return database_configuration_dir()."/{$this->uuid}";
} }
public function delete_configurations() public function deleteConfigurations()
{ {
$server = data_get($this, 'destination.server'); $server = data_get($this, 'destination.server');
$workdir = $this->workdir(); $workdir = $this->workdir();
@@ -105,8 +104,9 @@ class StandaloneRedis extends BaseModel
} }
} }
public function delete_volumes(Collection $persistentStorages) public function deleteVolumes()
{ {
$persistentStorages = $this->persistentStorages()->get() ?? collect();
if ($persistentStorages->count() === 0) { if ($persistentStorages->count() === 0) {
return; return;
} }

View File

@@ -2,7 +2,7 @@
namespace App\Traits; namespace App\Traits;
use DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
trait DeletesUserSessions trait DeletesUserSessions

View File

@@ -28,6 +28,7 @@ function queue_application_deployment(Application $application, string $deployme
// Check if there's already a deployment in progress or queued for this application and commit // Check if there's already a deployment in progress or queued for this application and commit
$existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id) $existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
->where('commit', $commit) ->where('commit', $commit)
->where('pull_request_id', $pull_request_id)
->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value]) ->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])
->first(); ->first();

View File

@@ -2990,12 +2990,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first(); $applicationFound = ServiceApplication::where('name', $serviceName)->where('service_id', $resource->id)->first();
if ($applicationFound) { if ($applicationFound) {
$savedService = $applicationFound; $savedService = $applicationFound;
$savedService = ServiceDatabase::firstOrCreate([ // $savedService = ServiceDatabase::firstOrCreate([
'name' => $applicationFound->name, // 'name' => $applicationFound->name,
'image' => $applicationFound->image, // 'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id, // 'service_id' => $applicationFound->service_id,
]); // ]);
$applicationFound->delete(); // $applicationFound->delete();
} else { } else {
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,
@@ -3248,12 +3248,12 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int
$applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first(); $applicationFound = ServiceApplication::where('name', $serviceName)->where('image', $image)->where('service_id', $resource->id)->first();
if ($applicationFound) { if ($applicationFound) {
$savedService = $applicationFound; $savedService = $applicationFound;
$savedService = ServiceDatabase::firstOrCreate([ // $savedService = ServiceDatabase::firstOrCreate([
'name' => $applicationFound->name, // 'name' => $applicationFound->name,
'image' => $applicationFound->image, // 'image' => $applicationFound->image,
'service_id' => $applicationFound->service_id, // 'service_id' => $applicationFound->service_id,
]); // ]);
$applicationFound->delete(); // $applicationFound->delete();
} else { } else {
$savedService = ServiceDatabase::firstOrCreate([ $savedService = ServiceDatabase::firstOrCreate([
'name' => $serviceName, 'name' => $serviceName,

View File

@@ -2,7 +2,7 @@
return [ return [
'coolify' => [ 'coolify' => [
'version' => '4.0.0-beta.415', 'version' => '4.0.0-beta.416',
'helper_version' => '1.0.8', 'helper_version' => '1.0.8',
'realtime_version' => '1.0.8', 'realtime_version' => '1.0.8',
'self_hosted' => env('SELF_HOSTED', true), 'self_hosted' => env('SELF_HOSTED', true),

View File

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

View File

@@ -19,7 +19,7 @@ class ApplicationSeeder extends Seeder
'fqdn' => 'http://nodejs.127.0.0.1.sslip.io', 'fqdn' => 'http://nodejs.127.0.0.1.sslip.io',
'repository_project_id' => 603035348, 'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples', 'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'main', 'git_branch' => 'v4.x',
'base_directory' => '/nodejs', 'base_directory' => '/nodejs',
'build_pack' => 'nixpacks', 'build_pack' => 'nixpacks',
'ports_exposes' => '3000', 'ports_exposes' => '3000',
@@ -34,7 +34,7 @@ class ApplicationSeeder extends Seeder
'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io', 'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io',
'repository_project_id' => 603035348, 'repository_project_id' => 603035348,
'git_repository' => 'coollabsio/coolify-examples', 'git_repository' => 'coollabsio/coolify-examples',
'git_branch' => 'main', 'git_branch' => 'v4.x',
'base_directory' => '/dockerfile', 'base_directory' => '/dockerfile',
'build_pack' => 'dockerfile', 'build_pack' => 'dockerfile',
'ports_exposes' => '80', 'ports_exposes' => '80',
@@ -48,7 +48,7 @@ class ApplicationSeeder extends Seeder
'name' => 'Pure Dockerfile Example', 'name' => 'Pure Dockerfile Example',
'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io', 'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io',
'git_repository' => 'coollabsio/coolify', 'git_repository' => 'coollabsio/coolify',
'git_branch' => 'main', 'git_branch' => 'v4.x',
'git_commit_sha' => 'HEAD', 'git_commit_sha' => 'HEAD',
'build_pack' => 'dockerfile', 'build_pack' => 'dockerfile',
'ports_exposes' => '80', 'ports_exposes' => '80',

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.415" "version": "4.0.0-beta.416"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.416" "version": "4.0.0-beta.417"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"

86
package-lock.json generated
View File

@@ -22,7 +22,7 @@
"pusher-js": "8.4.0", "pusher-js": "8.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"vite": "^6.2.6", "vite": "^6.3.4",
"vue": "3.5.13" "vue": "3.5.13"
} }
}, },
@@ -2931,6 +2931,51 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2994,15 +3039,18 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.6", "version": "6.3.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", "integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.30.1" "rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -3076,6 +3124,34 @@
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
} }
}, },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.13", "version": "3.5.13",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",

View File

@@ -16,7 +16,7 @@
"pusher-js": "8.4.0", "pusher-js": "8.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "3.4.17", "tailwindcss": "3.4.17",
"vite": "^6.2.6", "vite": "^6.3.4",
"vue": "3.5.13" "vue": "3.5.13"
}, },
"dependencies": { "dependencies": {

View File

@@ -83,6 +83,9 @@
function checkTheme() { function checkTheme() {
theme = localStorage.theme theme = localStorage.theme
if (theme == 'system') {
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
}
if (theme == 'dark') { if (theme == 'dark') {
baseColor = '#FCD452' baseColor = '#FCD452'
textColor = '#ffffff' textColor = '#ffffff'

View File

@@ -91,8 +91,7 @@
<div x-data="{ expanded: false }"> <div x-data="{ expanded: false }">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="font-medium">Commit:</span> <span class="font-medium">Commit:</span>
<a .prevent <a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
target="_blank" class="underline"> target="_blank" class="underline">
{{ substr(data_get($deployment, 'commit'), 0, 7) }} {{ substr(data_get($deployment, 'commit'), 0, 7) }}
</a> </a>
@@ -117,8 +116,7 @@
@endif @endif
@if ($deployment->commitMessage()) @if ($deployment->commitMessage())
<span class="text-gray-600 dark:text-gray-400">-</span> <span class="text-gray-600 dark:text-gray-400">-</span>
<a .prevent <a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
target="_blank" target="_blank"
class="text-gray-600 dark:text-gray-400 truncate max-w-md underline"> class="text-gray-600 dark:text-gray-400 truncate max-w-md underline">
{{ Str::before($deployment->commitMessage(), "\n") }} {{ Str::before($deployment->commitMessage(), "\n") }}

View File

@@ -117,7 +117,8 @@
@script @script
<script> <script>
$wire.$on('stopEvent', () => { $wire.$on('stopEvent', () => {
$wire.$dispatch('info', 'Stopping application.'); $wire.$dispatch('info',
'Gracefully stopping application, it could take a while depending on the application.');
$wire.$call('stop'); $wire.$call('stop');
}); });
</script> </script>

View File

@@ -1,33 +1,52 @@
<div> <div>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
@forelse($database->scheduledBackups as $backup) @if ($database->is_migrated && blank($database->custom_type))
@if ($type == 'database') <div>
<a class="box" <div>Select the type of
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}"> database to enable automated backups.</div>
<div class="flex flex-col"> <div class="pb-4"> If your database is not listed, automated backups are not supported.</div>
<div>Frequency: {{ $backup->frequency }} <form wire:submit="setCustomType" class="flex gap-2 items-end">
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) <div class="w-96">
</div> <x-forms.select label="Type" id="custom_type">
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div> <option selected value="mysql">MySQL</option>
<option value="mariadb">MariaDB</option>
<option value="postgresql">PostgreSQL</option>
<option value="mongodb">MongoDB</option>
</x-forms.select>
</div> </div>
</a> <x-forms.button type="submit">Set</x-forms.button>
@else </form>
<div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')"> </div>
<div @class([ @else
'border-coollabs' => @forelse($database->scheduledBackups as $backup)
data_get($backup, 'id') === data_get($selectedBackup, 'id'), @if ($type == 'database')
'flex flex-col border-l-2 border-transparent', <a class="box"
])> href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
<div>Frequency: {{ $backup->frequency }} <div class="flex flex-col">
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }}) <div>Frequency: {{ $backup->frequency }}
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div>
</a>
@else
<div class="box" wire:click="setSelectedBackup('{{ data_get($backup, 'id') }}')">
<div @class([
'border-coollabs' =>
data_get($backup, 'id') === data_get($selectedBackup, 'id'),
'flex flex-col border-l-2 border-transparent',
])>
<div>Frequency: {{ $backup->frequency }}
({{ data_get($backup->server(), 'settings.server_timezone', 'Instance timezone') }})
</div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div> </div>
<div>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
</div> </div>
</div> @endif
@endif @empty
@empty <div>No scheduled backups configured.</div>
<div>No scheduled backups configured.</div> @endforelse
@endforelse @endif
</div> </div>
@if ($type === 'service-database' && $selectedBackup) @if ($type === 'service-database' && $selectedBackup)
<div class="pt-10"> <div class="pt-10">

View File

@@ -39,7 +39,7 @@
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1" wire:poll.10000ms="check_status"> <div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1" wire:poll.10000ms="check_status">
@foreach ($applications as $application) @foreach ($applications as $application)
<div @class([ <div @class([
'border-l border-dashed border-red-500 ' => str( 'border-l border-dashed border-red-500' => str(
$application->status)->contains(['exited']), $application->status)->contains(['exited']),
'border-l border-dashed border-success' => str( 'border-l border-dashed border-success' => str(
$application->status)->contains(['running']), $application->status)->contains(['running']),
@@ -138,7 +138,7 @@
<div class="text-xs">{{ $database->status }}</div> <div class="text-xs">{{ $database->status }}</div>
</div> </div>
<div class="flex items-center px-4"> <div class="flex items-center px-4">
@if ($database->isBackupSolutionAvailable()) @if ($database->isBackupSolutionAvailable() || $database->is_migrated)
<a class="mx-4 text-xs font-bold hover:underline" <a class="mx-4 text-xs font-bold hover:underline"
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups"> href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
Backups Backups

View File

@@ -7,6 +7,11 @@
<h2>{{ Str::headline($database->name) }}</h2> <h2>{{ Str::headline($database->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 wire:click="convertToApplication" title="Convert to Application"
buttonTitle="Convert to Application" submitAction="convertToApplication" :actions="['The selected resource will be converted to an application.']"
confirmationText="{{ Str::headline($database->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
shortConfirmationLabel="Service Database Name" step3ButtonText="Permanently Delete" />
<x-modal-confirmation title="Confirm Service Database Deletion?" buttonTitle="Delete" isErrorButton <x-modal-confirmation title="Confirm Service Database Deletion?" buttonTitle="Delete" isErrorButton
submitAction="delete" :actions="['The selected service database container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($database->name) }}" submitAction="delete" :actions="['The selected service database container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($database->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below" confirmationLabel="Please confirm the execution of the actions by entering the Service Database Name below"
@@ -18,7 +23,7 @@
<x-forms.input label="Description" id="database.description"></x-forms.input> <x-forms.input label="Description" id="database.description"></x-forms.input>
<x-forms.input required <x-forms.input required
helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>" helper="You can change the image you would like to deploy.<br><br><span class='dark:text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
label="Image Tag" id="database.image"></x-forms.input> label="Image" id="database.image"></x-forms.input>
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port" <x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"

View File

@@ -10,7 +10,7 @@
<a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'" <a class="menu-item" :class="activeTab === 'general' && 'menu-item-active'"
@click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''" @click.prevent="activeTab = 'general'; window.location.hash = 'general'; if(window.location.search) window.location.search = ''"
href="#">General</a> href="#">General</a>
@if ($serviceDatabase?->isBackupSolutionAvailable()) @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item" <a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a> @click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a>
@endif @endif
@@ -33,18 +33,20 @@
<div x-cloak x-show="activeTab === 'general'" class="h-full"> <div x-cloak x-show="activeTab === 'general'" class="h-full">
<livewire:project.service.database :database="$serviceDatabase" /> <livewire:project.service.database :database="$serviceDatabase" />
</div> </div>
@if ($serviceDatabase->isBackupSolutionAvailable()) @if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
<div x-cloak x-show="activeTab === 'backups'"> <div x-cloak x-show="activeTab === 'backups'">
<div class="flex gap-2 "> <div class="flex gap-2 ">
<h2 class="pb-4">Scheduled Backups</h2> <h2 class="pb-4">Scheduled Backups</h2>
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup"> @if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" /> <x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
</x-modal-input> <livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
</x-modal-input>
@endif
</div> </div>
<livewire:project.database.scheduled-backups :database="$serviceDatabase" /> <livewire:project.database.scheduled-backups :database="$serviceDatabase" />
</div>
@endif @endif
</div> @endisset
@endisset </div>
</div> </div>
</div> </div>
</div>

View File

@@ -138,7 +138,7 @@
@script @script
<script> <script>
$wire.$on('stopEvent', () => { $wire.$on('stopEvent', () => {
$wire.$dispatch('info', 'Stopping service.'); $wire.$dispatch('info', 'Gracefully stopping service, it could take a while depending on the service.');
$wire.$call('stop'); $wire.$call('stop');
}); });
$wire.$on('startEvent', async () => { $wire.$on('startEvent', async () => {

View File

@@ -7,11 +7,15 @@
<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 title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton <x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
submitAction="delete" {{-- :checkboxes="$checkboxes" --}} :actions="['The selected service application container will be stopped and permanently deleted.']" buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
confirmationText="{{ Str::headline($application->name) }}" confirmationText="{{ Str::headline($application->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below" confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" /> shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
submitAction="delete" :actions="['The selected service application container will be stopped and permanently deleted.']" confirmationText="{{ Str::headline($application->name) }}"
confirmationLabel="Please confirm the execution of the actions by entering the Service Application Name below"
shortConfirmationLabel="Service Application Name" step3ButtonText="Permanently Delete" />
</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">

View File

@@ -1,10 +1,10 @@
{ {
"coolify": { "coolify": {
"v4": { "v4": {
"version": "4.0.0-beta.415" "version": "4.0.0-beta.416"
}, },
"nightly": { "nightly": {
"version": "4.0.0-beta.416" "version": "4.0.0-beta.417"
}, },
"helper": { "helper": {
"version": "1.0.8" "version": "1.0.8"