mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(database): reject blocked starts before queueing operations
Check start prerequisites in database and deploy APIs and MCP controls. Return HTTP 422 for blocked API starts and restarts. Show restart notices and open UI logs only after an operation is queued. Add API regression tests for blocked ClickHouse starts and restarts.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user