From b9a25d7feda878c16674169a6191110767758b51 Mon Sep 17 00:00:00 2001 From: jasonzli-DEV Date: Thu, 16 Jul 2026 20:51:38 -0400 Subject: [PATCH] fix docker image becoming nill when importing eggs --- app/Models/Egg.php | 8 +- app/Services/Eggs/EggParserService.php | 6 +- .../Services/Eggs/EggParserServiceTest.php | 135 ++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 tests/Unit/Services/Eggs/EggParserServiceTest.php diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 37c7d4d2a..5a2c0f1f1 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -66,10 +66,16 @@ class Egg extends Model implements Identifiable */ public const RESOURCE_NAME = 'egg'; + /** + * Every egg version the panel can import sorted by newest first. + * Add to this list to release a new egg version. + */ + public const VERSIONS = ['RCYL_v26', 'PTDL_v2', 'PTDL_v1']; + /** * Defines the current egg export version. */ - public const EXPORT_VERSION = 'RCYL_v26'; + public const EXPORT_VERSION = self::VERSIONS[0]; /** * Different features that can be enabled on any given egg. These are used internally diff --git a/app/Services/Eggs/EggParserService.php b/app/Services/Eggs/EggParserService.php index da73b159a..c9d41636b 100644 --- a/app/Services/Eggs/EggParserService.php +++ b/app/Services/Eggs/EggParserService.php @@ -24,7 +24,7 @@ public function handle(UploadedFile $file): array /** @var array $parsed */ $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', 'RCYL_v26'])) { + if (! in_array(Arr::get($parsed, 'meta.version') ?? '', Egg::VERSIONS)) { throw new InvalidFileUploadException('The JSON file provided is not in a format that can be recognized.'); } @@ -56,13 +56,13 @@ public function fillFromParsed(Egg $model, array $parsed): Egg } /** - * Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles + * Converts a PTDL_v1 egg into the expected modern egg format. This just handles * the "docker_images" field potentially not being present, and not being in the * expected "key => value" format. */ protected function convertToV2(array $parsed): array { - if (Arr::get($parsed, 'meta.version') === Egg::EXPORT_VERSION) { + if (Arr::get($parsed, 'meta.version') !== 'PTDL_v1') { return $parsed; } diff --git a/tests/Unit/Services/Eggs/EggParserServiceTest.php b/tests/Unit/Services/Eggs/EggParserServiceTest.php new file mode 100644 index 000000000..34c11784a --- /dev/null +++ b/tests/Unit/Services/Eggs/EggParserServiceTest.php @@ -0,0 +1,135 @@ +service = new EggParserService(); + } + + public function test_it_keeps_the_docker_images_of_a_ptdl_v2_egg() + { + $parsed = $this->parse([ + 'meta' => ['version' => 'PTDL_v2', 'update_url' => null], + 'docker_images' => [ + 'Python 3.12' => 'ghcr.io/parkervcp/yolks:python_3.12', + 'Python 3.11' => 'ghcr.io/parkervcp/yolks:python_3.11', + ], + ]); + + $this->assertEquals([ + 'Python 3.12' => 'ghcr.io/parkervcp/yolks:python_3.12', + 'Python 3.11' => 'ghcr.io/parkervcp/yolks:python_3.11', + ], $parsed['docker_images']); + } + + public function test_it_keeps_the_docker_images_of_a_current_export() + { + $parsed = $this->parse([ + 'meta' => ['version' => Egg::EXPORT_VERSION, 'update_url' => null], + 'docker_images' => ['Python 3.12' => 'ghcr.io/parkervcp/yolks:python_3.12'], + ]); + + $this->assertEquals(['Python 3.12' => 'ghcr.io/parkervcp/yolks:python_3.12'], $parsed['docker_images']); + } + + public function test_it_converts_the_single_image_of_a_ptdl_v1_egg() + { + $parsed = $this->parse([ + 'meta' => ['version' => 'PTDL_v1', 'update_url' => null], + 'image' => 'quay.io/pterodactyl/core:python', + ]); + + $this->assertEquals([ + 'quay.io/pterodactyl/core:python' => 'quay.io/pterodactyl/core:python', + ], $parsed['docker_images']); + $this->assertArrayNotHasKey('image', $parsed); + } + + public function test_it_converts_the_image_list_of_a_ptdl_v1_egg() + { + $parsed = $this->parse([ + 'meta' => ['version' => 'PTDL_v1', 'update_url' => null], + 'images' => ['quay.io/pterodactyl/core:python', 'quay.io/pterodactyl/core:python-3.8'], + ]); + + $this->assertEquals([ + 'quay.io/pterodactyl/core:python' => 'quay.io/pterodactyl/core:python', + 'quay.io/pterodactyl/core:python-3.8' => 'quay.io/pterodactyl/core:python-3.8', + ], $parsed['docker_images']); + $this->assertArrayNotHasKey('images', $parsed); + } + + public function test_it_falls_back_to_nil_for_a_ptdl_v1_egg_without_an_image() + { + $parsed = $this->parse(['meta' => ['version' => 'PTDL_v1', 'update_url' => null]]); + + $this->assertEquals(['nil' => 'nil'], $parsed['docker_images']); + } + + public function test_it_rejects_an_unknown_egg_version() + { + $this->expectException(InvalidFileUploadException::class); + + $this->parse(['meta' => ['version' => 'PTDL_v9', 'update_url' => null]]); + } + + public function test_it_can_always_import_the_version_it_exports() + { + $this->assertContains(Egg::EXPORT_VERSION, Egg::VERSIONS); + } + + public function test_it_can_still_import_every_previous_egg_version() + { + foreach (['PTDL_v1', 'PTDL_v2', 'RCYL_v26'] as $version) { + $this->assertContains($version, Egg::VERSIONS); + } + } + + /** + * Parses an egg, filling in the fields that every version shares. + * + * @throws InvalidFileUploadException|\JsonException + */ + private function parse(array $egg): array + { + $path = tempnam(sys_get_temp_dir(), 'egg').'.json'; + file_put_contents($path, json_encode(array_merge([ + 'exported_at' => '2024-04-02T14:11:47+02:00', + 'name' => 'python generic', + 'author' => 'parker@parkervcp.com', + 'description' => 'A Generic Python Egg', + 'features' => null, + 'file_denylist' => [], + 'startup' => '/usr/local/bin/python /home/container/{{PY_FILE}}', + 'config' => ['files' => '{}', 'startup' => '{}', 'logs' => '{}', 'stop' => '^C'], + 'scripts' => ['installation' => ['script' => 'exit 0', 'container' => 'python:3.8-slim-bookworm', 'entrypoint' => 'bash']], + 'variables' => [[ + 'name' => 'App py file', + 'description' => 'The file that starts the App.', + 'env_variable' => 'PY_FILE', + 'default_value' => 'app.py', + 'user_viewable' => true, + 'user_editable' => true, + 'rules' => 'required|string', + ]], + ], $egg), JSON_THROW_ON_ERROR)); + + try { + return $this->service->handle(new UploadedFile($path, 'egg.json', 'application/json', null, true)); + } finally { + @unlink($path); + } + } +}