feat(api): add storages endpoints for applications

Add GET and PATCH /applications/{uuid}/storages routes to list and
update persistent and file storages for an application, including
support for toggling is_preview_suffix_enabled.
This commit is contained in:
Andras Bacsai 2026-03-16 15:34:27 +01:00
parent c9861e08e3
commit 0488a188a0
4 changed files with 374 additions and 0 deletions

View file

@ -3919,4 +3919,191 @@ private function validateDataApplications(Request $request, Server $server)
}
}
}
#[OA\Get(
summary: 'List Storages',
description: 'List all persistent storages and file storages by application UUID.',
path: '/applications/{uuid}/storages',
operationId: 'list-storages-by-application-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
responses: [
new OA\Response(
response: 200,
description: 'All storages by application UUID.',
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function storages(Request $request)
{
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'message' => 'Application not found',
], 404);
}
$this->authorize('view', $application);
$persistentStorages = $application->persistentStorages->sortBy('id')->values();
$fileStorages = $application->fileStorages->sortBy('id')->values();
return response()->json([
'persistent_storages' => $persistentStorages,
'file_storages' => $fileStorages,
]);
}
#[OA\Patch(
summary: 'Update Storage',
description: 'Update a persistent storage or file storage by application UUID.',
path: '/applications/{uuid}/storages',
operationId: 'update-storage-by-application-uuid',
security: [
['bearerAuth' => []],
],
tags: ['Applications'],
parameters: [
new OA\Parameter(
name: 'uuid',
in: 'path',
description: 'UUID of the application.',
required: true,
schema: new OA\Schema(
type: 'string',
)
),
],
requestBody: new OA\RequestBody(
description: 'Storage updated.',
required: true,
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'object',
required: ['id', 'type'],
properties: [
'id' => ['type' => 'integer', 'description' => 'The ID of the storage.'],
'type' => ['type' => 'string', 'enum' => ['persistent', 'file'], 'description' => 'The type of storage: persistent or file.'],
'is_preview_suffix_enabled' => ['type' => 'boolean', 'description' => 'Whether to add -pr-N suffix for preview deployments.'],
],
),
),
],
),
responses: [
new OA\Response(
response: 200,
description: 'Storage updated.',
),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 400,
ref: '#/components/responses/400',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function update_storage(Request $request)
{
$allowedFields = ['id', 'type', 'is_preview_suffix_enabled'];
$teamId = getTeamIdFromToken();
if (is_null($teamId)) {
return invalidTokenResponse();
}
$return = validateIncomingRequest($request);
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $request->uuid)->first();
if (! $application) {
return response()->json([
'message' => 'Application not found',
], 404);
}
$this->authorize('update', $application);
$validator = customApiValidator($request->all(), [
'id' => 'required|integer',
'type' => 'required|string|in:persistent,file',
'is_preview_suffix_enabled' => 'boolean',
]);
$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);
}
if ($request->type === 'persistent') {
$storage = $application->persistentStorages->where('id', $request->id)->first();
} else {
$storage = $application->fileStorages->where('id', $request->id)->first();
}
if (! $storage) {
return response()->json([
'message' => 'Storage not found.',
], 404);
}
if ($request->has('is_preview_suffix_enabled')) {
$storage->is_preview_suffix_enabled = $request->is_preview_suffix_enabled;
}
$storage->save();
return response()->json($storage);
}
}

View file

@ -3442,6 +3442,117 @@
]
}
},
"\/applications\/{uuid}\/storages": {
"get": {
"tags": [
"Applications"
],
"summary": "List Storages",
"description": "List all persistent storages and file storages by application UUID.",
"operationId": "list-storages-by-application-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "All storages by application UUID."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
},
"patch": {
"tags": [
"Applications"
],
"summary": "Update Storage",
"description": "Update a persistent storage or file storage by application UUID.",
"operationId": "update-storage-by-application-uuid",
"parameters": [
{
"name": "uuid",
"in": "path",
"description": "UUID of the application.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Storage updated.",
"required": true,
"content": {
"application\/json": {
"schema": {
"required": [
"id",
"type"
],
"properties": {
"id": {
"type": "integer",
"description": "The ID of the storage."
},
"type": {
"type": "string",
"enum": [
"persistent",
"file"
],
"description": "The type of storage: persistent or file."
},
"is_preview_suffix_enabled": {
"type": "boolean",
"description": "Whether to add -pr-N suffix for preview deployments."
}
},
"type": "object"
}
}
}
},
"responses": {
"200": {
"description": "Storage updated."
},
"401": {
"$ref": "#\/components\/responses\/401"
},
"400": {
"$ref": "#\/components\/responses\/400"
},
"404": {
"$ref": "#\/components\/responses\/404"
}
},
"security": [
{
"bearerAuth": []
}
]
}
},
"\/cloud-tokens": {
"get": {
"tags": [

View file

@ -2170,6 +2170,80 @@ paths:
security:
-
bearerAuth: []
'/applications/{uuid}/storages':
get:
tags:
- Applications
summary: 'List Storages'
description: 'List all persistent storages and file storages by application UUID.'
operationId: list-storages-by-application-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
responses:
'200':
description: 'All storages by application UUID.'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
patch:
tags:
- Applications
summary: 'Update Storage'
description: 'Update a persistent storage or file storage by application UUID.'
operationId: update-storage-by-application-uuid
parameters:
-
name: uuid
in: path
description: 'UUID of the application.'
required: true
schema:
type: string
requestBody:
description: 'Storage updated.'
required: true
content:
application/json:
schema:
required:
- id
- type
properties:
id:
type: integer
description: 'The ID of the storage.'
type:
type: string
enum: [persistent, file]
description: 'The type of storage: persistent or file.'
is_preview_suffix_enabled:
type: boolean
description: 'Whether to add -pr-N suffix for preview deployments.'
type: object
responses:
'200':
description: 'Storage updated.'
'401':
$ref: '#/components/responses/401'
'400':
$ref: '#/components/responses/400'
'404':
$ref: '#/components/responses/404'
security:
-
bearerAuth: []
/cloud-tokens:
get:
tags:

View file

@ -120,6 +120,8 @@
Route::patch('/applications/{uuid}/envs', [ApplicationsController::class, 'update_env_by_uuid'])->middleware(['api.ability:write']);
Route::delete('/applications/{uuid}/envs/{env_uuid}', [ApplicationsController::class, 'delete_env_by_uuid'])->middleware(['api.ability:write']);
Route::get('/applications/{uuid}/logs', [ApplicationsController::class, 'logs_by_uuid'])->middleware(['api.ability:read']);
Route::get('/applications/{uuid}/storages', [ApplicationsController::class, 'storages'])->middleware(['api.ability:read']);
Route::patch('/applications/{uuid}/storages', [ApplicationsController::class, 'update_storage'])->middleware(['api.ability:write']);
Route::match(['get', 'post'], '/applications/{uuid}/start', [ApplicationsController::class, 'action_deploy'])->middleware(['api.ability:deploy']);
Route::match(['get', 'post'], '/applications/{uuid}/restart', [ApplicationsController::class, 'action_restart'])->middleware(['api.ability:deploy']);