add backend for supporting multiple egg startup commands

This commit is contained in:
Boy132 2025-09-03 11:37:20 +02:00
parent 8f277aaca0
commit a4bc001728
9 changed files with 92 additions and 24 deletions

View File

@ -20,8 +20,7 @@ use Illuminate\Support\Str;
* @property string $name * @property string $name
* @property string|null $description * @property string|null $description
* @property string[]|null $features * @property string[]|null $features
* @property string $docker_image -- deprecated, use $docker_images * @property array<string, string> $docker_images
* @property array<array-key, string> $docker_images
* @property string|null $update_url * @property string|null $update_url
* @property bool $force_outgoing_ip * @property bool $force_outgoing_ip
* @property string[]|null $file_denylist * @property string[]|null $file_denylist
@ -30,7 +29,7 @@ use Illuminate\Support\Str;
* @property string|null $config_logs * @property string|null $config_logs
* @property string|null $config_stop * @property string|null $config_stop
* @property int|null $config_from * @property int|null $config_from
* @property string|null $startup * @property array<string, string> $startup_commands
* @property bool $script_is_privileged * @property bool $script_is_privileged
* @property string|null $script_install * @property string|null $script_install
* @property string $script_entry * @property string $script_entry
@ -69,7 +68,7 @@ class Egg extends Model implements Validatable
/** /**
* Defines the current egg export version. * Defines the current egg export version.
*/ */
public const EXPORT_VERSION = 'PLCN_v2'; public const EXPORT_VERSION = 'PLCN_v3';
/** /**
* Fields that are not mass assignable. * Fields that are not mass assignable.
@ -88,7 +87,7 @@ class Egg extends Model implements Validatable
'config_logs', 'config_logs',
'config_stop', 'config_stop',
'config_from', 'config_from',
'startup', 'startup_commands',
'update_url', 'update_url',
'script_is_privileged', 'script_is_privileged',
'script_install', 'script_install',
@ -109,7 +108,8 @@ class Egg extends Model implements Validatable
'file_denylist.*' => ['string'], 'file_denylist.*' => ['string'],
'docker_images' => ['required', 'array', 'min:1'], 'docker_images' => ['required', 'array', 'min:1'],
'docker_images.*' => ['required', 'string'], '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_from' => ['sometimes', 'bail', 'nullable', 'numeric', 'exists:eggs,id'],
'config_stop' => ['required_without:config_from', 'nullable', 'string', 'max:255'], 'config_stop' => ['required_without:config_from', 'nullable', 'string', 'max:255'],
'config_startup' => ['required_without:config_from', 'nullable', 'json'], 'config_startup' => ['required_without:config_from', 'nullable', 'json'],
@ -141,6 +141,7 @@ class Egg extends Model implements Validatable
'features' => 'array', 'features' => 'array',
'docker_images' => 'array', 'docker_images' => 'array',
'file_denylist' => 'array', 'file_denylist' => 'array',
'startup_commands' => 'array',
'tags' => 'array', 'tags' => 'array',
]; ];
} }

View File

@ -5,6 +5,7 @@ namespace App\Services\Eggs;
use App\Models\Egg; use App\Models\Egg;
use App\Models\Server; use App\Models\Server;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use Illuminate\Support\Arr;
class EggChangerService class EggChangerService
{ {
@ -21,8 +22,8 @@ class EggChangerService
// Change egg id, default image and startup command // Change egg id, default image and startup command
$server->forceFill([ $server->forceFill([
'egg_id' => $newEgg->id, 'egg_id' => $newEgg->id,
'image' => array_values($newEgg->docker_images)[0], 'image' => Arr::first($newEgg->docker_images),
'startup' => $newEgg->startup, 'startup' => Arr::first($newEgg->startup_commands),
])->saveOrFail(); ])->saveOrFail();
$oldVariables = []; $oldVariables = [];

View File

@ -33,7 +33,7 @@ class EggExporterService
'features' => $egg->features, 'features' => $egg->features,
'docker_images' => $egg->docker_images, 'docker_images' => $egg->docker_images,
'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(), 'file_denylist' => Collection::make($egg->inherit_file_denylist)->filter(fn ($v) => !empty($v))->values(),
'startup' => $egg->startup, 'startup_commands' => $egg->startup_commands,
'config' => [ 'config' => [
'files' => $egg->inherit_config_files, 'files' => $egg->inherit_config_files,
'startup' => $egg->inherit_config_startup, 'startup' => $egg->inherit_config_startup,

View File

@ -133,8 +133,9 @@ class EggImporterService
$version = $parsed['meta']['version'] ?? ''; $version = $parsed['meta']['version'] ?? '';
$parsed = match ($version) { $parsed = match ($version) {
'PTDL_v1' => $this->convertToV2($parsed), 'PTDL_v1' => $this->convertToV3($this->convertLegacy($parsed)),
'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $parsed, 'PTDL_v2', 'PLCN_v1', 'PLCN_v2' => $this->convertToV3($parsed),
'PLCN_v3' => $parsed,
default => throw new InvalidFileUploadException('The file format is not recognized.'), default => throw new InvalidFileUploadException('The file format is not recognized.'),
}; };
@ -180,7 +181,7 @@ class EggImporterService
if ($forbidden->count()) { if ($forbidden->count()) {
$parsed['variables'] = $allowed->merge($updatedVariables)->all(); $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/'; $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']; $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_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_logs' => json_encode(json_decode(Arr::get($parsed, 'config.logs')), JSON_PRETTY_PRINT),
'config_stop' => Arr::get($parsed, 'config.stop'), '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_install' => Arr::get($parsed, 'scripts.installation.script'),
'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'), 'script_entry' => Arr::get($parsed, 'scripts.installation.entrypoint'),
'script_container' => Arr::get($parsed, 'scripts.installation.container'), 'script_container' => Arr::get($parsed, 'scripts.installation.container'),
@ -217,7 +218,7 @@ class EggImporterService
* @param array<string, mixed> $parsed * @param array<string, mixed> $parsed
* @return array<string, mixed> * @return array<string, mixed>
*/ */
protected function convertToV2(array $parsed): array protected function convertLegacy(array $parsed): array
{ {
if (!isset($parsed['images'])) { if (!isset($parsed['images'])) {
$images = [Arr::get($parsed, 'image') ?? 'nil']; $images = [Arr::get($parsed, 'image') ?? 'nil'];
@ -234,4 +235,21 @@ class EggImporterService
return $parsed; return $parsed;
} }
/**
* @param array<string, mixed> $parsed
* @return array<string, mixed>
*/
protected function convertToV3(array $parsed): array
{
$startup = $parsed['startup'];
unset($parsed['startup']);
$parsed['startup_commands'] = [
$startup => $startup,
];
return $parsed;
}
} }

View File

@ -56,8 +56,8 @@ class ServerCreationService
$egg = Egg::query()->findOrFail($data['egg_id']); $egg = Egg::query()->findOrFail($data['egg_id']);
// Fill missing fields from egg // Fill missing fields from egg
$data['image'] ??= collect($egg->docker_images)->first(); $data['image'] ??= Arr::first($egg->docker_images);
$data['startup'] ??= $egg->startup; $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 a deployment object has been passed we need to get the allocation and node that the server should use.
if ($deployment) { if ($deployment) {

View File

@ -84,8 +84,8 @@ class StartupModificationService
]); ]);
// Fill missing fields from egg // Fill missing fields from egg
$data['docker_image'] = $data['docker_image'] ?? collect($egg->docker_images)->first(); $data['docker_image'] ??= Arr::first($egg->docker_images);
$data['startup'] = $data['startup'] ?? $egg->startup; $data['startup'] ??= Arr::first($egg->startup_commands);
} }
$server->fill([ $server->fill([

View File

@ -48,10 +48,7 @@ class EggTransformer extends BaseTransformer
'description' => $model->description, 'description' => $model->description,
'features' => $model->features, 'features' => $model->features,
'tags' => $model->tags, 'tags' => $model->tags,
// "docker_image" is deprecated, but left here to avoid breaking too many things at once 'docker_image' => Arr::first($model->docker_images, default: ''), // docker_images, use startup_commands
// 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_images' => $model->docker_images, 'docker_images' => $model->docker_images,
'config' => [ 'config' => [
'files' => $files, 'files' => $files,
@ -61,7 +58,8 @@ class EggTransformer extends BaseTransformer
'file_denylist' => $model->inherit_file_denylist, 'file_denylist' => $model->inherit_file_denylist,
'extends' => $model->config_from, 'extends' => $model->config_from,
], ],
'startup' => $model->startup, 'startup' => Arr::first($model->startup_commands, default: ''), // deprecated, use startup_commands
'startup_commands' => $model->startup_commands,
'script' => [ 'script' => [
'privileged' => $model->script_is_privileged, 'privileged' => $model->script_is_privileged,
'install' => $model->copy_script_install, 'install' => $model->copy_script_install,

View File

@ -30,7 +30,7 @@ class EggFactory extends Factory
'config_files' => '{}', 'config_files' => '{}',
'name' => $this->faker->name(), 'name' => $this->faker->name(),
'description' => implode(' ', $this->faker->sentences()), 'description' => implode(' ', $this->faker->sentences()),
'startup' => 'java -jar test.jar', 'startup_commands' => ['java -jar test.jar'],
]; ];
} }
} }

View File

@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('eggs', function (Blueprint $table) {
$table->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');
});
}
};