mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 04:23:34 +00:00
fix(storage): split S3 endpoint input and persist stopped status
Persist exited status across application, database, service, and preview stop paths, and reuse the split host/port/path input for S3 endpoints with linked validation feedback.
This commit is contained in:
@@ -28,7 +28,7 @@ class StopApplication
|
||||
if ($server->isSwarm()) {
|
||||
instant_remote_process(["docker stack rm {$application->uuid}"], $server);
|
||||
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
$containers = $previewDeployments
|
||||
@@ -57,17 +57,15 @@ class StopApplication
|
||||
}
|
||||
}
|
||||
|
||||
$status = ['status' => 'exited'];
|
||||
if ($resetRestartCount) {
|
||||
$application->update([
|
||||
$status = array_merge($status, [
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
]);
|
||||
} else {
|
||||
$application->update([
|
||||
'status' => 'exited',
|
||||
]);
|
||||
}
|
||||
$application->update($status);
|
||||
|
||||
ServiceStatusChanged::dispatch($application->environment->project->team->id);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class StopDatabase
|
||||
|
||||
// Reset restart tracking when database is manually stopped
|
||||
$database->update([
|
||||
'status' => 'exited',
|
||||
'restart_count' => 0,
|
||||
'last_restart_at' => null,
|
||||
'last_restart_type' => null,
|
||||
|
||||
@@ -49,6 +49,9 @@ class StopService
|
||||
$this->stopContainersInParallel($containersToStop, $server);
|
||||
}
|
||||
|
||||
$applications->each->update(['status' => 'exited']);
|
||||
$dbs->each->update(['status' => 'exited']);
|
||||
|
||||
if ($deleteConnectedNetworks) {
|
||||
$service->deleteConnectedNetworks();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Actions\Service;
|
||||
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use Lorisleiva\Actions\Concerns\AsAction;
|
||||
@@ -21,5 +22,8 @@ class StopServiceApplication
|
||||
instant_remote_process([
|
||||
"docker stop {$containerName}",
|
||||
], $server);
|
||||
|
||||
$serviceApplication->update(['status' => 'exited']);
|
||||
ServiceStatusChanged::dispatch($service->environment->project->team->id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Livewire\Project\Application;
|
||||
|
||||
use App\Actions\Docker\GetContainersStatus;
|
||||
use App\Events\ServiceStatusChanged;
|
||||
use App\Jobs\DeleteResourceJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
@@ -373,6 +374,11 @@ class Previews extends Component
|
||||
$this->stopContainers($containers, $server);
|
||||
}
|
||||
|
||||
ApplicationPreview::where('application_id', $this->application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->update(['status' => 'exited']);
|
||||
ServiceStatusChanged::dispatch($this->application->environment->project->team->id);
|
||||
|
||||
GetContainersStatus::run($server);
|
||||
$this->application->refresh();
|
||||
$this->dispatch('containerStatusUpdated');
|
||||
|
||||
@@ -26,7 +26,7 @@ class Create extends Component
|
||||
|
||||
public string $bucket;
|
||||
|
||||
public string $endpoint;
|
||||
public string $endpoint = '';
|
||||
|
||||
public S3Storage $storage;
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
'wire' => true,
|
||||
'value' => '',
|
||||
'errorId' => null,
|
||||
'hostLabel' => 'Domain',
|
||||
'hostPlaceholder' => 'app.example.com',
|
||||
])
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-[8rem_minmax(0,1fr)_8rem]" x-data="{
|
||||
@@ -51,13 +53,24 @@
|
||||
<div class="min-w-0">
|
||||
<div class="mb-1.5 flex h-4 w-full items-center gap-1.5">
|
||||
<label for="{{ $id }}" class="mb-0! flex items-center gap-1.5 leading-4">
|
||||
Domain <x-highlighted text="*" />
|
||||
{{ $hostLabel }} <x-highlighted text="*" />
|
||||
</label>
|
||||
</div>
|
||||
<input id="{{ $id }}" type="text" class="input" x-model="host" placeholder="app.example.com"
|
||||
<input id="{{ $id }}" type="text" class="input" x-model="host" placeholder="{{ $hostPlaceholder }}"
|
||||
autocomplete="off" required />
|
||||
@error($errorId ?? $id)
|
||||
<p class="mt-1 text-[12px] text-red-500">{{ $message }}</p>
|
||||
@php
|
||||
preg_match('/(https?:\/\/\S+)$/', $message, $validationLinkMatches);
|
||||
$validationLink = $validationLinkMatches[1] ?? null;
|
||||
@endphp
|
||||
<p class="mt-1 text-[12px] text-red-500">
|
||||
@if ($validationLink)
|
||||
{{ str($message)->beforeLast($validationLink)->trim() }}
|
||||
<a class="font-medium underline" href="{{ $validationLink }}">Set them here.</a>
|
||||
@else
|
||||
{{ $message }}
|
||||
@endif
|
||||
</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,18 +7,8 @@
|
||||
<x-forms.input required label="Name" id="name" />
|
||||
<x-forms.input label="Description" id="description" />
|
||||
</div>
|
||||
<x-forms.input required type="url" label="Endpoint" id="endpoint"
|
||||
x-on:blur="
|
||||
let value = $el.value.trim();
|
||||
const hasScheme = /^https?:/i.test(value) || /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
|
||||
if (value && !hasScheme) {
|
||||
value = `https://${value}`;
|
||||
}
|
||||
if ($el.value !== value) {
|
||||
$el.value = value;
|
||||
$el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
" />
|
||||
<x-forms.domain-input id="endpoint" host-label="Host"
|
||||
host-placeholder="minio.internal or 192.168.1.50" />
|
||||
<div class="flex gap-2">
|
||||
<x-forms.input required label="Bucket" id="bucket" />
|
||||
<x-forms.input required helper="Region only required for AWS. Leave it as-is for other providers."
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
<x-forms.input canGate="update" :canResource="$storage" label="Name" id="name" />
|
||||
<x-forms.input canGate="update" :canResource="$storage" label="Description" id="description" />
|
||||
<div class="lg:col-span-2">
|
||||
<x-forms.input canGate="update" :canResource="$storage" required label="Endpoint"
|
||||
id="endpoint" />
|
||||
@can('update', $storage)
|
||||
<x-forms.domain-input id="endpoint" host-label="Host"
|
||||
host-placeholder="minio.internal or 192.168.1.50" />
|
||||
@else
|
||||
<x-forms.input label="Endpoint" :value="$endpoint" disabled />
|
||||
@endcan
|
||||
</div>
|
||||
<x-forms.input canGate="update" :canResource="$storage" required label="Bucket" id="bucket" />
|
||||
<x-forms.input canGate="update" :canResource="$storage" required label="Region" id="region" />
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\MessageBag;
|
||||
use Illuminate\Support\ViewErrorBag;
|
||||
|
||||
it('renders configurable host copy for S3 endpoints', function () {
|
||||
View::share('errors', new ViewErrorBag);
|
||||
|
||||
$html = Blade::render(<<<'BLADE'
|
||||
<x-forms.domain-input id="endpoint" :wire="false" value="http://192.168.1.50:9000/s3"
|
||||
host-label="Host" host-placeholder="minio.internal or 192.168.1.50" />
|
||||
BLADE);
|
||||
|
||||
expect($html)
|
||||
->toContain('Host')
|
||||
->toContain('minio.internal or 192.168.1.50')
|
||||
->toContain("scheme: 'https'")
|
||||
->toContain("host: ''")
|
||||
->toContain("port: ''")
|
||||
->toContain("path: ''");
|
||||
});
|
||||
|
||||
it('keeps validation errors attached to the composed endpoint', function () {
|
||||
$settingsUrl = route('settings.advanced').'#endpoint-section';
|
||||
$errors = new ViewErrorBag;
|
||||
$errors->put('default', new MessageBag([
|
||||
'endpoint' => "The endpoint is invalid. Configure allowed internal targets: {$settingsUrl}",
|
||||
]));
|
||||
View::share('errors', $errors);
|
||||
|
||||
$html = Blade::render('<x-forms.domain-input id="endpoint" :wire="false" />');
|
||||
|
||||
expect($html)
|
||||
->toContain('The endpoint is invalid.')
|
||||
->toContain('href="'.$settingsUrl.'"')
|
||||
->toContain('Set them here.');
|
||||
});
|
||||
@@ -5,14 +5,18 @@ use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
it('normalizes S3 endpoints without a Livewire blur request', function () {
|
||||
$view = file_get_contents(resource_path('views/livewire/storage/create.blade.php'));
|
||||
it('uses the shared split URL input without a Livewire blur request', function () {
|
||||
$createView = file_get_contents(resource_path('views/livewire/storage/create.blade.php'));
|
||||
$editView = file_get_contents(resource_path('views/livewire/storage/form.blade.php'));
|
||||
|
||||
expect($view)
|
||||
expect($createView)
|
||||
->not->toContain('wire:model.blur="endpoint"')
|
||||
->toContain('x-on:blur')
|
||||
->toContain('const hasScheme')
|
||||
->toContain('dispatchEvent');
|
||||
->toContain('<x-forms.domain-input id="endpoint"')
|
||||
->toContain('host-label="Host"')
|
||||
->toContain('host-placeholder="minio.internal or 192.168.1.50"')
|
||||
->and($editView)
|
||||
->toContain('<x-forms.domain-input id="endpoint"')
|
||||
->toContain('@can(\'update\', $storage)');
|
||||
});
|
||||
|
||||
it('normalizes endpoints again on the backend', function (string $endpoint, string $expected) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
it('persists exited status when stopping standalone databases', function () {
|
||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Database/StopDatabase.php');
|
||||
|
||||
expect($action)->toContain("'status' => 'exited'");
|
||||
});
|
||||
|
||||
it('persists exited status for every full application stop path', function () {
|
||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Application/StopApplication.php');
|
||||
|
||||
expect($action)
|
||||
->toContain("\$status = ['status' => 'exited'];")
|
||||
->toContain('$application->update($status);')
|
||||
->not->toMatch('/docker stack rm .*?return;/s');
|
||||
});
|
||||
|
||||
it('persists exited status for all children when stopping a service', function () {
|
||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopService.php');
|
||||
|
||||
expect($action)
|
||||
->toContain("\$applications->each->update(['status' => 'exited']);")
|
||||
->toContain("\$dbs->each->update(['status' => 'exited']);");
|
||||
});
|
||||
|
||||
it('persists exited status when stopping an individual service resource', function () {
|
||||
$action = file_get_contents(__DIR__.'/../../app/Actions/Service/StopServiceApplication.php');
|
||||
|
||||
expect($action)
|
||||
->toContain("\$serviceApplication->update(['status' => 'exited']);")
|
||||
->toContain('ServiceStatusChanged::dispatch($service->environment->project->team->id);');
|
||||
});
|
||||
|
||||
it('persists exited status when stopping a preview deployment', function () {
|
||||
$component = file_get_contents(__DIR__.'/../../app/Livewire/Project/Application/Previews.php');
|
||||
|
||||
expect($component)
|
||||
->toContain("->update(['status' => 'exited']);")
|
||||
->toContain('ServiceStatusChanged::dispatch($this->application->environment->project->team->id);');
|
||||
});
|
||||
Reference in New Issue
Block a user