diff --git a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php index 4e319b3322..e0b5ae8888 100644 --- a/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php +++ b/app/Filament/Admin/Resources/Eggs/Pages/ListEggs.php @@ -82,9 +82,9 @@ public function table(Table $table): Table ->successRedirectUrl(fn (Egg $replica) => EditEgg::getUrl(['record' => $replica])), ]) ->toolbarActions([ + CreateAction::make(), ImportEggAction::make() ->multiple(), - CreateAction::make(), BulkActionGroup::make([ DeleteBulkAction::make() ->before(function (Collection &$records) { diff --git a/app/Filament/Admin/Resources/Plugins/PluginResource.php b/app/Filament/Admin/Resources/Plugins/PluginResource.php index a9c42da622..bb7ab24f5d 100644 --- a/app/Filament/Admin/Resources/Plugins/PluginResource.php +++ b/app/Filament/Admin/Resources/Plugins/PluginResource.php @@ -240,7 +240,7 @@ public static function table(Table $table): Table }), ]), ]) - ->headerActions([ + ->toolbarActions([ Action::make('import_from_file') ->hiddenLabel() ->tooltip(trans('admin/plugin.import_from_file')) diff --git a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php index a17cb979aa..638f981254 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/Servers/Pages/EditServer.php @@ -6,6 +6,7 @@ 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 @@ protected function getDefaultTabs(): array ->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([ diff --git a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php index 189ac4d717..7727979b28 100644 --- a/app/Filament/Admin/Resources/Servers/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/Servers/Pages/ListServers.php @@ -4,6 +4,7 @@ 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 @@ public function table(Table $table): Table ]) ->toolbarActions([ CreateAction::make(), + ImportServerConfigAction::make(), ]) ->searchable() ->emptyStateIcon(TablerIcon::BrandDocker) diff --git a/app/Filament/Components/Actions/ExportServerConfigAction.php b/app/Filament/Components/Actions/ExportServerConfigAction.php new file mode 100644 index 0000000000..dd3c080044 --- /dev/null +++ b/app/Filament/Components/Actions/ExportServerConfigAction.php @@ -0,0 +1,60 @@ +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', + ] + )); + } +} diff --git a/app/Filament/Components/Actions/ImportServerConfigAction.php b/app/Filament/Components/Actions/ImportServerConfigAction.php new file mode 100644 index 0000000000..6c7e024e6b --- /dev/null +++ b/app/Filament/Components/Actions/ImportServerConfigAction.php @@ -0,0 +1,87 @@ +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); + } + }); + } +} diff --git a/app/Http/Controllers/Api/Application/Servers/ServerConfigController.php b/app/Http/Controllers/Api/Application/Servers/ServerConfigController.php new file mode 100644 index 0000000000..350a43b1e7 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/ServerConfigController.php @@ -0,0 +1,76 @@ + $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); + } +} diff --git a/app/Services/Servers/Sharing/ServerConfigCreatorService.php b/app/Services/Servers/Sharing/ServerConfigCreatorService.php new file mode 100644 index 0000000000..c37cf013db --- /dev/null +++ b/app/Services/Servers/Sharing/ServerConfigCreatorService.php @@ -0,0 +1,267 @@ +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 $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 $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 $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])); + } +} diff --git a/app/Services/Servers/Sharing/ServerConfigExporterService.php b/app/Services/Servers/Sharing/ServerConfigExporterService.php new file mode 100644 index 0000000000..319be773d9 --- /dev/null +++ b/app/Services/Servers/Sharing/ServerConfigExporterService.php @@ -0,0 +1,106 @@ + $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|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; + } +} diff --git a/app/Services/Servers/Sharing/ServerConfigImporterService.php b/app/Services/Servers/Sharing/ServerConfigImporterService.php new file mode 100644 index 0000000000..91903c787e --- /dev/null +++ b/app/Services/Servers/Sharing/ServerConfigImporterService.php @@ -0,0 +1,194 @@ +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, + * limits?: array, + * feature_limits?: array, + * description?: string, + * variables?: array>, + * allocations?: array> + * } $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 $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> $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}"); + } +} diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php index e0bb2bebab..3a6b4a78f5 100644 --- a/lang/en/admin/server.php +++ b/lang/en/admin/server.php @@ -147,8 +147,44 @@ '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', + ], ]; diff --git a/routes/api-application.php b/routes/api-application.php index 07e20b51d4..32989267c0 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -75,10 +75,12 @@ 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');