From a4bc0017284685684fc304291efbbf1a5015c4c2 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Wed, 3 Sep 2025 11:37:20 +0200 Subject: [PATCH] add backend for supporting multiple egg startup commands --- app/Models/Egg.php | 13 ++--- app/Services/Eggs/EggChangerService.php | 5 +- .../Eggs/Sharing/EggExporterService.php | 2 +- .../Eggs/Sharing/EggImporterService.php | 28 +++++++++-- .../Servers/ServerCreationService.php | 4 +- .../Servers/StartupModificationService.php | 4 +- .../Api/Application/EggTransformer.php | 8 ++- database/Factories/EggFactory.php | 2 +- ...0706_support_multiple_startup_commands.php | 50 +++++++++++++++++++ 9 files changed, 92 insertions(+), 24 deletions(-) create mode 100644 database/migrations/2025_09_03_090706_support_multiple_startup_commands.php diff --git a/app/Models/Egg.php b/app/Models/Egg.php index dfa42e068..63cc19a39 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -20,8 +20,7 @@ use Illuminate\Support\Str; * @property string $name * @property string|null $description * @property string[]|null $features - * @property string $docker_image -- deprecated, use $docker_images - * @property array $docker_images + * @property array $docker_images * @property string|null $update_url * @property bool $force_outgoing_ip * @property string[]|null $file_denylist @@ -30,7 +29,7 @@ use Illuminate\Support\Str; * @property string|null $config_logs * @property string|null $config_stop * @property int|null $config_from - * @property string|null $startup + * @property array $startup_commands * @property bool $script_is_privileged * @property string|null $script_install * @property string $script_entry @@ -69,7 +68,7 @@ class Egg extends Model implements Validatable /** * Defines the current egg export version. */ - public const EXPORT_VERSION = 'PLCN_v2'; + public const EXPORT_VERSION = 'PLCN_v3'; /** * Fields that are not mass assignable. @@ -88,7 +87,7 @@ class Egg extends Model implements Validatable 'config_logs', 'config_stop', 'config_from', - 'startup', + 'startup_commands', 'update_url', 'script_is_privileged', 'script_install', @@ -109,7 +108,8 @@ class Egg extends Model implements Validatable 'file_denylist.*' => ['string'], 'docker_images' => ['required', 'array', 'min:1'], 'docker_images.*' => ['required', 'string'], - 'startup' => ['required', 'nullable', 'string'], + 'startup_commands' => ['required', 'array', 'min:1'], + 'startup_commands.*' => ['required', 'string'], 'config_from' => ['sometimes', 'bail', 'nullable', 'numeric', 'exists:eggs,id'], 'config_stop' => ['required_without:config_from', 'nullable', 'string', 'max:255'], 'config_startup' => ['required_without:config_from', 'nullable', 'json'], @@ -141,6 +141,7 @@ class Egg extends Model implements Validatable 'features' => 'array', 'docker_images' => 'array', 'file_denylist' => 'array', + 'startup_commands' => 'array', 'tags' => 'array', ]; } diff --git a/app/Services/Eggs/EggChangerService.php b/app/Services/Eggs/EggChangerService.php index 3b0f30bf6..52abad1dc 100644 --- a/app/Services/Eggs/EggChangerService.php +++ b/app/Services/Eggs/EggChangerService.php @@ -5,6 +5,7 @@ namespace App\Services\Eggs; use App\Models\Egg; use App\Models\Server; use App\Models\ServerVariable; +use Illuminate\Support\Arr; class EggChangerService { @@ -21,8 +22,8 @@ class EggChangerService // Change egg id, default image and startup command $server->forceFill([ 'egg_id' => $newEgg->id, - 'image' => array_values($newEgg->docker_images)[0], - 'startup' => $newEgg->startup, + 'image' => Arr::first($newEgg->docker_images), + 'startup' => Arr::first($newEgg->startup_commands), ])->saveOrFail(); $oldVariables = []; diff --git a/app/Services/Eggs/Sharing/EggExporterService.php b/app/Services/Eggs/Sharing/EggExporterService.php index 553e8befe..51b0d6e38 100644 --- a/app/Services/Eggs/Sharing/EggExporterService.php +++ b/app/Services/Eggs/Sharing/EggExporterService.php @@ -33,7 +33,7 @@ class EggExporterService 'features' => $egg->features, 'docker_images' => $egg->docker_images, 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(), - 'startup' => $egg->startup, + 'startup_commands' => $egg->startup_commands, 'config' => [ 'files' => $egg->inherit_config_files, 'startup' => $egg->inherit_config_startup, diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index e5d8f5eea..354ab66ea 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -133,8 +133,9 @@ class EggImporterService $version = $parsed['meta']['version'] ?? ''; $parsed = match ($version) { - 'PTDL_v1' => $this->convertToV2($parsed), - 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $parsed, + 'PTDL_v1' => $this->convertToV3($this->convertLegacy($parsed)), + 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $this->convertToV3($parsed), + 'PLCN_v3' => $parsed, default => throw new InvalidFileUploadException('The file format is not recognized.'), }; @@ -180,7 +181,7 @@ class EggImporterService if ($forbidden->count()) { $parsed['variables'] = $allowed->merge($updatedVariables)->all(); - if (!empty($parsed['startup'])) { + if (count($parsed['startup_commands']) > 0) { $pattern = '/\b(' . collect($forbidden)->map(fn ($variable) => preg_quote($variable['env_variable']))->join('|') . ')\b/'; $parsed['startup'] = preg_replace($pattern, 'SERVER_$1', $parsed['startup']) ?? $parsed['startup']; } @@ -206,7 +207,7 @@ class EggImporterService 'config_startup' => json_encode(json_decode(Arr::get($parsed, 'config.startup')), JSON_PRETTY_PRINT), 'config_logs' => json_encode(json_decode(Arr::get($parsed, 'config.logs')), JSON_PRETTY_PRINT), 'config_stop' => Arr::get($parsed, 'config.stop'), - 'startup' => Arr::get($parsed, 'startup'), + 'startup_commands' => Arr::get($parsed, 'startup_commands'), 'script_install' => Arr::get($parsed, 'scripts.installation.script'), 'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'), 'script_container' => Arr::get($parsed, 'scripts.installation.container'), @@ -217,7 +218,7 @@ class EggImporterService * @param array $parsed * @return array */ - protected function convertToV2(array $parsed): array + protected function convertLegacy(array $parsed): array { if (!isset($parsed['images'])) { $images = [Arr::get($parsed, 'image') ?? 'nil']; @@ -234,4 +235,21 @@ class EggImporterService return $parsed; } + + /** + * @param array $parsed + * @return array + */ + protected function convertToV3(array $parsed): array + { + $startup = $parsed['startup']; + + unset($parsed['startup']); + + $parsed['startup_commands'] = [ + $startup => $startup, + ]; + + return $parsed; + } } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 887c01e47..4d035fa81 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -56,8 +56,8 @@ class ServerCreationService $egg = Egg::query()->findOrFail($data['egg_id']); // Fill missing fields from egg - $data['image'] ??= collect($egg->docker_images)->first(); - $data['startup'] ??= $egg->startup; + $data['image'] ??= Arr::first($egg->docker_images); + $data['startup'] ??= Arr::first($egg->startup_commands); // If a deployment object has been passed we need to get the allocation and node that the server should use. if ($deployment) { diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index fe8a74c9c..d145488d2 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -84,8 +84,8 @@ class StartupModificationService ]); // Fill missing fields from egg - $data['docker_image'] = $data['docker_image'] ?? collect($egg->docker_images)->first(); - $data['startup'] = $data['startup'] ?? $egg->startup; + $data['docker_image'] ??= Arr::first($egg->docker_images); + $data['startup'] ??= Arr::first($egg->startup_commands); } $server->fill([ diff --git a/app/Transformers/Api/Application/EggTransformer.php b/app/Transformers/Api/Application/EggTransformer.php index 87955c0a1..86333c49a 100644 --- a/app/Transformers/Api/Application/EggTransformer.php +++ b/app/Transformers/Api/Application/EggTransformer.php @@ -48,10 +48,7 @@ class EggTransformer extends BaseTransformer 'description' => $model->description, 'features' => $model->features, 'tags' => $model->tags, - // "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 ? Arr::first($model->docker_images) : '', + 'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands 'docker_images' => $model->docker_images, 'config' => [ 'files' => $files, @@ -61,7 +58,8 @@ class EggTransformer extends BaseTransformer 'file_denylist' => $model->inherit_file_denylist, 'extends' => $model->config_from, ], - 'startup' => $model->startup, + 'startup' => Arr::first($model->startup_commands, default: ''), // deprecated, use startup_commands + 'startup_commands' => $model->startup_commands, 'script' => [ 'privileged' => $model->script_is_privileged, 'install' => $model->copy_script_install, diff --git a/database/Factories/EggFactory.php b/database/Factories/EggFactory.php index 219a179b0..9a8bef7b8 100644 --- a/database/Factories/EggFactory.php +++ b/database/Factories/EggFactory.php @@ -30,7 +30,7 @@ class EggFactory extends Factory 'config_files' => '{}', 'name' => $this->faker->name(), 'description' => implode(' ', $this->faker->sentences()), - 'startup' => 'java -jar test.jar', + 'startup_commands' => ['java -jar test.jar'], ]; } } diff --git a/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php b/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php new file mode 100644 index 000000000..db6a45180 --- /dev/null +++ b/database/migrations/2025_09_03_090706_support_multiple_startup_commands.php @@ -0,0 +1,50 @@ +json('startup_commands')->after('startup')->nullable(); + }); + + DB::table('eggs')->select(['id', 'startup'])->cursor()->each(function ($egg) { + $startup = $egg->startup; + + DB::table('eggs')->where('id', $egg->id)->update(['startup_commands' => [$startup => $startup]]); + }); + + Schema::table('eggs', function (Blueprint $table) { + $table->dropColumn('startup'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('eggs', function (Blueprint $table) { + $table->text('startup')->after('startup_commands'); + }); + + DB::table('eggs')->select(['id', 'startup_commands'])->cursor()->each(function ($egg) { + DB::table('eggs')->where('id', $egg->id)->update([ + 'startup' => Arr::first(json_decode($egg->startup_commands, true, 512, JSON_THROW_ON_ERROR)), + ]); + }); + + Schema::table('eggs', function (Blueprint $table) { + $table->dropColumn('startup_commands'); + }); + } +};