Skip to content

Commit c8faf64

Browse files
committed
Support naming docker images on eggs; closes pterodactyl#4052
Bumps PTDL_v1 export images to PTDL_v2, updates the Minecraft specific eggs to use named images.
1 parent 53207ab commit c8faf64

File tree

17 files changed

+212
-261
lines changed

17 files changed

+212
-261
lines changed

app/Http/Controllers/Admin/Nests/EggController.php

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,7 @@ public function create(): View
7474
public function store(EggFormRequest $request): RedirectResponse
7575
{
7676
$data = $request->normalize();
77-
if (!empty($data['docker_images']) && !is_array($data['docker_images'])) {
78-
$data['docker_images'] = array_map(function ($value) {
79-
return trim($value);
80-
}, explode("\n", $data['docker_images']));
81-
}
77+
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
8278

8379
$egg = $this->creationService->handle($data);
8480
$this->alert->success(trans('admin/nests.eggs.notices.egg_created'))->flash();
@@ -91,7 +87,14 @@ public function store(EggFormRequest $request): RedirectResponse
9187
*/
9288
public function view(Egg $egg): View
9389
{
94-
return view('admin.eggs.view', ['egg' => $egg]);
90+
return view('admin.eggs.view', [
91+
'egg' => $egg,
92+
'images' => array_map(
93+
fn ($key, $value) => $key === $value ? $value : "$key|$value",
94+
array_keys($egg->docker_images),
95+
$egg->docker_images,
96+
),
97+
]);
9598
}
9699

97100
/**
@@ -104,11 +107,7 @@ public function view(Egg $egg): View
104107
public function update(EggFormRequest $request, Egg $egg): RedirectResponse
105108
{
106109
$data = $request->normalize();
107-
if (!empty($data['docker_images']) && !is_array($data['docker_images'])) {
108-
$data['docker_images'] = array_map(function ($value) {
109-
return trim($value);
110-
}, explode("\n", $data['docker_images']));
111-
}
110+
$data['docker_images'] = $this->normalizeDockerImages($data['docker_images'] ?? null);
112111

113112
$this->updateService->handle($egg, $data);
114113
$this->alert->success(trans('admin/nests.eggs.notices.updated'))->flash();
@@ -129,4 +128,22 @@ public function destroy(Egg $egg): RedirectResponse
129128

130129
return redirect()->route('admin.nests.view', $egg->nest_id);
131130
}
131+
132+
/**
133+
* Normalizes a string of docker image data into the expected egg format.
134+
*/
135+
protected function normalizeDockerImages(string $input = null): array
136+
{
137+
$data = array_map(fn ($value) => trim($value), explode("\n", $input ?? ''));
138+
139+
$images = [];
140+
// Iterate over the image data provided and convert it into a name => image
141+
// pairing that is used to improve the display on the front-end.
142+
foreach ($data as $value) {
143+
$parts = explode('|', $value, 2);
144+
$images[$parts[0]] = empty($parts[1]) ? $parts[0] : $parts[1];
145+
}
146+
147+
return $images;
148+
}
132149
}

app/Http/Controllers/Api/Client/Servers/SettingsController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function reinstall(ReinstallServerRequest $request, Server $server)
7878
*/
7979
public function dockerImage(SetDockerImageRequest $request, Server $server)
8080
{
81-
if (!in_array($server->image, $server->egg->docker_images)) {
81+
if (!in_array($server->image, array_values($server->egg->docker_images))) {
8282
throw new BadRequestHttpException('This server\'s Docker image has been manually set by an administrator and cannot be updated.');
8383
}
8484

app/Http/Requests/Api/Client/Servers/Settings/SetDockerImageRequest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function rules(): array
2727
Assert::isInstanceOf($server, Server::class);
2828

2929
return [
30-
'docker_image' => ['required', 'string', Rule::in($server->egg->docker_images)],
30+
'docker_image' => ['required', 'string', Rule::in(array_values($server->egg->docker_images))],
3131
];
3232
}
3333
}

app/Models/Egg.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* @property array|null $features
1313
* @property string $docker_image -- deprecated, use $docker_images
1414
* @property string $update_url
15-
* @property array $docker_images
15+
* @property array<string, string> $docker_images
1616
* @property array|null $file_denylist
1717
* @property string|null $config_files
1818
* @property string|null $config_startup
@@ -50,6 +50,11 @@ class Egg extends Model
5050
*/
5151
public const RESOURCE_NAME = 'egg';
5252

53+
/**
54+
* Defines the current egg export version.
55+
*/
56+
public const EXPORT_VERSION = 'PTDL_v2';
57+
5358
/**
5459
* Different features that can be enabled on any given egg. These are used internally
5560
* to determine which types of frontend functionality should be shown to the user. Eggs

app/Services/Eggs/Sharing/EggExporterService.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pterodactyl\Services\Eggs\Sharing;
44

55
use Carbon\Carbon;
6+
use Pterodactyl\Models\Egg;
67
use Illuminate\Support\Collection;
78
use Pterodactyl\Models\EggVariable;
89
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
@@ -34,15 +35,15 @@ public function handle(int $egg): string
3435
$struct = [
3536
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
3637
'meta' => [
37-
'version' => 'PTDL_v1',
38+
'version' => Egg::EXPORT_VERSION,
3839
'update_url' => $egg->update_url,
3940
],
4041
'exported_at' => Carbon::now()->toIso8601String(),
4142
'name' => $egg->name,
4243
'author' => $egg->author,
4344
'description' => $egg->description,
4445
'features' => $egg->features,
45-
'images' => $egg->docker_images,
46+
'docker_images' => $egg->docker_images,
4647
'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(function ($value) {
4748
return !empty($value);
4849
}),
@@ -63,6 +64,7 @@ public function handle(int $egg): string
6364
'variables' => $egg->variables->transform(function (EggVariable $item) {
6465
return Collection::make($item->toArray())
6566
->except(['id', 'egg_id', 'created_at', 'updated_at'])
67+
->merge(['field_type' => 'text'])
6668
->toArray();
6769
}),
6870
];

app/Services/Eggs/Sharing/EggImporterService.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use Illuminate\Database\ConnectionInterface;
1111
use Pterodactyl\Contracts\Repository\EggRepositoryInterface;
1212
use Pterodactyl\Contracts\Repository\NestRepositoryInterface;
13-
use Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException;
1413
use Pterodactyl\Exceptions\Service\InvalidFileUploadException;
1514
use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface;
1615

@@ -56,8 +55,8 @@ public function __construct(
5655
*
5756
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
5857
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
59-
* @throws \Pterodactyl\Exceptions\Service\Egg\BadJsonFormatException
6058
* @throws \Pterodactyl\Exceptions\Service\InvalidFileUploadException
59+
* @throws \JsonException
6160
*/
6261
public function handle(UploadedFile $file, int $nest): Egg
6362
{
@@ -66,13 +65,13 @@ public function handle(UploadedFile $file, int $nest): Egg
6665
}
6766

6867
/** @var array $parsed */
69-
$parsed = json_decode($file->openFile()->fread($file->getSize()), true);
70-
if (json_last_error() !== 0) {
71-
throw new BadJsonFormatException(trans('exceptions.nest.importer.json_error', ['error' => json_last_error_msg()]));
68+
$parsed = json_decode($file->openFile()->fread($file->getSize()), true, 512, JSON_THROW_ON_ERROR);
69+
if (!in_array(Arr::get($parsed, 'meta.version') ?? '', ['PTDL_v1', 'PTDL_v2'])) {
70+
throw new InvalidFileUploadException(trans('exceptions.nest.importer.invalid_json_provided'));
7271
}
7372

74-
if (Arr::get($parsed, 'meta.version') !== 'PTDL_v1') {
75-
throw new InvalidFileUploadException(trans('exceptions.nest.importer.invalid_json_provided'));
73+
if ($parsed['meta']['version'] !== Egg::EXPORT_VERSION) {
74+
$parsed = $this->convertV1ToV2($parsed);
7675
}
7776

7877
$nest = $this->nestRepository->getWithEggs($nest);
@@ -86,9 +85,7 @@ public function handle(UploadedFile $file, int $nest): Egg
8685
'name' => Arr::get($parsed, 'name'),
8786
'description' => Arr::get($parsed, 'description'),
8887
'features' => Arr::get($parsed, 'features'),
89-
// Maintain backwards compatability for eggs that are still using the old single image
90-
// string format. New eggs can provide an array of Docker images that can be used.
91-
'docker_images' => Arr::get($parsed, 'images') ?? [Arr::get($parsed, 'image')],
88+
'docker_images' => Arr::get($parsed, 'docker_images'),
9289
'file_denylist' => Collection::make(Arr::get($parsed, 'file_denylist'))->filter(function ($value) {
9390
return !empty($value);
9491
}),
@@ -105,6 +102,8 @@ public function handle(UploadedFile $file, int $nest): Egg
105102
], true, true);
106103

107104
Collection::make($parsed['variables'] ?? [])->each(function (array $variable) use ($egg) {
105+
unset($variable['field_type']);
106+
108107
$this->eggVariableRepository->create(array_merge($variable, [
109108
'egg_id' => $egg->id,
110109
]));
@@ -114,4 +113,33 @@ public function handle(UploadedFile $file, int $nest): Egg
114113

115114
return $egg;
116115
}
116+
117+
/**
118+
* Converts a PTDL_V1 egg into the expected PTDL_V2 egg format. This just handles
119+
* the "docker_images" field potentially not being present, and not being in the
120+
* expected "key => value" format.
121+
*/
122+
protected function convertV1ToV2(array $parsed): array
123+
{
124+
// Maintain backwards compatability for eggs that are still using the old single image
125+
// string format. New eggs can provide an array of Docker images that can be used.
126+
if (!isset($parsed['images'])) {
127+
$images = [Arr::get($parsed, 'image') ?? 'nil'];
128+
} else {
129+
$images = $parsed['images'];
130+
}
131+
132+
unset($parsed['images'], $parsed['image']);
133+
134+
$parsed['docker_images'] = [];
135+
foreach ($images as $image) {
136+
$parsed['docker_images'][$image] = $image;
137+
}
138+
139+
$parsed['variables'] = array_map(function ($value) {
140+
return array_merge($value, ['field_type' => 'text']);
141+
}, $parsed['variables']);
142+
143+
return $parsed;
144+
}
117145
}

app/Transformers/Api/Application/EggTransformer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pterodactyl\Transformers\Api\Application;
44

55
use Pterodactyl\Models\Egg;
6+
use Illuminate\Support\Arr;
67
use Pterodactyl\Models\Nest;
78
use Pterodactyl\Models\Server;
89
use Pterodactyl\Models\EggVariable;
@@ -49,7 +50,7 @@ public function transform(Egg $model)
4950
// "docker_image" is deprecated, but left here to avoid breaking too many things at once
5051
// in external software. We'll remove it down the road once things have gotten the chance
5152
// to upgrade to using "docker_images".
52-
'docker_image' => count($model->docker_images) > 0 ? $model->docker_images[0] : '',
53+
'docker_image' => count($model->docker_images) > 0 ? Arr::first($model->docker_images) : '',
5354
'docker_images' => $model->docker_images,
5455
'config' => [
5556
'files' => json_decode($model->config_files, true),

database/Seeders/eggs/minecraft/egg-bungeecord.json

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
33
"meta": {
4-
"version": "PTDL_v1",
4+
"version": "PTDL_v2",
55
"update_url": null
66
},
7-
"exported_at": "2021-11-14T19:23:12+00:00",
7+
"exported_at": "2022-05-07T17:35:07-04:00",
88
"name": "Bungeecord",
99
"author": "support@pterodactyl.io",
1010
"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 @@
1313
"java_version",
1414
"pid_limit"
1515
],
16-
"images": [
17-
"ghcr.io\/pterodactyl\/yolks:java_8",
18-
"ghcr.io\/pterodactyl\/yolks:java_11",
19-
"ghcr.io\/pterodactyl\/yolks:java_16",
20-
"ghcr.io\/pterodactyl\/yolks:java_17"
21-
],
16+
"docker_images": {
17+
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
18+
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
19+
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
20+
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
21+
},
2222
"file_denylist": [],
2323
"startup": "java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}",
2424
"config": {
@@ -42,7 +42,8 @@
4242
"default_value": "latest",
4343
"user_viewable": true,
4444
"user_editable": true,
45-
"rules": "required|alpha_num|between:1,6"
45+
"rules": "required|alpha_num|between:1,6",
46+
"field_type": "text"
4647
},
4748
{
4849
"name": "Bungeecord Jar File",
@@ -51,7 +52,8 @@
5152
"default_value": "bungeecord.jar",
5253
"user_viewable": true,
5354
"user_editable": true,
54-
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
55+
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
56+
"field_type": "text"
5557
}
5658
]
57-
}
59+
}

database/Seeders/eggs/minecraft/egg-forge-minecraft.json

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"_comment": "DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO",
33
"meta": {
4-
"version": "PTDL_v1",
4+
"version": "PTDL_v2",
55
"update_url": null
66
},
7-
"exported_at": "2021-12-11T22:51:29+00:00",
7+
"exported_at": "2022-05-07T17:35:08-04:00",
88
"name": "Forge Minecraft",
99
"author": "support@pterodactyl.io",
1010
"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 @@
1313
"java_version",
1414
"pid_limit"
1515
],
16-
"images": [
17-
"ghcr.io\/pterodactyl\/yolks:java_17",
18-
"ghcr.io\/pterodactyl\/yolks:java_16",
19-
"ghcr.io\/pterodactyl\/yolks:java_11",
20-
"ghcr.io\/pterodactyl\/yolks:java_8"
21-
],
16+
"docker_images": {
17+
"Java 17": "ghcr.io\/pterodactyl\/yolks:java_17",
18+
"Java 16": "ghcr.io\/pterodactyl\/yolks:java_16",
19+
"Java 11": "ghcr.io\/pterodactyl\/yolks:java_11",
20+
"Java 8": "ghcr.io\/pterodactyl\/yolks:java_8"
21+
},
2222
"file_denylist": [],
2323
"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\" )",
2424
"config": {
@@ -42,7 +42,8 @@
4242
"default_value": "server.jar",
4343
"user_viewable": true,
4444
"user_editable": true,
45-
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/"
45+
"rules": "required|regex:\/^([\\w\\d._-]+)(\\.jar)$\/",
46+
"field_type": "text"
4647
},
4748
{
4849
"name": "Minecraft Version",
@@ -51,7 +52,8 @@
5152
"default_value": "latest",
5253
"user_viewable": true,
5354
"user_editable": true,
54-
"rules": "required|string|max:9"
55+
"rules": "required|string|max:9",
56+
"field_type": "text"
5557
},
5658
{
5759
"name": "Build Type",
@@ -60,7 +62,8 @@
6062
"default_value": "recommended",
6163
"user_viewable": true,
6264
"user_editable": true,
63-
"rules": "required|string|in:recommended,latest"
65+
"rules": "required|string|in:recommended,latest",
66+
"field_type": "text"
6467
},
6568
{
6669
"name": "Forge Version",
@@ -69,7 +72,8 @@
6972
"default_value": "",
7073
"user_viewable": true,
7174
"user_editable": true,
72-
"rules": "nullable|string|max:25"
75+
"rules": "nullable|string|max:25",
76+
"field_type": "text"
7377
}
7478
]
7579
}

0 commit comments

Comments
 (0)