Basic implemention of multiple selectable images for an egg

The admin side of this is quite ugly when creating/editing a server, but I'm not putting effort into that right now with React Admin soon™
This commit is contained in:
Dane Everitt 2020-12-13 09:53:17 -08:00
parent 3e65a2d055
commit 78c4ac80bc
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
11 changed files with 123 additions and 31 deletions

View file

@ -111,17 +111,19 @@ class CreateServerController extends Controller
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableAllocationException
* @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException * @throws \Pterodactyl\Exceptions\Service\Deployment\NoViableNodeException
* @throws \Throwable * @throws \Throwable
*/ */
public function store(ServerFormRequest $request) public function store(ServerFormRequest $request)
{ {
$server = $this->creationService->handle( $data = $request->except(['_token']);
$request->except(['_token']) if (!empty($data['custom_image'])) {
); $data['image'] = $data['custom_image'];
unset($data['custom_image']);
}
$server = $this->creationService->handle($data);
$this->alert->success( $this->alert->success(
trans('admin/server.alerts.server_created') trans('admin/server.alerts.server_created')

View file

@ -334,14 +334,19 @@ class ServersController extends Controller
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Illuminate\Validation\ValidationException * @throws \Illuminate\Validation\ValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function saveStartup(Request $request, Server $server) public function saveStartup(Request $request, Server $server)
{ {
$data = $request->except('_token');
if (!empty($data['custom_docker_image'])) {
$data['docker_image'] = $data['custom_docker_image'];
unset($data['custom_docker_image']);
}
try { try {
$this->startupModificationService $this->startupModificationService
->setUserLevel(User::USER_LEVEL_ADMIN) ->setUserLevel(User::USER_LEVEL_ADMIN)
->handle($server, $request->except('_token')); ->handle($server, $data);
} catch (DataValidationException $exception) { } catch (DataValidationException $exception) {
throw new ValidationException($exception->validator); throw new ValidationException($exception->validator);
} }

View file

@ -1,11 +1,4 @@
<?php <?php
/**
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* This software is licensed under the terms of the MIT license.
* https://opensource.org/licenses/MIT
*/
namespace Pterodactyl\Http\Requests\Admin; namespace Pterodactyl\Http\Requests\Admin;
@ -23,6 +16,7 @@ class ServerFormRequest extends AdminFormRequest
{ {
$rules = Server::getRules(); $rules = Server::getRules();
$rules['description'][] = 'nullable'; $rules['description'][] = 'nullable';
$rules['custom_image'] = 'sometimes|nullable|string';
return $rules; return $rules;
} }

View file

@ -10,7 +10,9 @@ namespace Pterodactyl\Models;
* @property string $name * @property string $name
* @property string|null $description * @property string|null $description
* @property array|null $features * @property array|null $features
* @property string $docker_image * @property string $docker_image -- deprecated, use $docker_images
* @property string $update_url
* @property array $docker_images
* @property string|null $config_files * @property string|null $config_files
* @property string|null $config_startup * @property string|null $config_startup
* @property string|null $config_logs * @property string|null $config_logs
@ -76,7 +78,7 @@ class Egg extends Model
'name', 'name',
'description', 'description',
'features', 'features',
'docker_image', 'docker_images',
'config_files', 'config_files',
'config_startup', 'config_startup',
'config_logs', 'config_logs',
@ -101,6 +103,7 @@ class Egg extends Model
'script_is_privileged' => 'boolean', 'script_is_privileged' => 'boolean',
'copy_script_from' => 'integer', 'copy_script_from' => 'integer',
'features' => 'array', 'features' => 'array',
'docker_images' => 'array',
]; ];
/** /**
@ -113,13 +116,15 @@ class Egg extends Model
'description' => 'string|nullable', 'description' => 'string|nullable',
'features' => 'array|nullable', 'features' => 'array|nullable',
'author' => 'required|string|email', 'author' => 'required|string|email',
'docker_image' => 'required|string|max:191', 'docker_images' => 'required|array|min:1',
'docker_images.*' => 'required|string',
'startup' => 'required|nullable|string', 'startup' => 'required|nullable|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:191', 'config_stop' => 'required_without:config_from|nullable|string|max:191',
'config_startup' => 'required_without:config_from|nullable|json', 'config_startup' => 'required_without:config_from|nullable|json',
'config_logs' => 'required_without:config_from|nullable|json', 'config_logs' => 'required_without:config_from|nullable|json',
'config_files' => 'required_without:config_from|nullable|json', 'config_files' => 'required_without:config_from|nullable|json',
'update_url' => 'sometimes|nullable|string',
]; ];
/** /**
@ -131,6 +136,7 @@ class Egg extends Model
'config_startup' => null, 'config_startup' => null,
'config_logs' => null, 'config_logs' => null,
'config_files' => null, 'config_files' => null,
'update_url' => null,
]; ];
/** /**

View file

@ -38,13 +38,14 @@ class EggExporterService
'_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO', '_comment' => 'DO NOT EDIT: FILE GENERATED AUTOMATICALLY BY PTERODACTYL PANEL - PTERODACTYL.IO',
'meta' => [ 'meta' => [
'version' => 'PTDL_v1', 'version' => 'PTDL_v1',
'update_url' => $egg->update_url,
], ],
'exported_at' => Carbon::now()->toIso8601String(), 'exported_at' => Carbon::now()->toIso8601String(),
'name' => $egg->name, 'name' => $egg->name,
'author' => $egg->author, 'author' => $egg->author,
'description' => $egg->description, 'description' => $egg->description,
'features' => $egg->features, 'features' => $egg->features,
'image' => $egg->docker_image, 'images' => $egg->docker_images,
'startup' => $egg->startup, 'startup' => $egg->startup,
'config' => [ 'config' => [
'files' => $egg->inherit_config_files, 'files' => $egg->inherit_config_files,

View file

@ -102,7 +102,10 @@ class EggImporterService
'name' => object_get($parsed, 'name'), 'name' => object_get($parsed, 'name'),
'description' => object_get($parsed, 'description'), 'description' => object_get($parsed, 'description'),
'features' => object_get($parsed, 'features'), 'features' => object_get($parsed, 'features'),
'docker_image' => object_get($parsed, 'image'), // 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' => object_get($parsed, 'images') ?? [object_get($parsed, 'image')],
'update_url' => object_get($parsed, 'meta.update_url'),
'config_files' => object_get($parsed, 'config.files'), 'config_files' => object_get($parsed, 'config.files'),
'config_startup' => object_get($parsed, 'config.startup'), 'config_startup' => object_get($parsed, 'config.startup'),
'config_logs' => object_get($parsed, 'config.logs'), 'config_logs' => object_get($parsed, 'config.logs'),

View file

@ -45,7 +45,11 @@ class EggTransformer extends BaseTransformer
'nest' => $model->nest_id, 'nest' => $model->nest_id,
'author' => $model->author, 'author' => $model->author,
'description' => $model->description, 'description' => $model->description,
'docker_image' => $model->docker_image, // "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_images' => $model->docker_images,
'config' => [ 'config' => [
'files' => json_decode($model->config_files, true), 'files' => json_decode($model->config_files, true),
'startup' => json_decode($model->config_startup, true), 'startup' => json_decode($model->config_startup, true),

View file

@ -0,0 +1,51 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class SupportMultipleDockerImagesAndUpdates extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('eggs', function (Blueprint $table) {
$table->json('docker_images')->after('docker_image')->nullable();
$table->text('update_url')->after('docker_images')->nullable();
});
Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_images` = JSON_ARRAY(docker_image)');
});
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('docker_image');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('eggs', function (Blueprint $table) {
$table->text('docker_image')->after('docker_images');
});
Schema::table('eggs', function (Blueprint $table) {
DB::statement('UPDATE `eggs` SET `docker_image` = JSON_UNQUOTE(JSON_EXTRACT(docker_images, "$[0]"))');
});
Schema::table('eggs', function (Blueprint $table) {
$table->dropColumn('docker_images');
$table->dropColumn('update_url');
});
}
}

View file

@ -82,7 +82,13 @@ $('#pEggId').on('change', function (event) {
let parentChain = _.get(Pterodactyl.nests, $('#pNestId').val(), null); let parentChain = _.get(Pterodactyl.nests, $('#pNestId').val(), null);
let objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null); let objectChain = _.get(parentChain, 'eggs.' + $(this).val(), null);
$('#pDefaultContainer').val(_.get(objectChain, 'docker_image', 'not defined!')); const images = _.get(objectChain, 'docker_images', [])
for (let i = 0; i < images.length; i++) {
let opt = document.createElement('option');
opt.value = images[i];
opt.innerHTML = images[i];
$('#pDefaultContainer').append(opt);
}
if (!_.get(objectChain, 'startup', false)) { if (!_.get(objectChain, 'startup', false)) {
$('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!')); $('#pStartup').val(_.get(parentChain, 'startup', 'ERROR: Startup Not Defined!'));

View file

@ -265,8 +265,9 @@
<div class="box-body row"> <div class="box-body row">
<div class="form-group col-xs-12"> <div class="form-group col-xs-12">
<label for="pDefaultContainer">Docker Image</label> <label for="pDefaultContainer">Docker Image</label>
<input id="pDefaultContainer" name="image" value="{{ old('image') }}" class="form-control" /> <select id="pDefaultContainer" name="image" class="form-control"></select>
<p class="small text-muted no-margin">This is the default Docker image that will be used to run this server.</p> <input id="pDefaultContainerCustom" name="custom_image" value="{{ old('custom_image') }}" class="form-control" placeholder="Or enter a custom image..." style="margin-top:1rem"/>
<p class="small text-muted no-margin">This is the default Docker image that will be used to run this server. Select an image from the dropdown above, or enter a custom image in the text field above.</p>
</div> </div>
</div> </div>
</div> </div>
@ -323,11 +324,14 @@
@endforeach @endforeach
@endif @endif
@endif @endif
@if(old('image'))
$('#pDefaultContainer').val('{{ old('image') }}');
@endif
} }
// END Persist 'Service Variables' // END Persist 'Service Variables'
</script> </script>
{!! Theme::js('js/admin/new-server.js?v=20201003') !!} {!! Theme::js('js/admin/new-server.js?v=20201212') !!}
<script type="application/javascript"> <script type="application/javascript">
$(document).ready(function() { $(document).ready(function() {

View file

@ -89,13 +89,14 @@
</div> </div>
<div class="box"> <div class="box">
<div class="box-header with-border"> <div class="box-header with-border">
<h3 class="box-title">Docker Container Configuration</h3> <h3 class="box-title">Docker Image Configuration</h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="form-group"> <div class="form-group">
<label for="pDockerImage" class="control-label">Image</label> <label for="pDockerImage">Image</label>
<input type="text" name="docker_image" id="pDockerImage" value="{{ $server->image }}" class="form-control" /> <select id="pDockerImage" name="docker_image" class="form-control"></select>
<p class="text-muted small">The Docker image to use for this server. The default image for the selected egg is <code id="setDefaultImage"></code>.</p> <input id="pDockerImageCustom" name="custom_docker_image" value="{{ old('custom_docker_image') }}" class="form-control" placeholder="Or enter a custom image..." style="margin-top:1rem"/>
<p class="small text-muted no-margin">This is the Docker image that will be used to run this server. Select an image from the dropdown or enter a custom image in the text field above.</p>
</div> </div>
</div> </div>
</div> </div>
@ -117,10 +118,25 @@
var parentChain = _.get(Pterodactyl.nests, $("#pNestId").val()); var parentChain = _.get(Pterodactyl.nests, $("#pNestId").val());
var objectChain = _.get(parentChain, 'eggs.' + selectedEgg); var objectChain = _.get(parentChain, 'eggs.' + selectedEgg);
$('#setDefaultImage').html(_.get(objectChain, 'docker_image', 'undefined')); $('#setDefaultImage').html(_.get(objectChain, 'docker_images.0', 'undefined'));
$('#pDockerImage').val(_.get(objectChain, 'docker_image', 'undefined')); const images = _.get(objectChain, 'docker_images', [])
for (let i = 0; i < images.length; i++) {
let opt = document.createElement('option');
opt.value = images[i];
opt.innerHTML = images[i];
if (objectChain.id === parseInt(Pterodactyl.server.egg_id) && Pterodactyl.server.image == opt.value) {
opt.checked = true
}
$('#pDockerImage').append(opt);
}
$('#pDockerImage').on('change', function () {
$('#pDockerImageCustom').val('');
})
if (objectChain.id === parseInt(Pterodactyl.server.egg_id)) { if (objectChain.id === parseInt(Pterodactyl.server.egg_id)) {
$('#pDockerImage').val(Pterodactyl.server.image); if ($('#pDockerImage').val() != Pterodactyl.server.image) {
$('#pDockerImageCustom').val(Pterodactyl.server.image);
}
} }
if (!_.get(objectChain, 'startup', false)) { if (!_.get(objectChain, 'startup', false)) {