mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 10:14:47 +00:00
Merge branch 'next' into feat/servicedatabase-restore
This commit is contained in:
commit
360a82ec5d
290 files changed
+18733
-3489
No files matched your search
@@ -20,12 +20,13 @@ class Show extends Component
|
||||
|
||||
public bool $is_debug_enabled = false;
|
||||
|
||||
public bool $fullscreen = false;
|
||||
|
||||
private bool $deploymentFinishedDispatched = false;
|
||||
|
||||
public function getListeners()
|
||||
{
|
||||
$teamId = auth()->user()->currentTeam()->id;
|
||||
|
||||
return [
|
||||
"echo-private:team.{$teamId},ServiceChecked" => '$refresh',
|
||||
'refreshQueue',
|
||||
];
|
||||
}
|
||||
@@ -91,24 +92,52 @@ class Show extends Component
|
||||
|
||||
public function polling()
|
||||
{
|
||||
$this->dispatch('deploymentFinished');
|
||||
$this->application_deployment_queue->refresh();
|
||||
$this->horizon_job_status = $this->application_deployment_queue->getHorizonJobStatus();
|
||||
$this->isKeepAliveOn();
|
||||
|
||||
// Dispatch event when deployment finishes to stop auto-scroll (only once)
|
||||
if (! $this->isKeepAliveOn && ! $this->deploymentFinishedDispatched) {
|
||||
$this->deploymentFinishedDispatched = true;
|
||||
$this->dispatch('deploymentFinished');
|
||||
}
|
||||
}
|
||||
|
||||
public function getLogLinesProperty()
|
||||
{
|
||||
return decode_remote_command_output($this->application_deployment_queue)->map(function ($logLine) {
|
||||
$logLine['line'] = e($logLine['line']);
|
||||
$logLine['line'] = preg_replace(
|
||||
'/(https?:\/\/[^\s]+)/',
|
||||
'<a href="$1" target="_blank" rel="noopener noreferrer" class="underline text-neutral-400">$1</a>',
|
||||
$logLine['line'],
|
||||
);
|
||||
return decode_remote_command_output($this->application_deployment_queue);
|
||||
}
|
||||
|
||||
return $logLine;
|
||||
});
|
||||
public function copyLogs(): string
|
||||
{
|
||||
$logs = decode_remote_command_output($this->application_deployment_queue)
|
||||
->map(function ($line) {
|
||||
return $line['timestamp'].' '.
|
||||
(isset($line['command']) && $line['command'] ? '[CMD]: ' : '').
|
||||
trim($line['line']);
|
||||
})
|
||||
->join("\n");
|
||||
|
||||
return sanitizeLogsForExport($logs);
|
||||
}
|
||||
|
||||
public function downloadAllLogs(): string
|
||||
{
|
||||
$logs = decode_remote_command_output($this->application_deployment_queue, includeAll: true)
|
||||
->map(function ($line) {
|
||||
$prefix = '';
|
||||
if ($line['hidden']) {
|
||||
$prefix = '[DEBUG] ';
|
||||
}
|
||||
if (isset($line['command']) && $line['command']) {
|
||||
$prefix .= '[CMD]: ';
|
||||
}
|
||||
|
||||
return $line['timestamp'].' '.$prefix.trim($line['line']);
|
||||
})
|
||||
->join("\n");
|
||||
|
||||
return sanitizeLogsForExport($logs);
|
||||
}
|
||||
|
||||
public function render()
|
||||
|
||||
@@ -558,8 +558,11 @@ class General extends Component
|
||||
$this->dispatch('refreshStorages');
|
||||
$this->dispatch('refreshEnvs');
|
||||
} catch (\Throwable $e) {
|
||||
$this->application->docker_compose_location = $this->initialDockerComposeLocation;
|
||||
$this->application->save();
|
||||
// Refresh model to get restored values from Application::loadComposeFile
|
||||
$this->application->refresh();
|
||||
// Sync restored values back to component properties for UI update
|
||||
|
||||
$this->syncData();
|
||||
|
||||
return handleError($e, $this);
|
||||
} finally {
|
||||
@@ -936,73 +939,6 @@ class General extends Component
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateServiceEnvironmentVariables()
|
||||
{
|
||||
$domains = collect(json_decode($this->application->docker_compose_domains, true)) ?? collect([]);
|
||||
|
||||
foreach ($domains as $serviceName => $service) {
|
||||
$serviceNameFormatted = str($serviceName)->upper()->replace('-', '_')->replace('.', '_');
|
||||
$domain = data_get($service, 'domain');
|
||||
// Delete SERVICE_FQDN_ and SERVICE_URL_ variables if domain is removed
|
||||
$this->application->environment_variables()->where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_FQDN_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
$this->application->environment_variables()->where('resourceable_type', Application::class)
|
||||
->where('resourceable_id', $this->application->id)
|
||||
->where('key', 'LIKE', "SERVICE_URL_{$serviceNameFormatted}%")
|
||||
->delete();
|
||||
|
||||
if ($domain) {
|
||||
// Create or update SERVICE_FQDN_ and SERVICE_URL_ variables
|
||||
$fqdn = Url::fromString($domain);
|
||||
$port = $fqdn->getPort();
|
||||
$path = $fqdn->getPath();
|
||||
$urlValue = $fqdn->getScheme().'://'.$fqdn->getHost();
|
||||
if ($path !== '/') {
|
||||
$urlValue = $urlValue.$path;
|
||||
}
|
||||
$fqdnValue = str($domain)->after('://');
|
||||
if ($path !== '/') {
|
||||
$fqdnValue = $fqdnValue.$path;
|
||||
}
|
||||
|
||||
// Create/update SERVICE_FQDN_
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
// Create/update SERVICE_URL_
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
// Create/update port-specific variables if port exists
|
||||
if (filled($port)) {
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_FQDN_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $fqdnValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
|
||||
$this->application->environment_variables()->updateOrCreate([
|
||||
'key' => "SERVICE_URL_{$serviceNameFormatted}_{$port}",
|
||||
], [
|
||||
'value' => $urlValue,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getDetectedPortInfoProperty(): ?array
|
||||
{
|
||||
$detectedPort = $this->application->detectPortFromEnvironment();
|
||||
|
||||
@@ -100,19 +100,17 @@ class Heading extends Component
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
force_rebuild: $force_rebuild,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('error', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset restart count on successful deployment
|
||||
$this->application->update([
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
]);
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
@@ -151,19 +149,17 @@ class Heading extends Component
|
||||
deployment_uuid: $this->deploymentUuid,
|
||||
restart_only: true,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('success', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset restart count on manual restart
|
||||
$this->application->update([
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => now(),
|
||||
'last_restart_type' => 'manual',
|
||||
]);
|
||||
|
||||
return $this->redirectRoute('project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
|
||||
@@ -249,6 +249,11 @@ class Previews extends Component
|
||||
pull_request_id: $pull_request_id,
|
||||
git_type: $found->git_type ?? null,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('success', 'Deployment skipped', $result['message']);
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class Rollback extends Component
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
|
||||
queue_application_deployment(
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: $commit,
|
||||
@@ -60,7 +60,13 @@ class Rollback extends Component
|
||||
force_rebuild: false,
|
||||
);
|
||||
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return redirectRoute($this, 'project.application.deployment.show', [
|
||||
'project_uuid' => $this->parameters['project_uuid'],
|
||||
'application_uuid' => $this->parameters['application_uuid'],
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
@@ -154,12 +151,8 @@ class BackupEdit extends Component
|
||||
{
|
||||
$this->authorize('manageBackups', $this->backup->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class BackupExecutions extends Component
|
||||
@@ -69,12 +67,8 @@ class BackupExecutions extends Component
|
||||
|
||||
public function deleteBackup($executionId, $password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$execution = $this->backup->executions()->where('id', $executionId)->first();
|
||||
|
||||
@@ -39,7 +39,7 @@ class DeleteEnvironment extends Component
|
||||
if ($environment->isEmpty()) {
|
||||
$environment->delete();
|
||||
|
||||
return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $this->parameters['project_uuid']]);
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Environment {$environment->name}</strong> has defined resources, please delete them first.");
|
||||
|
||||
@@ -35,7 +35,7 @@ class DeleteProject extends Component
|
||||
if ($project->isEmpty()) {
|
||||
$project->delete();
|
||||
|
||||
return redirect()->route('project.index');
|
||||
return redirectRoute($this, 'project.index');
|
||||
}
|
||||
|
||||
return $this->dispatch('error', "<strong>Project {$project->name}</strong> has resources defined, please delete them first.");
|
||||
|
||||
@@ -63,7 +63,7 @@ class EnvironmentEdit extends Component
|
||||
{
|
||||
try {
|
||||
$this->syncData(true);
|
||||
$this->redirectRoute('project.environment.edit', [
|
||||
redirectRoute($this, 'project.environment.edit', [
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'project_uuid' => $this->project->uuid,
|
||||
]);
|
||||
|
||||
@@ -154,7 +154,7 @@ class DockerImage extends Component
|
||||
'fqdn' => $fqdn,
|
||||
]);
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
return redirectRoute($this, 'project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
|
||||
@@ -16,6 +16,6 @@ class EmptyProject extends Component
|
||||
'uuid' => (string) new Cuid2,
|
||||
]);
|
||||
|
||||
return redirect()->route('project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,7 @@ class GithubPrivateRepository extends Component
|
||||
$this->loadBranchByPage();
|
||||
}
|
||||
}
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ class Select extends Component
|
||||
|
||||
protected $queryString = [
|
||||
'server_id',
|
||||
'type' => ['except' => ''],
|
||||
'destination_uuid' => ['except' => '', 'as' => 'destination'],
|
||||
];
|
||||
|
||||
public function mount()
|
||||
@@ -66,6 +68,20 @@ class Select extends Component
|
||||
$project = Project::whereUuid($projectUuid)->firstOrFail();
|
||||
$this->environments = $project->environments;
|
||||
$this->selectedEnvironment = $this->environments->where('uuid', data_get($this->parameters, 'environment_uuid'))->firstOrFail()->name;
|
||||
|
||||
// Check if we have all required params for PostgreSQL type selection
|
||||
// This handles navigation from global search
|
||||
$queryType = request()->query('type');
|
||||
$queryServerId = request()->query('server_id');
|
||||
$queryDestination = request()->query('destination');
|
||||
|
||||
if ($queryType === 'postgresql' && $queryServerId !== null && $queryDestination) {
|
||||
$this->type = $queryType;
|
||||
$this->server_id = $queryServerId;
|
||||
$this->destination_uuid = $queryDestination;
|
||||
$this->server = Server::find($queryServerId);
|
||||
$this->current_step = 'select-postgresql-type';
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,13 @@ class Create extends Component
|
||||
|
||||
if (in_array($type, DATABASE_TYPES)) {
|
||||
if ($type->value() === 'postgresql') {
|
||||
// PostgreSQL requires database_image to be explicitly set
|
||||
// If not provided, fall through to Select component for version selection
|
||||
if (! $database_image) {
|
||||
$this->type = $type->value();
|
||||
|
||||
return;
|
||||
}
|
||||
$database = create_standalone_postgresql(
|
||||
environmentId: $environment->id,
|
||||
destinationUuid: $destination_uuid,
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\Resource;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
class EnvironmentSelect extends Component
|
||||
{
|
||||
public Collection $environments;
|
||||
|
||||
public string $project_uuid = '';
|
||||
|
||||
public string $selectedEnvironment = '';
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->selectedEnvironment = request()->route('environment_uuid');
|
||||
$this->project_uuid = request()->route('project_uuid');
|
||||
}
|
||||
|
||||
public function updatedSelectedEnvironment($value)
|
||||
{
|
||||
if ($value === 'edit') {
|
||||
return redirect()->route('project.show', [
|
||||
'project_uuid' => $this->project_uuid,
|
||||
]);
|
||||
} else {
|
||||
return redirect()->route('project.resource.index', [
|
||||
'project_uuid' => $this->project_uuid,
|
||||
'environment_uuid' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,9 @@ namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Actions\Database\StartDatabaseProxy;
|
||||
use App\Actions\Database\StopDatabaseProxy;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Database extends Component
|
||||
@@ -96,18 +93,14 @@ class Database extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->database);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->delete();
|
||||
$this->dispatch('success', 'Database deleted.');
|
||||
|
||||
return redirect()->route('project.service.configuration', $this->parameters);
|
||||
return redirectRoute($this, 'project.service.configuration', $this->parameters);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
@@ -171,7 +164,7 @@ class Database extends Component
|
||||
$serviceDatabase->delete();
|
||||
});
|
||||
|
||||
return redirect()->route('project.service.configuration', $redirectParams);
|
||||
return redirectRoute($this, 'project.service.configuration', $redirectParams);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalFileVolume;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
@@ -16,8 +15,6 @@ use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -62,7 +59,7 @@ class FileStorage extends Component
|
||||
$this->fs_path = $this->fileStorage->fs_path;
|
||||
}
|
||||
|
||||
$this->isReadOnly = $this->fileStorage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->fileStorage->shouldBeReadOnlyInUI();
|
||||
$this->syncData();
|
||||
}
|
||||
|
||||
@@ -104,7 +101,8 @@ class FileStorage extends Component
|
||||
public function loadStorageOnServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->resource);
|
||||
// Loading content is a read operation, so we use 'view' permission
|
||||
$this->authorize('view', $this->resource);
|
||||
|
||||
$this->fileStorage->loadStorageOnServer();
|
||||
$this->syncData();
|
||||
@@ -140,12 +138,8 @@ class FileStorage extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
|
||||
namespace App\Livewire\Project\Service;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
@@ -128,12 +125,8 @@ class ServiceApplicationView extends Component
|
||||
try {
|
||||
$this->authorize('delete', $this->application);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->application->delete();
|
||||
|
||||
@@ -67,7 +67,7 @@ class Storage extends Component
|
||||
public function refreshStorages()
|
||||
{
|
||||
$this->fileStorage = $this->resource->fileStorages()->get();
|
||||
$this->resource->refresh();
|
||||
$this->resource->load('persistentStorages.resource');
|
||||
}
|
||||
|
||||
public function getFilesProperty()
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
namespace App\Livewire\Project\Shared;
|
||||
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -93,12 +90,8 @@ class Danger extends Component
|
||||
|
||||
public function delete($password)
|
||||
{
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $this->resource) {
|
||||
@@ -118,7 +111,7 @@ class Danger extends Component
|
||||
$this->docker_cleanup
|
||||
);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
'project_uuid' => $this->projectUuid,
|
||||
'environment_uuid' => $this->environmentUuid,
|
||||
]);
|
||||
|
||||
@@ -5,12 +5,9 @@ namespace App\Livewire\Project\Shared;
|
||||
use App\Actions\Application\StopApplicationOneServer;
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Events\ApplicationStatusChanged;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
@@ -89,13 +86,18 @@ class Destination extends Component
|
||||
only_this_server: true,
|
||||
no_questions_asked: true,
|
||||
);
|
||||
if ($result['status'] === 'queue_full') {
|
||||
$this->dispatch('error', 'Deployment queue full', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
if ($result['status'] === 'skipped') {
|
||||
$this->dispatch('success', 'Deployment skipped', $result['message']);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.deployment.show', [
|
||||
return redirectRoute($this, 'project.application.deployment.show', [
|
||||
'project_uuid' => data_get($this->resource, 'environment.project.uuid'),
|
||||
'application_uuid' => data_get($this->resource, 'uuid'),
|
||||
'deployment_uuid' => $deployment_uuid,
|
||||
@@ -135,12 +137,8 @@ class Destination extends Component
|
||||
public function removeServer(int $network_id, int $server_id, $password)
|
||||
{
|
||||
try {
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {
|
||||
|
||||
@@ -21,6 +21,10 @@ use Livewire\Component;
|
||||
|
||||
class GetLogs extends Component
|
||||
{
|
||||
public const MAX_LOG_LINES = 50000;
|
||||
|
||||
public const MAX_DOWNLOAD_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
public string $outputs = '';
|
||||
|
||||
public string $errors = '';
|
||||
@@ -67,11 +71,6 @@ class GetLogs extends Component
|
||||
}
|
||||
}
|
||||
|
||||
public function doSomethingWithThisChunkOfOutput($output)
|
||||
{
|
||||
$this->outputs .= removeAnsiColors($output);
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if (! is_null($this->resource)) {
|
||||
@@ -128,6 +127,9 @@ class GetLogs extends Component
|
||||
if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) {
|
||||
$this->numberOfLines = 1000;
|
||||
}
|
||||
if ($this->numberOfLines > self::MAX_LOG_LINES) {
|
||||
$this->numberOfLines = self::MAX_LOG_LINES;
|
||||
}
|
||||
if ($this->container) {
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
@@ -162,23 +164,107 @@ class GetLogs extends Component
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
}
|
||||
}
|
||||
if ($refresh) {
|
||||
$this->outputs = '';
|
||||
}
|
||||
Process::run($sshCommand, function (string $type, string $output) {
|
||||
$this->doSomethingWithThisChunkOfOutput($output);
|
||||
// Collect new logs into temporary variable first to prevent flickering
|
||||
// (avoids clearing output before new data is ready)
|
||||
// Use array accumulation + implode for O(n) instead of O(n²) string concatenation
|
||||
$logChunks = [];
|
||||
Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks) {
|
||||
$logChunks[] = removeAnsiColors($output);
|
||||
});
|
||||
$newOutputs = implode('', $logChunks);
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
$this->outputs = str($this->outputs)->split('/\n/')->sort(function ($a, $b) {
|
||||
$newOutputs = str($newOutputs)->split('/\n/')->sort(function ($a, $b) {
|
||||
$a = explode(' ', $a);
|
||||
$b = explode(' ', $b);
|
||||
|
||||
return $a[0] <=> $b[0];
|
||||
})->join("\n");
|
||||
}
|
||||
|
||||
// Only update outputs after new data is ready (atomic update prevents flicker)
|
||||
$this->outputs = $newOutputs;
|
||||
}
|
||||
}
|
||||
|
||||
public function copyLogs(): string
|
||||
{
|
||||
return sanitizeLogsForExport($this->outputs);
|
||||
}
|
||||
|
||||
public function downloadAllLogs(): string
|
||||
{
|
||||
if (! $this->server->isFunctional() || ! $this->container) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
if ($this->server->isSwarm()) {
|
||||
$command = "docker service logs -t {$this->container}";
|
||||
} else {
|
||||
$command = "docker logs -t {$this->container}";
|
||||
}
|
||||
} else {
|
||||
if ($this->server->isSwarm()) {
|
||||
$command = "docker service logs {$this->container}";
|
||||
} else {
|
||||
$command = "docker logs {$this->container}";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->server->isNonRoot()) {
|
||||
$command = parseCommandsByLineForSudo(collect($command), $this->server);
|
||||
$command = $command[0];
|
||||
}
|
||||
|
||||
$sshCommand = SshMultiplexingHelper::generateSshCommand($this->server, $command);
|
||||
|
||||
// Use array accumulation + implode for O(n) instead of O(n²) string concatenation
|
||||
// Enforce 50MB size limit to prevent memory exhaustion from large logs
|
||||
$logChunks = [];
|
||||
$accumulatedBytes = 0;
|
||||
$truncated = false;
|
||||
|
||||
Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand, function (string $type, string $output) use (&$logChunks, &$accumulatedBytes, &$truncated) {
|
||||
if ($truncated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$output = removeAnsiColors($output);
|
||||
$outputBytes = strlen($output);
|
||||
|
||||
if ($accumulatedBytes + $outputBytes > self::MAX_DOWNLOAD_SIZE_BYTES) {
|
||||
$remaining = self::MAX_DOWNLOAD_SIZE_BYTES - $accumulatedBytes;
|
||||
if ($remaining > 0) {
|
||||
$logChunks[] = substr($output, 0, $remaining);
|
||||
}
|
||||
$truncated = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$logChunks[] = $output;
|
||||
$accumulatedBytes += $outputBytes;
|
||||
});
|
||||
|
||||
$allLogs = implode('', $logChunks);
|
||||
|
||||
if ($truncated) {
|
||||
$allLogs .= "\n\n[... Output truncated at 50MB limit ...]";
|
||||
}
|
||||
|
||||
if ($this->showTimeStamps) {
|
||||
$allLogs = str($allLogs)->split('/\n/')->sort(function ($a, $b) {
|
||||
$a = explode(' ', $a);
|
||||
$b = explode(' ', $b);
|
||||
|
||||
return $a[0] <=> $b[0];
|
||||
})->join("\n");
|
||||
}
|
||||
|
||||
return sanitizeLogsForExport($allLogs);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.project.shared.get-logs');
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
|
||||
namespace App\Livewire\Project\Shared\Storages;
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\LocalPersistentVolume;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
@@ -67,7 +64,7 @@ class Show extends Component
|
||||
public function mount()
|
||||
{
|
||||
$this->syncData(false);
|
||||
$this->isReadOnly = $this->storage->isReadOnlyVolume();
|
||||
$this->isReadOnly = $this->storage->shouldBeReadOnlyInUI();
|
||||
}
|
||||
|
||||
public function submit()
|
||||
@@ -84,12 +81,8 @@ class Show extends Component
|
||||
{
|
||||
$this->authorize('update', $this->resource);
|
||||
|
||||
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
|
||||
if (! Hash::check($password, Auth::user()->password)) {
|
||||
$this->addError('password', 'The provided password is incorrect.');
|
||||
|
||||
return;
|
||||
}
|
||||
if (! verifyPasswordConfirmation($password, $this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->storage->delete();
|
||||
|
||||
@@ -57,7 +57,14 @@ class Terminal extends Component
|
||||
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
|
||||
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
|
||||
'if [ -n "$SHELL" ] && [ -x "$SHELL" ]; then exec $SHELL; else sh; fi';
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'");
|
||||
|
||||
// Add sudo for non-root users to access Docker socket
|
||||
$dockerCommand = "docker exec -it {$escapedIdentifier} sh -c '{$shellCommand}'";
|
||||
if ($server->isNonRoot()) {
|
||||
$dockerCommand = "sudo {$dockerCommand}";
|
||||
}
|
||||
|
||||
$command = SshMultiplexingHelper::generateSshCommand($server, $dockerCommand);
|
||||
} else {
|
||||
$shellCommand = 'PATH=$PATH:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin && '.
|
||||
'if [ -f ~/.profile ]; then . ~/.profile; fi && '.
|
||||
|
||||
@@ -48,7 +48,7 @@ class Show extends Component
|
||||
'uuid' => (string) new Cuid2,
|
||||
]);
|
||||
|
||||
return redirect()->route('project.resource.index', [
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
]);
|
||||
@@ -59,7 +59,7 @@ class Show extends Component
|
||||
|
||||
public function navigateToEnvironment($projectUuid, $environmentUuid)
|
||||
{
|
||||
return redirect()->route('project.resource.index', [
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
'project_uuid' => $projectUuid,
|
||||
'environment_uuid' => $environmentUuid,
|
||||
]);
|
||||
|
||||
Reference in new issue
Block a user