Merge branch 'next' into improve-cleanup

This commit is contained in:
Andras Bacsai
2024-09-27 16:40:48 +02:00
committed by GitHub
208 changed files with 3593 additions and 2772 deletions

View File

@@ -141,7 +141,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
if (! $this->createdServer) {
return $this->dispatch('error', 'Localhost server is not found. Something went wrong during installation. Please try to reinstall or contact support.');
}
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
return $this->validateServer('localhost');
} elseif ($this->selectedServerType === 'remote') {
@@ -175,7 +175,7 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
return;
}
$this->selectedExistingPrivateKey = $this->createdServer->privateKey->id;
$this->serverPublicKey = $this->createdServer->privateKey->publicKey();
$this->serverPublicKey = $this->createdServer->privateKey->getPublicKey();
$this->updateServerDetails();
$this->currentState = 'validate-server';
}
@@ -231,17 +231,24 @@ uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
public function savePrivateKey()
{
$this->validate([
'privateKeyName' => 'required',
'privateKey' => 'required',
'privateKeyName' => 'required|string|max:255',
'privateKeyDescription' => 'nullable|string|max:255',
'privateKey' => 'required|string',
]);
$this->createdPrivateKey = PrivateKey::create([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id,
]);
$this->createdPrivateKey->save();
$this->currentState = 'create-server';
try {
$privateKey = PrivateKey::createAndStore([
'name' => $this->privateKeyName,
'description' => $this->privateKeyDescription,
'private_key' => $this->privateKey,
'team_id' => currentTeam()->id,
]);
$this->createdPrivateKey = $privateKey;
$this->currentState = 'create-server';
} catch (\Exception $e) {
$this->addError('privateKey', 'Failed to save private key: '.$e->getMessage());
}
}
public function saveServer()

View File

@@ -30,7 +30,6 @@ class Dashboard extends Component
public function cleanup_queue()
{
$this->dispatch('success', 'Cleanup started.');
Artisan::queue('cleanup:application-deployment-queue', [
'--team-id' => currentTeam()->id,
]);

View File

@@ -38,7 +38,7 @@ class Form extends Component
}
$this->destination->delete();
return redirect()->route('dashboard');
return redirect()->route('destination.all');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -2,13 +2,28 @@
namespace App\Livewire;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class NavbarDeleteTeam extends Component
{
public function delete()
public $team;
public function mount()
{
$this->team = currentTeam()->name;
}
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$currentTeam = currentTeam();
$currentTeam->delete();

View File

@@ -4,7 +4,6 @@ namespace App\Livewire\Project\Application\Deployment;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use Illuminate\Support\Collection;
use Livewire\Component;
class Show extends Component

View File

@@ -21,6 +21,8 @@ class Heading extends Component
protected string $deploymentUuid;
public bool $docker_cleanup = true;
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
@@ -102,7 +104,7 @@ class Heading extends Component
public function stop()
{
StopApplication::run($this->application);
StopApplication::run($this->application, false, $this->docker_cleanup);
$this->application->status = 'exited';
$this->application->save();
if ($this->application->additional_servers->count() > 0) {
@@ -135,4 +137,13 @@ class Heading extends Component
'environment_name' => $this->parameters['environment_name'],
]);
}
public function render()
{
return view('livewire.project.application.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
}

View File

@@ -5,7 +5,9 @@ namespace App\Livewire\Project\Application;
use App\Actions\Docker\GetContainersStatus;
use App\Models\Application;
use App\Models\ApplicationPreview;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
use Spatie\Url\Url;
use Visus\Cuid2\Cuid2;
@@ -184,17 +186,20 @@ class Previews extends Component
public function stop(int $pull_request_id)
{
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}"], $this->application->destination->server);
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
}
GetContainersStatus::dispatchSync($this->application->destination->server)->onQueue('high');
$this->dispatch('reloadWindow');
GetContainersStatus::run($server);
$this->application->refresh();
$this->dispatch('containerStatusUpdated');
$this->dispatch('success', 'Preview Deployment stopped.');
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -203,16 +208,21 @@ class Previews extends Component
public function delete(int $pull_request_id)
{
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}"], $this->application->destination->server);
instant_remote_process(["docker stack rm {$this->application->uuid}-{$pull_request_id}"], $server);
} else {
$containers = getCurrentApplicationContainerStatus($this->application->destination->server, $this->application->id, $pull_request_id);
foreach ($containers as $container) {
$name = str_replace('/', '', $container['Names']);
instant_remote_process(["docker rm -f $name"], $this->application->destination->server, throwError: false);
}
$containers = getCurrentApplicationContainerStatus($server, $this->application->id, $pull_request_id)->toArray();
$this->stopContainers($containers, $server, $timeout);
}
ApplicationPreview::where('application_id', $this->application->id)->where('pull_request_id', $pull_request_id)->first()->delete();
ApplicationPreview::where('application_id', $this->application->id)
->where('pull_request_id', $pull_request_id)
->first()
->delete();
$this->application->refresh();
$this->dispatch('update_links');
$this->dispatch('success', 'Preview deleted.');
@@ -220,4 +230,49 @@ class Previews extends Component
return handleError($e, $this);
}
}
private function stopContainers(array $containers, $server, int $timeout)
{
$processes = [];
foreach ($containers as $container) {
$containerName = str_replace('/', '', $container['Names']);
$processes[$containerName] = $this->stopContainer($containerName, $timeout);
}
$startTime = time();
while (count($processes) > 0) {
$finishedProcesses = array_filter($processes, function ($process) {
return ! $process->running();
});
foreach (array_keys($finishedProcesses) as $containerName) {
unset($processes[$containerName]);
$this->removeContainer($containerName, $server);
}
if (time() - $startTime >= $timeout) {
$this->forceStopRemainingContainers(array_keys($processes), $server);
break;
}
usleep(100000);
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function removeContainer(string $containerName, $server)
{
instant_remote_process(["docker rm -f $containerName"], $server, throwError: false);
}
private function forceStopRemainingContainers(array $containerNames, $server)
{
foreach ($containerNames as $containerName) {
instant_remote_process(["docker kill $containerName"], $server, throwError: false);
$this->removeContainer($containerName, $server);
}
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Spatie\Url\Url;
@@ -12,6 +14,12 @@ class BackupEdit extends Component
public $s3s;
public bool $delete_associated_backups_locally = false;
public bool $delete_associated_backups_s3 = false;
public bool $delete_associated_backups_sftp = false;
public ?string $status = null;
public array $parameters;
@@ -46,10 +54,24 @@ class BackupEdit extends Component
}
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
if ($this->delete_associated_backups_locally) {
$this->deleteAssociatedBackupsLocally();
}
if ($this->delete_associated_backups_s3) {
$this->deleteAssociatedBackupsS3();
}
$this->backup->delete();
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$previousUrl = url()->previous();
$url = Url::fromString($previousUrl);
@@ -104,4 +126,66 @@ class BackupEdit extends Component
$this->dispatch('error', $e->getMessage());
}
}
public function deleteAssociatedBackupsLocally()
{
$executions = $this->backup->executions;
$backupFolder = null;
foreach ($executions as $execution) {
if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
$server = $this->backup->database->service->destination->server;
} else {
$server = $this->backup->database->destination->server;
}
if (! $backupFolder) {
$backupFolder = dirname($execution->filename);
}
delete_backup_locally($execution->filename, $server);
$execution->delete();
}
if ($backupFolder) {
$this->deleteEmptyBackupFolder($backupFolder, $server);
}
}
public function deleteAssociatedBackupsS3()
{
//Add function to delete backups from S3
}
public function deleteAssociatedBackupsSftp()
{
//Add function to delete backups from SFTP
}
private function deleteEmptyBackupFolder($folderPath, $server)
{
$checkEmpty = instant_remote_process(["[ -z \"$(ls -A '$folderPath')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkEmpty) === 'empty') {
instant_remote_process(["rmdir '$folderPath'"], $server);
$parentFolder = dirname($folderPath);
$checkParentEmpty = instant_remote_process(["[ -z \"$(ls -A '$parentFolder')\" ] && echo 'empty' || echo 'not empty'"], $server);
if (trim($checkParentEmpty) === 'empty') {
instant_remote_process(["rmdir '$parentFolder'"], $server);
}
}
}
public function render()
{
return view('livewire.project.database.backup-edit', [
'checkboxes' => [
['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected S3 Storage.']
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this backup job from this database will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -3,18 +3,28 @@
namespace App\Livewire\Project\Database;
use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\On;
use Livewire\Component;
class BackupExecutions extends Component
{
public ?ScheduledDatabaseBackup $backup = null;
public $database;
public $executions = [];
public $setDeletableBackup;
public $delete_backup_s3 = true;
public $delete_backup_sftp = true;
public function getListeners()
{
$userId = auth()->user()->id;
$userId = Auth::id();
return [
"echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions',
@@ -31,19 +41,36 @@ class BackupExecutions extends Component
}
}
public function deleteBackup($exeuctionId)
#[On('deleteBackup')]
public function deleteBackup($executionId, $password)
{
$execution = $this->backup->executions()->where('id', $exeuctionId)->first();
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$execution = $this->backup->executions()->where('id', $executionId)->first();
if (is_null($execution)) {
$this->dispatch('error', 'Backup execution not found.');
return;
}
if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server);
} else {
delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server);
}
if ($this->delete_backup_s3) {
// Add logic to delete from S3
}
if ($this->delete_backup_sftp) {
// Add logic to delete from SFTP
}
$execution->delete();
$this->dispatch('success', 'Backup deleted.');
$this->refreshBackupExecutions();
@@ -82,16 +109,18 @@ class BackupExecutions extends Component
return $server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -104,6 +133,17 @@ class BackupExecutions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
public function render()
{
return view('livewire.project.database.backup-executions', [
'checkboxes' => [
['id' => 'delete_backup_s3', 'label' => 'Delete the selected backup permanently form S3 Storage'],
['id' => 'delete_backup_sftp', 'label' => 'Delete the selected backup permanently form SFTP Storage'],
],
]);
}
}

View File

@@ -56,7 +56,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -73,14 +73,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -95,7 +95,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -54,7 +54,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -14,6 +14,8 @@ class Heading extends Component
public array $parameters;
public $docker_cleanup = true;
public function getListeners()
{
$userId = auth()->user()->id;
@@ -54,7 +56,7 @@ class Heading extends Component
public function stop()
{
StopDatabase::run($this->database);
StopDatabase::run($this->database, false, $this->docker_cleanup);
$this->database->status = 'exited';
$this->database->save();
$this->check_status();
@@ -71,4 +73,13 @@ class Heading extends Component
$activity = StartDatabase::run($this->database);
$this->dispatch('activityMonitor', $activity->id);
}
public function render()
{
return view('livewire.project.database.heading', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => 'Cleanup docker build cache and unused images (next deployment could take longer).'],
],
]);
}
}

View File

@@ -57,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -94,14 +94,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -116,7 +116,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -63,7 +63,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -100,14 +100,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -122,7 +122,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -61,7 +61,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -101,14 +101,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -123,7 +123,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -62,7 +62,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -99,14 +99,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -121,7 +121,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -57,7 +57,7 @@ class General extends Component
public function instantSaveAdvanced()
{
try {
if (!$this->server->isLogDrainEnabled()) {
if (! $this->server->isLogDrainEnabled()) {
$this->database->is_log_drain_enabled = false;
$this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.');
@@ -88,14 +88,14 @@ class General extends Component
public function instantSave()
{
try {
if ($this->database->is_public && !$this->database->public_port) {
if ($this->database->is_public && ! $this->database->public_port) {
$this->dispatch('error', 'Public port is required.');
$this->database->is_public = false;
return;
}
if ($this->database->is_public) {
if (!str($this->database->status)->startsWith('running')) {
if (! str($this->database->status)->startsWith('running')) {
$this->dispatch('error', 'Database must be started to be publicly accessible.');
$this->database->is_public = false;
@@ -110,7 +110,7 @@ class General extends Component
$this->db_url_public = $this->database->external_db_url;
$this->database->save();
} catch (\Throwable $e) {
$this->database->is_public = !$this->database->is_public;
$this->database->is_public = ! $this->database->is_public;
return handleError($e, $this);
}

View File

@@ -13,9 +13,12 @@ class DeleteEnvironment extends Component
public bool $disabled = false;
public string $environmentName = '';
public function mount()
{
$this->parameters = get_route_parameters();
$this->environmentName = Environment::findOrFail($this->environment_id)->name;
}
public function delete()

View File

@@ -13,9 +13,12 @@ class DeleteProject extends Component
public bool $disabled = false;
public string $projectName = '';
public function mount()
{
$this->parameters = get_route_parameters();
$this->projectName = Project::findOrFail($this->project_id)->name;
}
public function delete()

View File

@@ -52,7 +52,7 @@ class Configuration extends Component
$application = $this->service->applications->find($id);
if ($application) {
$application->restart();
$this->dispatch('success', 'Application restarted successfully.');
$this->dispatch('success', 'Service application restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);
@@ -65,7 +65,7 @@ class Configuration extends Component
$database = $this->service->databases->find($id);
if ($database) {
$database->restart();
$this->dispatch('success', 'Database restarted successfully.');
$this->dispatch('success', 'Service database restarted successfully.');
}
} catch (\Exception $e) {
return handleError($e, $this);

View File

@@ -14,6 +14,8 @@ use App\Models\StandaloneMongodb;
use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class FileStorage extends Component
@@ -83,8 +85,14 @@ class FileStorage extends Component
}
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$message = 'File deleted.';
if ($this->fileStorage->is_directory) {
@@ -129,6 +137,13 @@ class FileStorage extends Component
public function render()
{
return view('livewire.project.service.file-storage');
return view('livewire.project.service.file-storage', [
'directoryDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected directory and all its contents will be permantely deleted form the server.'],
],
'fileDeletionCheckboxes' => [
['id' => 'permanently_delete', 'label' => 'The selected file will be permanently deleted form the server.'],
],
]);
}
}

View File

@@ -20,6 +20,8 @@ class Navbar extends Component
public $isDeploymentProgress = false;
public $docker_cleanup = true;
public $title = 'Configuration';
public function mount()
@@ -42,7 +44,7 @@ class Navbar extends Component
public function serviceStarted()
{
$this->dispatch('success', 'Service status changed.');
// $this->dispatch('success', 'Service status changed.');
if (is_null($this->service->config_hash) || $this->service->isConfigurationChanged()) {
$this->service->isConfigurationChanged(true);
$this->dispatch('configurationChanged');
@@ -62,11 +64,6 @@ class Navbar extends Component
$this->dispatch('success', 'Service status updated.');
}
public function render()
{
return view('livewire.project.service.navbar');
}
public function checkDeployments()
{
try {
@@ -97,14 +94,9 @@ class Navbar extends Component
$this->dispatch('activityMonitor', $activity->id);
}
public function stop(bool $forceCleanup = false)
public function stop()
{
StopService::run($this->service);
if ($forceCleanup) {
$this->dispatch('success', 'Containers cleaned up.');
} else {
$this->dispatch('success', 'Service stopped.');
}
StopService::run($this->service, false, $this->docker_cleanup);
ServiceStatusChanged::dispatch();
}
@@ -123,4 +115,13 @@ class Navbar extends Component
$activity = StartService::run($this->service);
$this->dispatch('activityMonitor', $activity->id);
}
public function render()
{
return view('livewire.project.service.navbar', [
'checkboxes' => [
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
],
]);
}
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Service;
use App\Models\ServiceApplication;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class ServiceApplicationView extends Component
@@ -11,6 +13,10 @@ class ServiceApplicationView extends Component
public $parameters;
public $docker_cleanup = true;
public $delete_volumes = true;
protected $rules = [
'application.human_name' => 'nullable',
'application.description' => 'nullable',
@@ -23,11 +29,6 @@ class ServiceApplicationView extends Component
'application.is_stripprefix_enabled' => 'nullable|boolean',
];
public function render()
{
return view('livewire.project.service.service-application-view');
}
public function updatedApplicationFqdn()
{
$this->application->fqdn = str($this->application->fqdn)->replaceEnd(',', '')->trim();
@@ -56,8 +57,14 @@ class ServiceApplicationView extends Component
$this->dispatch('success', 'You need to restart the service for the changes to take effect.');
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$this->application->delete();
$this->dispatch('success', 'Application deleted.');
@@ -91,4 +98,17 @@ class ServiceApplicationView extends Component
$this->dispatch('generateDockerCompose');
}
}
public function render()
{
return view('livewire.project.service.service-application-view', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -33,7 +33,7 @@ class StackForm extends Component
$key = data_get($field, 'key');
$value = data_get($field, 'value');
$rules = data_get($field, 'rules', 'nullable');
$isPassword = data_get($field, 'isPassword');
$isPassword = data_get($field, 'isPassword', false);
$this->fields->put($key, [
'serviceName' => $serviceName,
'key' => $key,
@@ -47,7 +47,15 @@ class StackForm extends Component
$this->validationAttributes["fields.$key.value"] = $fieldKey;
}
}
$this->fields = $this->fields->sortBy('name');
$this->fields = $this->fields->groupBy('serviceName')->map(function ($group) {
return $group->sortBy(function ($field) {
return data_get($field, 'isPassword') ? 1 : 0;
})->mapWithKeys(function ($field) {
return [$field['key'] => $field];
});
})->flatMap(function ($group) {
return $group;
});
}
public function saveCompose($raw)

View File

@@ -3,6 +3,11 @@
namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob;
use App\Models\Service;
use App\Models\ServiceApplication;
use App\Models\ServiceDatabase;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -10,6 +15,8 @@ class Danger extends Component
{
public $resource;
public $resourceName;
public $projectUuid;
public $environmentName;
@@ -18,22 +25,93 @@ class Danger extends Component
public bool $delete_volumes = true;
public bool $docker_cleanup = true;
public bool $delete_connected_networks = true;
public ?string $modalId = null;
public string $resourceDomain = '';
public function mount()
{
$this->modalId = new Cuid2;
$parameters = get_route_parameters();
$this->modalId = new Cuid2;
$this->projectUuid = data_get($parameters, 'project_uuid');
$this->environmentName = data_get($parameters, 'environment_name');
if ($this->resource === null) {
if (isset($parameters['service_uuid'])) {
$this->resource = Service::where('uuid', $parameters['service_uuid'])->first();
} elseif (isset($parameters['stack_service_uuid'])) {
$this->resource = ServiceApplication::where('uuid', $parameters['stack_service_uuid'])->first()
?? ServiceDatabase::where('uuid', $parameters['stack_service_uuid'])->first();
}
}
if ($this->resource === null) {
$this->resourceName = 'Unknown Resource';
return;
}
if (! method_exists($this->resource, 'type')) {
$this->resourceName = 'Unknown Resource';
return;
}
switch ($this->resource->type()) {
case 'application':
$this->resourceName = $this->resource->name ?? 'Application';
break;
case 'standalone-postgresql':
case 'standalone-redis':
case 'standalone-mongodb':
case 'standalone-mysql':
case 'standalone-mariadb':
case 'standalone-keydb':
case 'standalone-dragonfly':
case 'standalone-clickhouse':
$this->resourceName = $this->resource->name ?? 'Database';
break;
case 'service':
$this->resourceName = $this->resource->name ?? 'Service';
break;
case 'service-application':
$this->resourceName = $this->resource->name ?? 'Service Application';
break;
case 'service-database':
$this->resourceName = $this->resource->name ?? 'Service Database';
break;
default:
$this->resourceName = 'Unknown Resource';
}
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! $this->resource) {
$this->addError('resource', 'Resource not found.');
return;
}
try {
// $this->authorize('delete', $this->resource);
$this->resource->delete();
DeleteResourceJob::dispatch($this->resource, $this->delete_configurations, $this->delete_volumes);
DeleteResourceJob::dispatch(
$this->resource,
$this->delete_configurations,
$this->delete_volumes,
$this->docker_cleanup,
$this->delete_connected_networks
);
return redirect()->route('project.resource.index', [
'project_uuid' => $this->projectUuid,
@@ -43,4 +121,19 @@ class Danger extends Component
return handleError($e, $this);
}
}
public function render()
{
return view('livewire.project.shared.danger', [
'checkboxes' => [
['id' => 'delete_volumes', 'label' => __('resource.delete_volumes')],
['id' => 'delete_connected_networks', 'label' => __('resource.delete_connected_networks')],
['id' => 'delete_configurations', 'label' => __('resource.delete_configurations')],
['id' => 'docker_cleanup', 'label' => __('resource.docker_cleanup')],
// ['id' => 'delete_associated_backups_locally', 'label' => 'All backups associated with this Ressource will be permanently deleted from local storage.'],
// ['id' => 'delete_associated_backups_s3', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected S3 Storage.'],
// ['id' => 'delete_associated_backups_sftp', 'label' => 'All backups associated with this Ressource will be permanently deleted from the selected SFTP Storage.']
],
]);
}
}

View File

@@ -8,6 +8,8 @@ use App\Events\ApplicationStatusChanged;
use App\Jobs\ContainerStatusJob;
use App\Models\Server;
use App\Models\StandaloneDocker;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
use Visus\Cuid2\Cuid2;
@@ -115,8 +117,14 @@ class Destination extends Component
ApplicationStatusChanged::dispatch(data_get($this->resource, 'environment.project.team.id'));
}
public function removeServer(int $network_id, int $server_id)
public function removeServer(int $network_id, int $server_id, $password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
$this->dispatch('error', 'You cannot remove this destination server.', 'You are trying to remove the main server.');

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Application;
use App\Models\Server;
use App\Models\Service;
@@ -108,14 +109,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} -t {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
} else {
if ($this->server->isSwarm()) {
@@ -124,14 +125,14 @@ class GetLogs extends Component
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
} else {
$command = "docker logs -n {$this->numberOfLines} {$this->container}";
if ($this->server->isNonRoot()) {
$command = parseCommandsByLineForSudo(collect($command), $this->server);
$command = $command[0];
}
$sshCommand = generateSshCommand($this->server, $command);
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
}
}
if ($refresh) {

View File

@@ -7,7 +7,9 @@ use Livewire\Component;
class Executions extends Component
{
public $executions = [];
public $selectedKey;
public $task;
public function getListeners()
@@ -29,7 +31,7 @@ class Executions extends Component
public function server()
{
if (!$this->task) {
if (! $this->task) {
return null;
}
@@ -42,16 +44,18 @@ class Executions extends Component
return $this->task->service->destination->server;
}
}
return null;
}
public function getServerTimezone()
{
$server = $this->server();
if (!$server) {
if (! $server) {
return 'UTC';
}
$serverTimezone = $server->settings->server_timezone;
return $serverTimezone;
}
@@ -64,6 +68,7 @@ class Executions extends Component
} catch (\Exception $e) {
$dateObj->setTimezone(new \DateTimeZone('UTC'));
}
return $dateObj->format('Y-m-d H:i:s T');
}
}

View File

@@ -20,6 +20,8 @@ class Show extends Component
public string $type;
public string $scheduledTaskName;
protected $rules = [
'task.enabled' => 'required|boolean',
'task.name' => 'required|string',
@@ -49,6 +51,7 @@ class Show extends Component
$this->modalId = new Cuid2;
$this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first();
$this->scheduledTaskName = $this->task->name;
}
public function instantSave()
@@ -75,9 +78,9 @@ class Show extends Component
$this->task->delete();
if ($this->type == 'application') {
return redirect()->route('project.application.configuration', $this->parameters);
return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName);
} else {
return redirect()->route('project.service.configuration', $this->parameters);
return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName);
}
} catch (\Exception $e) {
return handleError($e);

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Project\Shared\Storages;
use App\Models\LocalPersistentVolume;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Show extends Component
@@ -36,8 +38,14 @@ class Show extends Component
$this->dispatch('success', 'Storage updated successfully');
}
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
$this->storage->delete();
$this->dispatch('refreshStorages');
}

View File

@@ -2,12 +2,27 @@
namespace App\Livewire\Project\Shared;
use App\Helpers\SshMultiplexingHelper;
use App\Models\Server;
use Livewire\Attributes\On;
use Livewire\Component;
class Terminal extends Component
{
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},ApplicationStatusChanged" => 'closeTerminal',
];
}
public function closeTerminal()
{
$this->dispatch('reloadWindow');
}
#[On('send-terminal-command')]
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
{
@@ -19,9 +34,9 @@ class Terminal extends Component
if ($status !== 'running') {
return;
}
$command = generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$identifier} sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
} else {
$command = generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
$command = SshMultiplexingHelper::generateSshCommand($server, "sh -c 'if [ -f ~/.profile ]; then . ~/.profile; fi; if [ -n \"\$SHELL\" ]; then exec \$SHELL; else sh; fi'");
}
// ssh command is sent back to frontend then to websocket

View File

@@ -3,17 +3,13 @@
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
use Livewire\Component;
use phpseclib3\Crypt\PublicKeyLoader;
class Create extends Component
{
use WithRateLimiting;
public string $name = '';
public string $name;
public string $value;
public string $value = '';
public ?string $from = null;
@@ -26,72 +22,69 @@ class Create extends Component
'value' => 'required|string',
];
protected $validationAttributes = [
'name' => 'name',
'value' => 'private Key',
];
public function generateNewRSAKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey();
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->generateNewKey('rsa');
}
public function generateNewEDKey()
{
try {
$this->rateLimit(10);
$this->name = generate_random_name();
$this->description = 'Created by Coolify';
['private' => $this->value, 'public' => $this->publicKey] = generateSSHKey('ed25519');
} catch (\Throwable $e) {
return handleError($e, $this);
}
$this->generateNewKey('ed25519');
}
public function updated($updateProperty)
private function generateNewKey($type)
{
if ($updateProperty === 'value') {
try {
$this->publicKey = PublicKeyLoader::load($this->$updateProperty)->getPublicKey()->toString('OpenSSH', ['comment' => '']);
} catch (\Throwable $e) {
if ($this->$updateProperty === '') {
$this->publicKey = '';
} else {
$this->publicKey = 'Invalid private key';
}
}
$keyData = PrivateKey::generateNewKeyPair($type);
$this->setKeyData($keyData);
}
public function updated($property)
{
if ($property === 'value') {
$this->validatePrivateKey();
}
$this->validateOnly($updateProperty);
}
public function createPrivateKey()
{
$this->validate();
try {
$this->value = trim($this->value);
if (! str_ends_with($this->value, "\n")) {
$this->value .= "\n";
}
$private_key = PrivateKey::create([
$privateKey = PrivateKey::createAndStore([
'name' => $this->name,
'description' => $this->description,
'private_key' => $this->value,
'private_key' => trim($this->value)."\n",
'team_id' => currentTeam()->id,
]);
if ($this->from === 'server') {
return redirect()->route('dashboard');
}
return redirect()->route('security.private-key.show', ['private_key_uuid' => $private_key->uuid]);
return $this->redirectAfterCreation($privateKey);
} catch (\Throwable $e) {
return handleError($e, $this);
}
}
private function setKeyData(array $keyData)
{
$this->name = $keyData['name'];
$this->description = $keyData['description'];
$this->value = $keyData['private_key'];
$this->publicKey = $keyData['public_key'];
}
private function validatePrivateKey()
{
$validationResult = PrivateKey::validateAndExtractPublicKey($this->value);
$this->publicKey = $validationResult['publicKey'];
if (! $validationResult['isValid']) {
$this->addError('value', 'Invalid private key');
}
}
private function redirectAfterCreation(PrivateKey $privateKey)
{
return $this->from === 'server'
? redirect()->route('dashboard')
: redirect()->route('security.private-key.show', ['private_key_uuid' => $privateKey->uuid]);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Livewire\Security\PrivateKey;
use App\Models\PrivateKey;
use Livewire\Component;
class Index extends Component
{
public function render()
{
$privateKeys = PrivateKey::ownedByCurrentTeam(['name', 'uuid', 'is_git_related', 'description'])->get();
return view('livewire.security.private-key.index', [
'privateKeys' => $privateKeys,
])->layout('components.layout');
}
public function cleanupUnusedKeys()
{
PrivateKey::cleanupUnusedKeys();
$this->dispatch('success', 'Unused keys have been cleaned up.');
}
}

View File

@@ -29,25 +29,27 @@ class Show extends Component
try {
$this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail();
} catch (\Throwable $e) {
return handleError($e, $this);
abort(404);
}
}
public function loadPublicKey()
{
$this->public_key = $this->private_key->publicKey();
$this->public_key = $this->private_key->getPublicKey();
if ($this->public_key === 'Error loading private key') {
$this->dispatch('error', 'Failed to load public key. The private key may be invalid.');
}
}
public function delete()
{
try {
if ($this->private_key->isEmpty()) {
$this->private_key->delete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
$this->private_key->safeDelete();
currentTeam()->privateKeys = PrivateKey::where('team_id', currentTeam()->id)->get();
return redirect()->route('security.private-key.index');
}
$this->dispatch('error', 'This private key is in use and cannot be deleted. Please delete all servers, applications, and GitHub/GitLab apps that use this private key before deleting it.');
return redirect()->route('security.private-key.index');
} catch (\Exception $e) {
$this->dispatch('error', $e->getMessage());
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -56,8 +58,9 @@ class Show extends Component
public function changePrivateKey()
{
try {
$this->private_key->private_key = formatPrivateKey($this->private_key->private_key);
$this->private_key->save();
$this->private_key->updatePrivateKey([
'private_key' => formatPrivateKey($this->private_key->private_key),
]);
refresh_server_connection($this->private_key);
$this->dispatch('success', 'Private key updated.');
} catch (\Throwable $e) {

View File

@@ -31,13 +31,12 @@ class ConfigureCloudflareTunnels extends Component
{
try {
$server = Server::ownedByCurrentTeam()->where('id', $this->server_id)->firstOrFail();
ConfigureCloudflared::run($server, $this->cloudflare_token);
ConfigureCloudflared::dispatch($server, $this->cloudflare_token);
$server->settings->is_cloudflare_tunnel = true;
$server->ip = $this->ssh_domain;
$server->save();
$server->settings->save();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
$this->dispatch('refreshServerShow');
$this->dispatch('warning', 'Cloudflare Tunnels configuration started.');
} catch (\Throwable $e) {
return handleError($e, $this);
}

View File

@@ -3,6 +3,8 @@
namespace App\Livewire\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class Delete extends Component
@@ -11,8 +13,13 @@ class Delete extends Component
public $server;
public function delete()
public function delete($password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
try {
$this->authorize('delete', $this->server);
if ($this->server->hasDefinedResources()) {

View File

@@ -28,11 +28,16 @@ class Form extends Component
public $delete_unused_volumes = false;
public $delete_unused_networks = false;
protected $listeners = [
'serverInstalled',
'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh',
];
public function getListeners()
{
$teamId = auth()->user()->currentTeam()->id;
return [
"echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured',
'refreshServerShow' => 'serverInstalled',
'revalidate' => '$refresh',
];
}
protected $rules = [
'server.name' => 'required',
@@ -106,6 +111,12 @@ class Form extends Component
}
}
public function cloudflareTunnelConfigured()
{
$this->serverInstalled();
$this->dispatch('success', 'Cloudflare Tunnels configured successfully.');
}
public function serverInstalled()
{
$this->server->refresh();
@@ -260,4 +271,11 @@ class Form extends Component
return handleError($e, $this);
}
}
public function manualCloudflareConfig()
{
$this->server->settings->is_cloudflare_tunnel = true;
$this->server->settings->save();
$this->server->refresh();
$this->dispatch('success', 'Cloudflare Tunnels enabled.');
}
}

View File

@@ -39,6 +39,7 @@ class Proxy extends Component
{
$this->server->proxy = null;
$this->server->save();
$this->dispatch('proxyChanged');
}
public function selectProxy($proxy_type)
@@ -47,7 +48,7 @@ class Proxy extends Component
$this->server->proxy->set('type', $proxy_type);
$this->server->save();
$this->selectedProxy = $this->server->proxy->type;
if ($this->selectedProxy !== 'NONE') {
if ($this->server->proxySet()) {
StartProxy::run($this->server, false);
}
$this->dispatch('proxyStatusUpdated');

View File

@@ -6,6 +6,8 @@ use App\Actions\Proxy\CheckProxy;
use App\Actions\Proxy\StartProxy;
use App\Events\ProxyStatusChanged;
use App\Models\Server;
use Illuminate\Process\InvokedProcess;
use Illuminate\Support\Facades\Process;
use Livewire\Component;
class Deploy extends Component
@@ -29,6 +31,7 @@ class Deploy extends Component
'serverRefresh' => 'proxyStatusUpdated',
'checkProxy',
'startProxy',
'proxyChanged' => 'proxyStatusUpdated',
];
}
@@ -94,21 +97,43 @@ class Deploy extends Component
public function stop(bool $forceStop = true)
{
try {
if ($this->server->isSwarm()) {
instant_remote_process([
'docker service rm coolify-proxy_traefik',
], $this->server);
} else {
instant_remote_process([
'docker rm -f coolify-proxy',
], $this->server);
$containerName = $this->server->isSwarm() ? 'coolify-proxy_traefik' : 'coolify-proxy';
$timeout = 30;
$process = $this->stopContainer($containerName, $timeout);
$startTime = time();
while ($process->running()) {
if (time() - $startTime >= $timeout) {
$this->forceStopContainer($containerName);
break;
}
usleep(100000);
}
$this->server->proxy->status = 'exited';
$this->server->proxy->force_stop = $forceStop;
$this->server->save();
$this->dispatch('proxyStatusUpdated');
$this->removeContainer($containerName);
} catch (\Throwable $e) {
return handleError($e, $this);
} finally {
$this->server->proxy->force_stop = $forceStop;
$this->server->proxy->status = 'exited';
$this->server->save();
$this->dispatch('proxyStatusUpdated');
}
}
private function stopContainer(string $containerName, int $timeout): InvokedProcess
{
return Process::timeout($timeout)->start("docker stop --time=$timeout $containerName");
}
private function forceStopContainer(string $containerName)
{
instant_remote_process(["docker kill $containerName"], $this->server, throwError: false);
}
private function removeContainer(string $containerName)
{
instant_remote_process(["docker rm -f $containerName"], $this->server, throwError: false);
}
}

View File

@@ -11,7 +11,7 @@ class Show extends Component
public $parameters = [];
protected $listeners = ['proxyStatusUpdated'];
protected $listeners = ['proxyStatusUpdated', 'proxyChanged' => 'proxyStatusUpdated'];
public function proxyStatusUpdated()
{

View File

@@ -49,6 +49,10 @@ class Status extends Component
if ($this->server->proxy->status === 'running') {
$this->polling = false;
$notification && $this->dispatch('success', 'Proxy is running.');
} elseif ($this->server->proxy->status === 'exited' and ! $this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy has exited.');
} elseif ($this->server->proxy->force_stop) {
$notification && $this->dispatch('error', 'Proxy is stopped manually.');
} else {
$notification && $this->dispatch('error', 'Proxy is not running.');
}

View File

@@ -2,6 +2,7 @@
namespace App\Livewire\Server;
use App\Models\PrivateKey;
use App\Models\Server;
use Livewire\Component;
@@ -13,25 +14,15 @@ class ShowPrivateKey extends Component
public $parameters;
public function setPrivateKey($newPrivateKeyId)
public function setPrivateKey($privateKeyId)
{
try {
$oldPrivateKeyId = $this->server->private_key_id;
refresh_server_connection($this->server->privateKey);
$this->server->update([
'private_key_id' => $newPrivateKeyId,
]);
$privateKey = PrivateKey::findOrFail($privateKeyId);
$this->server->update(['private_key_id' => $privateKey->id]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
$this->checkConnection();
} catch (\Throwable $e) {
$this->server->update([
'private_key_id' => $oldPrivateKeyId,
]);
$this->server->refresh();
refresh_server_connection($this->server->privateKey);
return handleError($e, $this);
$this->dispatch('success', 'Private key updated successfully.');
} catch (\Exception $e) {
$this->dispatch('error', 'Failed to update private key: '.$e->getMessage());
}
}
@@ -43,7 +34,7 @@ class ShowPrivateKey extends Component
$this->dispatch('success', 'Server is reachable.');
} else {
ray($error);
$this->dispatch('error', 'Server is not reachable.<br>Please validate your configuration and connection.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.');
$this->dispatch('error', 'Server is not reachable.<br><br>Check this <a target="_blank" class="underline" href="https://coolify.io/docs/knowledge-base/server/openssh">documentation</a> for further help.<br><br>Error: '.$error);
return;
}

View File

@@ -4,6 +4,8 @@ namespace App\Livewire\Team;
use App\Models\Team;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component;
class AdminView extends Component
@@ -73,8 +75,13 @@ class AdminView extends Component
$team->delete();
}
public function delete($id)
public function delete($id, $password)
{
if (! Hash::check($password, Auth::user()->password)) {
$this->addError('password', 'The provided password is incorrect.');
return;
}
if (! auth()->user()->isInstanceAdmin()) {
return $this->dispatch('error', 'You are not authorized to delete users');
}