From c8faf640592a5753d110a556dc3525edddacf606 Mon Sep 17 00:00:00 2001 From: DaneEveritt Date: Sat, 7 May 2022 17:45:22 -0400 Subject: [PATCH] Support naming docker images on eggs; closes #4052 Bumps PTDL_v1 export images to PTDL_v2, updates the Minecraft specific eggs to use named images. --- .../Controllers/Admin/Nests/EggController.php | 39 ++-- .../Api/Client/Servers/SettingsController.php | 2 +- .../Settings/SetDockerImageRequest.php | 2 +- app/Models/Egg.php | 7 +- .../Eggs/Sharing/EggExporterService.php | 6 +- .../Eggs/Sharing/EggImporterService.php | 48 +++-- .../Api/Application/EggTransformer.php | 3 +- .../eggs/minecraft/egg-bungeecord.json | 24 +-- .../eggs/minecraft/egg-forge-minecraft.json | 28 +-- .../Seeders/eggs/minecraft/egg-paper.json | 30 ++-- .../minecraft/egg-sponge--sponge-vanilla.json | 22 +-- .../eggs/minecraft/egg-vanilla-minecraft.json | 24 +-- ...migrate_egg_images_array_to_new_format.php | 40 +++++ resources/scripts/api/swr/getServerStartup.ts | 8 +- .../server/startup/StartupContainer.tsx | 14 +- resources/views/admin/eggs/view.blade.php | 9 +- .../Services/Api/KeyCreationServiceTest.php | 167 ------------------ 17 files changed, 212 insertions(+), 261 deletions(-) create mode 100644 database/migrations/2022_05_07_165334_migrate_egg_images_array_to_new_format.php delete mode 100644 tests/Unit/Services/Api/KeyCreationServiceTest.php diff --git a/app/Http/Controllers/Admin/Nests/EggController.php b/app/Http/Controllers/Admin/Nests/EggController.php index f8d365515..11f9c77ad 100644 --- a/app/Http/Controllers/Admin/Nests/EggController.php +++ b/app/Http/Controllers/Admin/Nests/EggController.php @@ -74,11 +74,7 @@ class EggController extends Controller public function store(EggFormRequest $request): RedirectResponse { $data = $request->normalize(); - if (!empty($data['docker_images']) && !is_array($data['docker_images'])) { - $data['docker_images'] = array_map(function ($value) { - return trim($value); - }, explode("\n", $data['docker_images'])); - } + $data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null); $egg = $this->creationService->handle($data); $this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash(); @@ -91,7 +87,14 @@ class EggController extends Controller */ public function view(Egg $egg): View { - return view('admin.eggs.view', ['egg' => $egg]); + return view('admin.eggs.view', [ + 'egg' => $egg, + 'images' => array_map( + fn ($key, $value) => $key === $value ? $value : "$key|$value", + array_keys($egg->docker_images), + $egg->docker_images, + ), + ]); } /** @@ -104,11 +107,7 @@ class EggController extends Controller public function update(EggFormRequest $request, Egg $egg): RedirectResponse { $data = $request->normalize(); - if (!empty($data['docker_images']) && !is_array($data['docker_images'])) { - $data['docker_images'] = array_map(function ($value) { - return trim($value); - }, explode("\n", $data['docker_images'])); - } + $data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null); $this->updateService->handle($egg, $data); $this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash(); @@ -129,4 +128,22 @@ class EggController extends Controller return redirect()->route('admin.nests.view', $egg->nest_id); } + + /** + * Normalizes a string of docker image data into the expected egg format. + */ + protected function normalizeDockerImages(string $input = null): array + { + $data = array_map(fn ($value) => trim($value), explode("\n", $input ?? '')); + + $images = []; + // Iterate over the image data provided and convert it into a name => image + // pairing that is used to improve the display on the front-end. + foreach ($data as $value) { + $parts = explode('|', $value, 2); + $images[$parts[0]] = empty($parts[1]) ? $parts[0] : $parts[1]; + } + + return $images; + } } diff --git a/app/Http/Controllers/Api/Client/Servers/SettingsController.php b/app/Http/Controllers/Api/Client/Servers/SettingsController.php index 110cd0868..a7fbb8a75 100644 --- a/app/Http/Controllers/Api/Client/Servers/SettingsController.php +++ b/app/Http/Controllers/Api/Client/Servers/SettingsController.php @@ -78,7 +78,7 @@ class SettingsController extends ClientApiController */ public function dockerImage(SetDockerImageRequest $request, Server $server) { - if (!in_array($server->image, $server->egg->docker_images)) { + if (!in_array($server->image, array_values($server->egg->docker_images))) { throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.'); } diff --git a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php index bd3a1e65f..abe5d0436 100644 --- a/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php @@ -27,7 +27,7 @@ class SetDockerImageRequest extends ClientApiRequest implements ClientPermission Assert::isInstanceOf($server, Server::class); return [ - 'docker_image' => ['required', 'string', Rule::in($server->egg->docker_images)], + 'docker_image' => ['required', 'string', Rule::in(array_values($server->egg->docker_images))], ]; } } diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 6e2c26802..dbb90d301 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -12,7 +12,7 @@ namespace Pterodactyl\Models; * @property array|null $features * @property string $docker_image -- deprecated, use $docker_images * @property string $update_url - * @property array $docker_images + * @property array $docker_images * @property array|null $file_denylist * @property string|null $config_files * @property string|null $config_startup @@ -50,6 +50,11 @@ class Egg extends Model */ public const RESOURCE_NAME = 'egg'; + /** + * Defines the current egg export version. + */ + public const EXPORT_VERSION = 'PTDL_v2'; + /** * Different features that can be enabled on any given egg. These are used internally * to determine which types of frontend functionality should be shown to the user. Eggs diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index f64656150..26723747a 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Services\Eggs\Sharing; use Carbon\Carbon; +use Pterodactyl\Models\Egg; use Illuminate\Support\Collection; use Pterodactyl\Models\EggVariable; use Pterodactyl\Contracts\Repository\EggRepositoryInterface; @@ -34,7 +35,7 @@ class EggExporterService $struct = [ '_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO', 'meta' => [ - 'version' => 'PTDL_v1', + 'version' => Egg::EXPORT_VERSION, 'update_url' => $egg->update_url, ], 'exported_at' => Carbon::now()->toIso8601String(), @@ -42,7 +43,7 @@ class EggExporterService 'author' => $egg->author, 'description' => $egg->description, 'features' => $egg->features, - 'images' => $egg->docker_images, + 'docker_images' => $egg->docker_images, 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(function ($value) { return !empty($value); }), @@ -63,6 +64,7 @@ class EggExporterService 'variables' => $egg->variables->transform(function (EggVariable $item) { return Collection::make($item->toArray()) ->except(['id', 'egg_id', 'created_at', 'updated_at']) + ->merge(['field_type' => 'text']) ->toArray(); }), ]; diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 729277690..74f71e1bd 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -10,7 +10,6 @@ use Illuminate\Support\Collection; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\NestRepositoryInterface; -use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException; use Pterodactyl\Exceptions\Service\InvalidFileUploadException; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; @@ -56,8 +55,8 @@ class EggImporterService * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException * @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException + * @throws \JsonException */ public function handle(UploadedFile $file, int $nest): Egg { @@ -66,13 +65,13 @@ class EggImporterService } /** @var array $parsed */ - $parsed = json_decode($file->openFile()->fread($file->getSize()), true); - if (json_last_error() !== 0) { - throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()])); + $parsed = json_decode($file->openFile()->fread($file->getSize()), true, 512, JSON_THROW_ON_ERROR); + if (!in_array(Arr::get($parsed, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) { + throw new InvalidFileUploadException(trans('exceptions.nest.importer.invalid_json_provided')); } - if (Arr::get($parsed, 'meta.version') !== 'PTDL_v1') { - throw new InvalidFileUploadException(trans('exceptions.nest.importer.invalid_json_provided')); + if ($parsed['meta']['version'] !== Egg::EXPORT_VERSION) { + $parsed = $this->convertV1ToV2($parsed); } $nest = $this->nestRepository->getWithEggs($nest); @@ -86,9 +85,7 @@ class EggImporterService 'name' => Arr::get($parsed, 'name'), 'description' => Arr::get($parsed, 'description'), 'features' => Arr::get($parsed, 'features'), - // 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' => Arr::get($parsed, 'images') ?? [Arr::get($parsed, 'image')], + 'docker_images' => Arr::get($parsed, 'docker_images'), 'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))->filter(function ($value) { return !empty($value); }), @@ -105,6 +102,8 @@ class EggImporterService ], true, true); Collection::make($parsed['variables'] ?? [])->each(function (array $variable) use ($egg) { + unset($variable['field_type']); + $this->eggVariableRepository->create(array_merge($variable, [ 'egg_id' => $egg->id, ])); @@ -114,4 +113,33 @@ class EggImporterService return $egg; } + + /** + * Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles + * the "docker_images" field potentially not being present, and not being in the + * expected "key => value" format. + */ + protected function convertV1ToV2(array $parsed): array + { + // 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. + if (!isset($parsed['images'])) { + $images = [Arr::get($parsed, 'image') ?? 'nil']; + } else { + $images = $parsed['images']; + } + + unset($parsed['images'], $parsed['image']); + + $parsed['docker_images'] = []; + foreach ($images as $image) { + $parsed['docker_images'][$image] = $image; + } + + $parsed['variables'] = array_map(function ($value) { + return array_merge($value, ['field_type' => 'text']); + }, $parsed['variables']); + + return $parsed; + } } diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index 8dccb7552..d2b269b73 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Transformers\Api\Application; use Pterodactyl\Models\Egg; +use Illuminate\Support\Arr; use Pterodactyl\Models\Nest; use Pterodactyl\Models\Server; use Pterodactyl\Models\EggVariable; @@ -49,7 +50,7 @@ class EggTransformer extends BaseTransformer // "docker_image" is deprecated, but left here to avoid breaking too many things at once // in external software. We'll remove it down the road once things have gotten the chance // to upgrade to using "docker_images". - 'docker_image' => count($model->docker_images) > 0 ? $model->docker_images[0] : '', + 'docker_image' => count($model->docker_images) > 0 ? Arr::first($model->docker_images) : '', 'docker_images' => $model->docker_images, 'config' => [ 'files' => json_decode($model->config_files, true), diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json index ac3749c8c..cc7440823 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json @@ -1,10 +1,10 @@ { "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { - "version": "PTDL_v1", + "version": "PTDL_v2", "update_url": null }, - "exported_at": "2021-11-14T19:23:12+00:00", + "exported_at": "2022-05-07T17:35:07-04:00", "name": "Bungeecord", "author": "support@pterodactyl.io", "description": "For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community's full potential.", @@ -13,12 +13,12 @@ "java_version", "pid_limit" ], - "images": [ - "ghcr.io\/pterodactyl\/yolks:java_8", - "ghcr.io\/pterodactyl\/yolks:java_11", - "ghcr.io\/pterodactyl\/yolks:java_16", - "ghcr.io\/pterodactyl\/yolks:java_17" - ], + "docker_images": { + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", "config": { @@ -42,7 +42,8 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|alpha_num|between:1,6" + "rules": "required|alpha_num|between:1,6", + "field_type": "text" }, { "name": "Bungeecord Jar File", @@ -51,7 +52,8 @@ "default_value": "bungeecord.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "field_type": "text" } ] -} +} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json index a077df6d1..ae105110d 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json @@ -1,10 +1,10 @@ { "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { - "version": "PTDL_v1", + "version": "PTDL_v2", "update_url": null }, - "exported_at": "2021-12-11T22:51:29+00:00", + "exported_at": "2022-05-07T17:35:08-04:00", "name": "Forge Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft Forge Server. Minecraft Forge is a modding API (Application Programming Interface), which makes it easier to create mods, and also make sure mods are compatible with each other.", @@ -13,12 +13,12 @@ "java_version", "pid_limit" ], - "images": [ - "ghcr.io\/pterodactyl\/yolks:java_17", - "ghcr.io\/pterodactyl\/yolks:java_16", - "ghcr.io\/pterodactyl\/yolks:java_11", - "ghcr.io\/pterodactyl\/yolks:java_8" - ], + "docker_images": { + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true $( [[ ! -f unix_args.txt ]] && printf %s \"-jar {{SERVER_JARFILE}}\" || printf %s \"@unix_args.txt\" )", "config": { @@ -42,7 +42,8 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "field_type": "text" }, { "name": "Minecraft Version", @@ -51,7 +52,8 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:9" + "rules": "required|string|max:9", + "field_type": "text" }, { "name": "Build Type", @@ -60,7 +62,8 @@ "default_value": "recommended", "user_viewable": true, "user_editable": true, - "rules": "required|string|in:recommended,latest" + "rules": "required|string|in:recommended,latest", + "field_type": "text" }, { "name": "Forge Version", @@ -69,7 +72,8 @@ "default_value": "", "user_viewable": true, "user_editable": true, - "rules": "nullable|string|max:25" + "rules": "nullable|string|max:25", + "field_type": "text" } ] } \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-paper.json b/database/Seeders/eggs/minecraft/egg-paper.json index 39ab21665..d6db44fd5 100644 --- a/database/Seeders/eggs/minecraft/egg-paper.json +++ b/database/Seeders/eggs/minecraft/egg-paper.json @@ -1,10 +1,10 @@ { "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { - "version": "PTDL_v1", + "version": "PTDL_v2", "update_url": null }, - "exported_at": "2022-03-11T10:21:01-05:00", + "exported_at": "2022-05-07T17:35:09-04:00", "name": "Paper", "author": "parker@pterodactyl.io", "description": "High performance Spigot fork that aims to fix gameplay and mechanics inconsistencies.", @@ -13,12 +13,12 @@ "java_version", "pid_limit" ], - "images": [ - "ghcr.io\/pterodactyl\/yolks:java_8", - "ghcr.io\/pterodactyl\/yolks:java_11", - "ghcr.io\/pterodactyl\/yolks:java_16", - "ghcr.io\/pterodactyl\/yolks:java_17" - ], + "docker_images": { + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -Dterminal.jline=false -Dterminal.ansi=true -jar {{SERVER_JARFILE}}", "config": { @@ -42,7 +42,8 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "nullable|string|max:20" + "rules": "nullable|string|max:20", + "field_type": "text" }, { "name": "Server Jar File", @@ -51,7 +52,8 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "field_type": "text" }, { "name": "Download Path", @@ -60,7 +62,8 @@ "default_value": "", "user_viewable": false, "user_editable": false, - "rules": "nullable|string" + "rules": "nullable|string", + "field_type": "text" }, { "name": "Build Number", @@ -69,7 +72,8 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|max:20" + "rules": "required|string|max:20", + "field_type": "text" } ] -} +} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json index 908c1f4e5..70b247d2e 100644 --- a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json +++ b/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json @@ -1,10 +1,10 @@ { "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { - "version": "PTDL_v1", + "version": "PTDL_v2", "update_url": null }, - "exported_at": "2021-10-22T19:19:17+02:00", + "exported_at": "2022-05-07T17:35:10-04:00", "name": "Sponge (SpongeVanilla)", "author": "support@pterodactyl.io", "description": "SpongeVanilla is the SpongeAPI implementation for Vanilla Minecraft.", @@ -13,11 +13,11 @@ "java_version", "pid_limit" ], - "images": [ - "ghcr.io\/pterodactyl\/yolks:java_8", - "ghcr.io\/pterodactyl\/yolks:java_11", - "ghcr.io\/pterodactyl\/yolks:java_16" - ], + "docker_images": { + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", "config": { @@ -41,7 +41,8 @@ "default_value": "1.12.2-7.3.0", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/" + "rules": "required|regex:\/^([a-zA-Z0-9.\\-_]+)$\/", + "field_type": "text" }, { "name": "Server Jar File", @@ -50,7 +51,8 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "field_type": "text" } ] -} +} \ No newline at end of file diff --git a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json index 2361a2974..9965c3b2c 100644 --- a/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json @@ -1,10 +1,10 @@ { "_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO", "meta": { - "version": "PTDL_v1", + "version": "PTDL_v2", "update_url": null }, - "exported_at": "2021-11-14T19:18:30+00:00", + "exported_at": "2022-05-07T17:35:11-04:00", "name": "Vanilla Minecraft", "author": "support@pterodactyl.io", "description": "Minecraft is a game about placing blocks and going on adventures. Explore randomly generated worlds and build amazing things from the simplest of homes to the grandest of castles. Play in Creative Mode with unlimited resources or mine deep in Survival Mode, crafting weapons and armor to fend off dangerous mobs. Do all this alone or with friends.", @@ -13,12 +13,12 @@ "java_version", "pid_limit" ], - "images": [ - "ghcr.io\/pterodactyl\/yolks:java_8", - "ghcr.io\/pterodactyl\/yolks:java_11", - "ghcr.io\/pterodactyl\/yolks:java_16", - "ghcr.io\/pterodactyl\/yolks:java_17" - ], + "docker_images": { + "Java 17": "ghcr.io\/pterodactyl\/yolks:java_17", + "Java 16": "ghcr.io\/pterodactyl\/yolks:java_16", + "Java 11": "ghcr.io\/pterodactyl\/yolks:java_11", + "Java 8": "ghcr.io\/pterodactyl\/yolks:java_8" + }, "file_denylist": [], "startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}", "config": { @@ -42,7 +42,8 @@ "default_value": "server.jar", "user_viewable": true, "user_editable": true, - "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/" + "rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/", + "field_type": "text" }, { "name": "Server Version", @@ -51,7 +52,8 @@ "default_value": "latest", "user_viewable": true, "user_editable": true, - "rules": "required|string|between:3,15" + "rules": "required|string|between:3,15", + "field_type": "text" } ] -} +} \ No newline at end of file diff --git a/database/migrations/2022_05_07_165334_migrate_egg_images_array_to_new_format.php b/database/migrations/2022_05_07_165334_migrate_egg_images_array_to_new_format.php new file mode 100644 index 000000000..78dfe6e37 --- /dev/null +++ b/database/migrations/2022_05_07_165334_migrate_egg_images_array_to_new_format.php @@ -0,0 +1,40 @@ + value pairings to support naming the + * images provided. + */ + public function up() + { + DB::table('eggs')->select(['id', 'docker_images'])->cursor()->each(function ($egg) { + $images = is_null($egg->docker_images) ? [] : json_decode($egg->docker_images, true, 512, JSON_THROW_ON_ERROR); + + $results = []; + foreach ($images as $key => $value) { + $results[is_int($key) ? $value : $key] = $value; + } + + DB::table('eggs')->where('id', $egg->id)->update(['docker_images' => $results]); + }); + } + + /** + * Reverse the migrations. This just keeps the values from the docker images array. + * + * @return void + */ + public function down() + { + DB::table('eggs')->select(['id', 'docker_images'])->cursor()->each(function ($egg) { + DB::table('eggs')->where('id', $egg->id)->update([ + 'docker_images' => array_values(json_decode($egg->docker_images, true, 512, JSON_THROW_ON_ERROR)), + ]); + }); + } +} diff --git a/resources/scripts/api/swr/getServerStartup.ts b/resources/scripts/api/swr/getServerStartup.ts index b7089b7bd..0755be211 100644 --- a/resources/scripts/api/swr/getServerStartup.ts +++ b/resources/scripts/api/swr/getServerStartup.ts @@ -6,7 +6,7 @@ import { ServerEggVariable } from '@/api/server/types'; interface Response { invocation: string; variables: ServerEggVariable[]; - dockerImages: string[]; + dockerImages: Record; } export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise => { @@ -14,5 +14,9 @@ export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startu const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable); - return { invocation: data.meta.startup_command, variables, dockerImages: data.meta.docker_images || [] }; + return { + variables, + invocation: data.meta.startup_command, + dockerImages: data.meta.docker_images || {}, + }; }, { initialData, errorRetryCount: 3 }); diff --git a/resources/scripts/components/server/startup/StartupContainer.tsx b/resources/scripts/components/server/startup/StartupContainer.tsx index b5bb1fea5..3737c54f3 100644 --- a/resources/scripts/components/server/startup/StartupContainer.tsx +++ b/resources/scripts/components/server/startup/StartupContainer.tsx @@ -29,11 +29,11 @@ const StartupContainer = () => { const { data, error, isValidating, mutate } = getServerStartup(uuid, { ...variables, - dockerImages: [ variables.dockerImage ], + dockerImages: { [variables.dockerImage]: variables.dockerImage }, }); const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState); - const isCustomImage = data && !data.dockerImages.map(v => v.toLowerCase()).includes(variables.dockerImage.toLowerCase()); + const isCustomImage = data && !Object.values(data.dockerImages).map(v => v.toLowerCase()).includes(variables.dockerImage.toLowerCase()); useEffect(() => { // Since we're passing in initial data this will not trigger on mount automatically. We @@ -87,16 +87,18 @@ const StartupContainer = () => { - {data.dockerImages.length > 1 && !isCustomImage ? + {Object.keys(data.dockerImages).length > 1 && !isCustomImage ? <> diff --git a/resources/views/admin/eggs/view.blade.php b/resources/views/admin/eggs/view.blade.php index 7a235e069..a2cda15e8 100644 --- a/resources/views/admin/eggs/view.blade.php +++ b/resources/views/admin/eggs/view.blade.php @@ -83,8 +83,13 @@
- -

The docker images available to servers using this egg. Enter one per line. Users will be able to select from this list of images if more than one value is provided.

+ +

+ The docker images available to servers using this egg. Enter one per line. Users + will be able to select from this list of images if more than one value is provided. + Optionally, a display name may be provided by prefixing the image with the name + followed by a pipe character, and then the image URL. Example: Display Name|ghcr.io/my/egg +

diff --git a/tests/Unit/Services/Api/KeyCreationServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php deleted file mode 100644 index 5c847f144..000000000 --- a/tests/Unit/Services/Api/KeyCreationServiceTest.php +++ /dev/null @@ -1,167 +0,0 @@ -encrypter = m::mock(Encrypter::class); - $this->repository = m::mock(ApiKeyRepositoryInterface::class); - } - - /** - * Test that the service is able to create a keypair and assign the correct permissions. - */ - public function testKeyIsCreated() - { - $model = ApiKey::factory()->make(); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturnCallback(function ($length) { - return 'str_' . $length; - }); - - $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); - - $this->repository->shouldReceive('create')->with([ - 'test-data' => 'test', - 'key_type' => ApiKey::TYPE_NONE, - 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, - 'token' => $model->token, - ], true, true)->once()->andReturn($model); - - $response = $this->getService()->handle(['test-data' => 'test']); - - $this->assertNotEmpty($response); - $this->assertInstanceOf(ApiKey::class, $response); - $this->assertSame($model, $response); - } - - /** - * Test that an identifier is only set by the function. - */ - public function testIdentifierAndTokenAreOnlySetByFunction() - { - $model = ApiKey::factory()->make(); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturnCallback(function ($length) { - return 'str_' . $length; - }); - - $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); - - $this->repository->shouldReceive('create')->with([ - 'key_type' => ApiKey::TYPE_NONE, - 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, - 'token' => $model->token, - ], true, true)->once()->andReturn($model); - - $response = $this->getService()->handle(['identifier' => 'customIdentifier', 'token' => 'customToken']); - - $this->assertNotEmpty($response); - $this->assertInstanceOf(ApiKey::class, $response); - $this->assertSame($model, $response); - } - - /** - * Test that permissions passed in are loaded onto the key data. - */ - public function testPermissionsAreRetrievedForApplicationKeys() - { - $model = ApiKey::factory()->make(); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturnCallback(function ($length) { - return 'str_' . $length; - }); - - $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); - - $this->repository->shouldReceive('create')->with([ - 'key_type' => ApiKey::TYPE_APPLICATION, - 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, - 'token' => $model->token, - 'permission-key' => 'exists', - ], true, true)->once()->andReturn($model); - - $response = $this->getService()->setKeyType(ApiKey::TYPE_APPLICATION)->handle([], ['permission-key' => 'exists']); - - $this->assertNotEmpty($response); - $this->assertInstanceOf(ApiKey::class, $response); - $this->assertSame($model, $response); - } - - /** - * Test that permissions are not retrieved for any key that is not an application key. - * - * @dataProvider keyTypeDataProvider - */ - public function testPermissionsAreNotRetrievedForNonApplicationKeys($keyType) - { - $model = ApiKey::factory()->make(); - - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'str_random') - ->expects($this->exactly(2))->willReturnCallback(function ($length) { - return 'str_' . $length; - }); - - $this->encrypter->shouldReceive('encrypt')->with('str_' . ApiKey::KEY_LENGTH)->once()->andReturn($model->token); - - $this->repository->shouldReceive('create')->with([ - 'key_type' => $keyType, - 'identifier' => 'str_' . ApiKey::IDENTIFIER_LENGTH, - 'token' => $model->token, - ], true, true)->once()->andReturn($model); - - $response = $this->getService()->setKeyType($keyType)->handle([], ['fake-permission' => 'should-not-exist']); - - $this->assertNotEmpty($response); - $this->assertInstanceOf(ApiKey::class, $response); - $this->assertSame($model, $response); - } - - /** - * Provide key types that are not an application specific key. - */ - public function keyTypeDataProvider(): array - { - return [ - [ApiKey::TYPE_NONE], [ApiKey::TYPE_ACCOUNT], [ApiKey::TYPE_DAEMON_USER], [ApiKey::TYPE_DAEMON_APPLICATION], - ]; - } - - /** - * Return an instance of the service with mocked dependencies for testing. - */ - private function getService(): KeyCreationService - { - return new KeyCreationService($this->repository, $this->encrypter); - } -}