diff --git a/app/Http/Controllers/Api/Application/Mounts/MountController.php b/app/Http/Controllers/Api/Application/Mounts/MountController.php new file mode 100644 index 0000000000..55bcc16040 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Mounts/MountController.php @@ -0,0 +1,165 @@ +allowedFilters(['uuid', 'name']) + ->allowedSorts(['id', 'uuid']) + ->paginate($request->query('per_page') ?? 50); + + return $this->fractal->collection($mounts) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + /** + * Return data for a single instance of a mount. + */ + public function view(GetMountRequest $request, Mount $mount): array + { + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + /** + * Create a new mount on the Panel. Returns the created mount and an HTTP/201 + * status response on success. + * + * @throws \App\Exceptions\Model\DataValidationException + */ + public function store(StoreMountRequest $request): JsonResponse + { + $model = (new Mount())->fill($request->validated()); + $model->forceFill(['uuid' => Uuid::uuid4()->toString()]); + + $model->saveOrFail(); + $mount = $model->fresh(); + + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->addMeta([ + 'resource' => route('api.application.mounts.view', [ + 'mount' => $mount->id, + ]), + ]) + ->respond(201); + } + + /** + * Update an existing mount on the Panel. + * + * @throws \Throwable + */ + public function update(UpdateMountRequest $request, Mount $mount): array + { + $mount->forceFill($request->validated())->save(); + + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + /** + * Deletes a given mount from the Panel as long as there are no servers + * currently attached to it. + * + * @throws \App\Exceptions\Service\HasActiveServersException + */ + public function delete(DeleteMountRequest $request, Mount $mount): JsonResponse + { + if ($mount->servers()->count() > 0) { + throw new HasActiveServersException($this->translator->get('exceptions.mount.servers_attached')); + } + + $mount->delete(); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } + + /** + * Adds eggs to the mount's many-to-many relation. + */ + public function addEggs(Request $request, Mount $mount): array + { + $validatedData = $request->validate([ + 'eggs' => 'required|exists:eggs,id', + ]); + + $eggs = $validatedData['eggs'] ?? []; + if (count($eggs) > 0) { + $mount->eggs()->attach($eggs); + } + + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + /** + * Adds nodes to the mount's many-to-many relation. + */ + public function addNodes(Request $request, Mount $mount): array + { + $data = $request->validate(['nodes' => 'required|exists:nodes,id']); + + $nodes = $data['nodes'] ?? []; + if (count($nodes) > 0) { + $mount->nodes()->attach($nodes); + } + + return $this->fractal->item($mount) + ->transformWith($this->getTransformer(MountTransformer::class)) + ->toArray(); + } + + /** + * Deletes an egg from the mount's many-to-many relation. + */ + public function deleteEgg(Mount $mount, int $egg_id): JsonResponse + { + $mount->eggs()->detach($egg_id); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } + + /** + * Deletes a node from the mount's many-to-many relation. + */ + public function deleteNode(Mount $mount, int $node_id): JsonResponse + { + $mount->nodes()->detach($node_id); + + return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); + } +} diff --git a/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php new file mode 100644 index 0000000000..927db27b56 --- /dev/null +++ b/app/Http/Requests/Api/Application/Mounts/DeleteMountRequest.php @@ -0,0 +1,13 @@ +route()->parameter('mount'); + + return parent::rules(Mount::getRulesForUpdate($mount->id)); + } +} diff --git a/app/Models/ApiKey.php b/app/Models/ApiKey.php index b4d48a075a..629212526f 100644 --- a/app/Models/ApiKey.php +++ b/app/Models/ApiKey.php @@ -28,6 +28,7 @@ * @property int $r_eggs * @property int $r_database_hosts * @property int $r_server_databases + * @property int $r_mounts * @property \App\Models\User $tokenable * @property \App\Models\User $user * @@ -83,7 +84,7 @@ class ApiKey extends Model */ public const KEY_LENGTH = 32; - public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases']; + public const RESOURCES = ['servers', 'nodes', 'allocations', 'users', 'eggs', 'database_hosts', 'server_databases', 'mounts']; /** * The table associated with the model. @@ -109,6 +110,7 @@ class ApiKey extends Model 'r_' . AdminAcl::RESOURCE_EGGS, 'r_' . AdminAcl::RESOURCE_NODES, 'r_' . AdminAcl::RESOURCE_SERVERS, + 'r_' . AdminAcl::RESOURCE_MOUNTS, ]; /** @@ -137,6 +139,7 @@ class ApiKey extends Model 'r_' . AdminAcl::RESOURCE_EGGS => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_NODES => 'integer|min:0|max:3', 'r_' . AdminAcl::RESOURCE_SERVERS => 'integer|min:0|max:3', + 'r_' . AdminAcl::RESOURCE_MOUNTS => 'integer|min:0|max:3', ]; protected function casts(): array @@ -155,6 +158,7 @@ protected function casts(): array 'r_' . AdminAcl::RESOURCE_EGGS => 'int', 'r_' . AdminAcl::RESOURCE_NODES => 'int', 'r_' . AdminAcl::RESOURCE_SERVERS => 'int', + 'r_' . AdminAcl::RESOURCE_MOUNTS => 'int', ]; } diff --git a/app/Services/Acl/Api/AdminAcl.php b/app/Services/Acl/Api/AdminAcl.php index b5f9077fc2..3a0e4961c1 100644 --- a/app/Services/Acl/Api/AdminAcl.php +++ b/app/Services/Acl/Api/AdminAcl.php @@ -31,6 +31,7 @@ class AdminAcl public const RESOURCE_EGGS = 'eggs'; public const RESOURCE_DATABASE_HOSTS = 'database_hosts'; public const RESOURCE_SERVER_DATABASES = 'server_databases'; + public const RESOURCE_MOUNTS = 'mounts'; /** * Determine if an API key has permission to perform a specific read/write operation. diff --git a/app/Transformers/Api/Application/MountTransformer.php b/app/Transformers/Api/Application/MountTransformer.php new file mode 100644 index 0000000000..c658f30fa2 --- /dev/null +++ b/app/Transformers/Api/Application/MountTransformer.php @@ -0,0 +1,89 @@ +toArray(); + } + + /** + * Return the eggs associated with this mount. + * + * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeEggs(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_EGGS)) { + return $this->null(); + } + + $mount->loadMissing('eggs'); + + return $this->collection( + $mount->getRelation('eggs'), + $this->makeTransformer(EggTransformer::class), + 'egg' + ); + } + + /** + * Return the nodes associated with this mount. + * + * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeNodes(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { + return $this->null(); + } + + $mount->loadMissing('nodes'); + + return $this->collection( + $mount->getRelation('nodes'), + $this->makeTransformer(NodeTransformer::class), + 'node' + ); + } + + /** + * Return the servers associated with this mount. + * + * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException + */ + public function includeServers(Mount $mount): Collection|NullResource + { + if (!$this->authorize(AdminAcl::RESOURCE_SERVERS)) { + return $this->null(); + } + + $mount->loadMissing('servers'); + + return $this->collection( + $mount->getRelation('servers'), + $this->makeTransformer(ServerTransformer::class), + 'server' + ); + } +} diff --git a/database/migrations/2024_04_28_184102_add_mounts_to_api_keys.php b/database/migrations/2024_04_28_184102_add_mounts_to_api_keys.php new file mode 100644 index 0000000000..bb5a02766f --- /dev/null +++ b/database/migrations/2024_04_28_184102_add_mounts_to_api_keys.php @@ -0,0 +1,28 @@ +unsignedTinyInteger('r_mounts')->default(0); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('api_keys', function (Blueprint $table) { + $table->dropColumn('r_mounts'); + }); + } +}; diff --git a/lang/en/exceptions.php b/lang/en/exceptions.php index 3977c87c24..3c9adf4c90 100644 --- a/lang/en/exceptions.php +++ b/lang/en/exceptions.php @@ -52,4 +52,7 @@ 'api' => [ 'resource_not_found' => 'The requested resource does not exist on this server.', ], + 'mount' => [ + 'servers_attached' => 'A mount must have no servers attached to it in order to be deleted.', + ], ]; diff --git a/routes/api-application.php b/routes/api-application.php index f6dfb84885..c213c74ab3 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -118,3 +118,26 @@ Route::delete('/{database_host:id}', [Application\DatabaseHosts\DatabaseHostController::class, 'delete']); }); + +/* +|-------------------------------------------------------------------------- +| Mount Controller Routes +|-------------------------------------------------------------------------- +| +| Endpoint: /api/application/mounts +| +*/ +Route::prefix('mounts')->group(function () { + Route::get('/', [Application\Mounts\MountController::class, 'index'])->name('api.application.mounts'); + Route::get('/{mount:id}', [Application\Mounts\MountController::class, 'view'])->name('api.application.mounts.view'); + + Route::post('/', [Application\Mounts\MountController::class, 'store']); + Route::post('/{mount:id}/eggs', [Application\Mounts\MountController::class, 'addEggs'])->name('api.application.mounts.eggs'); + Route::post('/{mount:id}/nodes', [Application\Mounts\MountController::class, 'addNodes'])->name('api.application.mounts.nodes'); + + Route::patch('/{mount:id}', [Application\Mounts\MountController::class, 'update']); + + Route::delete('/{mount:id}', [Application\Mounts\MountController::class, 'delete']); + Route::delete('/{mount:id}/eggs/{egg_id}', [Application\Mounts\MountController::class, 'deleteEgg']); + Route::delete('/{mount:id}/nodes/{node_id}', [Application\Mounts\MountController::class, 'deleteNode']); +}); diff --git a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php index 3c14bbc4ec..183b48c392 100644 --- a/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php +++ b/tests/Integration/Api/Application/ApplicationApiIntegrationTestCase.php @@ -87,6 +87,7 @@ protected function createApiKey(User $user, array $permissions = []): ApiKey 'r_eggs' => AdminAcl::READ | AdminAcl::WRITE, 'r_database_hosts' => AdminAcl::READ | AdminAcl::WRITE, 'r_server_databases' => AdminAcl::READ | AdminAcl::WRITE, + 'r_mounts' => AdminAcl::READ | AdminAcl::WRITE, ], $permissions)); }