mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-24 19:08:53 +03:00
Compare commits
8 Commits
main
...
charles/ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb4d55c651 | ||
|
|
adb0f1202a | ||
|
|
5a56af418a | ||
|
|
826701c164 | ||
|
|
2ce53b2a4f | ||
|
|
ebc5164a53 | ||
|
|
21ca789158 | ||
|
|
158a5bcf96 |
@@ -82,9 +82,9 @@ class ListEggs extends ListRecords
|
||||
->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])),
|
||||
])
|
||||
->toolbarActions([
|
||||
CreateAction::make(),
|
||||
ImportEggAction::make()
|
||||
->multiple(),
|
||||
CreateAction::make(),
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->before(function (Collection &$records) {
|
||||
|
||||
@@ -240,7 +240,7 @@ class PluginResource extends Resource
|
||||
}),
|
||||
]),
|
||||
])
|
||||
->headerActions([
|
||||
->toolbarActions([
|
||||
Action::make('import_from_file')
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/plugin.import_from_file'))
|
||||
|
||||
@@ -6,6 +6,7 @@ use App\Enums\SuspendAction;
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Servers\ServerResource;
|
||||
use App\Filament\Components\Actions\DeleteServerIcon;
|
||||
use App\Filament\Components\Actions\ExportServerConfigAction;
|
||||
use App\Filament\Components\Actions\PreviewStartupAction;
|
||||
use App\Filament\Components\Forms\Fields\MonacoEditor;
|
||||
use App\Filament\Components\Forms\Fields\StartupVariable;
|
||||
@@ -1004,6 +1005,16 @@ class EditServer extends EditRecord
|
||||
->hiddenLabel()
|
||||
->hint(new HtmlString(trans('admin/server.transfer_help'))),
|
||||
]),
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
Actions::make([
|
||||
ExportServerConfigAction::make(),
|
||||
])->fullWidth(),
|
||||
ToggleButtons::make('export_help')
|
||||
->hiddenLabel()
|
||||
->hint(trans('admin/server.import_export.export_description')),
|
||||
]),
|
||||
Grid::make()
|
||||
->columnSpan(3)
|
||||
->schema([
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Admin\Resources\Servers\Pages;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Servers\ServerResource;
|
||||
use App\Filament\Components\Actions\ImportServerConfigAction;
|
||||
use App\Filament\Server\Pages\Console;
|
||||
use App\Models\Server;
|
||||
use App\Traits\Filament\CanCustomizeHeaderActions;
|
||||
@@ -99,6 +100,7 @@ class ListServers extends ListRecords
|
||||
])
|
||||
->toolbarActions([
|
||||
CreateAction::make(),
|
||||
ImportServerConfigAction::make(),
|
||||
])
|
||||
->searchable()
|
||||
->emptyStateIcon(TablerIcon::BrandDocker)
|
||||
|
||||
60
app/Filament/Components/Actions/ExportServerConfigAction.php
Normal file
60
app/Filament/Components/Actions/ExportServerConfigAction.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\Sharing\ServerConfigExporterService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Support\Enums\Alignment;
|
||||
use Filament\Support\Enums\IconSize;
|
||||
|
||||
class ExportServerConfigAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'export_config';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->label(trans('filament-actions::export.modal.actions.export.label'));
|
||||
|
||||
$this->iconSize(IconSize::ExtraLarge);
|
||||
|
||||
$this->authorize(fn () => user()?->can('view server'));
|
||||
|
||||
$this->modalHeading(fn (Server $server) => trans('admin/server.import_export.export_heading', ['name' => $server->name]));
|
||||
|
||||
$this->modalDescription(trans('admin/server.import_export.export_description'));
|
||||
|
||||
$this->modalFooterActionsAlignment(Alignment::Center);
|
||||
|
||||
$this->schema([
|
||||
Toggle::make('include_description')
|
||||
->label(trans('admin/server.import_export.include_description'))
|
||||
->helperText(trans('admin/server.import_export.include_description_help'))
|
||||
->default(true),
|
||||
Toggle::make('include_allocations')
|
||||
->label(trans('admin/server.import_export.include_allocations'))
|
||||
->helperText(trans('admin/server.import_export.include_allocations_help'))
|
||||
->default(true),
|
||||
Toggle::make('include_variable_values')
|
||||
->label(trans('admin/server.import_export.include_variables'))
|
||||
->helperText(trans('admin/server.import_export.include_variables_help'))
|
||||
->default(true),
|
||||
]);
|
||||
|
||||
$this->action(fn (ServerConfigExporterService $service, Server $server, array $data) => response()->streamDownload(
|
||||
function () use ($service, $server, $data) {
|
||||
echo $service->handle($server, $data);
|
||||
},
|
||||
'server-' . str($server->name)->kebab()->lower()->trim() . '.yaml',
|
||||
[
|
||||
'Content-Type' => 'application/x-yaml',
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
87
app/Filament/Components/Actions/ImportServerConfigAction.php
Normal file
87
app/Filament/Components/Actions/ImportServerConfigAction.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Components\Actions;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Services\Servers\Sharing\ServerConfigCreatorService;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
class ImportServerConfigAction extends Action
|
||||
{
|
||||
public static function getDefaultName(): ?string
|
||||
{
|
||||
return 'import_config';
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->hiddenLabel();
|
||||
|
||||
$this->icon('tabler-file-import');
|
||||
|
||||
$this->tooltip(trans('admin/server.import_export.import_tooltip'));
|
||||
|
||||
$this->authorize(fn () => user()?->can('create server'));
|
||||
|
||||
$this->modalHeading(trans('admin/server.import_export.import_heading'));
|
||||
|
||||
$this->modalDescription(trans('admin/server.import_export.import_description'));
|
||||
|
||||
$this->schema([
|
||||
FileUpload::make('file')
|
||||
->label(trans('admin/server.import_export.config_file'))
|
||||
->hint(trans('admin/server.import_export.config_file_hint'))
|
||||
->acceptedFileTypes(['application/x-yaml', 'text/yaml', 'text/x-yaml', '.yaml', '.yml'])
|
||||
->preserveFilenames()
|
||||
->previewable(false)
|
||||
->storeFiles(false)
|
||||
->required()
|
||||
->maxSize(1024), // 1MB max
|
||||
Select::make('node_id')
|
||||
->label(trans('admin/server.import_export.node_select'))
|
||||
->hint(trans('admin/server.import_export.node_select_hint'))
|
||||
->options(fn () => user()?->accessibleNodes()->pluck('name', 'id') ?? [])
|
||||
->searchable()
|
||||
->required()
|
||||
->visible(fn () => (user()?->accessibleNodes()->count() ?? 0) > 1),
|
||||
]);
|
||||
|
||||
$this->action(function (ServerConfigCreatorService $createService, array $data): void {
|
||||
/** @var UploadedFile $file */
|
||||
$file = $data['file'];
|
||||
$nodeId = $data['node_id'] ?? user()->accessibleNodes()->first()->id;
|
||||
|
||||
try {
|
||||
$server = $createService->fromFile($file, $nodeId);
|
||||
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.import_created'))
|
||||
->body(trans('admin/server.notifications.import_created_body', ['name' => $server->name]))
|
||||
->success()
|
||||
->send();
|
||||
|
||||
redirect()->route('filament.admin.resources.servers.edit', ['record' => $server]);
|
||||
} catch (InvalidFileUploadException $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.import_failed'))
|
||||
->body($exception->getMessage())
|
||||
->danger()
|
||||
->send();
|
||||
} catch (\Exception $exception) {
|
||||
Notification::make()
|
||||
->title(trans('admin/server.notifications.import_failed'))
|
||||
->body(trans('admin/server.notifications.import_failed_body', ['error' => $exception->getMessage()]))
|
||||
->danger()
|
||||
->send();
|
||||
|
||||
report($exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Servers;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Servers\GetServerRequest;
|
||||
use App\Models\Server;
|
||||
use App\Services\Servers\Sharing\ServerConfigCreatorService;
|
||||
use App\Services\Servers\Sharing\ServerConfigExporterService;
|
||||
use App\Transformers\Api\Application\ServerTransformer;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
||||
#[Group('Server Config', weight: 1)]
|
||||
class ServerConfigController extends ApplicationApiController
|
||||
{
|
||||
public function __construct(
|
||||
private ServerConfigExporterService $exporterService,
|
||||
private ServerConfigCreatorService $creatorService
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Export server configuration
|
||||
*
|
||||
* Export a server's configuration to YAML format. Returns the configuration as a
|
||||
* downloadable YAML file containing settings, limits, allocations, and variable values.
|
||||
*/
|
||||
public function export(GetServerRequest $request, Server $server): Response
|
||||
{
|
||||
$options = [
|
||||
'include_description' => $request->boolean('include_description', true),
|
||||
'include_allocations' => $request->boolean('include_allocations', true),
|
||||
'include_variable_values' => $request->boolean('include_variable_values', true),
|
||||
];
|
||||
|
||||
$yaml = $this->exporterService->handle($server, $options);
|
||||
|
||||
$filename = 'server-config-' . str($server->name)->kebab()->lower()->trim() . '.yaml';
|
||||
|
||||
return response($yaml, 200, [
|
||||
'Content-Type' => 'application/x-yaml',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create server from configuration
|
||||
*
|
||||
* Create a new server from a YAML configuration file. The configuration must
|
||||
* include a valid egg UUID that exists in the system. Optionally specify a
|
||||
* node_id to create the server on a specific node.
|
||||
*
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
public function create(Request $request): JsonResponse
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:yaml,yml|max:1024',
|
||||
'node_id' => 'required|integer|exists:nodes,id',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$nodeId = $request->input('node_id');
|
||||
|
||||
$server = $this->creatorService->fromFile($file, $nodeId);
|
||||
|
||||
return $this->fractal->item($server)
|
||||
->transformWith($this->getTransformer(ServerTransformer::class))
|
||||
->respond(201);
|
||||
}
|
||||
}
|
||||
267
app/Services/Servers/Sharing/ServerConfigCreatorService.php
Normal file
267
app/Services/Servers/Sharing/ServerConfigCreatorService.php
Normal file
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Servers\Sharing;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\EggVariable;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServerConfigCreatorService
|
||||
{
|
||||
/**
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
public function fromFile(UploadedFile $file, ?int $nodeId = null): Server
|
||||
{
|
||||
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.file_error'));
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = Yaml::parse($file->getContent());
|
||||
} catch (\Exception $exception) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.parse_error_desc', ['error' => $exception->getMessage()]));
|
||||
}
|
||||
|
||||
return $this->createServer($parsed, $nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server from configuration array.
|
||||
*
|
||||
* @param array<string, mixed> $config
|
||||
*
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
protected function createServer(array $config, ?int $nodeId = null): Server
|
||||
{
|
||||
$eggUuid = Arr::get($config, 'egg.uuid');
|
||||
$eggName = Arr::get($config, 'egg.name');
|
||||
|
||||
if (!$eggUuid) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.egg_uuid_required'));
|
||||
}
|
||||
|
||||
$egg = Egg::where('uuid', $eggUuid)->first();
|
||||
|
||||
if (!$egg) {
|
||||
throw new InvalidFileUploadException(
|
||||
trans('admin/server.import_errors.egg_not_found_desc', [
|
||||
'uuid' => $eggUuid,
|
||||
'name' => $eggName ?: trans('admin/server.none'),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($nodeId) {
|
||||
$node = Node::whereIn('id', user()?->accessibleNodes()->pluck('id'))
|
||||
->where('id', $nodeId)
|
||||
->first();
|
||||
|
||||
if (!$node) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.node_not_accessible'));
|
||||
}
|
||||
} else {
|
||||
$node = Node::whereIn('id', user()?->accessibleNodes()->pluck('id'))->first();
|
||||
|
||||
if (!$node) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.no_nodes'));
|
||||
}
|
||||
}
|
||||
|
||||
$allocations = Arr::get($config, 'allocations', []);
|
||||
$primaryAllocation = null;
|
||||
$createdAllocations = [];
|
||||
|
||||
if (!empty($allocations)) {
|
||||
foreach ($allocations as $allocationData) {
|
||||
$ip = Arr::get($allocationData, 'ip');
|
||||
$port = Arr::get($allocationData, 'port');
|
||||
$isPrimary = Arr::get($allocationData, 'is_primary', false);
|
||||
|
||||
$allocation = Allocation::where('node_id', $node->id)
|
||||
->where('ip', $ip)
|
||||
->where('port', $port)
|
||||
->whereNull('server_id')
|
||||
->first();
|
||||
|
||||
if (!$allocation) {
|
||||
$existingAllocation = Allocation::where('node_id', $node->id)
|
||||
->where('ip', $ip)
|
||||
->where('port', $port)
|
||||
->first();
|
||||
|
||||
if ($existingAllocation) {
|
||||
$port = $this->findNextAvailablePort($node->id, $ip, $port);
|
||||
}
|
||||
|
||||
$allocation = Allocation::create([
|
||||
'node_id' => $node->id,
|
||||
'ip' => $ip,
|
||||
'port' => $port,
|
||||
]);
|
||||
}
|
||||
|
||||
$createdAllocations[] = $allocation;
|
||||
|
||||
if ($isPrimary && !$primaryAllocation) {
|
||||
$primaryAllocation = $allocation;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$primaryAllocation && !empty($createdAllocations)) {
|
||||
$primaryAllocation = $createdAllocations[0];
|
||||
}
|
||||
}
|
||||
|
||||
$owner = user();
|
||||
|
||||
if (!$owner) {
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.no_user'));
|
||||
}
|
||||
|
||||
$serverName = Arr::get($config, 'name', 'Imported Server');
|
||||
|
||||
$startupCommand = Arr::get($config, 'settings.startup');
|
||||
if ($startupCommand === null) {
|
||||
$startupCommand = array_values($egg->startup_commands)[0];
|
||||
}
|
||||
|
||||
$dockerImage = Arr::get($config, 'settings.image');
|
||||
if ($dockerImage === null) {
|
||||
$dockerImage = array_values($egg->docker_images)[0];
|
||||
}
|
||||
|
||||
$uuid = Uuid::uuid4()->toString();
|
||||
|
||||
$server = Server::create([
|
||||
'uuid' => $uuid,
|
||||
'uuid_short' => substr($uuid, 0, 8),
|
||||
'name' => $serverName,
|
||||
'description' => Arr::get($config, 'description', ''),
|
||||
'owner_id' => $owner->id,
|
||||
'node_id' => $node->id,
|
||||
'allocation_id' => $primaryAllocation?->id,
|
||||
'egg_id' => $egg->id,
|
||||
'startup' => $startupCommand,
|
||||
'image' => $dockerImage,
|
||||
'skip_scripts' => Arr::get($config, 'settings.skip_scripts', false),
|
||||
'memory' => Arr::get($config, 'limits.memory', 512),
|
||||
'swap' => Arr::get($config, 'limits.swap', 0),
|
||||
'disk' => Arr::get($config, 'limits.disk', 1024),
|
||||
'io' => Arr::get($config, 'limits.io', 500),
|
||||
'cpu' => Arr::get($config, 'limits.cpu', 0),
|
||||
'threads' => Arr::get($config, 'limits.threads'),
|
||||
'oom_killer' => Arr::get($config, 'limits.oom_killer', false),
|
||||
'database_limit' => Arr::get($config, 'feature_limits.databases', 0),
|
||||
'allocation_limit' => Arr::get($config, 'feature_limits.allocations', 0),
|
||||
'backup_limit' => Arr::get($config, 'feature_limits.backups', 0),
|
||||
]);
|
||||
|
||||
if ($primaryAllocation) {
|
||||
$primaryAllocation->update(['server_id' => $server->id]);
|
||||
}
|
||||
|
||||
foreach ($createdAllocations as $allocation) {
|
||||
if ($allocation->id !== $primaryAllocation?->id) {
|
||||
$allocation->update(['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($config['variables'])) {
|
||||
$this->importVariables($server, $config['variables']);
|
||||
}
|
||||
|
||||
if (isset($config['icon'])) {
|
||||
$this->importServerIcon($server, $config['icon']);
|
||||
}
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import server icon from base64 encoded data.
|
||||
*
|
||||
* @param array<string, string> $iconData
|
||||
*/
|
||||
protected function importServerIcon(Server $server, array $iconData): void
|
||||
{
|
||||
$base64Data = Arr::get($iconData, 'data');
|
||||
$extension = Arr::get($iconData, 'extension');
|
||||
|
||||
if (!$base64Data || !$extension) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!array_key_exists($extension, Server::IMAGE_FORMATS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$imageData = base64_decode($base64Data, true);
|
||||
|
||||
if ($imageData === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$path = Server::ICON_STORAGE_PATH . "/{$server->uuid}.{$extension}";
|
||||
Storage::disk('public')->put($path, $imageData);
|
||||
} catch (\Exception $e) {
|
||||
// Log the error but do not fail the entire import process
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{env_variable: string, value: string|null}> $variables
|
||||
*/
|
||||
protected function importVariables(Server $server, array $variables): void
|
||||
{
|
||||
foreach ($variables as $variable) {
|
||||
$envVariable = Arr::get($variable, 'env_variable');
|
||||
$value = Arr::get($variable, 'value');
|
||||
|
||||
/** @var EggVariable $eggVariable */
|
||||
$eggVariable = $server->egg->variables()->where('env_variable', $envVariable)->first();
|
||||
|
||||
ServerVariable::create([
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $eggVariable->id,
|
||||
'variable_value' => $value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
protected function findNextAvailablePort(int $nodeId, string $ip, int $startPort): int
|
||||
{
|
||||
$port = $startPort + 1;
|
||||
$maxPort = 65535;
|
||||
|
||||
while ($port <= $maxPort) {
|
||||
$exists = Allocation::where('node_id', $nodeId)
|
||||
->where('ip', $ip)
|
||||
->where('port', $port)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
return $port;
|
||||
}
|
||||
|
||||
$port++;
|
||||
}
|
||||
|
||||
throw new InvalidFileUploadException(trans('admin/server.import_errors.port_exhausted_desc', ['ip' => $ip, 'port' => $startPort]));
|
||||
}
|
||||
}
|
||||
106
app/Services/Servers/Sharing/ServerConfigExporterService.php
Normal file
106
app/Services/Servers/Sharing/ServerConfigExporterService.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Servers\Sharing;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServerConfigExporterService
|
||||
{
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public function handle(Server|int $server, array $options = []): string
|
||||
{
|
||||
if (!$server instanceof Server) {
|
||||
$server = Server::with(['egg', 'allocations', 'serverVariables.variable'])->findOrFail($server);
|
||||
}
|
||||
|
||||
$includeDescription = $options['include_description'] ?? true;
|
||||
$includeAllocations = $options['include_allocations'] ?? true;
|
||||
$includeVariableValues = $options['include_variable_values'] ?? true;
|
||||
|
||||
$data = [
|
||||
'name' => $server->name,
|
||||
'egg' => [
|
||||
'uuid' => $server->egg->uuid,
|
||||
'name' => $server->egg->name,
|
||||
],
|
||||
'settings' => [
|
||||
'startup' => $server->startup,
|
||||
'image' => $server->image,
|
||||
'skip_scripts' => $server->skip_scripts,
|
||||
],
|
||||
'limits' => [
|
||||
'memory' => $server->memory,
|
||||
'swap' => $server->swap,
|
||||
'disk' => $server->disk,
|
||||
'io' => $server->io,
|
||||
'cpu' => $server->cpu,
|
||||
'threads' => $server->threads,
|
||||
'oom_killer' => $server->oom_killer,
|
||||
],
|
||||
'feature_limits' => [
|
||||
'databases' => $server->database_limit,
|
||||
'allocations' => $server->allocation_limit,
|
||||
'backups' => $server->backup_limit,
|
||||
],
|
||||
];
|
||||
|
||||
if ($includeDescription && !empty($server->description)) {
|
||||
$data['description'] = $server->description;
|
||||
}
|
||||
|
||||
// Export server icon if exists
|
||||
$iconData = $this->exportServerIcon($server);
|
||||
if ($iconData) {
|
||||
$data['icon'] = $iconData;
|
||||
}
|
||||
|
||||
if ($includeAllocations && $server->allocations->isNotEmpty()) {
|
||||
$data['allocations'] = $server->allocations->map(function ($allocation) use ($server) {
|
||||
return [
|
||||
'ip' => $allocation->ip,
|
||||
'port' => $allocation->port,
|
||||
'is_primary' => $allocation->id === $server->allocation_id,
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
if ($includeVariableValues && $server->serverVariables->isNotEmpty()) {
|
||||
$data['variables'] = $server->serverVariables->map(function ($serverVar) {
|
||||
return [
|
||||
'env_variable' => $serverVar->variable->env_variable,
|
||||
'value' => $serverVar->variable_value,
|
||||
];
|
||||
})->values()->all();
|
||||
}
|
||||
|
||||
return Yaml::dump($data, 4, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export server icon as base64 encoded string with mime type.
|
||||
*
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
protected function exportServerIcon(Server $server): ?array
|
||||
{
|
||||
foreach (array_keys(Server::IMAGE_FORMATS) as $ext) {
|
||||
$path = Server::ICON_STORAGE_PATH . "/{$server->uuid}.{$ext}";
|
||||
if (Storage::disk('public')->exists($path)) {
|
||||
$contents = Storage::disk('public')->get($path);
|
||||
$mimeType = Server::IMAGE_FORMATS[$ext];
|
||||
|
||||
return [
|
||||
'data' => base64_encode($contents),
|
||||
'mime_type' => $mimeType,
|
||||
'extension' => $ext,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
194
app/Services/Servers/Sharing/ServerConfigImporterService.php
Normal file
194
app/Services/Servers/Sharing/ServerConfigImporterService.php
Normal file
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Servers\Sharing;
|
||||
|
||||
use App\Exceptions\Service\InvalidFileUploadException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\EggVariable;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Arr;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class ServerConfigImporterService
|
||||
{
|
||||
/**
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
public function fromFile(UploadedFile $file, Server $server): void
|
||||
{
|
||||
if ($file->getError() !== UPLOAD_ERR_OK) {
|
||||
throw new InvalidFileUploadException('The selected file was not uploaded successfully');
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = Yaml::parse($file->getContent());
|
||||
} catch (\Exception $exception) {
|
||||
throw new InvalidFileUploadException('Could not parse YAML file: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
$this->applyConfiguration($server, $parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{
|
||||
* egg: array{uuid: string, name?: string},
|
||||
* settings?: array<string, mixed>,
|
||||
* limits?: array<string, mixed>,
|
||||
* feature_limits?: array<string, mixed>,
|
||||
* description?: string,
|
||||
* variables?: array<int, array<string, mixed>>,
|
||||
* allocations?: array<int, array<string, mixed>>
|
||||
* } $config
|
||||
*
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
public function applyConfiguration(Server $server, array $config): void
|
||||
{
|
||||
$eggUuid = Arr::get($config, 'egg.uuid');
|
||||
$eggName = Arr::get($config, 'egg.name');
|
||||
|
||||
if (!$eggUuid) {
|
||||
throw new InvalidFileUploadException('Egg UUID is required in the configuration file');
|
||||
}
|
||||
|
||||
$egg = Egg::where('uuid', $eggUuid)->first();
|
||||
|
||||
if (!$egg) {
|
||||
throw new InvalidFileUploadException(
|
||||
"Egg with UUID '{$eggUuid}'" .
|
||||
($eggName ? " (name: {$eggName})" : '') .
|
||||
' does not exist in the system'
|
||||
);
|
||||
}
|
||||
|
||||
$server->update([
|
||||
'egg_id' => $egg->id,
|
||||
'startup' => Arr::get($config, 'settings.startup', $server->startup),
|
||||
'image' => Arr::get($config, 'settings.image', $server->image),
|
||||
'skip_scripts' => Arr::get($config, 'settings.skip_scripts', $server->skip_scripts),
|
||||
'memory' => Arr::get($config, 'limits.memory', $server->memory),
|
||||
'swap' => Arr::get($config, 'limits.swap', $server->swap),
|
||||
'disk' => Arr::get($config, 'limits.disk', $server->disk),
|
||||
'io' => Arr::get($config, 'limits.io', $server->io),
|
||||
'cpu' => Arr::get($config, 'limits.cpu', $server->cpu),
|
||||
'threads' => Arr::get($config, 'limits.threads', $server->threads),
|
||||
'oom_killer' => Arr::get($config, 'limits.oom_killer', $server->oom_killer),
|
||||
'database_limit' => Arr::get($config, 'feature_limits.databases', $server->database_limit),
|
||||
'allocation_limit' => Arr::get($config, 'feature_limits.allocations', $server->allocation_limit),
|
||||
'backup_limit' => Arr::get($config, 'feature_limits.backups', $server->backup_limit),
|
||||
]);
|
||||
|
||||
if (isset($config['description'])) {
|
||||
$server->update(['description' => $config['description']]);
|
||||
}
|
||||
|
||||
if (isset($config['variables'])) {
|
||||
$this->importVariables($server, $config['variables']);
|
||||
}
|
||||
|
||||
if (isset($config['allocations'])) {
|
||||
$this->importAllocations($server, $config['allocations']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{env_variable: string, value: string|null}> $variables
|
||||
*/
|
||||
protected function importVariables(Server $server, array $variables): void
|
||||
{
|
||||
foreach ($variables as $variable) {
|
||||
$envVariable = Arr::get($variable, 'env_variable');
|
||||
$value = Arr::get($variable, 'value');
|
||||
|
||||
$eggVariable = EggVariable::where('egg_id', $server->egg_id)
|
||||
->where('env_variable', $envVariable)
|
||||
->first();
|
||||
|
||||
if ($eggVariable) {
|
||||
ServerVariable::updateOrCreate(
|
||||
[
|
||||
'server_id' => $server->id,
|
||||
'variable_id' => $eggVariable->id,
|
||||
],
|
||||
[
|
||||
'variable_value' => $value,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $allocations
|
||||
*
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
protected function importAllocations(Server $server, array $allocations): void
|
||||
{
|
||||
$nodeId = $server->node_id;
|
||||
$primaryAllocationSet = false;
|
||||
|
||||
foreach ($allocations as $allocationData) {
|
||||
$ip = Arr::get($allocationData, 'ip');
|
||||
$port = Arr::get($allocationData, 'port');
|
||||
$isPrimary = Arr::get($allocationData, 'is_primary', false);
|
||||
|
||||
$allocation = Allocation::where('node_id', $nodeId)
|
||||
->where('ip', $ip)
|
||||
->where('port', $port)
|
||||
->first();
|
||||
|
||||
if (!$allocation) {
|
||||
$allocation = Allocation::create([
|
||||
'node_id' => $nodeId,
|
||||
'ip' => $ip,
|
||||
'port' => $port,
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
} elseif ($allocation->server_id && $allocation->server_id !== $server->id) {
|
||||
$newPort = $this->findNextAvailablePort($nodeId, $ip, $port);
|
||||
|
||||
$allocation = Allocation::create([
|
||||
'node_id' => $nodeId,
|
||||
'ip' => $ip,
|
||||
'port' => $newPort,
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
} elseif (!$allocation->server_id) {
|
||||
$allocation->update(['server_id' => $server->id]);
|
||||
}
|
||||
|
||||
if ($isPrimary && !$primaryAllocationSet) {
|
||||
$server->update(['allocation_id' => $allocation->id]);
|
||||
$primaryAllocationSet = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidFileUploadException
|
||||
*/
|
||||
protected function findNextAvailablePort(int $nodeId, string $ip, int $startPort): int
|
||||
{
|
||||
$port = $startPort + 1;
|
||||
$maxPort = 65535;
|
||||
|
||||
while ($port <= $maxPort) {
|
||||
$exists = Allocation::where('node_id', $nodeId)
|
||||
->where('ip', $ip)
|
||||
->where('port', $port)
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
return $port;
|
||||
}
|
||||
|
||||
$port++;
|
||||
}
|
||||
|
||||
throw new InvalidFileUploadException("Could not find an available port for IP {$ip} starting from port {$startPort}");
|
||||
}
|
||||
}
|
||||
@@ -147,8 +147,44 @@ return [
|
||||
'transfer_failed' => 'Transfer failed',
|
||||
'already_transfering' => 'Server is currently being transferred.',
|
||||
'backup_transfer_failed' => 'Backup Transfer Failed',
|
||||
'import_created' => 'Server Created',
|
||||
'import_created_body' => 'Server \':name\' has been successfully created from configuration.',
|
||||
'import_failed' => 'Import Failed',
|
||||
'import_failed_body' => 'An unexpected error occurred: :error',
|
||||
],
|
||||
'notes' => 'Notes',
|
||||
'no_notes' => 'No Notes',
|
||||
'none' => 'None',
|
||||
'import_export' => [
|
||||
'import_tooltip' => 'Import server configuration from YAML file',
|
||||
'import_heading' => 'Import Server Configuration',
|
||||
'import_description' => 'Import server configuration from a YAML file to create a new server.',
|
||||
'export_tooltip' => 'Export server configuration to YAML file',
|
||||
'export_heading' => 'Export Configuration: :name',
|
||||
'export_description' => 'Export the server configuration, settings, limits, allocations, and variable values to a YAML file.',
|
||||
'config_file' => 'Configuration File',
|
||||
'config_file_hint' => 'Upload a YAML file exported from another panel',
|
||||
'node_select' => 'Node',
|
||||
'node_select_hint' => 'Select the node where the server will be created',
|
||||
'include_description' => 'Include Description?',
|
||||
'include_description_help' => 'Export the server description',
|
||||
'include_allocations' => 'Include Allocations?',
|
||||
'include_allocations_help' => 'Export IP addresses and ports assigned to the server',
|
||||
'include_variables' => 'Include Variable Values?',
|
||||
'include_variables_help' => 'Export environment variable values',
|
||||
],
|
||||
'import_errors' => [
|
||||
'file_error' => 'The selected file was not uploaded successfully',
|
||||
'file_error_desc' => 'Please check the file and try again',
|
||||
'parse_error' => 'Could not parse YAML file',
|
||||
'parse_error_desc' => 'The uploaded file is not valid YAML: :error',
|
||||
'egg_uuid_required' => 'Egg UUID is required in the configuration file',
|
||||
'egg_not_found' => 'Egg does not exist in the panel',
|
||||
'egg_not_found_desc' => 'Egg with UUID \':uuid\' (name: :name) does not exist in the panel',
|
||||
'node_not_accessible' => 'Selected node is not accessible or does not exist',
|
||||
'no_nodes' => 'No accessible nodes found',
|
||||
'no_user' => 'No authenticated user found',
|
||||
'port_exhausted' => 'Could not find an available port',
|
||||
'port_exhausted_desc' => 'Could not find an available port for IP :ip starting from port :port',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -75,10 +75,12 @@ Route::prefix('/servers')->group(function () {
|
||||
Route::post('/{server:id}/transfer', [Application\Servers\ServerManagementController::class, 'startTransfer'])->name('api.application.servers.transfer');
|
||||
Route::post('/{server:id}/transfer/cancel', [Application\Servers\ServerManagementController::class, 'cancelTransfer'])->name('api.application.servers.transfer.cancel');
|
||||
|
||||
Route::get('/{server:id}/config/export', [Application\Servers\ServerConfigController::class, 'export'])->name('api.application.servers.config.export');
|
||||
Route::post('/config/create', [Application\Servers\ServerConfigController::class, 'create'])->name('api.application.servers.config.create');
|
||||
|
||||
Route::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']);
|
||||
Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']);
|
||||
|
||||
// Database Management Endpoint
|
||||
Route::prefix('/{server:id}/databases')->group(function () {
|
||||
Route::get('/', [Application\Servers\DatabaseController::class, 'index'])->name('api.application.servers.databases');
|
||||
Route::get('/{database:id}', [Application\Servers\DatabaseController::class, 'view'])->name('api.application.servers.databases.view');
|
||||
|
||||
Reference in New Issue
Block a user