diff --git a/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php b/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php new file mode 100644 index 000000000..411b7d457 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Nodes/NodeDeploymentController.php @@ -0,0 +1,51 @@ +viableNodesService = $viableNodesService; + } + + /** + * Finds any nodes that are available using the given deployment criteria. This works + * similarly to the server creation process, but allows you to pass the deployment object + * to this endpoint and get back a list of all Nodes satisfying the requirements. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Nodes\GetDeployableNodesRequest $request + * @return array + * + * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException + */ + public function __invoke(GetDeployableNodesRequest $request): array + { + $data = $request->validated(); + $nodes = $this->viableNodesService->setLocations($data['location_ids'] ?? []) + ->setMemory($data['memory']) + ->setDisk($data['disk']) + ->handle($request->input('page') ?? 0); + + return $this->fractal->collection($nodes) + ->transformWith($this->getTransformer(NodeTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Nodes/GetDeployableNodesRequest.php b/app/Http/Requests/Api/Application/Nodes/GetDeployableNodesRequest.php new file mode 100644 index 000000000..0eae14008 --- /dev/null +++ b/app/Http/Requests/Api/Application/Nodes/GetDeployableNodesRequest.php @@ -0,0 +1,20 @@ + 'integer', + 'memory' => 'required|integer|min:0', + 'disk' => 'required|integer|min:0', + 'location_ids' => 'array', + 'location_ids.*' => 'integer', + ]; + } +} diff --git a/app/Models/Egg.php b/app/Models/Egg.php index aed4be7e3..03d5fa5f8 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -13,6 +13,7 @@ namespace Pterodactyl\Models; * @property string $docker_image -- deprecated, use $docker_images * @property string $update_url * @property array $docker_images + * @property string $file_denylist * @property string|null $config_files * @property string|null $config_startup * @property string|null $config_logs @@ -34,6 +35,7 @@ namespace Pterodactyl\Models; * @property string|null $inherit_config_startup * @property string|null $inherit_config_logs * @property string|null $inherit_config_stop + * @property string $inherit_file_denylist * @property array|null $inherit_features * * @property \Pterodactyl\Models\Nest $nest @@ -79,6 +81,7 @@ class Egg extends Model 'description', 'features', 'docker_images', + 'file_denylist', 'config_files', 'config_startup', 'config_logs', @@ -255,6 +258,21 @@ class Egg extends Model return $this->configFrom->features; } + /** + * Returns the features available to this egg from the parent configuration if there are + * no features defined for this egg specifically and there is a parent egg configured. + * + * @return string + */ + public function getInheritFileDenylistAttribute() + { + if (is_null($this->config_from)) { + return $this->file_denylist; + } + + return $this->configFrom->file_denylist; + } + /** * Gets nest associated with an egg. * diff --git a/app/Services/Deployment/FindViableNodesService.php b/app/Services/Deployment/FindViableNodesService.php index 8c3d7c757..0963d88ea 100644 --- a/app/Services/Deployment/FindViableNodesService.php +++ b/app/Services/Deployment/FindViableNodesService.php @@ -77,10 +77,15 @@ class FindViableNodesService * are tossed out, as are any nodes marked as non-public, meaning automatic * deployments should not be done against them. * - * @return \Pterodactyl\Models\Node[]|\Illuminate\Support\Collection + * @param int|null $page If provided the results will be paginated by returning + * up to 50 nodes at a time starting at the provided page. + * If "null" is provided as the value no pagination will + * be used. + * @return \Illuminate\Support\Collection|\Illuminate\Contracts\Pagination\LengthAwarePaginator + * * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException */ - public function handle() + public function handle(int $page = null) { Assert::integer($this->disk, 'Disk space must be an int, got %s'); Assert::integer($this->memory, 'Memory usage must be an int, got %s'); @@ -97,9 +102,13 @@ class FindViableNodesService $results = $query->groupBy('nodes.id') ->havingRaw('(IFNULL(SUM(servers.memory), 0) + ?) <= (nodes.memory * (1 + (nodes.memory_overallocate / 100)))', [$this->memory]) - ->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]) - ->get() - ->toBase(); + ->havingRaw('(IFNULL(SUM(servers.disk), 0) + ?) <= (nodes.disk * (1 + (nodes.disk_overallocate / 100)))', [$this->disk]); + + if (! is_null($page)) { + $results = $results->paginate(50, ['*'], 'page', $page); + } else { + $results = $results->get()->toBase(); + } if ($results->isEmpty()) { throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes')); diff --git a/app/Services/Eggs/EggUpdateService.php b/app/Services/Eggs/EggUpdateService.php index dc0bffc1f..697ed0599 100644 --- a/app/Services/Eggs/EggUpdateService.php +++ b/app/Services/Eggs/EggUpdateService.php @@ -53,6 +53,10 @@ class EggUpdateService } } + // TODO(dane): Once the admin UI is done being reworked and this is exposed + // in said UI, remove this so that you can actually update the denylist. + unset($data['file_denylist']); + $this->repository->withoutFreshModel()->update($egg->id, $data); } } diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index bd26a1ea1..7b0cb3a49 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -46,6 +46,7 @@ class EggExporterService 'description' => $egg->description, 'features' => $egg->features, 'images' => $egg->docker_images, + 'file_denylist' => $egg->inherit_file_denylist, 'startup' => $egg->startup, 'config' => [ 'files' => $egg->inherit_config_files, diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 8955b1870..19d1c7d2a 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -105,6 +105,7 @@ class EggImporterService // Maintain backwards compatability for eggs that are still using the old single image // string format. New eggs can provide an array of Docker images that can be used. 'docker_images' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')], + 'file_denylist' => implode(PHP_EOL, object_get($parsed, 'file_denylist') ?? []), 'update_url' => object_get($parsed, 'meta.update_url'), 'config_files' => object_get($parsed, 'config.files'), 'config_startup' => object_get($parsed, 'config.startup'), @@ -118,7 +119,7 @@ class EggImporterService ], true, true); collect($parsed->variables)->each(function ($variable) use ($egg) { - $this->eggVariableRepository->create(array_merge((array) $variable, [ + $this->eggVariableRepository->create(array_merge((array)$variable, [ 'egg_id' => $egg->id, ])); }); diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 790e9ecc1..b942a270a 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -91,6 +91,14 @@ class ServerConfigurationStructureService 'read_only' => $mount->read_only, ]; }), + 'egg' => [ + 'id' => $server->egg->uuid, + 'file_denylist' => [ + 'config.yml', + '**/*.json' + ] + // 'file_denylist' => explode(PHP_EOL, $server->egg->inherit_file_denylist), + ] ]; } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index d185089c4..76d371ab2 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -197,6 +197,7 @@ class ServerCreationService */ private function configureDeployment(array $data, DeploymentObject $deployment): Allocation { + /** @var \Illuminate\Support\Collection $nodes */ $nodes = $this->findViableNodesService->setLocations($deployment->getLocations()) ->setDisk(Arr::get($data, 'disk')) ->setMemory(Arr::get($data, 'memory')) diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index cbfe21e56..0e4c91d35 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -56,6 +56,7 @@ class EggTransformer extends BaseTransformer 'startup' => json_decode($model->config_startup, true), 'stop' => $model->config_stop, 'logs' => json_decode($model->config_logs, true), + 'file_denylist' => explode(PHP_EOL, $model->file_denylist), 'extends' => $model->config_from, ], 'startup' => $model->startup, diff --git a/database/migrations/2021_01_10_153937_add_file_denylist_to_egg_configs.php b/database/migrations/2021_01_10_153937_add_file_denylist_to_egg_configs.php new file mode 100644 index 000000000..a5f2c716a --- /dev/null +++ b/database/migrations/2021_01_10_153937_add_file_denylist_to_egg_configs.php @@ -0,0 +1,32 @@ +text('file_denylist')->after('docker_images'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('eggs', function (Blueprint $table) { + $table->dropColumn('file_denylist'); + }); + } +} diff --git a/routes/api-application.php b/routes/api-application.php index 08d8a54c9..5702ccbb8 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -109,6 +109,7 @@ Route::group(['prefix' => '/nests'], function () { */ Route::group(['prefix' => '/nodes'], function () { Route::get('/', 'Nodes\NodeController@index')->name('api.application.nodes'); + Route::get('/deployable', 'Nodes\NodeDeploymentController'); Route::get('/{node}', 'Nodes\NodeController@view')->name('api.application.nodes.view'); Route::get('/{node}/configuration', 'Nodes\NodeConfigurationController');