diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 10a213e5f3..d71b64563b 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -139,6 +139,19 @@ class DatabasesController extends Controller return $storage; } + /** + * Store numeric resource limits (e.g. 0 or 1.5) as the strings the string limit columns expect. + */ + private function normalizeNumericLimits(Request $request): void + { + foreach (['limits_memory', 'limits_memory_swap', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset'] as $field) { + $value = $request->input($field); + if (is_int($value) || is_float($value)) { + $request->offsetSet($field, (string) $value); + } + } + } + private function removeSensitiveData($database, bool $loadNestedServerSecrets = false) { $database->makeHidden([ @@ -2023,6 +2036,7 @@ class DatabasesController extends Controller return response()->json(['message' => 'Public port already used by another database.'], 400); } } + $this->normalizeNumericLimits($request); $validator = customApiValidator($request->all(), [ 'name' => 'string|max:255', 'description' => 'string|nullable', diff --git a/tests/Feature/Api/DatabaseCreateNumericLimitsTest.php b/tests/Feature/Api/DatabaseCreateNumericLimitsTest.php new file mode 100644 index 0000000000..4feee3f74d --- /dev/null +++ b/tests/Feature/Api/DatabaseCreateNumericLimitsTest.php @@ -0,0 +1,101 @@ + 'file']); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true])); + Server::flushIdentityMap(); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + + Queue::fake(); +}); + +function databaseCreateNumericLimitsHeaders(User $user, int $teamId): array +{ + $plainTextToken = Str::random(40); + $token = $user->tokens()->create([ + 'name' => 'database-create-numeric-limits-test', + 'token' => hash('sha256', $plainTextToken), + 'abilities' => ['write'], + 'team_id' => $teamId, + ]); + + return ['Authorization' => 'Bearer '.$token->getKey().'|'.$plainTextToken]; +} + +function databaseCreateNumericLimitsPayload(object $test, array $overrides = []): array +{ + return array_merge([ + 'project_uuid' => $test->project->uuid, + 'environment_uuid' => $test->environment->uuid, + 'server_uuid' => $test->server->uuid, + ], $overrides); +} + +test('numeric resource limits are accepted and stored as strings', function (string $type) { + $response = $this->withHeaders(databaseCreateNumericLimitsHeaders($this->user, $this->team->id)) + ->postJson("/api/v1/databases/{$type}", databaseCreateNumericLimitsPayload($this, [ + 'limits_memory' => 0, + 'limits_memory_swap' => 1024, + 'limits_memory_reservation' => 512, + 'limits_cpus' => 1.5, + 'limits_cpuset' => 1, + ])) + ->assertCreated(); + + $database = queryDatabaseByUuidWithinTeam($response->json('uuid'), $this->team->id); + + expect($database->limits_memory)->toBe('0') + ->and($database->limits_memory_swap)->toBe('1024') + ->and($database->limits_memory_reservation)->toBe('512') + ->and($database->limits_cpus)->toBe('1.5') + ->and($database->limits_cpuset)->toBe('1'); +})->with(['postgresql', 'mysql', 'mariadb', 'mongodb', 'redis', 'keydb', 'dragonfly', 'clickhouse', 'sqlite']); + +test('string resource limits are still accepted', function () { + $response = $this->withHeaders(databaseCreateNumericLimitsHeaders($this->user, $this->team->id)) + ->postJson('/api/v1/databases/postgresql', databaseCreateNumericLimitsPayload($this, [ + 'limits_memory' => '512m', + 'limits_cpus' => '0.5', + ])) + ->assertCreated(); + + $database = queryDatabaseByUuidWithinTeam($response->json('uuid'), $this->team->id); + + expect($database->limits_memory)->toBe('512m') + ->and($database->limits_cpus)->toBe('0.5'); +}); + +test('non-scalar or boolean resource limits are rejected', function (string $field, mixed $value) { + $this->withHeaders(databaseCreateNumericLimitsHeaders($this->user, $this->team->id)) + ->postJson('/api/v1/databases/postgresql', databaseCreateNumericLimitsPayload($this, [$field => $value])) + ->assertUnprocessable() + ->assertJsonValidationErrors($field); + + expect(StandalonePostgresql::count())->toBe(0); +})->with([ + 'array memory' => ['limits_memory', ['512m']], + 'array cpus' => ['limits_cpus', [1.5]], + 'boolean cpus' => ['limits_cpus', true], + 'object memory swap' => ['limits_memory_swap', ['value' => 1]], +]);