diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index b836876d81..aef9b67faf 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -21,6 +21,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\StandalonePostgresql; +use App\Models\StandaloneSqlite; use App\Models\SwarmDocker; use App\Support\ValidationPatterns; use Illuminate\Database\Eloquent\Model; @@ -482,6 +483,7 @@ class DatabasesController extends Controller 'mysql_user' => ['type' => 'string', 'description' => 'MySQL user'], 'mysql_database' => ['type' => 'string', 'description' => 'MySQL database'], 'mysql_conf' => ['type' => 'string', 'description' => 'MySQL conf'], + 'sqlite_databases' => ['type' => 'string', 'description' => 'Comma-separated SQLite database file names'], 'health_check_enabled' => ['type' => 'boolean', 'description' => 'Enable the database healthcheck probe.', 'default' => true], 'health_check_interval' => ['type' => 'integer', 'description' => 'Healthcheck interval in seconds.', 'minimum' => 1, 'default' => 15], 'health_check_timeout' => ['type' => 'integer', 'description' => 'Healthcheck timeout in seconds.', 'minimum' => 1, 'default' => 5], @@ -516,7 +518,7 @@ class DatabasesController extends Controller )] public function update_by_uuid(Request $request) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'sqlite_databases']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { return invalidTokenResponse(); @@ -752,6 +754,12 @@ class DatabasesController extends Controller $request->offsetSet('mysql_conf', $mysqlConf); } break; + case 'standalone-sqlite': + $allowedFields = ['name', 'description', 'image', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'sqlite_databases']; + $validator = customApiValidator($request->all(), [ + 'sqlite_databases' => ['string', 'max:255', 'regex:'.StandaloneSqlite::DATABASES_PATTERN], + ]); + break; } $allowedFields = array_merge($allowedFields, ['health_check_enabled', 'health_check_interval', 'health_check_timeout', 'health_check_retries', 'health_check_start_period']); $healthCheckValidator = customApiValidator($request->all(), [ @@ -1008,6 +1016,8 @@ class DatabasesController extends Controller $backupData['databases_to_backup'] = $database->mariadb_database; } elseif ($database->type() === 'standalone-clickhouse') { $backupData['databases_to_backup'] = $database->clickhouse_db; + } elseif ($database->type() === 'standalone-sqlite') { + $backupData['databases_to_backup'] = $database->sqlite_databases; } } @@ -1844,9 +1854,74 @@ class DatabasesController extends Controller return $this->create_database($request, NewDatabaseTypes::MONGODB); } + #[OA\Post( + summary: 'Create (SQLite)', + description: 'Create a new SQLite database.', + path: '/databases/sqlite', + operationId: 'create-database-sqlite', + security: [ + ['bearerAuth' => []], + ], + tags: ['Databases'], + + requestBody: new OA\RequestBody( + description: 'Database data', + required: true, + content: new OA\MediaType( + mediaType: 'application/json', + schema: new OA\Schema( + type: 'object', + required: ['server_uuid', 'project_uuid', 'environment_name', 'environment_uuid'], + properties: [ + 'server_uuid' => ['type' => 'string', 'description' => 'UUID of the server'], + 'project_uuid' => ['type' => 'string', 'description' => 'UUID of the project'], + 'environment_name' => ['type' => 'string', 'description' => 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.'], + 'environment_uuid' => ['type' => 'string', 'description' => 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.'], + 'destination_uuid' => ['type' => 'string', 'description' => 'UUID of the destination if the server has multiple destinations'], + 'sqlite_databases' => ['type' => 'string', 'description' => 'Comma-separated SQLite database file names'], + 'name' => ['type' => 'string', 'description' => 'Name of the database'], + 'description' => ['type' => 'string', 'description' => 'Description of the database'], + 'image' => ['type' => 'string', 'description' => 'Docker Image of the database'], + 'limits_memory' => ['type' => 'string', 'description' => 'Memory limit of the database'], + 'limits_memory_swap' => ['type' => 'string', 'description' => 'Memory swap limit of the database'], + 'limits_memory_swappiness' => ['type' => 'integer', 'description' => 'Memory swappiness of the database'], + 'limits_memory_reservation' => ['type' => 'string', 'description' => 'Memory reservation of the database'], + 'limits_cpus' => ['type' => 'string', 'description' => 'CPU limit of the database'], + 'limits_cpuset' => ['type' => 'string', 'description' => 'CPU set of the database'], + 'limits_cpu_shares' => ['type' => 'integer', 'description' => 'CPU shares of the database'], + 'instant_deploy' => ['type' => 'boolean', 'description' => 'Instant deploy the database'], + 'tags' => ['type' => 'array', 'items' => new OA\Items(type: 'string'), 'description' => 'Tags to assign to the database.'], + ], + ), + ) + ), + responses: [ + new OA\Response( + response: 200, + description: 'Database updated', + ), + new OA\Response( + response: 401, + ref: '#/components/responses/401', + ), + new OA\Response( + response: 400, + ref: '#/components/responses/400', + ), + new OA\Response( + response: 422, + ref: '#/components/responses/422', + ), + ] + )] + public function create_database_sqlite(Request $request) + { + return $this->create_database($request, NewDatabaseTypes::SQLITE); + } + public function create_database(Request $request, NewDatabaseTypes $type) { - $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'tags']; + $allowedFields = ['name', 'description', 'image', 'public_port', 'public_port_timeout', 'is_public', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'postgres_user', 'postgres_password', 'postgres_db', 'postgres_initdb_args', 'postgres_host_auth_method', 'postgres_conf', 'clickhouse_admin_user', 'clickhouse_admin_password', 'dragonfly_password', 'redis_password', 'redis_conf', 'keydb_password', 'keydb_conf', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database', 'mongo_conf', 'mongo_initdb_root_username', 'mongo_initdb_root_password', 'mongo_initdb_database', 'mysql_root_password', 'mysql_password', 'mysql_user', 'mysql_database', 'mysql_conf', 'sqlite_databases', 'tags']; $teamId = getTeamIdFromToken(); if (is_null($teamId)) { @@ -2488,6 +2563,47 @@ class DatabasesController extends Controller ]); return response()->json(serializeApiResponse($payload))->setStatusCode(201); + } elseif ($type === NewDatabaseTypes::SQLITE) { + $allowedFields = ['name', 'description', 'image', 'project_uuid', 'environment_name', 'environment_uuid', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'sqlite_databases', 'tags']; + $validator = customApiValidator($request->all(), [ + 'sqlite_databases' => ['string', 'max:255', 'regex:'.StandaloneSqlite::DATABASES_PATTERN], + ]); + $extraFields = array_diff(array_keys($request->all()), $allowedFields); + if ($validator->fails() || ! empty($extraFields)) { + $errors = $validator->errors(); + if (! empty($extraFields)) { + foreach ($extraFields as $field) { + $errors->add($field, 'This field is not allowed.'); + } + } + + return response()->json([ + 'message' => 'Validation failed.', + 'errors' => $errors, + ], 422); + } + removeUnnecessaryFieldsFromRequest($request); + $database = create_standalone_sqlite($environment->id, $destination, $request->only($allowedFields)); + if ($instantDeploy) { + StartDatabase::dispatch($database); + } + if ($tagNames !== []) { + $this->attachTagsToResource($database, $tagNames, $teamId); + } + + auditLog('api.database.created', [ + 'team_id' => $teamId, + 'database_uuid' => $database->uuid, + 'database_name' => $database->name, + 'database_type' => $type->value, + 'server_uuid' => $serverUuid, + 'is_public' => (bool) $database->is_public, + 'instant_deploy' => (bool) $instantDeploy, + ]); + + return response()->json(serializeApiResponse([ + 'uuid' => $database->uuid, + ]))->setStatusCode(201); } return response()->json(['message' => 'Invalid database type requested.'], 400); @@ -4900,6 +5016,7 @@ class DatabasesController extends Controller str_starts_with($originalName, 'mongodb-data-') => 'mongodb-data-'.$newDatabase->uuid, str_starts_with($originalName, 'keydb-data-') => 'keydb-data-'.$newDatabase->uuid, str_starts_with($originalName, 'dragonfly-data-') => 'dragonfly-data-'.$newDatabase->uuid, + str_starts_with($originalName, 'sqlite-data-') => 'sqlite-data-'.$newDatabase->uuid, str_starts_with($volume->name, $database->uuid) => str($volume->name)->replace($database->uuid, $newDatabase->uuid)->toString(), default => $newDatabase->uuid.'-'.$volume->name, }; diff --git a/app/Http/Controllers/Api/DestinationsController.php b/app/Http/Controllers/Api/DestinationsController.php index 53b4ef8b15..83ec65f1f8 100644 --- a/app/Http/Controllers/Api/DestinationsController.php +++ b/app/Http/Controllers/Api/DestinationsController.php @@ -405,6 +405,7 @@ class DestinationsController extends Controller || (method_exists($destination, 'keydbs') && $destination->keydbs()->exists()) || (method_exists($destination, 'dragonflies') && $destination->dragonflies()->exists()) || (method_exists($destination, 'clickhouses') && $destination->clickhouses()->exists()) + || (method_exists($destination, 'sqlites') && $destination->sqlites()->exists()) || (method_exists($destination, 'services') && $destination->services()->exists()); if ($hasAttached) { return response()->json(['message' => 'Destination has attached resources, detach first.'], 409); diff --git a/openapi.json b/openapi.json index 964b50fe61..ed6abd43ff 100644 --- a/openapi.json +++ b/openapi.json @@ -6706,6 +6706,10 @@ "type": "string", "description": "MySQL conf" }, + "sqlite_databases": { + "type": "string", + "description": "Comma-separated SQLite database file names" + }, "health_check_enabled": { "type": "boolean", "description": "Enable the database healthcheck probe.", @@ -8118,6 +8122,129 @@ ] } }, + "\/databases\/sqlite": { + "post": { + "tags": [ + "Databases" + ], + "summary": "Create (SQLite)", + "description": "Create a new SQLite database.", + "operationId": "create-database-sqlite", + "requestBody": { + "description": "Database data", + "required": true, + "content": { + "application\/json": { + "schema": { + "required": [ + "server_uuid", + "project_uuid", + "environment_name", + "environment_uuid" + ], + "properties": { + "server_uuid": { + "type": "string", + "description": "UUID of the server" + }, + "project_uuid": { + "type": "string", + "description": "UUID of the project" + }, + "environment_name": { + "type": "string", + "description": "Name of the environment. You need to provide at least one of environment_name or environment_uuid." + }, + "environment_uuid": { + "type": "string", + "description": "UUID of the environment. You need to provide at least one of environment_name or environment_uuid." + }, + "destination_uuid": { + "type": "string", + "description": "UUID of the destination if the server has multiple destinations" + }, + "sqlite_databases": { + "type": "string", + "description": "Comma-separated SQLite database file names" + }, + "name": { + "type": "string", + "description": "Name of the database" + }, + "description": { + "type": "string", + "description": "Description of the database" + }, + "image": { + "type": "string", + "description": "Docker Image of the database" + }, + "limits_memory": { + "type": "string", + "description": "Memory limit of the database" + }, + "limits_memory_swap": { + "type": "string", + "description": "Memory swap limit of the database" + }, + "limits_memory_swappiness": { + "type": "integer", + "description": "Memory swappiness of the database" + }, + "limits_memory_reservation": { + "type": "string", + "description": "Memory reservation of the database" + }, + "limits_cpus": { + "type": "string", + "description": "CPU limit of the database" + }, + "limits_cpuset": { + "type": "string", + "description": "CPU set of the database" + }, + "limits_cpu_shares": { + "type": "integer", + "description": "CPU shares of the database" + }, + "instant_deploy": { + "type": "boolean", + "description": "Instant deploy the database" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to assign to the database." + } + }, + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "Database updated" + }, + "401": { + "$ref": "#\/components\/responses\/401" + }, + "400": { + "$ref": "#\/components\/responses\/400" + }, + "422": { + "$ref": "#\/components\/responses\/422" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "\/databases\/{uuid}\/logs": { "get": { "tags": [ diff --git a/openapi.yaml b/openapi.yaml index b0843f9194..a2866e8303 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4343,6 +4343,9 @@ paths: mysql_conf: type: string description: 'MySQL conf' + sqlite_databases: + type: string + description: 'Comma-separated SQLite database file names' health_check_enabled: type: boolean description: 'Enable the database healthcheck probe.' @@ -5342,6 +5345,93 @@ paths: security: - bearerAuth: [] + /databases/sqlite: + post: + tags: + - Databases + summary: 'Create (SQLite)' + description: 'Create a new SQLite database.' + operationId: create-database-sqlite + requestBody: + description: 'Database data' + required: true + content: + application/json: + schema: + required: + - server_uuid + - project_uuid + - environment_name + - environment_uuid + properties: + server_uuid: + type: string + description: 'UUID of the server' + project_uuid: + type: string + description: 'UUID of the project' + environment_name: + type: string + description: 'Name of the environment. You need to provide at least one of environment_name or environment_uuid.' + environment_uuid: + type: string + description: 'UUID of the environment. You need to provide at least one of environment_name or environment_uuid.' + destination_uuid: + type: string + description: 'UUID of the destination if the server has multiple destinations' + sqlite_databases: + type: string + description: 'Comma-separated SQLite database file names' + name: + type: string + description: 'Name of the database' + description: + type: string + description: 'Description of the database' + image: + type: string + description: 'Docker Image of the database' + limits_memory: + type: string + description: 'Memory limit of the database' + limits_memory_swap: + type: string + description: 'Memory swap limit of the database' + limits_memory_swappiness: + type: integer + description: 'Memory swappiness of the database' + limits_memory_reservation: + type: string + description: 'Memory reservation of the database' + limits_cpus: + type: string + description: 'CPU limit of the database' + limits_cpuset: + type: string + description: 'CPU set of the database' + limits_cpu_shares: + type: integer + description: 'CPU shares of the database' + instant_deploy: + type: boolean + description: 'Instant deploy the database' + tags: + type: array + items: { type: string } + description: 'Tags to assign to the database.' + type: object + responses: + '200': + description: 'Database updated' + '401': + $ref: '#/components/responses/401' + '400': + $ref: '#/components/responses/400' + '422': + $ref: '#/components/responses/422' + security: + - + bearerAuth: [] '/databases/{uuid}/logs': get: tags: diff --git a/routes/api.php b/routes/api.php index d1ce47dfaa..b90345c24c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -309,6 +309,7 @@ Route::group([ Route::post('/databases/clickhouse', [DatabasesController::class, 'create_database_clickhouse'])->middleware(['api.ability:write']); Route::post('/databases/dragonfly', [DatabasesController::class, 'create_database_dragonfly'])->middleware(['api.ability:write']); Route::post('/databases/keydb', [DatabasesController::class, 'create_database_keydb'])->middleware(['api.ability:write']); + Route::post('/databases/sqlite', [DatabasesController::class, 'create_database_sqlite'])->middleware(['api.ability:write']); Route::get('/databases/{uuid}', [DatabasesController::class, 'database_by_uuid'])->middleware(['api.ability:read']); Route::post('/databases/{uuid}/imports/uploads', [DatabasesController::class, 'upload_import'])->middleware(['api.ability:deploy'])->name('api.databases.imports.upload');