diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 84486f6a9f..aa646abe32 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -3364,6 +3364,10 @@ class DatabasesController extends Controller response: 409, description: 'Another start, restart or import of this database is already in progress.', ), + new OA\Response( + response: 422, + description: 'The database cannot start yet, for example because a CA certificate is missing.', + ), ] )] public function action_deploy(Request $request) @@ -3386,6 +3390,10 @@ class DatabasesController extends Controller if (str($database->status)->contains('running')) { return response()->json(['message' => 'Database is already running.'], 400); } + $prerequisiteError = StartDatabase::prerequisiteError($database); + if ($prerequisiteError !== null) { + return response()->json(['message' => $prerequisiteError], 422); + } $reservation = StartDatabase::reserveOperation($database); if ($reservation === null) { return response()->json(['message' => ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE], 409); @@ -3558,6 +3566,10 @@ class DatabasesController extends Controller response: 409, description: 'Another start, restart or import of this database is already in progress.', ), + new OA\Response( + response: 422, + description: 'The database cannot start yet, for example because a CA certificate is missing.', + ), ] )] public function action_restart(Request $request) @@ -3577,6 +3589,10 @@ class DatabasesController extends Controller $this->authorize('manage', $database); + $prerequisiteError = StartDatabase::prerequisiteError($database); + if ($prerequisiteError !== null) { + return response()->json(['message' => $prerequisiteError], 422); + } $reservation = StartDatabase::reserveOperation($database); if ($reservation === null) { return response()->json(['message' => ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE], 409); diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index b58a72d6ff..e5aac43b85 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -559,6 +559,11 @@ class DeployController extends Controller } catch (AuthorizationException $e) { return ['message' => 'Unauthorized to start this database.', 'deployment_uuid' => null]; } + $prerequisiteError = StartDatabase::prerequisiteError($resource); + if ($prerequisiteError !== null) { + $message = $prerequisiteError; + break; + } $reservation = StartDatabase::reserveOperation($resource); if ($reservation === null) { $message = ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE; diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index a7e8cfedd8..af6f7405e0 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -158,6 +158,7 @@ class Heading extends Component return; } $this->auditDatabaseAction('ui.database.restarted'); + $this->dispatch('info', 'Restarting database.'); $this->markDeploymentRunning($activity); $this->js("window.dispatchEvent(new CustomEvent('startdatabase'))"); $this->dispatch('activityMonitor', $activity->id, ServiceStatusChanged::class); diff --git a/app/Mcp/Tools/Control.php b/app/Mcp/Tools/Control.php index aca6b46564..65ec7ca4c1 100644 --- a/app/Mcp/Tools/Control.php +++ b/app/Mcp/Tools/Control.php @@ -141,6 +141,11 @@ class Control extends Tool return ['message' => 'Database is already running.']; } + $prerequisiteError = StartDatabase::prerequisiteError($database); + if ($prerequisiteError !== null) { + throw new \RuntimeException($prerequisiteError); + } + $reservation = StartDatabase::reserveOperation($database); if ($reservation === null) { throw new \RuntimeException(ResourceStartActivity::DATABASE_OPERATION_IN_PROGRESS_MESSAGE); diff --git a/resources/views/livewire/project/database/heading.blade.php b/resources/views/livewire/project/database/heading.blade.php index abf58177c7..eec4add287 100644 --- a/resources/views/livewire/project/database/heading.blade.php +++ b/resources/views/livewire/project/database/heading.blade.php @@ -183,7 +183,7 @@ window.dispatchEvent(new CustomEvent('database-action-finished')); return; } - window.dispatchEvent(new CustomEvent('startdatabase')); + // start() opens the log dialog only when the start was queued. try { await $wire.$call('start'); } finally { @@ -197,8 +197,7 @@ return; } window.dispatchEvent(new CustomEvent('database-busy')); - $wire.$dispatch('info', 'Restarting database.'); - window.dispatchEvent(new CustomEvent('startdatabase')); + // restart() opens the log dialog only when the restart was queued. try { await $wire.$call('restart'); } finally { diff --git a/tests/Feature/StartClickhouseVolumesTest.php b/tests/Feature/StartClickhouseVolumesTest.php index faa13a0944..8193cdb95e 100644 --- a/tests/Feature/StartClickhouseVolumesTest.php +++ b/tests/Feature/StartClickhouseVolumesTest.php @@ -19,6 +19,7 @@ use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Queue; +use Illuminate\Support\Str; use Livewire\Livewire; use Spatie\Activitylog\Models\Activity; use Symfony\Component\Yaml\Yaml; @@ -174,3 +175,43 @@ it('reads the data mount with sudo on a non-root server', function () { expect($database->fresh()->anonymousDataVolume())->toBe('3f1c0ffee') ->and(ranRemoteCommandContaining("sudo docker inspect --format '{{range .Mounts}}{{if eq .Destination \"/var/lib/clickhouse\"}}{{.Type}} {{.Name}}{{end}}{{end}}' '{$database->uuid}' 2>/dev/null || sudo true"))->toBeTrue(); }); + +it('rejects an API start or restart of a blocked database at once', function (string $method, string $path) { + $database = create_standalone_clickhouse($this->environment->id, $this->destination); + fakeClickhouseDataMount('3f1c0ffee'); + $plainToken = Str::random(40); + $token = $this->owner->tokens()->create([ + 'name' => 'clickhouse-test', + 'token' => hash('sha256', $plainToken), + 'abilities' => ['*'], + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken) + ->json($method, str_replace('{uuid}', $database->uuid, $path)); + + $response->assertStatus(422); + expect($response->json('message'))->toContain('unnamed Docker volume 3f1c0ffee'); + Queue::assertNothingPushed(); +})->with([ + 'start' => ['POST', '/api/v1/databases/{uuid}/start'], + 'restart' => ['POST', '/api/v1/databases/{uuid}/restart'], +]); + +it('reports a blocked database in the deploy API', function () { + $database = create_standalone_clickhouse($this->environment->id, $this->destination); + fakeClickhouseDataMount('3f1c0ffee'); + $plainToken = Str::random(40); + $token = $this->owner->tokens()->create([ + 'name' => 'clickhouse-test', + 'token' => hash('sha256', $plainToken), + 'abilities' => ['*'], + 'team_id' => $this->team->id, + ]); + + $response = $this->withHeader('Authorization', 'Bearer '.$token->getKey().'|'.$plainToken) + ->postJson('/api/v1/deploy', ['uuid' => $database->uuid]); + + expect($response->json('deployments.0.message'))->toContain('unnamed Docker volume 3f1c0ffee'); + Queue::assertNothingPushed(); +});