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:
@@ -30,7 +30,7 @@ class StopApplication
|
||||
$application->stopContainers($containersToStop, $server);
|
||||
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
$application->delete_connected_networks($application->uuid);
|
||||
$application->deleteConnectedNetworks();
|
||||
}
|
||||
|
||||
if ($dockerCleanup) {
|
||||
|
@@ -85,7 +85,6 @@ class RunRemoteProcess
|
||||
]);
|
||||
|
||||
$processResult = $process->wait();
|
||||
// $processResult = Process::timeout($timeout)->run($this->getCommand(), $this->handleOutput(...));
|
||||
if ($this->activity->properties->get('status') === ProcessStatus::ERROR->value) {
|
||||
$status = ProcessStatus::ERROR;
|
||||
} else {
|
||||
|
@@ -11,7 +11,6 @@ use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopDatabase
|
||||
@@ -25,7 +24,7 @@ class StopDatabase
|
||||
return 'Server is not functional';
|
||||
}
|
||||
|
||||
$this->stopContainer($database, $database->uuid, 300);
|
||||
$this->stopContainer($database, $database->uuid, 30);
|
||||
if ($isDeleteOperation) {
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
@@ -39,37 +38,12 @@ class StopDatabase
|
||||
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;
|
||||
|
||||
$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);
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
}
|
||||
|
@@ -3,54 +3,27 @@
|
||||
namespace App\Actions\Proxy;
|
||||
|
||||
use App\Models\Server;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
|
||||
class StopProxy
|
||||
{
|
||||
use AsAction;
|
||||
|
||||
public function handle(Server $server, bool $forceStop = true)
|
||||
public function handle(Server $server, bool $forceStop = true, int $timeout = 30)
|
||||
{
|
||||
try {
|
||||
$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->status = 'exited';
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
@@ -99,7 +99,8 @@ class ServerCheck
|
||||
return data_get($value, 'Name') === '/coolify-proxy';
|
||||
}
|
||||
})->first();
|
||||
if (! $foundProxyContainer) {
|
||||
$proxyStatus = data_get($foundProxyContainer, 'State.Status', 'exited');
|
||||
if (! $foundProxyContainer || $proxyStatus !== 'running') {
|
||||
try {
|
||||
$shouldStart = CheckProxy::run($this->server);
|
||||
if ($shouldStart) {
|
||||
|
@@ -15,19 +15,18 @@ class StartLogDrain
|
||||
{
|
||||
if ($server->settings->is_logdrain_newrelic_enabled) {
|
||||
$type = 'newrelic';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_highlight_enabled) {
|
||||
$type = 'highlight';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_axiom_enabled) {
|
||||
$type = 'axiom';
|
||||
StopLogDrain::run($server);
|
||||
} elseif ($server->settings->is_logdrain_custom_enabled) {
|
||||
$type = 'custom';
|
||||
StopLogDrain::run($server);
|
||||
} else {
|
||||
$type = 'none';
|
||||
}
|
||||
if ($type !== 'none') {
|
||||
StopLogDrain::run($server);
|
||||
}
|
||||
try {
|
||||
if ($type === 'none') {
|
||||
return 'No log drain is enabled.';
|
||||
@@ -186,7 +185,6 @@ Files:
|
||||
"echo '{$compose}' | base64 -d | tee $compose_path > /dev/null",
|
||||
"echo '{$readme}' | base64 -d | tee $readme_path > /dev/null",
|
||||
"test -f $config_path/.env && rm $config_path/.env",
|
||||
|
||||
];
|
||||
if ($type === 'newrelic') {
|
||||
$add_envs_command = [
|
||||
|
@@ -48,7 +48,7 @@ class DeleteService
|
||||
}
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
$service->deleteConnectedNetworks();
|
||||
}
|
||||
|
||||
instant_remote_process(["docker rm -f $service->uuid"], $server, throwError: false);
|
||||
@@ -56,7 +56,7 @@ class DeleteService
|
||||
throw new \Exception($e->getMessage());
|
||||
} finally {
|
||||
if ($deleteConfigurations) {
|
||||
$service->delete_configurations();
|
||||
$service->deleteConfigurations();
|
||||
}
|
||||
foreach ($service->applications()->get() as $application) {
|
||||
$application->forceDelete();
|
||||
|
@@ -24,7 +24,7 @@ class StopService
|
||||
$service->stopContainers($containersToStop, $server);
|
||||
|
||||
if ($isDeleteOperation) {
|
||||
$service->delete_connected_networks($service->uuid);
|
||||
$service->deleteConnectedNetworks();
|
||||
if ($dockerCleanup) {
|
||||
CleanupDocker::dispatch($server, true);
|
||||
}
|
||||
|
@@ -12,21 +12,22 @@ class ApplicationStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -12,21 +12,22 @@ class BackupCreated implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -12,21 +12,22 @@ class CloudflareTunnelConfigured implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -7,27 +7,27 @@ use Illuminate\Broadcasting\PrivateChannel;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class DatabaseProxyStopped implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = Auth::user()?->currentTeam()?->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -13,28 +13,24 @@ class DatabaseStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $userId = null;
|
||||
public int|string|null $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = Auth::id() ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
if (is_null($this->userId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -12,18 +12,22 @@ class FileStorageChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -12,21 +12,22 @@ class ProxyStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -12,21 +12,22 @@ class ScheduledTaskDone implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
public function __construct($teamId = null)
|
||||
{
|
||||
if (is_null($teamId)) {
|
||||
$teamId = auth()->user()->currentTeam()->id ?? null;
|
||||
}
|
||||
if (is_null($teamId)) {
|
||||
throw new \Exception('Team id is null');
|
||||
if (is_null($teamId) && auth()->check() && auth()->user()->currentTeam()) {
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
}
|
||||
$this->teamId = $teamId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -13,27 +13,24 @@ class ServiceStatusChanged implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public ?string $userId = null;
|
||||
public int|string|null $userId = null;
|
||||
|
||||
public function __construct($userId = null)
|
||||
{
|
||||
if (is_null($userId)) {
|
||||
$userId = Auth::id() ?? null;
|
||||
}
|
||||
if (is_null($userId)) {
|
||||
return false;
|
||||
}
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public function broadcastOn(): ?array
|
||||
{
|
||||
if (! is_null($this->userId)) {
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
if (is_null($this->userId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return null;
|
||||
return [
|
||||
new PrivateChannel("user.{$this->userId}"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@@ -12,15 +12,21 @@ class TestEvent implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $teamId;
|
||||
public ?int $teamId = null;
|
||||
|
||||
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
|
||||
{
|
||||
if (is_null($this->teamId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
new PrivateChannel("team.{$this->teamId}"),
|
||||
];
|
||||
|
@@ -27,7 +27,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
use RuntimeException;
|
||||
@@ -2246,43 +2245,16 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
||||
$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 {
|
||||
$process = Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
|
||||
|
||||
$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) {
|
||||
$this->execute_remote_command(
|
||||
["docker stop --time=$timeout $containerName", 'hidden' => true, 'ignore_errors' => true],
|
||||
["docker rm -f $containerName", 'hidden' => true, 'ignore_errors' => true]
|
||||
);
|
||||
} catch (Exception $error) {
|
||||
$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)
|
||||
|
@@ -23,7 +23,7 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho
|
||||
|
||||
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
|
||||
|
@@ -390,7 +390,7 @@ class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
$commands[] = 'mkdir -p '.$this->backup_dir;
|
||||
$backupCommand = 'docker exec';
|
||||
if ($this->postgres_password) {
|
||||
$backupCommand .= " -e PGPASSWORD=$this->postgres_password";
|
||||
$backupCommand .= " -e PGPASSWORD=\"{$this->postgres_password}\"";
|
||||
}
|
||||
if ($this->backup->dump_all) {
|
||||
$backupCommand .= " $this->container_name pg_dumpall --username {$this->database->postgres_user} | gzip > $this->backup_location";
|
||||
|
@@ -42,10 +42,8 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
public function handle()
|
||||
{
|
||||
try {
|
||||
$persistentStorages = collect();
|
||||
switch ($this->resource->type()) {
|
||||
case 'application':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopApplication::run($this->resource, previewDeployments: true);
|
||||
break;
|
||||
case 'standalone-postgresql':
|
||||
@@ -56,53 +54,52 @@ class DeleteResourceJob implements ShouldBeEncrypted, ShouldQueue
|
||||
case 'standalone-keydb':
|
||||
case 'standalone-dragonfly':
|
||||
case 'standalone-clickhouse':
|
||||
$persistentStorages = $this->resource?->persistentStorages()?->get();
|
||||
StopDatabase::run($this->resource, true);
|
||||
break;
|
||||
case 'service':
|
||||
StopService::run($this->resource, true);
|
||||
DeleteService::run($this->resource, $this->deleteConfigurations, $this->deleteVolumes, $this->dockerCleanup, $this->deleteConnectedNetworks);
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->deleteVolumes && $this->resource->type() !== 'service') {
|
||||
$this->resource->delete_volumes($persistentStorages);
|
||||
$this->resource->persistentStorages()->delete();
|
||||
return;
|
||||
}
|
||||
$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) {
|
||||
$this->resource->delete_configurations(); // rename to FileStorages
|
||||
$this->resource->fileStorages()->delete();
|
||||
$this->resource->deleteConfigurations();
|
||||
}
|
||||
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) {
|
||||
$this->resource->sslCertificates()->delete();
|
||||
$this->resource->scheduledBackups()->delete();
|
||||
$this->resource->environment_variables()->delete();
|
||||
$this->resource->tags()->detach();
|
||||
}
|
||||
$this->resource->environment_variables()->delete();
|
||||
|
||||
$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);
|
||||
if ($this->deleteConnectedNetworks && $this->resource->type() === 'application') {
|
||||
$this->resource->deleteConnectedNetworks();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
throw $e;
|
||||
} finally {
|
||||
$this->resource->forceDelete();
|
||||
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');
|
||||
}
|
||||
|
@@ -24,7 +24,7 @@ class RestartProxyJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
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) {}
|
||||
|
@@ -28,7 +28,7 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue
|
||||
|
||||
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) {}
|
||||
|
@@ -5,10 +5,7 @@ namespace App\Livewire\Project\Application;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
@@ -193,13 +190,12 @@ class Previews extends Component
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
|
||||
$this->stopContainers($containers, $server, $timeout);
|
||||
$this->stopContainers($containers, $server);
|
||||
}
|
||||
|
||||
GetContainersStatus::run($server);
|
||||
@@ -215,13 +211,12 @@ class Previews extends Component
|
||||
{
|
||||
try {
|
||||
$server = $this->application->destination->server;
|
||||
$timeout = 300;
|
||||
|
||||
if ($this->application->destination->server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
|
||||
} else {
|
||||
$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)
|
||||
@@ -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) {
|
||||
$containerName = str_replace('/', '', $container['Names']);
|
||||
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
|
||||
}
|
||||
|
||||
$startTime = Carbon::now()->getTimestamp();
|
||||
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);
|
||||
instant_remote_process(command: [
|
||||
"docker stop --time=$timeout $containerName",
|
||||
"docker rm -f $containerName",
|
||||
], server: $server, throwError: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -120,6 +120,8 @@ class General extends Component
|
||||
try {
|
||||
$this->database->save();
|
||||
$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) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
@@ -19,6 +19,8 @@ class ScheduledBackups extends Component
|
||||
|
||||
public $s3s;
|
||||
|
||||
public string $custom_type = 'mysql';
|
||||
|
||||
protected $listeners = ['refreshScheduledBackups'];
|
||||
|
||||
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
|
||||
{
|
||||
$this->database->scheduledBackups->find($scheduled_backup_id)->delete();
|
||||
@@ -62,5 +72,6 @@ class ScheduledBackups extends Component
|
||||
if ($id) {
|
||||
$this->setSelectedBackup($id);
|
||||
}
|
||||
$this->dispatch('refreshScheduledBackups');
|
||||
}
|
||||
}
|
||||
|
@@ -31,8 +31,9 @@ class Configuration extends Component
|
||||
|
||||
return [
|
||||
"echo-private:user.{$userId},ServiceStatusChanged" => 'check_status',
|
||||
'check_status',
|
||||
'refreshStatus' => '$refresh',
|
||||
'check_status',
|
||||
'refreshServices',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -63,6 +64,13 @@ class Configuration extends Component
|
||||
$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)
|
||||
{
|
||||
try {
|
||||
|
@@ -7,6 +7,7 @@ use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
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.');
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if ($this->database->is_public && ! $this->database->public_port) {
|
||||
|
@@ -24,7 +24,7 @@ class Index extends Component
|
||||
|
||||
public $s3s;
|
||||
|
||||
protected $listeners = ['generateDockerCompose'];
|
||||
protected $listeners = ['generateDockerCompose', 'refreshScheduledBackups' => '$refresh'];
|
||||
|
||||
public function mount()
|
||||
{
|
||||
|
@@ -5,6 +5,7 @@ namespace App\Livewire\Project\Service;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -73,6 +74,40 @@ class ServiceApplicationView extends Component
|
||||
$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()
|
||||
{
|
||||
try {
|
||||
|
@@ -82,6 +82,7 @@ class StackForm extends Component
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
$this->dispatch('refreshEnvs');
|
||||
$this->dispatch('refreshServices');
|
||||
$notify && $this->dispatch('success', 'Service saved.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
@@ -9,9 +9,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use OpenApi\Attributes as OA;
|
||||
@@ -270,51 +268,17 @@ class Application extends BaseModel
|
||||
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)
|
||||
public function stopContainers(array $containerNames, $server, int $timeout = 30)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
instant_remote_process(command: [
|
||||
"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');
|
||||
$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') {
|
||||
$server = data_get($this, 'destination.server');
|
||||
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');
|
||||
instant_remote_process(["docker network disconnect {$uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$uuid}"], $server, false);
|
||||
|
@@ -6,9 +6,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Process\InvokedProcess;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
@@ -158,51 +156,17 @@ class Service extends BaseModel
|
||||
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)
|
||||
public function stopContainers(array $containerNames, $server, int $timeout = 30)
|
||||
{
|
||||
foreach ($containerNames as $containerName) {
|
||||
instant_remote_process(command: ["docker kill $containerName"], server: $server, throwError: false);
|
||||
$this->removeContainer($containerName, $server);
|
||||
instant_remote_process(command: [
|
||||
"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');
|
||||
$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');
|
||||
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 disconnect {$this->uuid} coolify-proxy"], $server, false);
|
||||
instant_remote_process(["docker network rm {$this->uuid}"], $server, false);
|
||||
}
|
||||
|
||||
public function getStatusAttribute()
|
||||
|
@@ -16,6 +16,7 @@ class ServiceDatabase extends BaseModel
|
||||
static::deleting(function ($service) {
|
||||
$service->persistentStorages()->delete();
|
||||
$service->fileStorages()->delete();
|
||||
$service->scheduledBackups()->delete();
|
||||
});
|
||||
static::saving(function ($service) {
|
||||
if ($service->isDirty('status')) {
|
||||
@@ -77,6 +78,9 @@ class ServiceDatabase extends BaseModel
|
||||
|
||||
public function databaseType()
|
||||
{
|
||||
if (filled($this->custom_type)) {
|
||||
return 'standalone-'.$this->custom_type;
|
||||
}
|
||||
$image = str($this->image)->before(':');
|
||||
if ($image->contains('supabase/postgres')) {
|
||||
$finalImage = 'supabase/postgres';
|
||||
@@ -141,6 +145,7 @@ class ServiceDatabase extends BaseModel
|
||||
str($this->databaseType())->contains('postgres') ||
|
||||
str($this->databaseType())->contains('postgis') ||
|
||||
str($this->databaseType())->contains('mariadb') ||
|
||||
str($this->databaseType())->contains('mongo');
|
||||
str($this->databaseType())->contains('mongo') ||
|
||||
filled($this->custom_type);
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -94,7 +93,7 @@ class StandaloneClickhouse extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -94,7 +93,7 @@ class StandaloneDragonfly extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -94,7 +93,7 @@ class StandaloneKeydb extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,8 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class StandaloneMariadb extends BaseModel
|
||||
@@ -94,7 +94,7 @@ class StandaloneMariadb extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
@@ -253,7 +254,7 @@ class StandaloneMariadb extends BaseModel
|
||||
return $this->morphMany(LocalFileVolume::class, 'resource');
|
||||
}
|
||||
|
||||
public function destination()
|
||||
public function destination(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -98,7 +97,7 @@ class StandaloneMongodb extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -95,7 +94,7 @@ class StandaloneMysql extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
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');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
@@ -96,7 +95,7 @@ class StandaloneRedis extends BaseModel
|
||||
return database_configuration_dir()."/{$this->uuid}";
|
||||
}
|
||||
|
||||
public function delete_configurations()
|
||||
public function deleteConfigurations()
|
||||
{
|
||||
$server = data_get($this, 'destination.server');
|
||||
$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) {
|
||||
return;
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use DB;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
trait DeletesUserSessions
|
||||
|
@@ -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
|
||||
$existing_deployment = ApplicationDeploymentQueue::where('application_id', $application_id)
|
||||
->where('commit', $commit)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])
|
||||
->first();
|
||||
|
||||
|
@@ -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();
|
||||
if ($applicationFound) {
|
||||
$savedService = $applicationFound;
|
||||
$savedService = ServiceDatabase::firstOrCreate([
|
||||
'name' => $applicationFound->name,
|
||||
'image' => $applicationFound->image,
|
||||
'service_id' => $applicationFound->service_id,
|
||||
]);
|
||||
$applicationFound->delete();
|
||||
// $savedService = ServiceDatabase::firstOrCreate([
|
||||
// 'name' => $applicationFound->name,
|
||||
// 'image' => $applicationFound->image,
|
||||
// 'service_id' => $applicationFound->service_id,
|
||||
// ]);
|
||||
// $applicationFound->delete();
|
||||
} else {
|
||||
$savedService = ServiceDatabase::firstOrCreate([
|
||||
'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();
|
||||
if ($applicationFound) {
|
||||
$savedService = $applicationFound;
|
||||
$savedService = ServiceDatabase::firstOrCreate([
|
||||
'name' => $applicationFound->name,
|
||||
'image' => $applicationFound->image,
|
||||
'service_id' => $applicationFound->service_id,
|
||||
]);
|
||||
$applicationFound->delete();
|
||||
// $savedService = ServiceDatabase::firstOrCreate([
|
||||
// 'name' => $applicationFound->name,
|
||||
// 'image' => $applicationFound->image,
|
||||
// 'service_id' => $applicationFound->service_id,
|
||||
// ]);
|
||||
// $applicationFound->delete();
|
||||
} else {
|
||||
$savedService = ServiceDatabase::firstOrCreate([
|
||||
'name' => $serviceName,
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
return [
|
||||
'coolify' => [
|
||||
'version' => '4.0.0-beta.415',
|
||||
'version' => '4.0.0-beta.416',
|
||||
'helper_version' => '1.0.8',
|
||||
'realtime_version' => '1.0.8',
|
||||
'self_hosted' => env('SELF_HOSTED', true),
|
||||
|
@@ -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');
|
||||
});
|
||||
}
|
||||
};
|
@@ -19,7 +19,7 @@ class ApplicationSeeder extends Seeder
|
||||
'fqdn' => 'http://nodejs.127.0.0.1.sslip.io',
|
||||
'repository_project_id' => 603035348,
|
||||
'git_repository' => 'coollabsio/coolify-examples',
|
||||
'git_branch' => 'main',
|
||||
'git_branch' => 'v4.x',
|
||||
'base_directory' => '/nodejs',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
@@ -34,7 +34,7 @@ class ApplicationSeeder extends Seeder
|
||||
'fqdn' => 'http://dockerfile.127.0.0.1.sslip.io',
|
||||
'repository_project_id' => 603035348,
|
||||
'git_repository' => 'coollabsio/coolify-examples',
|
||||
'git_branch' => 'main',
|
||||
'git_branch' => 'v4.x',
|
||||
'base_directory' => '/dockerfile',
|
||||
'build_pack' => 'dockerfile',
|
||||
'ports_exposes' => '80',
|
||||
@@ -48,7 +48,7 @@ class ApplicationSeeder extends Seeder
|
||||
'name' => 'Pure Dockerfile Example',
|
||||
'fqdn' => 'http://pure-dockerfile.127.0.0.1.sslip.io',
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
'git_branch' => 'v4.x',
|
||||
'git_commit_sha' => 'HEAD',
|
||||
'build_pack' => 'dockerfile',
|
||||
'ports_exposes' => '80',
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.415"
|
||||
"version": "4.0.0-beta.416"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.416"
|
||||
"version": "4.0.0-beta.417"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.8"
|
||||
|
86
package-lock.json
generated
86
package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.3.4",
|
||||
"vue": "3.5.13"
|
||||
}
|
||||
},
|
||||
@@ -2931,6 +2931,51 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -2994,15 +3039,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.2.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
|
||||
"integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.4.tgz",
|
||||
"integrity": "sha512-BiReIiMS2fyFqbqNT/Qqt4CVITDU9M9vE+DKcVAsB+ZV0wvTKd+3hMbkpxz1b+NmEDMegpVbisKiAZOnvO92Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.4",
|
||||
"picomatch": "^4.0.2",
|
||||
"postcss": "^8.5.3",
|
||||
"rollup": "^4.30.1"
|
||||
"rollup": "^4.34.9",
|
||||
"tinyglobby": "^0.2.13"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
@@ -3076,6 +3124,34 @@
|
||||
"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": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
||||
|
@@ -16,7 +16,7 @@
|
||||
"pusher-js": "8.4.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"tailwindcss": "3.4.17",
|
||||
"vite": "^6.2.6",
|
||||
"vite": "^6.3.4",
|
||||
"vue": "3.5.13"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@@ -83,6 +83,9 @@
|
||||
|
||||
function checkTheme() {
|
||||
theme = localStorage.theme
|
||||
if (theme == 'system') {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
if (theme == 'dark') {
|
||||
baseColor = '#FCD452'
|
||||
textColor = '#ffffff'
|
||||
|
@@ -91,8 +91,7 @@
|
||||
<div x-data="{ expanded: false }">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">Commit:</span>
|
||||
<a .prevent
|
||||
href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
<a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
target="_blank" class="underline">
|
||||
{{ substr(data_get($deployment, 'commit'), 0, 7) }}
|
||||
</a>
|
||||
@@ -117,8 +116,7 @@
|
||||
@endif
|
||||
@if ($deployment->commitMessage())
|
||||
<span class="text-gray-600 dark:text-gray-400">-</span>
|
||||
<a .prevent
|
||||
href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
<a href="{{ $application->gitCommitLink(data_get($deployment, 'commit')) }}"
|
||||
target="_blank"
|
||||
class="text-gray-600 dark:text-gray-400 truncate max-w-md underline">
|
||||
{{ Str::before($deployment->commitMessage(), "\n") }}
|
||||
|
@@ -117,7 +117,8 @@
|
||||
@script
|
||||
<script>
|
||||
$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');
|
||||
});
|
||||
</script>
|
||||
|
@@ -1,33 +1,52 @@
|
||||
<div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse($database->scheduledBackups as $backup)
|
||||
@if ($type == 'database')
|
||||
<a class="box"
|
||||
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
|
||||
<div class="flex flex-col">
|
||||
<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>
|
||||
@if ($database->is_migrated && blank($database->custom_type))
|
||||
<div>
|
||||
<div>Select the type of
|
||||
database to enable automated backups.</div>
|
||||
<div class="pb-4"> If your database is not listed, automated backups are not supported.</div>
|
||||
<form wire:submit="setCustomType" class="flex gap-2 items-end">
|
||||
<div class="w-96">
|
||||
<x-forms.select label="Type" id="custom_type">
|
||||
<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>
|
||||
</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') }})
|
||||
<x-forms.button type="submit">Set</x-forms.button>
|
||||
</form>
|
||||
</div>
|
||||
@else
|
||||
@forelse($database->scheduledBackups as $backup)
|
||||
@if ($type == 'database')
|
||||
<a class="box"
|
||||
href="{{ route('project.database.backup.execution', [...$parameters, 'backup_uuid' => $backup->uuid]) }}">
|
||||
<div class="flex flex-col">
|
||||
<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>Last backup: {{ data_get($backup->latest_log, 'status', 'No backup yet') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@empty
|
||||
<div>No scheduled backups configured.</div>
|
||||
@endforelse
|
||||
@endif
|
||||
@empty
|
||||
<div>No scheduled backups configured.</div>
|
||||
@endforelse
|
||||
@endif
|
||||
</div>
|
||||
@if ($type === 'service-database' && $selectedBackup)
|
||||
<div class="pt-10">
|
||||
|
@@ -39,7 +39,7 @@
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1" wire:poll.10000ms="check_status">
|
||||
@foreach ($applications as $application)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500 ' => str(
|
||||
'border-l border-dashed border-red-500' => str(
|
||||
$application->status)->contains(['exited']),
|
||||
'border-l border-dashed border-success' => str(
|
||||
$application->status)->contains(['running']),
|
||||
@@ -138,7 +138,7 @@
|
||||
<div class="text-xs">{{ $database->status }}</div>
|
||||
</div>
|
||||
<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"
|
||||
href="{{ route('project.service.index', ['project_uuid' => $project->uuid, 'environment_uuid' => $environment->uuid, 'service_uuid' => $service->uuid, 'stack_service_uuid' => $database->uuid]) }}#backups">
|
||||
Backups
|
||||
|
@@ -7,6 +7,11 @@
|
||||
<h2>{{ Str::headline($database->name) }}</h2>
|
||||
@endif
|
||||
<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
|
||||
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"
|
||||
@@ -18,7 +23,7 @@
|
||||
<x-forms.input label="Description" id="database.description"></x-forms.input>
|
||||
<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>"
|
||||
label="Image Tag" id="database.image"></x-forms.input>
|
||||
label="Image" id="database.image"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<x-forms.input placeholder="5432" disabled="{{ $database->is_public }}" id="database.public_port"
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<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 = ''"
|
||||
href="#">General</a>
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable())
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<a :class="activeTab === 'backups' && 'menu-item-active'" class="menu-item"
|
||||
@click.prevent="activeTab = 'backups'; window.location.hash = 'backups'" href="#backups">Backups</a>
|
||||
@endif
|
||||
@@ -33,18 +33,20 @@
|
||||
<div x-cloak x-show="activeTab === 'general'" class="h-full">
|
||||
<livewire:project.service.database :database="$serviceDatabase" />
|
||||
</div>
|
||||
@if ($serviceDatabase->isBackupSolutionAvailable())
|
||||
@if ($serviceDatabase?->isBackupSolutionAvailable() || $serviceDatabase?->is_migrated)
|
||||
<div x-cloak x-show="activeTab === 'backups'">
|
||||
<div class="flex gap-2 ">
|
||||
<h2 class="pb-4">Scheduled Backups</h2>
|
||||
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
|
||||
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
|
||||
</x-modal-input>
|
||||
@if (filled($serviceDatabase->custom_type) || !$serviceDatabase->is_migrated)
|
||||
<x-modal-input buttonTitle="+ Add" title="New Scheduled Backup">
|
||||
<livewire:project.database.create-scheduled-backup :database="$serviceDatabase" />
|
||||
</x-modal-input>
|
||||
@endif
|
||||
</div>
|
||||
<livewire:project.database.scheduled-backups :database="$serviceDatabase" />
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endisset
|
||||
@endisset
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -138,7 +138,7 @@
|
||||
@script
|
||||
<script>
|
||||
$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.$on('startEvent', async () => {
|
||||
|
@@ -7,11 +7,15 @@
|
||||
<h2>{{ Str::headline($application->name) }}</h2>
|
||||
@endif
|
||||
<x-forms.button type="submit">Save</x-forms.button>
|
||||
<x-modal-confirmation title="Confirm Service Application Deletion?" buttonTitle="Delete" isErrorButton
|
||||
submitAction="delete" {{-- :checkboxes="$checkboxes" --}} :actions="['The selected service application container will be stopped and permanently deleted.']"
|
||||
<x-modal-confirmation wire:click="convertToDatabase" title="Convert to Database"
|
||||
buttonTitle="Convert to Database" submitAction="convertToDatabase" :actions="['The selected resource will be converted to a service database.']"
|
||||
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" />
|
||||
<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 class="flex flex-col gap-2">
|
||||
<div class="flex gap-2">
|
||||
|
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"coolify": {
|
||||
"v4": {
|
||||
"version": "4.0.0-beta.415"
|
||||
"version": "4.0.0-beta.416"
|
||||
},
|
||||
"nightly": {
|
||||
"version": "4.0.0-beta.416"
|
||||
"version": "4.0.0-beta.417"
|
||||
},
|
||||
"helper": {
|
||||
"version": "1.0.8"
|
||||
|
Reference in New Issue
Block a user