diff --git a/app/Casts/EndpointCollection.php b/app/Casts/EndpointCollection.php new file mode 100644 index 0000000000..f4a1e21cad --- /dev/null +++ b/app/Casts/EndpointCollection.php @@ -0,0 +1,45 @@ +map(function ($value) { + return new Endpoint($value); + }); + } + + public function set($model, $key, $value, $attributes) + { + if (!is_array($value) && !$value instanceof Collection) { + return new Collection(); + } + + if (!$value instanceof Collection) { + $value = new Collection($value); + } + + return [ + 'ports' => $value->toJson(), + ]; + } + }; + } +} diff --git a/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php b/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php deleted file mode 100644 index fa47c2f94c..0000000000 --- a/app/Exceptions/Service/Allocation/AllocationDoesNotBelongToServerException.php +++ /dev/null @@ -1,9 +0,0 @@ - $port])); - } -} diff --git a/app/Exceptions/Service/Allocation/NoAutoAllocationSpaceAvailableException.php b/app/Exceptions/Service/Allocation/NoAutoAllocationSpaceAvailableException.php deleted file mode 100644 index 23570445b9..0000000000 --- a/app/Exceptions/Service/Allocation/NoAutoAllocationSpaceAvailableException.php +++ /dev/null @@ -1,18 +0,0 @@ -numeric() ->default(3306) ->minValue(0) - ->maxValue(65535), + ->maxValue(Endpoint::PORT_CEIL), TextInput::make('max_databases') ->label('Max databases') ->helpertext('Blank is unlimited.') diff --git a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php index 5d0176699a..1b32c5b751 100644 --- a/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php +++ b/app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php @@ -5,6 +5,7 @@ use App\Filament\Resources\DatabaseHostResource; use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager; use App\Models\DatabaseHost; +use App\Models\Objects\Endpoint; use App\Services\Databases\Hosts\HostUpdateService; use Closure; use Exception; @@ -55,7 +56,7 @@ public function form(Form $form): Form ->required() ->numeric() ->minValue(0) - ->maxValue(65535), + ->maxValue(Endpoint::PORT_CEIL), TextInput::make('max_databases') ->label('Max databases') ->helpertext('Blank is unlimited.') diff --git a/app/Filament/Resources/EggResource/Pages/CreateEgg.php b/app/Filament/Resources/EggResource/Pages/CreateEgg.php index 4ec91ea24c..87ebbc557c 100644 --- a/app/Filament/Resources/EggResource/Pages/CreateEgg.php +++ b/app/Filament/Resources/EggResource/Pages/CreateEgg.php @@ -65,7 +65,7 @@ public function form(Form $form): Form ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]), Toggle::make('force_outgoing_ip') ->hintIcon('tabler-question-mark') - ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. + ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary endpoint. Required for certain games to work properly when the Node has multiple public IP addresses. Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), Hidden::make('script_is_privileged') diff --git a/app/Filament/Resources/EggResource/Pages/EditEgg.php b/app/Filament/Resources/EggResource/Pages/EditEgg.php index 28a09103c0..76e6885ab1 100644 --- a/app/Filament/Resources/EggResource/Pages/EditEgg.php +++ b/app/Filament/Resources/EggResource/Pages/EditEgg.php @@ -83,7 +83,7 @@ public function form(Form $form): Form Toggle::make('force_outgoing_ip') ->inline(false) ->hintIcon('tabler-question-mark') - ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP. + ->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's endpoint. Required for certain games to work properly when the Node has multiple public IP addresses. Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."), Hidden::make('script_is_privileged') diff --git a/app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php b/app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php index 0d7bb5b034..def9e40123 100644 --- a/app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php +++ b/app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php @@ -4,7 +4,6 @@ use App\Models\Server; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -33,11 +32,6 @@ public function table(Table $table): Table ->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])), TextColumn::make('image') ->label('Docker Image'), - SelectColumn::make('allocation.id') - ->label('Primary Allocation') - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) - ->sortable(), ]); } } diff --git a/app/Filament/Resources/NodeResource.php b/app/Filament/Resources/NodeResource.php index 2b7080c146..2fe42ae590 100644 --- a/app/Filament/Resources/NodeResource.php +++ b/app/Filament/Resources/NodeResource.php @@ -3,7 +3,6 @@ namespace App\Filament\Resources; use App\Filament\Resources\NodeResource\Pages; -use App\Filament\Resources\NodeResource\RelationManagers\AllocationsRelationManager; use App\Filament\Resources\NodeResource\RelationManagers\NodesRelationManager; use App\Models\Node; use Filament\Resources\Resource; @@ -24,7 +23,6 @@ public static function getNavigationBadge(): ?string public static function getRelations(): array { return [ - AllocationsRelationManager::class, NodesRelationManager::class, ]; } diff --git a/app/Filament/Resources/NodeResource/Pages/CreateNode.php b/app/Filament/Resources/NodeResource/Pages/CreateNode.php index c8d9bbc30a..8dd71bf7fe 100644 --- a/app/Filament/Resources/NodeResource/Pages/CreateNode.php +++ b/app/Filament/Resources/NodeResource/Pages/CreateNode.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\NodeResource\Pages; use App\Filament\Resources\NodeResource; +use App\Models\Objects\Endpoint; use Filament\Forms; use Filament\Forms\Components\Actions\Action; use Filament\Forms\Components\Grid; @@ -139,7 +140,7 @@ public function form(Forms\Form $form): Forms\Form ->label(trans('strings.port')) ->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.') ->minValue(1) - ->maxValue(65535) + ->maxValue(Endpoint::PORT_CEIL) ->default(8080) ->required() ->integer(), @@ -244,7 +245,7 @@ public function form(Forms\Form $form): Forms\Form ->columnSpan(1) ->label('SFTP Port') ->minValue(1) - ->maxValue(65535) + ->maxValue(Endpoint::PORT_CEIL) ->default(2022) ->required() ->integer(), diff --git a/app/Filament/Resources/NodeResource/Pages/EditNode.php b/app/Filament/Resources/NodeResource/Pages/EditNode.php index 10d5d10626..d655746e02 100644 --- a/app/Filament/Resources/NodeResource/Pages/EditNode.php +++ b/app/Filament/Resources/NodeResource/Pages/EditNode.php @@ -4,6 +4,7 @@ use App\Filament\Resources\NodeResource; use App\Models\Node; +use App\Models\Objects\Endpoint; use App\Services\Nodes\NodeUpdateService; use Filament\Actions; use Filament\Forms; @@ -165,7 +166,7 @@ public function form(Forms\Form $form): Forms\Form ->label(trans('strings.port')) ->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.') ->minValue(1) - ->maxValue(65535) + ->maxValue(Endpoint::PORT_CEIL) ->default(8080) ->required() ->integer(), @@ -243,7 +244,7 @@ public function form(Forms\Form $form): Forms\Form ->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3]) ->label('SFTP Port') ->minValue(1) - ->maxValue(65535) + ->maxValue(Endpoint::PORT_CEIL) ->default(2022) ->required() ->integer(), diff --git a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php deleted file mode 100644 index 5cdf2b1721..0000000000 --- a/app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php +++ /dev/null @@ -1,160 +0,0 @@ -schema([ - TextInput::make('ip') - ->required() - ->maxLength(255), - ]); - } - - public function table(Table $table): Table - { - return $table - ->recordTitleAttribute('ip') - - // Non Primary Allocations - // ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id) - - // All assigned allocations - ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null) - ->searchable() - ->columns([ - TextColumn::make('id'), - TextColumn::make('port') - ->searchable() - ->label('Port'), - TextColumn::make('server.name') - ->label('Server') - ->icon('tabler-brand-docker') - ->searchable() - ->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''), - TextInputColumn::make('ip_alias') - ->searchable() - ->label('Alias'), - TextInputColumn::make('ip') - ->searchable() - ->label('IP'), - ]) - ->filters([ - // - ]) - ->actions([ - // - ]) - ->headerActions([ - Tables\Actions\Action::make('create new allocation')->label('Create Allocations') - ->form(fn () => [ - TextInput::make('allocation_ip') - ->datalist($this->getOwnerRecord()->ipAddresses()) - ->label('IP Address') - ->inlineLabel() - ->ipv4() - ->helperText("Usually your machine's public IP unless you are port forwarding.") - ->required(), - TextInput::make('allocation_alias') - ->label('Alias') - ->inlineLabel() - ->default(null) - ->helperText('Optional display name to help you remember what these are.') - ->required(false), - TagsInput::make('allocation_ports') - ->placeholder('Examples: 27015, 27017-27019') - ->helperText(new HtmlString(' - These are the ports that users can connect to this Server through. -
- You would have to port forward these on your home network. - ')) - ->label('Ports') - ->inlineLabel() - ->live() - ->afterStateUpdated(function ($state, Set $set) { - $ports = collect(); - $update = false; - foreach ($state as $portEntry) { - if (!str_contains($portEntry, '-')) { - if (is_numeric($portEntry)) { - $ports->push((int) $portEntry); - - continue; - } - - // Do not add non numerical ports - $update = true; - - continue; - } - - $update = true; - [$start, $end] = explode('-', $portEntry); - if (!is_numeric($start) || !is_numeric($end)) { - continue; - } - - $start = max((int) $start, 0); - $end = min((int) $end, 2 ** 16 - 1); - foreach (range($start, $end) as $i) { - $ports->push($i); - } - } - - $uniquePorts = $ports->unique()->values(); - if ($ports->count() > $uniquePorts->count()) { - $update = true; - $ports = $uniquePorts; - } - - $sortedPorts = $ports->sort()->values(); - if ($sortedPorts->all() !== $ports->all()) { - $update = true; - $ports = $sortedPorts; - } - - $ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values(); - - if ($update) { - $set('allocation_ports', $ports->all()); - } - }) - ->splitKeys(['Tab', ' ', ',']) - ->required(), - ]) - ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)), - ]) - ->bulkActions([ - BulkActionGroup::make([ - DeleteBulkAction::make() - ->authorize(fn () => auth()->user()->can('delete allocation')), - ]), - ]); - } -} diff --git a/app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php b/app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php index 09c6c9854e..d9fb0165ca 100644 --- a/app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php +++ b/app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php @@ -4,7 +4,6 @@ use App\Models\Server; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -33,11 +32,6 @@ public function table(Table $table): Table ->icon('tabler-egg') ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user])) ->sortable(), - SelectColumn::make('allocation.id') - ->label('Primary Allocation') - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) - ->sortable(), TextColumn::make('memory')->icon('tabler-device-desktop-analytics'), TextColumn::make('cpu')->icon('tabler-cpu'), TextColumn::make('databases_count') diff --git a/app/Filament/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Resources/ServerResource/Pages/CreateServer.php index 99fd1d4337..041a6d8f04 100644 --- a/app/Filament/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Resources/ServerResource/Pages/CreateServer.php @@ -3,11 +3,10 @@ namespace App\Filament\Resources\ServerResource\Pages; use App\Filament\Resources\ServerResource; -use App\Models\Allocation; use App\Models\Egg; use App\Models\Node; +use App\Models\Objects\Endpoint; use App\Models\User; -use App\Services\Allocations\AssignmentService; use App\Services\Servers\RandomWordService; use App\Services\Servers\ServerCreationService; use App\Services\Users\UserCreationService; @@ -25,7 +24,6 @@ use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Section; use Filament\Forms\Components\Select; -use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; @@ -35,7 +33,6 @@ use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Resources\Pages\CreateRecord; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Validator; @@ -50,6 +47,12 @@ class CreateServer extends CreateRecord public ?Node $node = null; + public ?Egg $egg = null; + + public array $ports = []; + + public array $eggDefaultPorts = []; + private ServerCreationService $serverCreationService; public function boot(ServerCreationService $serverCreationService): void @@ -147,175 +150,11 @@ public function form(Form $form): Form ->relationship('node', 'name') ->searchable() ->preload() - ->afterStateUpdated(function (Set $set, $state) { - $set('allocation_id', null); + ->afterStateUpdated(function (Forms\Set $set, $state) { $this->node = Node::find($state); }) ->required(), - Select::make('allocation_id') - ->preload() - ->live() - ->prefixIcon('tabler-network') - ->label('Primary Allocation') - ->columnSpan([ - 'default' => 2, - 'sm' => 3, - 'md' => 2, - 'lg' => 3, - ]) - ->disabled(fn (Get $get) => $get('node_id') === null) - ->searchable(['ip', 'port', 'ip_alias']) - ->afterStateUpdated(function (Set $set) { - $set('allocation_additional', null); - $set('allocation_additional.needstobeastringhere.extra_allocations', null); - }) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) - ->placeholder(function (Get $get) { - $node = Node::find($get('node_id')); - - if ($node?->allocations) { - return 'Select an Allocation'; - } - - return 'Create a New Allocation'; - }) - ->relationship( - 'allocation', - 'ip', - fn (Builder $query, Get $get) => $query - ->where('node_id', $get('node_id')) - ->whereNull('server_id'), - ) - ->createOptionForm(fn (Get $get) => [ - TextInput::make('allocation_ip') - ->datalist(Node::find($get('node_id'))?->ipAddresses() ?? []) - ->label('IP Address') - ->inlineLabel() - ->ipv4() - ->helperText("Usually your machine's public IP unless you are port forwarding.") - // ->selectablePlaceholder(false) - ->required(), - TextInput::make('allocation_alias') - ->label('Alias') - ->inlineLabel() - ->default(null) - ->datalist([ - $get('name'), - Egg::find($get('egg_id'))?->name, - ]) - ->helperText('Optional display name to help you remember what these are.') - ->required(false), - TagsInput::make('allocation_ports') - ->placeholder('Examples: 27015, 27017-27019') - ->helperText(new HtmlString(' - These are the ports that users can connect to this Server through. -
- You would have to port forward these on your home network. - ')) - ->label('Ports') - ->inlineLabel() - ->live() - ->afterStateUpdated(function ($state, Set $set) { - $ports = collect(); - $update = false; - foreach ($state as $portEntry) { - if (!str_contains($portEntry, '-')) { - if (is_numeric($portEntry)) { - $ports->push((int) $portEntry); - - continue; - } - - // Do not add non-numerical ports - $update = true; - - continue; - } - - $update = true; - [$start, $end] = explode('-', $portEntry); - if (!is_numeric($start) || !is_numeric($end)) { - continue; - } - - $start = max((int) $start, 0); - $end = min((int) $end, 2 ** 16 - 1); - $range = $start <= $end ? range($start, $end) : range($end, $start); - foreach ($range as $i) { - if ($i > 1024 && $i <= 65535) { - $ports->push($i); - } - } - } - - $uniquePorts = $ports->unique()->values(); - if ($ports->count() > $uniquePorts->count()) { - $update = true; - $ports = $uniquePorts; - } - - $sortedPorts = $ports->sort()->values(); - if ($sortedPorts->all() !== $ports->all()) { - $update = true; - $ports = $sortedPorts; - } - - if ($update) { - $set('allocation_ports', $ports->all()); - } - }) - ->splitKeys(['Tab', ' ', ',']) - ->required(), - ]) - ->createOptionUsing(function (array $data, Get $get, AssignmentService $assignmentService): int { - return collect( - $assignmentService->handle(Node::find($get('node_id')), $data) - )->first(); - }) - ->required(), - - Repeater::make('allocation_additional') - ->label('Additional Allocations') - ->columnSpan([ - 'default' => 2, - 'sm' => 3, - 'md' => 3, - 'lg' => 3, - ]) - ->addActionLabel('Add Allocation') - ->disabled(fn (Get $get) => $get('allocation_id') === null) - // ->addable() TODO disable when all allocations are taken - // ->addable() TODO disable until first additional allocation is selected - ->simple( - Select::make('extra_allocations') - ->live() - ->preload() - ->disableOptionsWhenSelectedInSiblingRepeaterItems() - ->prefixIcon('tabler-network') - ->label('Additional Allocations') - ->columnSpan(2) - ->disabled(fn (Get $get) => $get('../../node_id') === null) - ->searchable(['ip', 'port', 'ip_alias']) - ->getOptionLabelFromRecordUsing( - fn (Allocation $allocation) => "$allocation->ip:$allocation->port" . - ($allocation->ip_alias ? " ($allocation->ip_alias)" : '') - ) - ->placeholder('Select additional Allocations') - ->disableOptionsWhenSelectedInSiblingRepeaterItems() - ->relationship( - 'allocations', - 'ip', - fn (Builder $query, Get $get, Select $component, $state) => $query - ->where('node_id', $get('../../node_id')) - ->whereNot('id', $get('../../allocation_id')) - ->whereNull('server_id'), - ), - ), - Textarea::make('description') ->placeholder('Description') ->rows(3) @@ -341,40 +180,26 @@ public function form(Form $form): Form ->schema([ Select::make('egg_id') ->prefixIcon('tabler-egg') - ->relationship('egg', 'name') ->columnSpan([ - 'default' => 1, + 'default' => 2, 'sm' => 2, 'md' => 2, 'lg' => 4, ]) + ->relationship('egg', 'name') ->searchable() ->preload() ->live() ->afterStateUpdated(function ($state, Set $set, Get $get, $old) { - $egg = Egg::query()->find($state); - $set('startup', $egg->startup ?? ''); + $this->egg = Egg::query()->find($state); + $set('startup', $this->egg?->startup); $set('image', ''); - $variables = $egg->variables ?? []; - $serverVariables = collect(); - foreach ($variables as $variable) { - $serverVariables->add($variable->toArray()); - } - - $variables = []; - $set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all()); - for ($i = 0; $i < $serverVariables->count(); $i++) { - $set("$path.$i.variable_value", $serverVariables[$i]['default_value']); - $set("$path.$i.variable_id", $serverVariables[$i]['id']); - $variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value']; - } - - $set('environment', $variables); + $this->resetEggVariables($set, $get); $previousEgg = Egg::query()->find($old); if (!$get('name') || $previousEgg?->getKebabName() === $get('name')) { - $set('name', $egg->getKebabName()); + $set('name', $this->egg->getKebabName()); } }) ->required(), @@ -430,13 +255,21 @@ public function form(Form $form): Form Textarea::make('startup') ->hintIcon('tabler-code') ->label('Startup Command') - ->hidden(fn (Get $get) => $get('egg_id') === null) + ->hidden(fn () => !$this->egg) ->required() ->live() + ->disabled(fn (Forms\Get $get) => $this->egg === null) + ->afterStateUpdated($this->resetEggVariables(...)) + ->columnSpan([ + 'default' => 1, + 'sm' => 2, + 'md' => 2, + 'lg' => 4, + ]) ->rows(function ($state) { return str($state)->explode("\n")->reduce( fn (int $carry, $line) => $carry + floor(strlen($line) / 125), - 1 + 0 ); }) ->columnSpan([ @@ -520,6 +353,70 @@ public function form(Form $form): Form ->columnSpan(2), ]), ]), + + Wizard\Step::make('Allocation') + ->label('Allocation') + ->icon('tabler-transfer-in') + ->completedIcon('tabler-check') + ->columns(4) + ->schema([ + + Forms\Components\TagsInput::make('ports') + ->columnSpan(2) + ->hintIcon('tabler-question-mark') + ->hintIconTooltip('Ports are limited from 1025 to 65535') + ->placeholder('Example: 25565, 8080, 1337-1340') + ->splitKeys(['Tab', ' ', ',']) + ->helperText(new HtmlString(' + These are the ports that users can connect to this Server through. + You would typically port forward these on your home network. + ')) + ->label('Ports') + ->afterStateUpdated(self::ports(...)) + ->live(), + + Forms\Components\Repeater::make('assignments') + ->columnSpan(2) + ->defaultItems(fn () => count($this->eggDefaultPorts)) + ->label('Port Assignments') + ->helperText(function (Forms\Get $get) { + if (empty($this->eggDefaultPorts)) { + return "This egg doesn't have any ports defined."; + } + + if (empty($get('ports'))) { + return 'You must add ports to assign them!'; + } + + return ''; + }) + ->live() + ->addable(false) + ->deletable(false) + ->reorderable(false) + ->simple( + Forms\Components\Select::make('port') + ->live() + ->placeholder('Select a Port') + ->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments'))) + ->prefix(function (Forms\Components\Component $component) { + $key = str($component->getStatePath())->beforeLast('.')->afterLast('.')->toString(); + + return $key; + }) + ->disableOptionsWhenSelectedInSiblingRepeaterItems() + ->options(fn (Forms\Get $get) => $this->ports) + ->required(), + ), + + Forms\Components\Select::make('ip') + ->label('IP Address') + ->options(fn () => collect($this->node?->ipAddresses())->mapWithKeys(fn ($ip) => [$ip => $ip])) + ->placeholder('Any') + ->columnSpan(1), + + ]), + Step::make('Environment Configuration') ->label('Environment Configuration') ->icon('tabler-brand-docker') @@ -679,7 +576,7 @@ public function form(Form $form): Form Hidden::make('io') ->helperText('The IO performance relative to other running containers') ->label('Block IO Proportion') - ->default(500), + ->default(config('panel.default_io_weight')), Grid::make() ->columns(4) @@ -832,9 +729,16 @@ protected function getFormActions(): array protected function handleRecordCreation(array $data): Model { - $data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all(); + $ipAddress = $data['ip'] ?? Endpoint::INADDR_ANY; + foreach ($data['ports'] ?? [] as $i => $port) { + $data['ports'][$i] = (string) new Endpoint($port, $ipAddress); + } + + foreach (array_keys($this->eggDefaultPorts) as $i => $env) { + $data['environment'][$env] = $data['ports'][$data['assignments'][$i]]; + } - return $this->serverCreationService->handle($data); + return $this->serverCreationService->handle($data, validateVariables: false); } private function shouldHideComponent(Get $get, Component $component): bool @@ -867,4 +771,79 @@ private function getSelectOptionsFromRules(Get $get): array ->mapWithKeys(fn ($value) => [$value => $value]) ->all(); } + + public function ports(array $state, Forms\Set $set): void + { + $ports = collect(); + foreach ($state as $portEntry) { + if (str_contains($portEntry, '-')) { + [$start, $end] = explode('-', $portEntry); + if (!is_numeric($start) || !is_numeric($end)) { + continue; + } + + $start = max((int) $start, Endpoint::PORT_FLOOR); + $end = min((int) $end, Endpoint::PORT_CEIL); + for ($i = $start; $i <= $end; $i++) { + $ports->push($i); + } + } + + if (!is_numeric($portEntry)) { + continue; + } + + $ports->push((int) $portEntry); + } + + $uniquePorts = $ports->unique()->values(); + if ($ports->count() > $uniquePorts->count()) { + $ports = $uniquePorts; + } + + $ports = $ports->filter(fn ($port) => $port > Endpoint::PORT_FLOOR && $port < Endpoint::PORT_CEIL)->values(); + + $set('ports', $ports->all()); + $this->ports = $ports->all(); + } + + public function resetEggVariables(Forms\Set $set, Forms\Get $get): void + { + $set('assignments', []); + + $i = 0; + $this->eggDefaultPorts = []; + if (str_contains($get('startup'), '{{SERVER_PORT}}') || str_contains($this->egg->config_files, '{{server.allocations.default.port}}')) { + $this->eggDefaultPorts['SERVER_PORT'] = null; + $set('assignments.SERVER_PORT', ['port' => null]); + } + + $variables = $this->egg->variables ?? []; + $serverVariables = collect(); + $this->ports = []; + foreach ($variables as $variable) { + if (in_array('port', $variable->rules)) { + $this->eggDefaultPorts[$variable->env_variable] = $variable->default_value; + $this->ports[] = (int) $variable->default_value; + + $set("assignments.$variable->env_variable", ['port' => $i++]); + + continue; + } + + $serverVariables->add($variable->toArray()); + } + + $set('ports', $this->ports); + + $variables = []; + $set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all()); + for ($i = 0; $i < $serverVariables->count(); $i++) { + $set("$path.$i.variable_value", $serverVariables[$i]['default_value']); + $set("$path.$i.variable_id", $serverVariables[$i]['id']); + $variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value']; + } + + $set('environment', $variables); + } } diff --git a/app/Filament/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Resources/ServerResource/Pages/EditServer.php index fe1be62764..989ae92bf6 100644 --- a/app/Filament/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Resources/ServerResource/Pages/EditServer.php @@ -2,29 +2,36 @@ namespace App\Filament\Resources\ServerResource\Pages; -use App\Enums\ContainerStatus; -use App\Enums\ServerState; -use App\Filament\Resources\ServerResource; -use App\Http\Controllers\Admin\ServersController; +use App\Models\Node; +use App\Models\Objects\Endpoint; +use Exception; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\HtmlString; use App\Models\Database; -use App\Models\Egg; -use App\Models\Server; -use App\Models\ServerVariable; use App\Services\Databases\DatabaseManagementService; use App\Services\Databases\DatabasePasswordService; +use Filament\Forms\Components\Actions\Action; +use Filament\Forms\Components\Repeater; +use Filament\Forms\Get; +use Filament\Forms\Set; +use LogicException; +use App\Filament\Resources\ServerResource; +use App\Http\Controllers\Admin\ServersController; use App\Services\Servers\RandomWordService; -use App\Services\Servers\ServerDeletionService; use App\Services\Servers\SuspensionService; use App\Services\Servers\TransferServerService; -use Closure; -use Exception; use Filament\Actions; use Filament\Forms; -use Filament\Forms\Components\Actions\Action; +use App\Enums\ContainerStatus; +use App\Enums\ServerState; +use App\Models\Egg; +use App\Models\Server; +use App\Models\ServerVariable; +use App\Services\Servers\ServerDeletionService; +use Closure; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Fieldset; use Filament\Forms\Components\Grid; -use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs\Tab; @@ -32,17 +39,21 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Components\ToggleButtons; use Filament\Forms\Form; -use Filament\Forms\Get; -use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Resources\Pages\EditRecord; -use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Validator; -use LogicException; use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction; class EditServer extends EditRecord { + public ?Node $node = null; + + public ?Egg $egg = null; + + public array $ports = []; + + public array $eggDefaultPorts = []; + protected static string $resource = ServerResource::class; public function form(Form $form): Form @@ -62,6 +73,26 @@ public function form(Form $form): Form Tab::make('Information') ->icon('tabler-info-circle') ->schema([ + Forms\Components\ToggleButtons::make('condition') + ->label('Status') + ->formatStateUsing(fn (Server $server) => $server->condition) + ->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases())) + ->filter(fn ($condition) => $condition->value === $state) + ->mapWithKeys(fn ($state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()]) + ) + ->colors(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys( + fn ($status) => [$status->value => $status->color()] + )) + ->icons(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys( + fn ($status) => [$status->value => $status->icon()] + )) + ->columnSpan([ + 'default' => 2, + 'sm' => 1, + 'md' => 1, + 'lg' => 1, + ]), + TextInput::make('name') ->prefixIcon('tabler-server') ->label('Display Name') @@ -119,7 +150,7 @@ public function form(Form $form): Form ]), Textarea::make('description') - ->label('Description') + ->label('Notes') ->columnSpanFull(), TextInput::make('uuid') @@ -163,6 +194,7 @@ public function form(Form $form): Form ]) ->disabled(), ]), + Tab::make('Environment') ->icon('tabler-brand-docker') ->schema([ @@ -460,6 +492,66 @@ public function form(Form $form): Form ]) ->required(), + Forms\Components\TagsInput::make('ports') + ->columnSpan(3) + ->placeholder('Example: 25565, 8080, 1337-1340') + ->splitKeys(['Tab', ' ', ',']) + ->helperText(new HtmlString(' + These are the ports that users can connect to this Server through. +
+ You would typically port forward these on your home network. + ')) + ->label('Ports') + ->formatStateUsing(fn (Server $server) => $server->ports->map(fn ($port) => (string) $port)->all()) + ->afterStateUpdated(self::ports(...)) + ->live(), + + Forms\Components\Repeater::make('portVariables') + ->label('Port Assignments') + ->columnSpan(3) + ->addable(false) + ->deletable(false) + + ->mutateRelationshipDataBeforeSaveUsing(function ($data) { + $portIndex = $data['port']; + unset($data['port']); + + return [ + 'variable_value' => (string) $this->ports[$portIndex], + ]; + }) + + ->relationship('serverVariables', function (Builder $query) { + $query->whereHas('variable', function (Builder $query) { + $query->where('rules', 'like', '%port%'); + }); + }) + + ->simple( + Forms\Components\Select::make('port') + ->live() + ->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments'))) + ->prefix(function (Forms\Components\Component $component, ServerVariable $serverVariable) { + return $serverVariable->variable->env_variable; + }) + + ->formatStateUsing(function (ServerVariable $serverVariable, Forms\Get $get) { + return array_search($serverVariable->variable_value, array_values($get('../../ports'))); + }) + + ->disableOptionsWhenSelectedInSiblingRepeaterItems() + ->options(fn (Forms\Get $get) => $this->ports) + ->required(), + ) + + ->afterStateHydrated(function (Forms\Set $set, Forms\Get $get, Server $server) { + $this->ports($ports = $get('ports'), $set); + + foreach ($this->portOptions($server->egg) as $key => $port) { + $set("assignments.$key", ['port' => $portIndex = array_search($port, array_values($ports))]); + } + }), + Textarea::make('startup') ->label('Startup Command') ->required() @@ -542,7 +634,7 @@ public function form(Form $form): Form ->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name) ->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules)) ->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}') - ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description); + ->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable?->description) ? '—' : $serverVariable->variable->description); } return $components; @@ -781,17 +873,14 @@ protected function mutateFormDataBeforeSave(array $data): array return $data; } - public function getRelationManagers(): array - { - return [ - ServerResource\RelationManagers\AllocationsRelationManager::class, - ]; - } - private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool { $containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false); + if (collect($serverVariable->variable->rules)->contains('port')) { + return true; + } + if ($component instanceof Select) { return !$containsRuleIn; } @@ -815,6 +904,76 @@ private function getSelectOptionsFromRules(ServerVariable $serverVariable): arra ->all(); } + public function ports(array $state, Forms\Set $set): void + { + $ports = collect(); + + foreach ($state as $portEntry) { + if (str_contains($portEntry, '-')) { + [$start, $end] = explode('-', $portEntry); + + try { + $startEndpoint = new Endpoint($start); + $endEndpoint = new Endpoint($end); + } catch (Exception) { + continue; + } + + if ($startEndpoint->ip !== $endEndpoint->ip) { + continue; + } + + foreach (range($startEndpoint->port, $endEndpoint->port) as $port) { + $ports->push(new Endpoint($port, $startEndpoint->ip)); + } + + for ($i = $start; $i <= $end; $i++) { + $ports->push($i); + } + + continue; + } + + try { + $ports->push(new Endpoint($portEntry)); + } catch (Exception) { + continue; + } + } + + $ports = $ports->map(fn ($endpoint) => (string) $endpoint); + + $uniquePorts = $ports->unique()->values(); + if ($ports->count() > $uniquePorts->count()) { + $ports = $uniquePorts; + } + + $set('ports', $ports->all()); + $this->ports = $ports->all(); + } + + public function portOptions(Egg $egg, ?string $startup = null): array + { + if (empty($startup)) { + $startup = $egg->startup; + } + + $options = []; + if (str_contains($startup, '{{SERVER_PORT}}')) { + $options['SERVER_PORT'] = null; + } + + foreach ($egg->variables as $variable) { + if (!in_array('port', $variable->rules)) { + continue; + } + + $options[$variable->env_variable] = $variable->default_value; + } + + return $options; + } + protected function rotatePassword(DatabasePasswordService $service, Database $record, Set $set, Get $get): void { $newPassword = $service->handle($record); diff --git a/app/Filament/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Resources/ServerResource/Pages/ListServers.php index dd6fc4a16a..afe5ca840d 100644 --- a/app/Filament/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Resources/ServerResource/Pages/ListServers.php @@ -9,7 +9,6 @@ use Filament\Tables\Actions\Action; use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\EditAction; -use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Grouping\Group; use Filament\Tables\Table; @@ -61,16 +60,6 @@ public function table(Table $table): Table ->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username') ->sortable() ->searchable(), - SelectColumn::make('allocation_id') - ->label('Primary Allocation') - ->hidden(!auth()->user()->can('update server')) - ->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) - ->selectablePlaceholder(false) - ->sortable(), - TextColumn::make('allocation_id_readonly') - ->label('Primary Allocation') - ->hidden(auth()->user()->can('update server')) - ->state(fn (Server $server) => $server->allocation->address), TextColumn::make('image')->hidden(), TextColumn::make('backups_count') ->counts('backups') @@ -78,6 +67,9 @@ public function table(Table $table): Table ->icon('tabler-file-download') ->numeric() ->sortable(), + TextColumn::make('ports') + ->badge() + ->separator(), ]) ->actions([ Action::make('View') diff --git a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php deleted file mode 100644 index aa6658b4d3..0000000000 --- a/app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ /dev/null @@ -1,161 +0,0 @@ -schema([ - TextInput::make('ip') - ->required() - ->maxLength(255), - ]); - } - - public function table(Table $table): Table - { - return $table - ->recordTitleAttribute('ip') - ->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port") - ->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id) - ->inverseRelationship('server') - ->columns([ - TextColumn::make('ip')->label('IP'), - TextColumn::make('port')->label('Port'), - TextInputColumn::make('ip_alias')->label('Alias'), - IconColumn::make('primary') - ->icon(fn ($state) => match ($state) { - true => 'tabler-star-filled', - default => 'tabler-star', - }) - ->color(fn ($state) => match ($state) { - true => 'warning', - default => 'gray', - }) - ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id])) - ->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id) - ->label('Primary'), - ]) - ->actions([ - Action::make('make-primary') - ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id])) - ->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'), - ]) - ->headerActions([ - CreateAction::make()->label('Create Allocation') - ->createAnother(false) - ->form(fn () => [ - TextInput::make('allocation_ip') - ->datalist($this->getOwnerRecord()->node->ipAddresses()) - ->label('IP Address') - ->inlineLabel() - ->ipv4() - ->helperText("Usually your machine's public IP unless you are port forwarding.") - ->required(), - TextInput::make('allocation_alias') - ->label('Alias') - ->inlineLabel() - ->default(null) - ->helperText('Optional display name to help you remember what these are.') - ->required(false), - TagsInput::make('allocation_ports') - ->placeholder('Examples: 27015, 27017-27019') - ->helperText(new HtmlString(' - These are the ports that users can connect to this Server through. -
- You would have to port forward these on your home network. - ')) - ->label('Ports') - ->inlineLabel() - ->live() - ->afterStateUpdated(function ($state, Set $set) { - $ports = collect(); - $update = false; - foreach ($state as $portEntry) { - if (!str_contains($portEntry, '-')) { - if (is_numeric($portEntry)) { - $ports->push((int) $portEntry); - - continue; - } - - // Do not add non numerical ports - $update = true; - - continue; - } - - $update = true; - [$start, $end] = explode('-', $portEntry); - if (!is_numeric($start) || !is_numeric($end)) { - continue; - } - - $start = max((int) $start, 0); - $end = min((int) $end, 2 ** 16 - 1); - foreach (range($start, $end) as $i) { - $ports->push($i); - } - } - - $uniquePorts = $ports->unique()->values(); - if ($ports->count() > $uniquePorts->count()) { - $update = true; - $ports = $uniquePorts; - } - - $sortedPorts = $ports->sort()->values(); - if ($sortedPorts->all() !== $ports->all()) { - $update = true; - $ports = $sortedPorts; - } - - $ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values(); - - if ($update) { - $set('allocation_ports', $ports->all()); - } - }) - ->splitKeys(['Tab', ' ', ',']) - ->required(), - ]) - ->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())), - AssociateAction::make() - ->multiple() - ->associateAnother(false) - ->preloadRecordSelect() - ->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)) - ->label('Add Allocation'), - ]) - ->bulkActions([ - Tables\Actions\BulkActionGroup::make([ - Tables\Actions\DissociateBulkAction::make(), - ]), - ]); - } -} diff --git a/app/Filament/Resources/UserResource/Pages/EditProfile.php b/app/Filament/Resources/UserResource/Pages/EditProfile.php index 5b35deff32..67c611b2d6 100644 --- a/app/Filament/Resources/UserResource/Pages/EditProfile.php +++ b/app/Filament/Resources/UserResource/Pages/EditProfile.php @@ -201,7 +201,7 @@ protected function getForms(): array Tab::make('API Keys') ->icon('tabler-key') ->schema([ - Grid::make('asdf')->columns(5)->schema([ + Grid::make(5)->schema([ Section::make('Create API Key')->columnSpan(3)->schema([ TextInput::make('description') ->live(), @@ -289,7 +289,7 @@ protected function handleRecordUpdate(Model $record, array $data): Model if ($token = $data['2facode'] ?? null) { $tokens = $this->toggleTwoFactorService->handle($record, $token, true); - cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15)); + cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), 15); $this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']); } diff --git a/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php index 01a73c3738..2a09973473 100644 --- a/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php +++ b/app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php @@ -8,7 +8,6 @@ use App\Services\Servers\SuspensionService; use Filament\Resources\RelationManagers\RelationManager; use Filament\Tables\Actions; -use Filament\Tables\Columns\SelectColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -66,11 +65,6 @@ public function table(Table $table): Table ->icon('tabler-egg') ->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg])) ->sortable(), - SelectColumn::make('allocation.id') - ->label('Primary Allocation') - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) - ->sortable(), TextColumn::make('image')->hidden(), TextColumn::make('databases_count') ->counts('databases') diff --git a/app/Http/Controllers/Admin/Nodes/NodeViewController.php b/app/Http/Controllers/Admin/Nodes/NodeViewController.php index 115b7b6b1d..845b5dc534 100644 --- a/app/Http/Controllers/Admin/Nodes/NodeViewController.php +++ b/app/Http/Controllers/Admin/Nodes/NodeViewController.php @@ -5,7 +5,6 @@ use Illuminate\View\View; use App\Models\Node; use Illuminate\Support\Collection; -use App\Models\Allocation; use App\Http\Controllers\Controller; use App\Traits\Controllers\JavascriptInjection; use App\Services\Helpers\SoftwareVersionService; @@ -57,32 +56,6 @@ public function configuration(Node $node): View return view('admin.nodes.view.configuration', compact('node')); } - /** - * Return the node allocation management page. - */ - public function allocations(Node $node): View - { - $node->setRelation( - 'allocations', - $node->allocations() - ->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL') - ->orderByRaw('INET_ATON(ip) ASC') - ->orderBy('port') - ->with('server:id,name') - ->paginate(50) - ); - - $this->plainInject(['node' => Collection::wrap($node)->only(['id'])]); - - return view('admin.nodes.view.allocation', [ - 'node' => $node, - 'allocations' => Allocation::query()->where('node_id', $node->id) - ->groupBy('ip') - ->orderByRaw('INET_ATON(ip) ASC') - ->get(['ip']), - ]); - } - /** * Return a listing of servers that exist for this specific node. */ diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 66ba487f6d..ebb1f56c3d 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -3,10 +3,7 @@ namespace App\Http\Controllers\Admin; use Illuminate\View\View; -use Illuminate\Http\Request; use App\Models\Node; -use Illuminate\Http\Response; -use App\Models\Allocation; use Illuminate\Http\RedirectResponse; use Prologue\Alerts\AlertsMessageBag; use Illuminate\View\Factory as ViewFactory; @@ -15,11 +12,8 @@ use Illuminate\Cache\Repository as CacheRepository; use App\Services\Nodes\NodeCreationService; use App\Services\Nodes\NodeDeletionService; -use App\Services\Allocations\AssignmentService; use App\Services\Helpers\SoftwareVersionService; use App\Http\Requests\Admin\Node\NodeFormRequest; -use App\Http\Requests\Admin\Node\AllocationFormRequest; -use App\Http\Requests\Admin\Node\AllocationAliasFormRequest; class NodesController extends Controller { @@ -28,7 +22,6 @@ class NodesController extends Controller */ public function __construct( protected AlertsMessageBag $alert, - protected AssignmentService $assignmentService, protected CacheRepository $cache, protected NodeCreationService $creationService, protected NodeDeletionService $deletionService, @@ -46,19 +39,6 @@ public function create(): View|RedirectResponse return view('admin.nodes.new'); } - /** - * Post controller to create a new node on the system. - * - * @throws \App\Exceptions\Model\DataValidationException - */ - public function store(NodeFormRequest $request): RedirectResponse - { - $node = $this->creationService->handle($request->normalize()); - $this->alert->info(trans('admin/node.notices.node_created'))->flash(); - - return redirect()->route('admin.nodes.view.allocation', $node->id); - } - /** * Updates settings for a node. * @@ -73,83 +53,6 @@ public function updateSettings(NodeFormRequest $request, Node $node): RedirectRe return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); } - /** - * Removes a single allocation from a node. - * - * @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException - */ - public function allocationRemoveSingle(int $node, Allocation $allocation): Response - { - $allocation->delete(); - - return response('', 204); - } - - /** - * Removes multiple individual allocations from a node. - * - * @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException - */ - public function allocationRemoveMultiple(Request $request, int $node): Response - { - $allocations = $request->input('allocations'); - foreach ($allocations as $rawAllocation) { - $allocation = new Allocation(); - $allocation->id = $rawAllocation['id']; - $this->allocationRemoveSingle($node, $allocation); - } - - return response('', 204); - } - - /** - * Remove all allocations for a specific IP at once on a node. - */ - public function allocationRemoveBlock(Request $request, int $node): RedirectResponse - { - /** @var Node $node */ - $node = Node::query()->findOrFail($node); - $node->allocations() - ->where('ip', $request->input('ip')) - ->whereNull('server_id') - ->delete(); - - $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')])) - ->flash(); - - return redirect()->route('admin.nodes.view.allocation', $node); - } - - /** - * Sets an alias for a specific allocation on a node. - * - * @throws \App\Exceptions\Model\DataValidationException - */ - public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response - { - $allocation = Allocation::query()->findOrFail($request->input('allocation_id')); - $alias = (empty($request->input('alias'))) ? null : $request->input('alias'); - $allocation->update(['ip_alias' => $alias]); - - return response('', 204); - } - - /** - * Creates new allocations on a node. - * - * @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException - * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException - */ - public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse - { - $this->assignmentService->handle($node, $request->normalize()); - $this->alert->success(trans('admin/node.notices.allocations_added'))->flash(); - - return redirect()->route('admin.nodes.view.allocation', $node->id); - } - /** * Deletes a node from the system. * diff --git a/app/Http/Controllers/Admin/Servers/CreateServerController.php b/app/Http/Controllers/Admin/Servers/CreateServerController.php index c59e22fbeb..0818ccca7d 100644 --- a/app/Http/Controllers/Admin/Servers/CreateServerController.php +++ b/app/Http/Controllers/Admin/Servers/CreateServerController.php @@ -36,11 +36,6 @@ public function index(): View|RedirectResponse $eggs = Egg::with('variables')->get(); - \JavaScript::put([ - 'nodeData' => Node::getForServerCreation(), - 'eggs' => $eggs->keyBy('id'), - ]); - return view('admin.servers.new', [ 'eggs' => $eggs, 'nodes' => Node::all(), @@ -52,7 +47,6 @@ public function index(): View|RedirectResponse * * @throws \Illuminate\Validation\ValidationException * @throws \App\Exceptions\DisplayException - * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException * @throws \Throwable */ public function store(ServerFormRequest $request): RedirectResponse diff --git a/app/Http/Controllers/Admin/Servers/ServerTransferController.php b/app/Http/Controllers/Admin/Servers/ServerTransferController.php index 9fdaa7bf8e..7421c3f827 100644 --- a/app/Http/Controllers/Admin/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Admin/Servers/ServerTransferController.php @@ -29,8 +29,6 @@ public function transfer(Request $request, Server $server): RedirectResponse { $validatedData = $request->validate([ 'node_id' => 'required|exists:nodes,id', - 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', - 'allocation_additional' => 'nullable', ]); if ($this->transferServerService->handle($server, $validatedData)) { diff --git a/app/Http/Controllers/Admin/Servers/ServerViewController.php b/app/Http/Controllers/Admin/Servers/ServerViewController.php index 594022a978..261f4ee22d 100644 --- a/app/Http/Controllers/Admin/Servers/ServerViewController.php +++ b/app/Http/Controllers/Admin/Servers/ServerViewController.php @@ -47,12 +47,8 @@ public function details(Server $server): View */ public function build(Server $server): View { - $allocations = $server->node->allocations->toBase(); - return view('admin.servers.view.build', [ 'server' => $server, - 'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'), - 'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'), ]); } @@ -121,10 +117,6 @@ public function manage(Server $server): View $canTransfer = true; } - \JavaScript::put([ - 'nodeData' => Node::getForServerCreation(), - ]); - return view('admin.servers.view.manage', [ 'nodes' => Node::all(), 'server' => $server, diff --git a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php b/app/Http/Controllers/Api/Application/Nodes/AllocationController.php deleted file mode 100644 index 18e2f5cfce..0000000000 --- a/app/Http/Controllers/Api/Application/Nodes/AllocationController.php +++ /dev/null @@ -1,79 +0,0 @@ -allocations()) - ->allowedFilters([ - AllowedFilter::exact('ip'), - AllowedFilter::exact('port'), - 'ip_alias', - AllowedFilter::callback('server_id', function (Builder $builder, $value) { - if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) { - return $builder->whereNull('server_id'); - } - - return $builder->where('server_id', $value); - }), - ]) - ->paginate($request->query('per_page') ?? 50); - - return $this->fractal->collection($allocations) - ->transformWith($this->getTransformer(AllocationTransformer::class)) - ->toArray(); - } - - /** - * Store new allocations for a given node. - * - * @throws \App\Exceptions\DisplayException - * @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException - * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException - */ - public function store(StoreAllocationRequest $request, Node $node): JsonResponse - { - $this->assignmentService->handle($node, $request->validated()); - - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); - } - - /** - * Delete a specific allocation from the Panel. - */ - public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse - { - $allocation->delete(); - - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); - } -} diff --git a/app/Http/Controllers/Api/Application/Servers/ServerController.php b/app/Http/Controllers/Api/Application/Servers/ServerController.php index 230d19794d..06e8ccbb44 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerController.php @@ -49,7 +49,6 @@ public function index(GetServersRequest $request): array * @throws \Illuminate\Validation\ValidationException * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Model\DataValidationException - * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException */ public function store(StoreServerRequest $request): JsonResponse { diff --git a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php index 4c9b512aeb..4428fef1ce 100644 --- a/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php +++ b/app/Http/Controllers/Api/Application/Servers/ServerManagementController.php @@ -69,8 +69,6 @@ public function startTransfer(ServerWriteRequest $request, Server $server): Resp { $validatedData = $request->validate([ 'node_id' => 'required|exists:nodes,id', - 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', - 'allocation_additional' => 'nullable', ]); if ($this->transferServerService->handle($server, $validatedData)) { diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php deleted file mode 100644 index 948fbbaced..0000000000 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ /dev/null @@ -1,137 +0,0 @@ -fractal->collection($server->allocations) - ->transformWith($this->getTransformer(AllocationTransformer::class)) - ->toArray(); - } - - /** - * Set the primary allocation for a server. - * - * @throws \App\Exceptions\Model\DataValidationException - */ - public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array - { - $original = $allocation->notes; - - $allocation->forceFill(['notes' => $request->input('notes')])->save(); - - if ($original !== $allocation->notes) { - Activity::event('server:allocation.notes') - ->subject($allocation) - ->property(['allocation' => $allocation->toString(), 'old' => $original, 'new' => $allocation->notes]) - ->log(); - } - - return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) - ->toArray(); - } - - /** - * Set the primary allocation for a server. - * - * @throws \App\Exceptions\Model\DataValidationException - */ - public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array - { - $server->allocation()->associate($allocation); - $server->save(); - - Activity::event('server:allocation.primary') - ->subject($allocation) - ->property('allocation', $allocation->toString()) - ->log(); - - return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) - ->toArray(); - } - - /** - * Set the notes for the allocation for a server. - *s. - * - * @throws \App\Exceptions\DisplayException - */ - public function store(NewAllocationRequest $request, Server $server): array - { - if ($server->allocations()->count() >= $server->allocation_limit) { - throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.'); - } - - $allocation = $this->assignableAllocationService->handle($server); - - Activity::event('server:allocation.create') - ->subject($allocation) - ->property('allocation', $allocation->toString()) - ->log(); - - return $this->fractal->item($allocation) - ->transformWith($this->getTransformer(AllocationTransformer::class)) - ->toArray(); - } - - /** - * Delete an allocation from a server. - * - * @throws \App\Exceptions\DisplayException - */ - public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse - { - // Don't allow the deletion of allocations if the server does not have an - // allocation limit set. - if (empty($server->allocation_limit)) { - throw new DisplayException('You cannot delete allocations for this server: no allocation limit is set.'); - } - - if ($allocation->id === $server->allocation_id) { - throw new DisplayException('You cannot delete the primary allocation for this server.'); - } - - Allocation::query()->where('id', $allocation->id)->update([ - 'notes' => null, - 'server_id' => null, - ]); - - Activity::event('server:allocation.delete') - ->subject($allocation) - ->property('allocation', $allocation->toString()) - ->log(); - - return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT); - } -} diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php index cd59c3d2cc..c4bae4f402 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerContainersController.php @@ -16,7 +16,7 @@ public function status(Server $server, Request $request): JsonResponse { $status = fluent($request->json()->all())->get('data.new_state'); - cache()->set("servers.$server->uuid.container.status", $status, now()->addHour()); + cache()->set("servers.$server->uuid.container.status", $status, 3600); return new JsonResponse([]); } diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php index 8244530453..88ad0ed91c 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerDetailsController.php @@ -48,7 +48,7 @@ public function list(Request $request): ServerConfigurationCollection // Avoid run-away N+1 SQL queries by preloading the relationships that are used // within each of the services called below. - $servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables') + $servers = Server::query()->with('egg', 'mounts', 'variables') ->where('node_id', $node->id) // If you don't cast this to a string you'll end up with a stringified per_page returned in // the metadata, and then daemon will panic crash as a result. diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 6a74521f94..95026b5d75 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -6,7 +6,6 @@ use App\Repositories\Daemon\DaemonServerRepository; use Illuminate\Http\Response; use Illuminate\Http\JsonResponse; -use App\Models\Allocation; use App\Models\ServerTransfer; use Illuminate\Database\ConnectionInterface; use App\Http\Controllers\Controller; @@ -53,13 +52,7 @@ public function success(Server $server): JsonResponse /** @var \App\Models\Server $server */ $server = $this->connection->transaction(function () use ($server, $transfer) { - $allocations = array_merge([$transfer->old_allocation], $transfer->old_additional_allocations); - - // Remove the old allocations for the server and re-assign the server to the new - // primary allocation and node. - Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); $server->update([ - 'allocation_id' => $transfer->new_allocation, 'node_id' => $transfer->new_node, ]); @@ -93,9 +86,6 @@ protected function processFailedTransfer(ServerTransfer $transfer): JsonResponse { $this->connection->transaction(function () use (&$transfer) { $transfer->forceFill(['successful' => false])->saveOrFail(); - - $allocations = array_merge([$transfer->new_allocation], $transfer->new_additional_allocations); - Allocation::query()->whereIn('id', $allocations)->update(['server_id' => null]); }); return new JsonResponse([], Response::HTTP_NO_CONTENT); diff --git a/app/Http/Middleware/Api/Client/Server/ResourceBelongsToServer.php b/app/Http/Middleware/Api/Client/Server/ResourceBelongsToServer.php index 0ead01f1d5..c6f9719920 100644 --- a/app/Http/Middleware/Api/Client/Server/ResourceBelongsToServer.php +++ b/app/Http/Middleware/Api/Client/Server/ResourceBelongsToServer.php @@ -10,7 +10,6 @@ use App\Models\Subuser; use App\Models\Database; use App\Models\Schedule; -use App\Models\Allocation; use Illuminate\Database\Eloquent\Model; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -48,7 +47,6 @@ public function handle(Request $request, \Closure $next): mixed switch (get_class($model)) { // All of these models use "server_id" as the field key for the server // they are assigned to, so the logic is identical for them all. - case Allocation::class: case Backup::class: case Database::class: case Schedule::class: diff --git a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php deleted file mode 100644 index 3da9f1896e..0000000000 --- a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php +++ /dev/null @@ -1,16 +0,0 @@ - 'present|nullable|string', - 'allocation_id' => 'required|numeric|exists:allocations,id', - ]; - } -} diff --git a/app/Http/Requests/Admin/Node/AllocationFormRequest.php b/app/Http/Requests/Admin/Node/AllocationFormRequest.php deleted file mode 100644 index 720488e188..0000000000 --- a/app/Http/Requests/Admin/Node/AllocationFormRequest.php +++ /dev/null @@ -1,17 +0,0 @@ - 'required|string', - 'allocation_alias' => 'sometimes|nullable|string|max:255', - 'allocation_ports' => 'required|array', - ]; - } -} diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index d3f9781288..aa59317313 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -3,7 +3,6 @@ namespace App\Http\Requests\Admin; use App\Models\Server; -use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; class ServerFormRequest extends AdminFormRequest @@ -25,34 +24,10 @@ public function rules(): array */ public function withValidator(Validator $validator): void { - $validator->after(function ($validator) { + $validator->after(function (Validator $validator) { $validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) { return !$input->auto_deploy; }); - - $validator->sometimes('allocation_id', [ - 'required', - 'numeric', - 'bail', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->where('node_id', $this->input('node_id')); - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->auto_deploy; - }); - - $validator->sometimes('allocation_additional.*', [ - 'sometimes', - 'required', - 'numeric', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->where('node_id', $this->input('node_id')); - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->auto_deploy; - }); }); } } diff --git a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php b/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php deleted file mode 100644 index 74077965e5..0000000000 --- a/app/Http/Requests/Api/Application/Allocations/DeleteAllocationRequest.php +++ /dev/null @@ -1,13 +0,0 @@ - 'required|string', - 'alias' => 'sometimes|nullable|string|max:255', - 'ports' => 'required|array', - 'ports.*' => 'string', - ]; - } - - public function validated($key = null, $default = null): array - { - $data = parent::validated(); - - return [ - 'allocation_ip' => $data['ip'], - 'allocation_ports' => $data['ports'], - 'allocation_alias' => $data['alias'] ?? null, - ]; - } -} diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 86ac941fd6..a6c2e5da7c 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -3,7 +3,6 @@ namespace App\Http\Requests\Api\Application\Servers; use App\Models\Server; -use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; use App\Services\Acl\Api\AdminAcl; use App\Models\Objects\DeploymentObject; @@ -49,10 +48,6 @@ public function rules(): array 'feature_limits.allocations' => $rules['allocation_limit'], 'feature_limits.backups' => $rules['backup_limit'], - // Placeholders for rules added in withValidator() function. - 'allocation.default' => '', - 'allocation.additional.*' => '', - // Automatic deployment rules 'deploy' => 'sometimes|required|array', 'deploy.locations' => 'array', @@ -87,8 +82,7 @@ public function validated($key = null, $default = null): array 'cpu' => array_get($data, 'limits.cpu'), 'threads' => array_get($data, 'limits.threads'), 'skip_scripts' => array_get($data, 'skip_scripts', false), - 'allocation_id' => array_get($data, 'allocation.default'), - 'allocation_additional' => array_get($data, 'allocation.additional'), + 'ports' => array_get($data, 'ports'), 'start_on_completion' => array_get($data, 'start_on_completion', false), 'database_limit' => array_get($data, 'feature_limits.databases'), 'allocation_limit' => array_get($data, 'feature_limits.allocations'), @@ -104,24 +98,6 @@ public function validated($key = null, $default = null): array */ public function withValidator(Validator $validator): void { - $validator->sometimes('allocation.default', [ - 'required', 'integer', 'bail', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->deploy; - }); - - $validator->sometimes('allocation.additional.*', [ - 'integer', - Rule::exists('allocations', 'id')->where(function ($query) { - $query->whereNull('server_id'); - }), - ], function ($input) { - return !$input->deploy; - }); - /** @deprecated use tags instead */ $validator->sometimes('deploy.locations', 'present', function ($input) { return $input->deploy; @@ -134,6 +110,10 @@ public function withValidator(Validator $validator): void $validator->sometimes('deploy.port_range', 'present', function ($input) { return $input->deploy; }); + + $validator->sometimes('deploy.node_id', 'present', function ($input) { + return $input->deploy; + }); } /** @@ -149,6 +129,7 @@ public function getDeploymentObject(): ?DeploymentObject $object->setDedicated($this->input('deploy.dedicated_ip', false)); $object->setTags($this->input('deploy.tags', $this->input('deploy.locations', []))); $object->setPorts($this->input('deploy.port_range', [])); + $object->setNode($this->input('deploy.node_id')); return $object; } diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php index 7b0fcad10b..f30e4d049b 100644 --- a/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerBuildConfigurationRequest.php @@ -15,7 +15,6 @@ public function rules(): array $rules = Server::getRulesForUpdate($this->parameter('server', Server::class)); return [ - 'allocation' => $rules['allocation_id'], 'oom_killer' => $rules['oom_killer'], 'limits' => 'sometimes|array', @@ -54,7 +53,6 @@ public function validated($key = null, $default = null): array { $data = parent::validated(); - $data['allocation_id'] = $data['allocation']; $data['database_limit'] = $data['feature_limits']['databases'] ?? null; $data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null; $data['backup_limit'] = $data['feature_limits']['backups'] ?? null; diff --git a/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php b/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php deleted file mode 100644 index a5d76472d6..0000000000 --- a/app/Http/Requests/Api/Client/Servers/Network/DeleteAllocationRequest.php +++ /dev/null @@ -1,14 +0,0 @@ - array_merge($rules['notes'], ['present']), - ]; - } -} diff --git a/app/Livewire/EndpointSynth.php b/app/Livewire/EndpointSynth.php new file mode 100644 index 0000000000..a359e4b071 --- /dev/null +++ b/app/Livewire/EndpointSynth.php @@ -0,0 +1,31 @@ + 'required|exists:nodes,id', - 'ip' => 'required|ip', - 'port' => 'required|numeric|between:1024,65535', - 'ip_alias' => 'nullable|string', - 'server_id' => 'nullable|exists:servers,id', - 'notes' => 'nullable|string|max:256', - ]; - - protected static function booted(): void - { - static::deleting(function (self $allocation) { - throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using'))); - }); - } - - protected function casts(): array - { - return [ - 'node_id' => 'integer', - 'port' => 'integer', - 'server_id' => 'integer', - ]; - } - - public function getRouteKeyName(): string - { - return $this->getKeyName(); - } - - /** - * Accessor to automatically provide the IP alias if defined. - */ - public function getAliasAttribute(?string $value): string - { - return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias; - } - - /** - * Accessor to quickly determine if this allocation has an alias. - */ - public function getHasAliasAttribute(?string $value): bool - { - return !is_null($this->ip_alias); - } - - /** @return Attribute */ - protected function address(): Attribute - { - return Attribute::make( - get: fn () => "$this->ip:$this->port", - ); - } - - public function toString(): string - { - return $this->address; - } - - /** - * Gets information for the server associated with this allocation. - */ - public function server(): BelongsTo - { - return $this->belongsTo(Server::class); - } - - /** - * Return the Node model associated with this allocation. - */ - public function node(): BelongsTo - { - return $this->belongsTo(Node::class); - } -} diff --git a/app/Models/AuditLog.php b/app/Models/AuditLog.php deleted file mode 100644 index d091773eae..0000000000 --- a/app/Models/AuditLog.php +++ /dev/null @@ -1,82 +0,0 @@ - 'required|uuid', - 'action' => 'required|string|max:255', - 'subaction' => 'nullable|string|max:255', - 'device' => 'array', - 'device.ip_address' => 'ip', - 'device.user_agent' => 'string', - 'metadata' => 'array', - ]; - - protected $table = 'audit_logs'; - - protected $guarded = [ - 'id', - 'created_at', - ]; - - protected function casts(): array - { - return [ - 'is_system' => 'bool', - 'device' => 'array', - 'metadata' => 'array', - 'created_at' => 'immutable_datetime', - ]; - } - - public function user(): BelongsTo - { - return $this->belongsTo(User::class); - } - - public function server(): BelongsTo - { - return $this->belongsTo(Server::class); - } - - /** - * Creates a new AuditLog model and returns it, attaching device information and the - * currently authenticated user if available. This model is not saved at this point, so - * you can always make modifications to it as needed before saving. - * - * @deprecated - */ - public static function instance(string $action, array $metadata, bool $isSystem = false): self - { - /** @var \Illuminate\Http\Request $request */ - $request = Container::getInstance()->make('request'); - if ($isSystem || !$request instanceof Request) { - $request = null; - } - - return (new self())->fill([ - 'uuid' => Uuid::uuid4()->toString(), - 'is_system' => $isSystem, - 'user_id' => ($request && $request->user()) ? $request->user()->id : null, - 'server_id' => null, - 'action' => $action, - 'device' => $request ? [ - 'ip_address' => $request->getClientIp() ?? '127.0.0.1', - 'user_agent' => $request->userAgent() ?? '', - ] : [], - 'metadata' => $metadata, - ]); - } -} diff --git a/app/Models/Backup.php b/app/Models/Backup.php index d6e4be65a2..8798cd55d3 100644 --- a/app/Models/Backup.php +++ b/app/Models/Backup.php @@ -23,7 +23,6 @@ * @property \Carbon\CarbonImmutable $updated_at * @property \Carbon\CarbonImmutable|null $deleted_at * @property \App\Models\Server $server - * @property \App\Models\AuditLog[] $audits */ class Backup extends Model { diff --git a/app/Models/Egg.php b/app/Models/Egg.php index 001be85e3f..25a1cd0ef5 100644 --- a/app/Models/Egg.php +++ b/app/Models/Egg.php @@ -322,6 +322,12 @@ public function configFrom(): BelongsTo public function getKebabName(): string { - return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join(''); + return str($this->name) + ->kebab() + ->replace('--', '-') + ->lower() + ->trim() + ->split('/[^\w\-]/') + ->join(''); } } diff --git a/app/Models/EggVariable.php b/app/Models/EggVariable.php index 2d3a4fb2c5..0b9dd9e32b 100644 --- a/app/Models/EggVariable.php +++ b/app/Models/EggVariable.php @@ -37,7 +37,7 @@ class EggVariable extends Model /** * Reserved environment variable names. */ - public const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; + public const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; /** * The table associated with the model. diff --git a/app/Models/Filters/MultiFieldServerFilter.php b/app/Models/Filters/MultiFieldServerFilter.php index 89bb15f49c..b2f8ae5393 100644 --- a/app/Models/Filters/MultiFieldServerFilter.php +++ b/app/Models/Filters/MultiFieldServerFilter.php @@ -2,7 +2,6 @@ namespace App\Models\Filters; -use Illuminate\Support\Str; use Spatie\QueryBuilder\Filters\Filter; use Illuminate\Database\Eloquent\Builder; @@ -32,26 +31,6 @@ public function __invoke(Builder $query, $value, string $property): void // Only select the server values, otherwise you'll end up merging the allocation and // server objects together, resulting in incorrect behavior and returned values. ->select('servers.*') - ->join('allocations', 'allocations.server_id', '=', 'servers.id') - ->where(function (Builder $builder) use ($value) { - $parts = explode(':', $value); - - $builder->when( - !Str::startsWith($value, ':'), - // When the string does not start with a ":" it means we're looking for an IP or IP:Port - // combo, so use a query to handle that. - function (Builder $builder) use ($parts) { - $builder->orWhere('allocations.ip', $parts[0]); - if (!is_null($parts[1] ?? null)) { - $builder->where('allocations.port', 'LIKE', "{$parts[1]}%"); - } - }, - // Otherwise, just try to search for that specific port in the allocations. - function (Builder $builder) use ($value) { - $builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%'); - } - ); - }) ->groupBy('servers.id'); return; diff --git a/app/Models/Node.php b/app/Models/Node.php index 64f44e3af0..c092c4a8e4 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -8,7 +8,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Symfony\Component\Yaml\Yaml; @@ -41,7 +40,6 @@ * @property \Carbon\Carbon $updated_at * @property \App\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts * @property \App\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers - * @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */ class Node extends Model { @@ -235,14 +233,6 @@ public function servers(): HasMany return $this->hasMany(Server::class); } - /** - * Gets the allocations associated with a node. - */ - public function allocations(): HasMany - { - return $this->hasMany(Allocation::class); - } - /** * Returns a boolean if the node is viable for an additional server to be placed on it. */ @@ -272,28 +262,6 @@ public function isViable(int $memory, int $disk, int $cpu): bool return true; } - public static function getForServerCreation(): Collection - { - return self::with('allocations')->get()->map(function (Node $item) { - $filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) { - return collect($map)->only(['id', 'ip', 'port']); - }); - - $ports = $filtered->map(function ($map) { - return [ - 'id' => $map['id'], - 'text' => sprintf('%s:%s', $map['ip'], $map['port']), - ]; - })->values(); - - return [ - 'id' => $item->id, - 'text' => $item->name, - 'allocations' => $ports, - ]; - })->values(); - } - public function systemInformation(): array { return once(function () { @@ -320,11 +288,10 @@ public function systemInformation(): array public function serverStatuses(): array { - $statuses = []; try { - $statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers')->json() ?? []; - } catch (Exception $exception) { - report($exception); + $statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->throw()->get('/api/servers')->json() ?? []; + } catch (Exception) { + $statuses = []; } foreach ($statuses as $status) { @@ -378,7 +345,10 @@ public function ipAddresses(): array // pass } - return $ips->all(); + return $ips + ->filter(fn ($ip) => preg_match('/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/', $ip)) + ->unique() + ->all(); }); } } diff --git a/app/Models/Objects/DeploymentObject.php b/app/Models/Objects/DeploymentObject.php index 692ca74873..be423402db 100644 --- a/app/Models/Objects/DeploymentObject.php +++ b/app/Models/Objects/DeploymentObject.php @@ -2,6 +2,8 @@ namespace App\Models\Objects; +use App\Models\Node; + class DeploymentObject { private bool $dedicated = false; @@ -10,6 +12,8 @@ class DeploymentObject private array $ports = []; + private Node $node; + public function isDedicated(): bool { return $this->dedicated; @@ -45,4 +49,16 @@ public function setTags(array $tags): self return $this; } + + public function getNode(): Node + { + return $this->node; + } + + public function setNode(Node $node): self + { + $this->node = $node; + + return $this; + } } diff --git a/app/Models/Objects/Endpoint.php b/app/Models/Objects/Endpoint.php new file mode 100644 index 0000000000..68361156d2 --- /dev/null +++ b/app/Models/Objects/Endpoint.php @@ -0,0 +1,64 @@ +ip = $ip ?? self::INADDR_ANY; + $this->port = (int) $port; + + if (str_contains($port, ':')) { + [$this->ip, $port] = explode(':', $port); + $this->port = (int) $port; + } + + throw_unless(filter_var($this->ip, FILTER_VALIDATE_IP) !== false, new InvalidArgumentException("$this->ip is an invalid IP address")); + throw_unless($this->port > self::PORT_FLOOR, "Port $this->port must be greater than " . self::PORT_FLOOR); + throw_unless($this->port < self::PORT_CEIL, "Port $this->port must be less than " . self::PORT_CEIL); + } + + public function __toString(): string + { + $ip = $this->ip; + + if ($ip === self::INADDR_ANY) { + return (string) $this->port; + } + + if ($ip === self::INADDR_LOOPBACK) { + $ip = 'localhost'; + } + + return "$ip:$this->port"; + } + + public function toJson($options = 0): string + { + return json_encode($this->__toString()); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php index 67b7190324..6217a2551f 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -2,14 +2,17 @@ namespace App\Models; +use App\Casts\EndpointCollection; use App\Enums\ContainerStatus; use App\Enums\ServerState; use App\Exceptions\Http\Connection\DaemonConnectionException; +use App\Models\Objects\Endpoint; use GuzzleHttp\Exception\GuzzleException; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Notifications\Notifiable; use Illuminate\Database\Query\JoinClause; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Http; use Psr\Http\Message\ResponseInterface; use Illuminate\Database\Eloquent\Relations\HasOne; @@ -38,7 +41,6 @@ * @property int $cpu * @property string|null $threads * @property bool $oom_killer - * @property int $allocation_id * @property int $egg_id * @property string $startup * @property string $image @@ -50,7 +52,6 @@ * @property \Illuminate\Support\Carbon|null $installed_at * @property \Illuminate\Database\Eloquent\Collection|\App\Models\ActivityLog[] $activity * @property int|null $activity_count - * @property \App\Models\Allocation|null $allocation * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Allocation[] $allocations * @property int|null $allocations_count * @property \Illuminate\Database\Eloquent\Collection|\App\Models\Backup[] $backups @@ -76,7 +77,6 @@ * @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|Server newQuery() * @method static \Illuminate\Database\Eloquent\Builder|Server query() - * @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationId($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationLimit($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereBackupLimit($value) * @method static \Illuminate\Database\Eloquent\Builder|Server whereCpu($value) @@ -104,7 +104,7 @@ * @method static \Illuminate\Database\Eloquent\Builder|Server whereuuid_short($value) * * @property array|null $docker_labels - * @property string|null $ports + * @property Collection|null $ports * @property-read mixed $condition * @property-read \Illuminate\Database\Eloquent\Collection $eggVariables * @property-read int|null $egg_variables_count @@ -143,11 +143,6 @@ class Server extends Model 'installed_at' => null, ]; - /** - * The default relationships to load for all server models. - */ - protected $with = ['allocation']; - /** * Fields that are not mass assignable. */ @@ -167,7 +162,6 @@ class Server extends Model 'threads' => 'nullable|regex:/^[0-9-,]+$/', 'oom_killer' => 'sometimes|boolean', 'disk' => 'required|numeric|min:0', - 'allocation_id' => 'required|bail|unique:servers|exists:allocations,id', 'egg_id' => 'required|exists:eggs,id', 'startup' => 'required|string', 'skip_scripts' => 'sometimes|boolean', @@ -175,6 +169,7 @@ class Server extends Model 'database_limit' => 'present|nullable|integer|min:0', 'allocation_limit' => 'sometimes|nullable|integer|min:0', 'backup_limit' => 'present|nullable|integer|min:0', + 'ports' => 'nullable|array', ]; protected function casts(): array @@ -190,27 +185,24 @@ protected function casts(): array 'io' => 'integer', 'cpu' => 'integer', 'oom_killer' => 'boolean', - 'allocation_id' => 'integer', 'egg_id' => 'integer', 'database_limit' => 'integer', 'allocation_limit' => 'integer', 'backup_limit' => 'integer', - self::CREATED_AT => 'datetime', - self::UPDATED_AT => 'datetime', 'deleted_at' => 'datetime', 'installed_at' => 'datetime', 'docker_labels' => 'array', + 'ports' => EndpointCollection::class, ]; } /** - * Returns the format for server allocations when communicating with the Daemon. + * Returns the format for server's ports when communicating with the Daemon. */ - public function getAllocationMappings(): array + public function getPortMappings(): array { - return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(); + return $this->ports->mapToGroups(fn (Endpoint $endpoint) => [$endpoint->ip => $endpoint->port] + )->toArray(); } public function isInstalled(): bool @@ -239,22 +231,6 @@ public function subusers(): HasMany return $this->hasMany(Subuser::class, 'server_id', 'id'); } - /** - * Gets the default allocation for a server. - */ - public function allocation(): BelongsTo - { - return $this->belongsTo(Allocation::class); - } - - /** - * Gets all allocations associated with this server. - */ - public function allocations(): HasMany - { - return $this->hasMany(Allocation::class); - } - /** * Gets information for the egg associated with this server. */ @@ -454,4 +430,21 @@ public function conditionColor(): string return $this->status->color(); } + + public function getPrimaryEndpoint(): ?Endpoint + { + $endpoint = $this->ports->first(); + + $portEggVariable = $this->variables->firstWhere('env_variable', 'SERVER_PORT'); + if ($portEggVariable) { + $portServerVariable = $this->serverVariables->firstWhere('variable_id', $portEggVariable->id); + if (!$portServerVariable) { + return null; + } + + $endpoint = new Endpoint($portServerVariable->variable_value); + } + + return $endpoint; + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4ead65cbbd..638fd9dc5b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,10 +3,12 @@ namespace App\Providers; use App\Extensions\Themes\Theme; +use App\Livewire\EndpointSynth; use App\Models; use App\Models\ApiKey; use App\Models\Node; use App\Models\User; +use App\Rules\Port; use Dedoc\Scramble\Scramble; use Dedoc\Scramble\Support\Generator\OpenApi; use Dedoc\Scramble\Support\Generator\SecurityScheme; @@ -20,10 +22,13 @@ use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; use Illuminate\Support\Str; +use Illuminate\Validation\InvokableValidationRule; use Laravel\Sanctum\Sanctum; +use Livewire\Livewire; class AppServiceProvider extends ServiceProvider { @@ -46,7 +51,6 @@ public function boot(Application $app): void } Relation::enforceMorphMap([ - 'allocation' => Models\Allocation::class, 'api_key' => Models\ApiKey::class, 'backup' => Models\Backup::class, 'database' => Models\Database::class, @@ -74,6 +78,22 @@ public function boot(Application $app): void $this->bootAuth(); $this->bootBroadcast(); + Livewire::propertySynthesizer(EndpointSynth::class); + + // Assign custom validation rules + Validator::extend('port', function ($attribute, $value, $parameters, $validator) { + $rule = InvokableValidationRule::make(new Port()); + $rule->setValidator($validator); // @phpstan-ignore-line + $rule->setData($validator->getData()); // @phpstan-ignore-line + + $result = $rule->passes($attribute, $value); + if (!$result) { + $validator->customMessages[$attribute] = $rule->message(); + } + + return $result; + }); + $bearerTokens = fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer')); Gate::define('viewApiDocs', fn () => true); Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]); diff --git a/app/Rules/Port.php b/app/Rules/Port.php index 7225509c03..9f99ed6223 100644 --- a/app/Rules/Port.php +++ b/app/Rules/Port.php @@ -2,33 +2,38 @@ namespace App\Rules; +use App\Models\Objects\Endpoint; use Closure; use Illuminate\Contracts\Validation\ValidationRule; class Port implements ValidationRule { - /** - * Run the validation rule. - * - * @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail - */ public function validate(string $attribute, mixed $value, Closure $fail): void { + // Allow port to be optional + if (empty($value)) { + return; + } + + // Require port to be a number if (!is_numeric($value)) { $fail('The :attribute must be numeric.'); } + // Require port to be an integer $value = intval($value); if (floatval($value) !== (float) $value) { $fail('The :attribute must be an integer.'); } - if ($value < 0) { - $fail('The :attribute must be greater or equal to 0.'); + // Require minimum valid port + if ($value <= Endpoint::PORT_FLOOR) { + $fail('The :attribute must be greater than 1024.'); } - if ($value > 65535) { - $fail('The :attribute must be less or equal to 65535.'); + // Require maximum valid port + if ($value > Endpoint::PORT_CEIL) { + $fail('The :attribute must be less than 65535.'); } } } diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php deleted file mode 100644 index 23f28e1224..0000000000 --- a/app/Services/Allocations/AssignmentService.php +++ /dev/null @@ -1,121 +0,0 @@ - self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) { - throw new CidrOutOfRangeException(); - } - } - - try { - // TODO: how should we approach supporting IPv6 with this? - // gethostbyname only supports IPv4, but the alternative (dns_get_record) returns - // an array of records, which is not ideal for this use case, we need a SINGLE - // IP to use, not multiple. - $underlying = gethostbyname($data['allocation_ip']); - $parsed = Network::parse($underlying); - } catch (\Exception $exception) { - throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception); - } - - $this->connection->beginTransaction(); - - $ids = []; - foreach ($parsed as $ip) { - foreach ($data['allocation_ports'] as $port) { - if (!is_digit($port) && !preg_match(self::PORT_RANGE_REGEX, $port)) { - throw new InvalidPortMappingException($port); - } - - $insertData = []; - if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) { - $block = range($matches[1], $matches[2]); - - if (count($block) > self::PORT_RANGE_LIMIT) { - throw new TooManyPortsInRangeException(); - } - - if ((int) $matches[1] < self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) { - throw new PortOutOfRangeException(); - } - - foreach ($block as $unit) { - $insertData[] = [ - 'node_id' => $node->id, - 'ip' => $ip->__toString(), - 'port' => (int) $unit, - 'ip_alias' => array_get($data, 'allocation_alias'), - 'server_id' => $server->id ?? null, - ]; - } - } else { - if ((int) $port < self::PORT_FLOOR || (int) $port > self::PORT_CEIL) { - throw new PortOutOfRangeException(); - } - - $insertData[] = [ - 'node_id' => $node->id, - 'ip' => $ip->__toString(), - 'port' => (int) $port, - 'ip_alias' => array_get($data, 'allocation_alias'), - 'server_id' => $server->id ?? null, - ]; - } - - foreach ($insertData as $insert) { - $allocation = Allocation::query()->create($insert); - $ids[] = $allocation->id; - } - } - } - - $this->connection->commit(); - - return $ids; - } -} diff --git a/app/Services/Allocations/FindAssignableAllocationService.php b/app/Services/Allocations/FindAssignableAllocationService.php index 5c62b7539e..dedfc65555 100644 --- a/app/Services/Allocations/FindAssignableAllocationService.php +++ b/app/Services/Allocations/FindAssignableAllocationService.php @@ -2,110 +2,48 @@ namespace App\Services\Allocations; +use App\Models\Objects\Endpoint; +use Illuminate\Support\Collection; use Webmozart\Assert\Assert; use App\Models\Server; -use App\Models\Allocation; -use App\Exceptions\Service\Allocation\AutoAllocationNotEnabledException; -use App\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException; class FindAssignableAllocationService { - /** - * FindAssignableAllocationService constructor. - */ - public function __construct(private AssignmentService $service) + public function __construct() { } - /** - * Finds an existing unassigned allocation and attempts to assign it to the given server. If - * no allocation can be found, a new one will be created with a random port between the defined - * range from the configuration. - * - * @throws \App\Exceptions\DisplayException - * @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException - * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException - */ - public function handle(Server $server): Allocation + public function handle(Server $server): int { - if (!config('panel.client_features.allocations.enabled')) { - throw new AutoAllocationNotEnabledException(); - } - - // Attempt to find a given available allocation for a server. If one cannot be found - // we will fall back to attempting to create a new allocation that can be used for the - // server. - /** @var \App\Models\Allocation|null $allocation */ - $allocation = $server->node->allocations() - ->where('ip', $server->allocation->ip) - ->whereNull('server_id') - ->inRandomOrder() - ->first(); - - $allocation = $allocation ?? $this->createNewAllocation($server); + abort_unless(config('panel.client_features.allocations.enabled'), 403, 'Auto Allocation is not enabled'); - $allocation->update(['server_id' => $server->id]); - - return $allocation->refresh(); + return $this->createNewAllocation($server); } /** * Create a new allocation on the server's node with a random port from the defined range * in the settings. If there are no matches in that range, or something is wrong with the * range information provided an exception will be raised. - * - * @throws \App\Exceptions\DisplayException - * @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException - * @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException - * @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException */ - protected function createNewAllocation(Server $server): Allocation + protected function createNewAllocation(Server $server): int { - $start = config('panel.client_features.allocations.range_start', null); - $end = config('panel.client_features.allocations.range_end', null); - - if (!$start || !$end) { - throw new NoAutoAllocationSpaceAvailableException(); - } + $start = config('panel.client_features.allocations.range_start'); + $end = config('panel.client_features.allocations.range_end'); Assert::integerish($start); Assert::integerish($end); - // Get all of the currently allocated ports for the node so that we can figure out - // which port might be available. - $ports = $server->node->allocations() - ->where('ip', $server->allocation->ip) - ->whereBetween('port', [$start, $end]) - ->pluck('port'); + $ports = $server->node->servers + ->reduce(fn (Collection $result, $value) => $result->merge($value), collect()) + ->map(fn (Endpoint $endpoint) => $endpoint->port) + ->filter(fn (int $port): bool => $port >= $start && $port <= $end); // Compute the difference of the range and the currently created ports, finding // any port that does not already exist in the database. We will then use this // array of ports to create a new allocation to assign to the server. $available = array_diff(range($start, $end), $ports->toArray()); - // If we've already allocated all of the ports, just abort. - if (empty($available)) { - throw new NoAutoAllocationSpaceAvailableException(); - } - // Pick a random port out of the remaining available ports. - /** @var int $port */ - $port = $available[array_rand($available)]; - - $this->service->handle($server->node, [ - 'allocation_ip' => $server->allocation->ip, - 'allocation_ports' => [$port], - ]); - - /** @var \App\Models\Allocation $allocation */ - $allocation = $server->node->allocations() - ->where('ip', $server->allocation->ip) - ->where('port', $port) - ->firstOrFail(); - - return $allocation; + return $available[array_rand($available)]; } } diff --git a/app/Services/Deployment/AllocationSelectionService.php b/app/Services/Deployment/AllocationSelectionService.php deleted file mode 100644 index 3868d0b383..0000000000 --- a/app/Services/Deployment/AllocationSelectionService.php +++ /dev/null @@ -1,150 +0,0 @@ -dedicated = $dedicated; - - return $this; - } - - /** - * A list of node IDs that should be used when selecting an allocation. If empty, all - * nodes will be used to filter with. - */ - public function setNodes(array $nodes): self - { - $this->nodes = $nodes; - - return $this; - } - - /** - * An array of individual ports or port ranges to use when selecting an allocation. If - * empty, all ports will be considered when finding an allocation. If set, only ports appearing - * in the array or range will be used. - * - * @throws \App\Exceptions\DisplayException - */ - public function setPorts(array $ports): self - { - $stored = []; - foreach ($ports as $port) { - if (is_digit($port)) { - $stored[] = $port; - } - - // Ranges are stored in the ports array as an array which can be - // better processed in the repository. - if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) { - if (abs((int) $matches[2] - (int) $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) { - throw new DisplayException(trans('exceptions.allocations.too_many_ports')); - } - - $stored[] = [$matches[1], $matches[2]]; - } - } - - $this->ports = $stored; - - return $this; - } - - /** - * Return a single allocation that should be used as the default allocation for a server. - * - * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException - */ - public function handle(): Allocation - { - $allocation = $this->getRandomAllocation($this->nodes, $this->ports, $this->dedicated); - - if (is_null($allocation)) { - throw new NoViableAllocationException(trans('exceptions.deployment.no_viable_allocations')); - } - - return $allocation; - } - - /** - * Return a single allocation from those meeting the requirements. - */ - private function getRandomAllocation(array $nodes = [], array $ports = [], bool $dedicated = false): ?Allocation - { - $query = Allocation::query() - ->whereNull('server_id') - ->whereIn('node_id', $nodes); - - if (!empty($ports)) { - $query->where(function ($inner) use ($ports) { - $whereIn = []; - foreach ($ports as $port) { - if (is_array($port)) { - $inner->orWhereBetween('port', $port); - - continue; - } - - $whereIn[] = $port; - } - - if (!empty($whereIn)) { - $inner->orWhereIn('port', $whereIn); - } - }); - } - - // If this allocation should not be shared with any other servers get - // the data and modify the query as necessary, - if ($dedicated) { - $discard = $this->getDiscardableDedicatedAllocations($nodes); - - if (!empty($discard)) { - $query->whereNotIn('ip', $discard); - } - } - - return $query->inRandomOrder()->first(); - } - - /** - * Return a result set of node ips that already have at least one - * server assigned to that IP. This allows for filtering out sets for - * dedicated allocation IPs. - * - * If an array of nodes is passed the results will be limited to allocations - * in those nodes. - */ - private function getDiscardableDedicatedAllocations(array $nodes = []): array - { - $query = Allocation::query()->whereNotNull('server_id'); - - if (!empty($nodes)) { - $query->whereIn('node_id', $nodes); - } - - return $query->groupBy('ip') - ->get() - ->pluck('ip') - ->toArray(); - } -} diff --git a/app/Services/Deployment/FindViableNodesService.php b/app/Services/Deployment/FindViableNodesService.php index d95c08c640..79dc624bec 100644 --- a/app/Services/Deployment/FindViableNodesService.php +++ b/app/Services/Deployment/FindViableNodesService.php @@ -8,8 +8,7 @@ class FindViableNodesService { /** - * Returns a collection of nodes that meet the provided requirements and can then - * be passed to the AllocationSelectionService to return a single allocation. + * Returns a collection of nodes that meet the provided requirements * * This functionality is used for automatic deployments of servers and will * attempt to find all nodes in the defined locations that meet the memory, disk diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index f7c7403249..3b74af0657 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -4,9 +4,7 @@ use Illuminate\Support\Arr; use App\Models\Server; -use App\Models\Allocation; use Illuminate\Database\ConnectionInterface; -use App\Exceptions\DisplayException; use App\Repositories\Daemon\DaemonServerRepository; use App\Exceptions\Http\Connection\DaemonConnectionException; @@ -32,20 +30,12 @@ public function handle(Server $server, array $data): Server { /** @var \App\Models\Server $server */ $server = $this->connection->transaction(function () use ($server, $data) { - $this->processAllocations($server, $data); - - if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { - $existingAllocation = $server->allocations()->findOrFail($data['allocation_id']); - - throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.')); - } - if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) { $data['oom_killer'] = !$data['oom_disabled']; } // If any of these values are passed through in the data array go ahead and set them correctly on the server model. - $merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']); + $merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'ports']); $server->forceFill(array_merge($merge, [ 'database_limit' => Arr::get($data, 'database_limit', 0) ?? null, @@ -72,59 +62,4 @@ public function handle(Server $server, array $data): Server return $server; } - - /** - * Process the allocations being assigned in the data and ensure they are available for a server. - * - * @throws \App\Exceptions\DisplayException - */ - private function processAllocations(Server $server, array &$data): void - { - if (empty($data['add_allocations']) && empty($data['remove_allocations'])) { - return; - } - - // Handle the addition of allocations to this server. Only assign allocations that are not currently - // assigned to a different server, and only allocations on the same node as the server. - if (!empty($data['add_allocations'])) { - $query = Allocation::query() - ->where('node_id', $server->node_id) - ->whereIn('id', $data['add_allocations']) - ->whereNull('server_id'); - - // Keep track of all the allocations we're just now adding so that we can use the first - // one to reset the default allocation to. - $freshlyAllocated = $query->first()?->id; - - $query->update(['server_id' => $server->id, 'notes' => null]); - } - - if (!empty($data['remove_allocations'])) { - foreach ($data['remove_allocations'] as $allocation) { - // If we are attempting to remove the default allocation for the server, see if we can reassign - // to the first provided value in add_allocations. If there is no new first allocation then we - // will throw an exception back. - if ($allocation === ($data['allocation_id'] ?? $server->allocation_id)) { - if (empty($freshlyAllocated)) { - throw new DisplayException('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'); - } - - // Update the default allocation to be the first allocation that we are creating. - $data['allocation_id'] = $freshlyAllocated; - } - } - - // Remove any of the allocations we got that are currently assigned to this server on - // this node. Also set the notes to null, otherwise when re-allocated to a new server those - // notes will be carried over. - Allocation::query()->where('node_id', $server->node_id) - ->where('server_id', $server->id) - // Only remove the allocations that we didn't also attempt to add to the server... - ->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? [])) - ->update([ - 'notes' => null, - 'server_id' => null, - ]); - } - } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index 7ac714ed51..ca9e5455d6 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -17,8 +17,8 @@ public function __construct(private EnvironmentService $environment) /** * Return a configuration array for a specific server when passed a server model. * - * DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for the new daemon - * daemon, if you modify the structure eggs will break unexpectedly. + * DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for wings + * if you modify the structure eggs will break unexpectedly. */ public function handle(Server $server, array $override = []): array { @@ -66,10 +66,10 @@ protected function returnFormat(Server $server): array 'allocations' => [ 'force_outgoing_ip' => $server->egg->force_outgoing_ip, 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, + 'ip' => $server->getPrimaryEndpoint()?->ip, + 'port' => $server->getPrimaryEndpoint()?->port, ], - 'mappings' => $server->getAllocationMappings(), + 'mappings' => $server->getPortMappings(), ], 'egg' => [ 'id' => $server->egg->uuid, diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 18eb790e53..af9e06d8d1 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -10,85 +10,54 @@ use Webmozart\Assert\Assert; use App\Models\Server; use Illuminate\Support\Collection; -use App\Models\Allocation; use Illuminate\Database\ConnectionInterface; use App\Models\Objects\DeploymentObject; use App\Repositories\Daemon\DaemonServerRepository; -use App\Services\Deployment\FindViableNodesService; -use App\Services\Deployment\AllocationSelectionService; use App\Exceptions\Http\Connection\DaemonConnectionException; use App\Models\Egg; class ServerCreationService { - /** - * ServerCreationService constructor. - */ public function __construct( - private AllocationSelectionService $allocationSelectionService, private ConnectionInterface $connection, private DaemonServerRepository $daemonServerRepository, - private FindViableNodesService $findViableNodesService, private ServerDeletionService $serverDeletionService, private VariableValidatorService $validatorService ) { } /** - * Create a server on the Panel and trigger a request to the Daemon to begin the server - * creation process. This function will attempt to set as many additional values - * as possible given the input data. For example, if an allocation_id is passed with - * no node_id the node_is will be picked from the allocation. - * - * @throws \Throwable - * @throws \App\Exceptions\DisplayException - * @throws \Illuminate\Validation\ValidationException - * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException + * Create a server on the Panel and trigger a request to the Daemon to begin the server creation process. + * This function will attempt to set as many additional values as possible given the input data. */ - public function handle(array $data, ?DeploymentObject $deployment = null): Server + public function handle(array $data, ?DeploymentObject $deployment = null, bool $validateVariables = true): Server { if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) { $data['oom_killer'] = !$data['oom_disabled']; } - /** @var Egg $egg */ $egg = Egg::query()->findOrFail($data['egg_id']); // Fill missing fields from egg $data['image'] = $data['image'] ?? collect($egg->docker_images)->first(); $data['startup'] = $data['startup'] ?? $egg->startup; - // If a deployment object has been passed we need to get the allocation - // that the server should use, and assign the node from that allocation. - if ($deployment instanceof DeploymentObject) { - $allocation = $this->configureDeployment($data, $deployment); - $data['allocation_id'] = $allocation->id; - $data['node_id'] = $allocation->node_id; - } - - // Auto-configure the node based on the selected allocation - // if no node was defined. - if (empty($data['node_id'])) { - Assert::false(empty($data['allocation_id']), 'Expected a non-empty allocation_id in server creation data.'); - - $data['node_id'] = Allocation::query()->findOrFail($data['allocation_id'])->node_id; - } + Assert::false(empty($data['node_id'])); $eggVariableData = $this->validatorService ->setUserLevel(User::USER_LEVEL_ADMIN) - ->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', [])); + ->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []), $validateVariables); // Due to the design of the Daemon, we need to persist this server to the disk // before we can actually create it on the Daemon. // // If that connection fails out we will attempt to perform a cleanup by just // deleting the server itself from the system. - /** @var \App\Models\Server $server */ + /** @var Server $server */ $server = $this->connection->transaction(function () use ($data, $eggVariableData) { // Create the server and assign any additional allocations to it. $server = $this->createModel($data); - $this->storeAssignedAllocations($server, $data); $this->storeEggVariables($server, $eggVariableData); return $server; @@ -96,7 +65,7 @@ public function handle(array $data, ?DeploymentObject $deployment = null): Serve try { $this->daemonServerRepository->setServer($server)->create( - Arr::get($data, 'start_on_completion', false) ?? false + Arr::get($data, 'start_on_completion', true) ?? true, ); } catch (DaemonConnectionException $exception) { $this->serverDeletionService->withForce()->handle($server); @@ -107,28 +76,6 @@ public function handle(array $data, ?DeploymentObject $deployment = null): Serve return $server; } - /** - * Gets an allocation to use for automatic deployment. - * - * @throws \App\Exceptions\DisplayException - * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException - */ - private function configureDeployment(array $data, DeploymentObject $deployment): Allocation - { - /** @var Collection<\App\Models\Node> $nodes */ - $nodes = $this->findViableNodesService->handle( - Arr::get($data, 'memory', 0), - Arr::get($data, 'disk', 0), - Arr::get($data, 'cpu', 0), - Arr::get($data, 'tags', []), - ); - - return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) - ->setNodes($nodes->pluck('id')->toArray()) - ->setPorts($deployment->getPorts()) - ->handle(); - } - /** * Store the server in the database and return the model. * @@ -155,7 +102,7 @@ private function createModel(array $data): Server 'cpu' => Arr::get($data, 'cpu'), 'threads' => Arr::get($data, 'threads'), 'oom_killer' => Arr::get($data, 'oom_killer') ?? false, - 'allocation_id' => Arr::get($data, 'allocation_id'), + 'ports' => Arr::get($data, 'ports') ?? [], 'egg_id' => Arr::get($data, 'egg_id'), 'startup' => Arr::get($data, 'startup'), 'image' => Arr::get($data, 'image'), @@ -166,21 +113,6 @@ private function createModel(array $data): Server ]); } - /** - * Configure the allocations assigned to this server. - */ - private function storeAssignedAllocations(Server $server, array $data): void - { - $records = [$data['allocation_id']]; - if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) { - $records = array_merge($records, $data['allocation_additional']); - } - - Allocation::query()->whereIn('id', $records)->update([ - 'server_id' => $server->id, - ]); - } - /** * Process environment variables passed for this server and store them in the database. */ diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index 97e6995c77..71e9ee19ee 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -77,8 +77,6 @@ public function handle(Server $server): void } } - $server->allocations()->update(['server_id' => null]); - $server->delete(); }); } diff --git a/app/Services/Servers/StartupCommandService.php b/app/Services/Servers/StartupCommandService.php index 4703eb0753..f75e613fc6 100644 --- a/app/Services/Servers/StartupCommandService.php +++ b/app/Services/Servers/StartupCommandService.php @@ -2,6 +2,7 @@ namespace App\Services\Servers; +use App\Models\Objects\Endpoint; use App\Models\Server; class StartupCommandService @@ -11,8 +12,10 @@ class StartupCommandService */ public function handle(Server $server, bool $hideAllValues = false): string { + $endpoint = $server->getPrimaryEndpoint(); + $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; - $replace = [$server->memory, $server->allocation->ip, $server->allocation->port]; + $replace = [$server->memory, $endpoint->ip ?? Endpoint::INADDR_ANY, $endpoint->port ?? '']; foreach ($server->variables as $variable) { $find[] = '{{' . $variable->env_variable . '}}'; diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index 8a7bcd3ea1..c948f3b9c5 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -3,7 +3,6 @@ namespace App\Services\Servers; use App\Exceptions\Http\Connection\DaemonConnectionException; -use App\Models\Allocation; use App\Models\Node; use App\Models\Server; use App\Models\ServerTransfer; @@ -52,8 +51,6 @@ private function notify(Server $server, Plain $token): void public function handle(Server $server, array $data): bool { $node_id = $data['node_id']; - $allocation_id = intval($data['allocation_id']); - $additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []); // Check if the node is viable for the transfer. $node = Node::query() @@ -71,23 +68,15 @@ public function handle(Server $server, array $data): bool $server->validateTransferState(); - $this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) { - // Create a new ServerTransfer entry. + $this->connection->transaction(function () use ($server, $node_id) { $transfer = new ServerTransfer(); $transfer->server_id = $server->id; $transfer->old_node = $server->node_id; $transfer->new_node = $node_id; - $transfer->old_allocation = $server->allocation_id; - $transfer->new_allocation = $allocation_id; - $transfer->old_additional_allocations = $server->allocations->where('id', '!=', $server->allocation_id)->pluck('id')->all(); - $transfer->new_additional_allocations = $additional_allocations; $transfer->save(); - // Add the allocations to the server, so they cannot be automatically assigned while the transfer is in progress. - $this->assignAllocationsToServer($server, $node_id, $allocation_id, $additional_allocations); - // Generate a token for the destination node that the source node can use to authenticate with. $token = $this->nodeJWTService ->setExpiresAt(CarbonImmutable::now()->addMinutes(15)) @@ -102,32 +91,4 @@ public function handle(Server $server, array $data): bool return true; } - - /** - * Assigns the specified allocations to the specified server. - */ - private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations): void - { - $allocations = $additional_allocations; - $allocations[] = $allocation_id; - - $node = Node::query()->findOrFail($node_id); - $unassigned = $node->allocations() - ->whereNull('server_id') - ->pluck('id') - ->toArray(); - - $updateIds = []; - foreach ($allocations as $allocation) { - if (!in_array($allocation, $unassigned)) { - continue; - } - - $updateIds[] = $allocation; - } - - if (!empty($updateIds)) { - Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]); - } - } } diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 75f4a59b14..f8c59b6955 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -25,7 +25,7 @@ public function __construct(private ValidationFactory $validator) * * @throws \Illuminate\Validation\ValidationException */ - public function handle(int $egg, array $fields = []): Collection + public function handle(int $egg, array $fields = [], bool $validate = true): Collection { $query = EggVariable::query()->where('egg_id', $egg); if (!$this->isUserLevel(User::USER_LEVEL_ADMIN)) { @@ -44,9 +44,11 @@ public function handle(int $egg, array $fields = []): Collection $customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]); } - $validator = $this->validator->make($data, $rules, [], $customAttributes); - if ($validator->fails()) { - throw new ValidationException($validator); + if ($validate) { + $validator = $this->validator->make($data, $rules, [], $customAttributes); + if ($validator->fails()) { + throw new ValidationException($validator); + } } return Collection::make($variables)->map(function ($item) use ($fields) { diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php deleted file mode 100644 index 7f5bf88270..0000000000 --- a/app/Transformers/Api/Application/AllocationTransformer.php +++ /dev/null @@ -1,77 +0,0 @@ - $allocation->id, - 'ip' => $allocation->ip, - 'alias' => $allocation->ip_alias, - 'port' => $allocation->port, - 'notes' => $allocation->notes, - 'assigned' => !is_null($allocation->server_id), - ]; - } - - /** - * Load the node relationship onto a given transformation. - * - * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeNode(Allocation $allocation): Item|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_NODES)) { - return $this->null(); - } - - return $this->item( - $allocation->node, - $this->makeTransformer(NodeTransformer::class), - Node::RESOURCE_NAME - ); - } - - /** - * Load the server relationship onto a given transformation. - * - * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeServer(Allocation $allocation): Item|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_SERVERS) || !$allocation->server) { - return $this->null(); - } - - return $this->item( - $allocation->server, - $this->makeTransformer(ServerTransformer::class), - Server::RESOURCE_NAME - ); - } -} diff --git a/app/Transformers/Api/Application/NodeTransformer.php b/app/Transformers/Api/Application/NodeTransformer.php index 670e4bcac5..dabf8c40df 100644 --- a/app/Transformers/Api/Application/NodeTransformer.php +++ b/app/Transformers/Api/Application/NodeTransformer.php @@ -12,7 +12,7 @@ class NodeTransformer extends BaseTransformer /** * List of resources that can be included. */ - protected array $availableIncludes = ['allocations', 'servers']; + protected array $availableIncludes = ['servers']; /** * Return the resource name for the JSONAPI output. @@ -45,26 +45,6 @@ public function transform(Node $node): array return $response; } - /** - * Return the nodes associated with this location. - * - * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeAllocations(Node $node): Collection|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { - return $this->null(); - } - - $node->loadMissing('allocations'); - - return $this->collection( - $node->getRelation('allocations'), - $this->makeTransformer(AllocationTransformer::class), - 'allocation' - ); - } - /** * Return the nodes associated with this location. * diff --git a/app/Transformers/Api/Application/ServerTransformer.php b/app/Transformers/Api/Application/ServerTransformer.php index b81baf2794..378cea65f9 100644 --- a/app/Transformers/Api/Application/ServerTransformer.php +++ b/app/Transformers/Api/Application/ServerTransformer.php @@ -17,7 +17,6 @@ class ServerTransformer extends BaseTransformer * List of resources that can be included. */ protected array $availableIncludes = [ - 'allocations', 'user', 'subusers', 'egg', @@ -76,7 +75,6 @@ public function transform(Server $server): array ], 'user' => $server->owner_id, 'node' => $server->node_id, - 'allocation' => $server->allocation_id, 'egg' => $server->egg_id, 'container' => [ 'startup_command' => $server->startup, @@ -87,23 +85,23 @@ public function transform(Server $server): array ], $server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at), $server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at), - ]; - } - - /** - * Return a generic array of allocations for this server. - * - * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeAllocations(Server $server): Collection|NullResource - { - if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) { - return $this->null(); - } - - $server->loadMissing('allocations'); - return $this->collection($server->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation'); + 'allocations' => collect($server->ports)->map(function ($port) { + $ip = '0.0.0.0'; + if (str_contains($port, ':')) { + [$ip, $port] = explode(':', $port); + } + + return [ + 'id' => random_int(1, PHP_INT_MAX), + 'ip' => $ip, + 'alias' => null, + 'port' => (int) $port, + 'notes' => null, + 'assigned' => false, + ]; + })->all(), + ]; } /** diff --git a/app/Transformers/Api/Client/AllocationTransformer.php b/app/Transformers/Api/Client/AllocationTransformer.php deleted file mode 100644 index 4fd84ed61d..0000000000 --- a/app/Transformers/Api/Client/AllocationTransformer.php +++ /dev/null @@ -1,28 +0,0 @@ - $model->id, - 'ip' => $model->ip, - 'ip_alias' => $model->ip_alias, - 'port' => $model->port, - 'notes' => $model->notes, - 'is_default' => $model->server->allocation_id === $model->id, - ]; - } -} diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index df8a36bb9b..2300b30aed 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -2,21 +2,20 @@ namespace App\Transformers\Api\Client; -use App\Models\Allocation; use App\Models\Egg; use App\Models\EggVariable; use App\Models\Permission; use App\Models\Server; use App\Models\Subuser; -use App\Services\Servers\StartupCommandService; -use Illuminate\Container\Container; use League\Fractal\Resource\Collection; -use League\Fractal\Resource\Item; use League\Fractal\Resource\NullResource; +use League\Fractal\Resource\Item; +use Illuminate\Container\Container; +use App\Services\Servers\StartupCommandService; class ServerTransformer extends BaseClientTransformer { - protected array $defaultIncludes = ['allocations', 'variables']; + protected array $defaultIncludes = ['variables']; protected array $availableIncludes = ['egg', 'subusers']; @@ -75,6 +74,7 @@ public function transform(Server $server): array // This field is deprecated, please use "status". 'is_installing' => !$server->isInstalled(), 'is_transferring' => !is_null($server->transfer), + 'ports' => $user->can(Permission::ACTION_ALLOCATION_READ, $server) ? $server->ports : collect(), ]; if (!config('panel.editable_server_descriptions')) { @@ -84,33 +84,6 @@ public function transform(Server $server): array return $data; } - /** - * Returns the allocations associated with this server. - * - * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException - */ - public function includeAllocations(Server $server): Collection - { - $transformer = $this->makeTransformer(AllocationTransformer::class); - - $user = $this->request->user(); - // While we include this permission, we do need to actually handle it slightly different here - // for the purpose of keeping things functionally working. If the user doesn't have read permissions - // for the allocations we'll only return the primary server allocation, and any notes associated - // with it will be hidden. - // - // This allows us to avoid too much permission regression, without also hiding information that - // is generally needed for the frontend to make sense when browsing or searching results. - if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) { - $primary = clone $server->allocation; - $primary->notes = null; - - return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME); - } - - return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME); - } - /** * @throws \App\Exceptions\Transformer\InvalidTransformerLevelException */ diff --git a/composer.json b/composer.json index 6b44652ffc..7e6394c079 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ "filament/filament": "^3.2", "guzzlehttp/guzzle": "^7.8.1", "laracasts/utilities": "~3.2.2", - "laravel/framework": "^11.7", + "laravel/framework": "^11.28.1", "laravel/helpers": "^1.7", "laravel/sanctum": "^4.0.2", "laravel/socialite": "^5.14", diff --git a/composer.lock b/composer.lock index ca8c6f54ab..da1c856972 100644 --- a/composer.lock +++ b/composer.lock @@ -2802,16 +2802,16 @@ }, { "name": "laravel/framework", - "version": "v11.10.0", + "version": "v11.28.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "99b4255194912044b75ab72329f8c19e6345720e" + "reference": "3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/99b4255194912044b75ab72329f8c19e6345720e", - "reference": "99b4255194912044b75ab72329f8c19e6345720e", + "url": "https://api.github.com/repos/laravel/framework/zipball/3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c", + "reference": "3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c", "shasum": "" }, "require": { @@ -2830,7 +2830,7 @@ "fruitcake/php-cors": "^1.3", "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.18", + "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", @@ -2864,6 +2864,7 @@ }, "provide": { "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", "psr/simple-cache-implementation": "1.0|2.0|3.0" }, "replace": { @@ -2872,6 +2873,7 @@ "illuminate/bus": "self.version", "illuminate/cache": "self.version", "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", "illuminate/conditionable": "self.version", "illuminate/config": "self.version", "illuminate/console": "self.version", @@ -2914,9 +2916,9 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.0.15", + "orchestra/testbench-core": "^9.5", "pda/pheanstalk": "^5.0", - "phpstan/phpstan": "^1.4.7", + "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", "predis/predis": "^2.0.2", "resend/resend-php": "^0.10.0", @@ -2972,6 +2974,8 @@ "src/Illuminate/Events/functions.php", "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", "src/Illuminate/Support/helpers.php" ], "psr-4": { @@ -3003,7 +3007,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-06-04T13:45:55+00:00" + "time": "2024-10-16T16:32:21+00:00" }, { "name": "laravel/helpers", diff --git a/config/panel.php b/config/panel.php index 487ee354b5..4ff4ee637a 100644 --- a/config/panel.php +++ b/config/panel.php @@ -166,5 +166,7 @@ 'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true), + 'default_io_weight' => env('PANEL_IO_WEIGHT', 500), + 'editable_server_descriptions' => env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', true), ]; diff --git a/database/Factories/AllocationFactory.php b/database/Factories/AllocationFactory.php deleted file mode 100644 index 19405b7024..0000000000 --- a/database/Factories/AllocationFactory.php +++ /dev/null @@ -1,36 +0,0 @@ - $this->faker->unique()->ipv4(), - 'port' => $this->faker->unique()->numberBetween(1024, 65535), - ]; - } - - /** - * Attaches the allocation to a specific server model. - */ - public function forServer(Server $server): self - { - return $this->for($server)->for($server->node); - } -} diff --git a/database/Seeders/eggs/minecraft/egg-bungeecord.json b/database/Seeders/eggs/minecraft/egg-bungeecord.json index e71b8d1ec1..66fcbc885f 100644 --- a/database/Seeders/eggs/minecraft/egg-bungeecord.json +++ b/database/Seeders/eggs/minecraft/egg-bungeecord.json @@ -65,6 +65,17 @@ ], "sort": 2, "field_type": "text" + }, + { + "name": "Server Port", + "description": "", + "env_variable": "SERVER_PORT", + "default_value": "25565", + "user_viewable": true, + "user_editable": false, + "rules": "required|port", + "sort": 3, + "field_type": "text" } ] } diff --git a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json index 13afe9748a..cd7560a12a 100644 --- a/database/Seeders/eggs/minecraft/egg-forge-minecraft.json +++ b/database/Seeders/eggs/minecraft/egg-forge-minecraft.json @@ -94,6 +94,17 @@ ], "sort": 4, "field_type": "text" + }, + { + "name": "Server Port", + "description": "", + "env_variable": "SERVER_PORT", + "default_value": "25565", + "user_viewable": true, + "user_editable": false, + "rules": "required|port", + "sort": 5, + "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json b/database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json similarity index 100% rename from database/Seeders/eggs/minecraft/egg-sponge--sponge-vanilla.json rename to database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json diff --git a/database/Seeders/eggs/rust/egg-rust.json b/database/Seeders/eggs/rust/egg-rust.json index e74368604a..cd0367c3f0 100644 --- a/database/Seeders/eggs/rust/egg-rust.json +++ b/database/Seeders/eggs/rust/egg-rust.json @@ -168,7 +168,7 @@ "user_editable": false, "rules": [ "required", - "integer" + "port" ], "sort": 10, "field_type": "text" @@ -182,7 +182,7 @@ "user_editable": false, "rules": [ "required", - "integer" + "port" ], "sort": 11, "field_type": "text" @@ -239,7 +239,7 @@ "user_editable": false, "rules": [ "required", - "integer" + "port" ], "sort": 15, "field_type": "text" @@ -288,4 +288,4 @@ "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json b/database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json similarity index 100% rename from database/Seeders/eggs/source-engine/egg-counter--strike--global-offensive.json rename to database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json diff --git a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json index 0543a16c04..d470ca3f20 100644 --- a/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json +++ b/database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json @@ -53,8 +53,7 @@ "user_editable": false, "rules": [ "required", - "integer", - "between:1025,65535" + "port" ], "sort": 2, "field_type": "text" @@ -69,7 +68,7 @@ "rules": [ "required", "integer", - "between:1025,65535" + "port" ], "sort": 3, "field_type": "text" @@ -96,10 +95,10 @@ "default_value": "10022", "user_viewable": true, "user_editable": false, + "rules": "required|port", "rules": [ "required", - "integer", - "between:1025,65535" + "port" ], "sort": 5, "field_type": "text" @@ -113,11 +112,10 @@ "user_editable": false, "rules": [ "required", - "integer", - "between:1025,65535" + "port", ], "sort": 6, "field_type": "text" } ] -} \ No newline at end of file +} diff --git a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php index ca2cabfd67..2c16118d42 100644 --- a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php +++ b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php @@ -31,6 +31,9 @@ public function up(): void $table->dropIndex('permissions_server_id_foreign'); $table->dropForeign('permissions_user_id_foreign'); $table->dropIndex('permissions_user_id_foreign'); + } else { + $table->dropForeign(['server_id']); + $table->dropForeign(['user_id']); } $table->dropColumn('server_id'); diff --git a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php index cc2695eddd..39f048b373 100644 --- a/database/migrations/2020_09_13_110007_drop_packs_from_servers.php +++ b/database/migrations/2020_09_13_110007_drop_packs_from_servers.php @@ -12,10 +12,7 @@ public function up(): void { Schema::table('servers', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign(['pack_id']); - } - + $table->dropForeign(['pack_id']); $table->dropColumn('pack_id'); }); } diff --git a/database/migrations/2024_03_12_154408_remove_nests_table.php b/database/migrations/2024_03_12_154408_remove_nests_table.php index cc95a5f8e1..42d1315b91 100644 --- a/database/migrations/2024_03_12_154408_remove_nests_table.php +++ b/database/migrations/2024_03_12_154408_remove_nests_table.php @@ -30,14 +30,15 @@ public function up(): void Schema::table('eggs', function (Blueprint $table) { if (Schema::getConnection()->getDriverName() !== 'sqlite') { $table->dropForeign('service_options_nest_id_foreign'); + } else { + $table->dropForeign(['nest_id']); } + $table->dropColumn('nest_id'); }); Schema::table('servers', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('servers_nest_id_foreign'); - } + $table->dropForeign(['nest_id']); $table->dropColumn('nest_id'); }); diff --git a/database/migrations/2024_03_14_055537_remove_locations_table.php b/database/migrations/2024_03_14_055537_remove_locations_table.php index f06ded66fd..bdd0c382ad 100644 --- a/database/migrations/2024_03_14_055537_remove_locations_table.php +++ b/database/migrations/2024_03_14_055537_remove_locations_table.php @@ -27,10 +27,7 @@ public function up(): void } Schema::table('nodes', function (Blueprint $table) { - if (Schema::getConnection()->getDriverName() !== 'sqlite') { - $table->dropForeign('nodes_location_id_foreign'); - } - + $table->dropForeign(['location_id']); $table->dropColumn('location_id'); }); diff --git a/database/migrations/2024_09_18_043350_modify_allocations.php b/database/migrations/2024_09_18_043350_modify_allocations.php new file mode 100644 index 0000000000..92e59495e3 --- /dev/null +++ b/database/migrations/2024_09_18_043350_modify_allocations.php @@ -0,0 +1,66 @@ +dropColumn(['old_allocation', 'new_allocation', 'old_additional_allocations', 'new_additional_allocations']); + }); + + Schema::table('servers', function (Blueprint $table) { + $table->json('ports')->nullable(); + }); + + $portMappings = []; + foreach (DB::table('allocations')->get() as $allocation) { + $portMappings[$allocation->server_id][] = "$allocation->ip:$allocation->port"; + } + + foreach ($portMappings as $serverId => $ports) { + /** @var Server $server */ + $server = Server::find($serverId); + if (!$server) { + // Orphaned Allocations + + continue; + } + + foreach ($ports as $port) { + $server->ports ??= collect(); + $server->ports->add(new Endpoint($port)); + } + $server->save(); + } + + Schema::table('servers', function (Blueprint $table) { + $table->dropForeign(['allocation_id']); + $table->dropUnique(['allocation_id']); + $table->dropColumn(['allocation_id']); + }); + + Schema::dropIfExists('allocations'); + + Schema::table('nodes', function (Blueprint $table) { + $table->boolean('strict_ports')->default(true); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Too much time to ensure this works correctly, please take a backup if necessary + } +}; diff --git a/resources/scripts/components/server/network/AllocationRow.tsx b/resources/scripts/components/server/network/AllocationRow.tsx deleted file mode 100644 index e68bc3359f..0000000000 --- a/resources/scripts/components/server/network/AllocationRow.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React, { memo, useCallback, useState } from 'react'; -import isEqual from 'react-fast-compare'; -import tw from 'twin.macro'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faNetworkWired } from '@fortawesome/free-solid-svg-icons'; -import InputSpinner from '@/components/elements/InputSpinner'; -import { Textarea } from '@/components/elements/Input'; -import Can from '@/components/elements/Can'; -import { Button } from '@/components/elements/button/index'; -import GreyRowBox from '@/components/elements/GreyRowBox'; -import { Allocation } from '@/api/server/getServer'; -import styled from 'styled-components/macro'; -import { debounce } from 'debounce'; -import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes'; -import { useFlashKey } from '@/plugins/useFlash'; -import { ServerContext } from '@/state/server'; -import CopyOnClick from '@/components/elements/CopyOnClick'; -import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton'; -import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation'; -import getServerAllocations from '@/api/swr/getServerAllocations'; -import { ip } from '@/lib/formatters'; -import Code from '@/components/elements/Code'; - -const Label = styled.label` - ${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`} -`; - -interface Props { - allocation: Allocation; -} - -const AllocationRow = ({ allocation }: Props) => { - const [loading, setLoading] = useState(false); - const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:network'); - const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid); - const { mutate } = getServerAllocations(); - - const onNotesChanged = useCallback((id: number, notes: string) => { - mutate((data) => data?.map((a) => (a.id === id ? { ...a, notes } : a)), false); - }, []); - - const setAllocationNotes = debounce((notes: string) => { - setLoading(true); - clearFlashes(); - - setServerAllocationNotes(uuid, allocation.id, notes) - .then(() => onNotesChanged(allocation.id, notes)) - .catch((error) => clearAndAddHttpError(error)) - .then(() => setLoading(false)); - }, 750); - - const setPrimaryAllocation = () => { - clearFlashes(); - mutate((data) => data?.map((a) => ({ ...a, isDefault: a.id === allocation.id })), false); - - setPrimaryServerAllocation(uuid, allocation.id).catch((error) => { - clearAndAddHttpError(error); - mutate(); - }); - }; - - return ( - -
-
- -
-
- {allocation.alias ? ( - - - {allocation.alias} - - - ) : ( - - {ip(allocation.ip)} - - )} - -
-
- {allocation.port} - -
-
-
- - -

A brief description of this server.

-
- -
-
- - -
-
- - - - - - -
-
-
- -
-

Allocation Management

-
- -
-
- - - -

The node which this server will be deployed to.

-
- -
- - -

The main allocation that will be assigned to this server.

-
- -
- - -

Additional allocations to assign to this server on creation.

-
-
-
-
-
- -
-
-
- -
-

Application Feature Limits

-
- -
-
- -
- -
-

The total number of databases a user is allowed to create for this server.

-
-
- -
- -
-

The total number of allocations a user is allowed to create for this server.

-
-
- -
- -
-

The total number of backups that can be created for this server.

-
-
-
-
-
-
-
-
-
-

Resource Management

-
- -
-
- - -
- - % -
- -

If you do not want to limit CPU usage, set the value to 0. To determine a value, take the number of threads and multiply it by 100. For example, on a quad core system without hyperthreading (4 * 100 = 400) there is 400% available. To limit a server to using half of a single thread, you would set the value to 50. To allow a server to use up to two threads, set the value to 200.

-

- -
- - -
- -
- -

Advanced: Enter the specific CPU threads that this process can run on, or leave blank to allow all threads. This can be a single number, or a comma separated list. Example: 0, 0-1,3, or 0,1,3,4.

-
-
- -
-
- - -
- - MiB -
- -

The maximum amount of memory allowed for this container. Setting this to 0 will allow unlimited memory in a container.

-
- -
- - -
- - MiB -
- -

Setting this to 0 will disable swap space on this server. Setting to -1 will allow unlimited swap.

-
-
- -
-
- - -
- - MiB -
- -

This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.

-
- -
- - -
- -
- -

Advanced: The IO performance of this server relative to other running containers on the system. Value should be between 10 and 1000. Please see this documentation for more information about it.

-
-
-
- - -
- -

Terminates the server if it breaches the memory limits. Enabling OOM killer may cause server processes to exit unexpectedly.

-
-
-
-
-
- -
-
-
-
-

Egg Configuration

-
- -
-
- - - -

Select the Egg that will define how this server should operate.

-
- -
-
- - -
- -

If the selected Egg has an install script attached to it, the script will run during the install. If you would like to skip this step, check this box.

-
-
-
-
- -
-
-
-

Docker Configuration

-
- -
-
- - - -

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.

-
-
-
-
-
- -
-
-
-
-

Startup Configuration

-
- -
-
- - -

The following data substitutes are available for the startup command: @{{SERVER_MEMORY}}, @{{SERVER_IP}}, and @{{SERVER_PORT}}. They will be replaced with the allocated memory, server IP, and server port respectively.

-
-
- -
-

Egg Variables

-
- -
- - -
-
-
- -@endsection - -@section('footer-scripts') - @parent - {!! Theme::js('vendor/lodash/lodash.js') !!} - - - - {!! Theme::js('js/admin/new-server.js?v=20220530') !!} - - -@endsection diff --git a/resources/views/admin/servers/view/manage.blade.php b/resources/views/admin/servers/view/manage.blade.php deleted file mode 100644 index eba2f8ab7b..0000000000 --- a/resources/views/admin/servers/view/manage.blade.php +++ /dev/null @@ -1,194 +0,0 @@ -@extends('layouts.admin') - -@section('title') - Server — {{ $server->name }}: Manage -@endsection - -@section('content-header') -

{{ $server->name }}Additional actions to control this server.

- -@endsection - -@section('content') - @include('admin.servers.partials.navigation') -
-
-
-
-

Reinstall Server

-
-
-

This will reinstall the server with the assigned egg scripts. Danger! This could overwrite server data.

-
- -
-
-
-
-
-

Install Status

-
-
-

If you need to change the install status from uninstalled to installed, or vice versa, you may do so with the button below.

-
- -
-
- - @if(! $server->isSuspended()) -
-
-
-

Suspend Server

-
-
-

This will suspend the server, stop any running processes, and immediately block the user from being able to access their files or otherwise manage the server through the panel or API.

-
- -
-
- @else -
-
-
-

Unsuspend Server

-
-
-

This will unsuspend the server and restore normal user access.

-
- -
-
- @endif - - @if(is_null($server->transfer)) -
-
-
-

Transfer Server

-
-
-

- Transfer this server to another node connected to this panel. - Warning! This feature has not been fully tested and may have bugs. -

-
- - -
-
- @else -
-
-
-

Transfer Server

-
-
-

- This server is currently being transferred to another node. - Transfer was initiated at {{ $server->transfer->created_at }} -

-
- - -
-
- @endif -
- - -@endsection - -@section('footer-scripts') - @parent - {!! Theme::js('vendor/lodash/lodash.js') !!} - - @if($canTransfer) - {!! Theme::js('js/admin/server/transfer.js') !!} - @endif -@endsection diff --git a/resources/views/livewire/node-system-information.blade.php b/resources/views/livewire/node-system-information.blade.php index 77436db06e..724e6282b9 100644 --- a/resources/views/livewire/node-system-information.blade.php +++ b/resources/views/livewire/node-system-information.blade.php @@ -1,8 +1,9 @@
- @switch($node->systemInformation()['version'] ?? 'false') + @switch($version = $node->systemInformation()['version'] ?? 'false') @case('false') true]) /> @@ -10,6 +11,7 @@ @default true]) @style([\Filament\Support\get_color_css_variables('success', shades: [400, 500], alias: 'tables::columns.icon-column.item') => true]) /> diff --git a/routes/admin.php b/routes/admin.php index 60d047e01f..ffac12d882 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -117,21 +117,14 @@ Route::get('/view/{node:id}', [Admin\Nodes\NodeViewController::class, 'index'])->name('admin.nodes.view'); Route::get('/view/{node:id}/settings', [Admin\Nodes\NodeViewController::class, 'settings'])->name('admin.nodes.view.settings'); Route::get('/view/{node:id}/configuration', [Admin\Nodes\NodeViewController::class, 'configuration'])->name('admin.nodes.view.configuration'); - Route::get('/view/{node:id}/allocation', [Admin\Nodes\NodeViewController::class, 'allocations'])->name('admin.nodes.view.allocation'); Route::get('/view/{node:id}/servers', [Admin\Nodes\NodeViewController::class, 'servers'])->name('admin.nodes.view.servers'); Route::get('/view/{node:id}/system-information', Admin\Nodes\SystemInformationController::class); - Route::post('/new', [Admin\NodesController::class, 'store']); - Route::post('/view/{node:id}/allocation', [Admin\NodesController::class, 'createAllocation']); - Route::post('/view/{node:id}/allocation/remove', [Admin\NodesController::class, 'allocationRemoveBlock'])->name('admin.nodes.view.allocation.removeBlock'); - Route::post('/view/{node:id}/allocation/alias', [Admin\NodesController::class, 'allocationSetAlias'])->name('admin.nodes.view.allocation.setAlias'); Route::post('/view/{node:id}/settings/token', Admin\NodeAutoDeployController::class)->name('admin.nodes.view.configuration.token'); Route::patch('/view/{node:id}/settings', [Admin\NodesController::class, 'updateSettings']); Route::delete('/view/{node:id}/delete', [Admin\NodesController::class, 'delete'])->name('admin.nodes.view.delete'); - Route::delete('/view/{node:id}/allocation/remove/{allocation:id}', [Admin\NodesController::class, 'allocationRemoveSingle'])->name('admin.nodes.view.allocation.removeSingle'); - Route::delete('/view/{node:id}/allocations', [Admin\NodesController::class, 'allocationRemoveMultiple'])->name('admin.nodes.view.allocation.removeMultiple'); }); /* diff --git a/routes/api-application.php b/routes/api-application.php index 1d7b8b8bef..bd3881f467 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -43,12 +43,6 @@ Route::patch('/{node:id}', [Application\Nodes\NodeController::class, 'update']); Route::delete('/{node:id}', [Application\Nodes\NodeController::class, 'delete']); - - Route::prefix('/{node:id}/allocations')->group(function () { - Route::get('/', [Application\Nodes\AllocationController::class, 'index'])->name('api.application.allocations'); - Route::post('/', [Application\Nodes\AllocationController::class, 'store']); - Route::delete('/{allocation:id}', [Application\Nodes\AllocationController::class, 'delete'])->name('api.application.allocations.view'); - }); }); /* @@ -75,7 +69,6 @@ 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::delete('/{server:id}', [Application\Servers\ServerController::class, 'delete']); Route::delete('/{server:id}/{force?}', [Application\Servers\ServerController::class, 'delete']); // Database Management Endpoint diff --git a/routes/api-client.php b/routes/api-client.php index ed1190af64..7da1ecfa68 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -96,14 +96,6 @@ Route::delete('/{schedule}/tasks/{task}', [Client\Servers\ScheduleTaskController::class, 'delete']); }); - Route::prefix('/network')->group(function () { - Route::get('/allocations', [Client\Servers\NetworkAllocationController::class, 'index']); - Route::post('/allocations', [Client\Servers\NetworkAllocationController::class, 'store']); - Route::post('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'update']); - Route::post('/allocations/{allocation}/primary', [Client\Servers\NetworkAllocationController::class, 'setPrimary']); - Route::delete('/allocations/{allocation}', [Client\Servers\NetworkAllocationController::class, 'delete']); - }); - Route::prefix('/users')->group(function () { Route::get('/', [Client\Servers\SubuserController::class, 'index']); Route::post('/', [Client\Servers\SubuserController::class, 'store']); diff --git a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php index d6dee00da0..cc98921a75 100644 --- a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php +++ b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php @@ -11,9 +11,7 @@ use App\Models\Database; use App\Models\Schedule; use Illuminate\Support\Collection; -use App\Models\Allocation; use App\Models\DatabaseHost; -use App\Tests\Integration\TestResponse; use App\Tests\Integration\IntegrationTestCase; use Illuminate\Database\Eloquent\Model as EloquentModel; use App\Transformers\Api\Client\BaseClientTransformer; @@ -59,9 +57,6 @@ protected function link(mixed $model, ?string $append = null): string case Task::class: $link = "/api/client/servers/{$model->schedule->server->uuid}/schedules/{$model->schedule->id}/tasks/$model->id"; break; - case Allocation::class: - $link = "/api/client/servers/{$model->server->uuid}/network/allocations/$model->id"; - break; case Backup::class: $link = "/api/client/servers/{$model->server->uuid}/backups/$model->uuid"; break; diff --git a/tests/Integration/Api/Client/ClientControllerTest.php b/tests/Integration/Api/Client/ClientControllerTest.php index e4059f6947..0975e68a79 100644 --- a/tests/Integration/Api/Client/ClientControllerTest.php +++ b/tests/Integration/Api/Client/ClientControllerTest.php @@ -2,10 +2,10 @@ namespace App\Tests\Integration\Api\Client; +use App\Models\Objects\Endpoint; use App\Models\User; use App\Models\Server; use App\Models\Subuser; -use App\Models\Allocation; use App\Models\Permission; use App\Models\Role; @@ -96,49 +96,6 @@ public function testServersAreFilteredUsingNameAndUuidInformation(): void ->assertJsonPath('data.1.attributes.identifier', $servers[2]->uuid_short); } - /** - * Test that using ?filter[*]=:25565 or ?filter[*]=192.168.1.1:25565 returns only those servers - * with the same allocation for the given user. - */ - public function testServersAreFilteredUsingAllocationInformation(): void - { - /** @var \App\Models\User $user */ - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - $server2 = $this->createServerModel(['user_id' => $user->id, 'node_id' => $server->node_id]); - - $allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id, 'ip' => '192.168.1.1', 'port' => 25565]); - $allocation2 = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server2->id, 'ip' => '192.168.1.1', 'port' => 25570]); - - $server->update(['allocation_id' => $allocation->id]); - $server2->update(['allocation_id' => $allocation2->id]); - - $server->refresh(); - $server2->refresh(); - - $this->actingAs($user)->getJson('/api/client?filter[*]=192.168.1.1') - ->assertOk() - ->assertJsonCount(2, 'data') - ->assertJsonPath('data.0.attributes.identifier', $server->uuid_short) - ->assertJsonPath('data.1.attributes.identifier', $server2->uuid_short); - - $this->actingAs($user)->getJson('/api/client?filter[*]=192.168.1.1:25565') - ->assertOk() - ->assertJsonCount(1, 'data') - ->assertJsonPath('data.0.attributes.identifier', $server->uuid_short); - - $this->actingAs($user)->getJson('/api/client?filter[*]=:25570') - ->assertOk() - ->assertJsonCount(1, 'data') - ->assertJsonPath('data.0.attributes.identifier', $server2->uuid_short); - - $this->actingAs($user)->getJson('/api/client?filter[*]=:255') - ->assertOk() - ->assertJsonCount(2, 'data') - ->assertJsonPath('data.0.attributes.identifier', $server->uuid_short) - ->assertJsonPath('data.1.attributes.identifier', $server2->uuid_short); - } - /** * Test that servers where the user is a subuser are returned by default in the API call. */ @@ -305,20 +262,16 @@ public function testNoServersAreReturnedIfAdminFilterIsPassedByRegularUser(strin } /** - * Test that a subuser without the allocation.read permission is only able to see the primary - * allocation for the server. + * Test that a subuser without the allocation.read permission cannot see any ports */ - public function testOnlyPrimaryAllocationIsReturnedToSubuser(): void + public function testNoPortsAreReturnedToSubuser(): void { /** @var \App\Models\Server $server */ [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); - $server->allocation->notes = 'Test notes'; - $server->allocation->save(); - - Allocation::factory()->times(2)->create([ - 'node_id' => $server->node_id, - 'server_id' => $server->id, - ]); + $server->ports->add(new Endpoint(1234)); + $server->ports->add(new Endpoint(2345, '1.2.3.4')); + $server->ports->add(new Endpoint(3456)); + $server->save(); $server->refresh(); $response = $this->actingAs($user)->getJson('/api/client'); @@ -327,9 +280,7 @@ public function testOnlyPrimaryAllocationIsReturnedToSubuser(): void $response->assertJsonCount(1, 'data'); $response->assertJsonPath('data.0.attributes.server_owner', false); $response->assertJsonPath('data.0.attributes.uuid', $server->uuid); - $response->assertJsonCount(1, 'data.0.attributes.relationships.allocations.data'); - $response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.id', $server->allocation->id); - $response->assertJsonPath('data.0.attributes.relationships.allocations.data.0.attributes.notes', null); + $response->assertJsonCount(0, 'data.0.attributes.ports'); } public static function filterTypeDataProvider(): array diff --git a/tests/Integration/Api/Client/Server/Allocation/AllocationAuthorizationTest.php b/tests/Integration/Api/Client/Server/Allocation/AllocationAuthorizationTest.php deleted file mode 100644 index 6cd798e165..0000000000 --- a/tests/Integration/Api/Client/Server/Allocation/AllocationAuthorizationTest.php +++ /dev/null @@ -1,57 +0,0 @@ -generateTestAccount(); - // Will be a subuser of $server2. - $server2 = $this->createServerModel(); - // And as no access to $server3. - $server3 = $this->createServerModel(); - - // Set the API $user as a subuser of server 2, but with no permissions - // to do anything with the allocations for that server. - Subuser::factory()->create(['server_id' => $server2->id, 'user_id' => $user->id]); - - $allocation1 = Allocation::factory()->create(['server_id' => $server1->id, 'node_id' => $server1->node_id]); - $allocation2 = Allocation::factory()->create(['server_id' => $server2->id, 'node_id' => $server2->node_id]); - $allocation3 = Allocation::factory()->create(['server_id' => $server3->id, 'node_id' => $server3->node_id]); - - // This is the only valid call for this test, accessing the allocation for the same - // server that the API user is the owner of. - $response = $this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation1->id . $endpoint)); - $this->assertTrue($response->status() <= 204 || $response->status() === 400 || $response->status() === 422); - - // This request fails because the allocation is valid for that server but the user - // making the request is not authorized to perform that action. - $this->actingAs($user)->json($method, $this->link($server2, '/network/allocations/' . $allocation2->id . $endpoint))->assertForbidden(); - - // Both of these should report a 404 error due to the allocations being linked to - // servers that are not the same as the server in the request, or are assigned - // to a server for which the user making the request has no access to. - $this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation2->id . $endpoint))->assertNotFound(); - $this->actingAs($user)->json($method, $this->link($server1, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound(); - $this->actingAs($user)->json($method, $this->link($server2, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound(); - $this->actingAs($user)->json($method, $this->link($server3, '/network/allocations/' . $allocation3->id . $endpoint))->assertNotFound(); - } - - public static function methodDataProvider(): array - { - return [ - ['POST', ''], - ['DELETE', ''], - ['POST', '/primary'], - ]; - } -} diff --git a/tests/Integration/Api/Client/Server/Allocation/CreateNewAllocationTest.php b/tests/Integration/Api/Client/Server/Allocation/CreateNewAllocationTest.php deleted file mode 100644 index 395e283a2d..0000000000 --- a/tests/Integration/Api/Client/Server/Allocation/CreateNewAllocationTest.php +++ /dev/null @@ -1,93 +0,0 @@ -set('panel.client_features.allocations.enabled', true); - config()->set('panel.client_features.allocations.range_start', 5000); - config()->set('panel.client_features.allocations.range_end', 5050); - } - - /** - * Tests that a new allocation can be properly assigned to a server. - * - * @dataProvider permissionDataProvider - */ - public function testNewAllocationCanBeAssignedToServer(array $permission): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount($permission); - $server->update(['allocation_limit' => 2]); - - $response = $this->actingAs($user)->postJson($this->link($server, '/network/allocations')); - $response->assertJsonPath('object', Allocation::RESOURCE_NAME); - - $matched = Allocation::query()->findOrFail($response->json('attributes.id')); - - $this->assertSame($server->id, $matched->server_id); - $this->assertJsonTransformedWith($response->json('attributes'), $matched); - } - - /** - * Test that a user without the required permissions cannot create an allocation for - * the server instance. - */ - public function testAllocationCannotBeCreatedIfUserDoesNotHavePermission(): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_UPDATE]); - $server->update(['allocation_limit' => 2]); - - $this->actingAs($user)->postJson($this->link($server, '/network/allocations'))->assertForbidden(); - } - - /** - * Test that an error is returned to the user if this feature is not enabled on the system. - */ - public function testAllocationCannotBeCreatedIfNotEnabled(): void - { - config()->set('panel.client_features.allocations.enabled', false); - - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - $server->update(['allocation_limit' => 2]); - - $this->actingAs($user)->postJson($this->link($server, '/network/allocations')) - ->assertStatus(Response::HTTP_BAD_REQUEST) - ->assertJsonPath('errors.0.code', 'AutoAllocationNotEnabledException') - ->assertJsonPath('errors.0.detail', 'Server auto-allocation is not enabled for this instance.'); - } - - /** - * Test that an allocation cannot be created if the server has reached its allocation limit. - */ - public function testAllocationCannotBeCreatedIfServerIsAtLimit(): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - $server->update(['allocation_limit' => 1]); - - $this->actingAs($user)->postJson($this->link($server, '/network/allocations')) - ->assertStatus(Response::HTTP_BAD_REQUEST) - ->assertJsonPath('errors.0.code', 'DisplayException') - ->assertJsonPath('errors.0.detail', 'Cannot assign additional allocations to this server: limit has been reached.'); - } - - public static function permissionDataProvider(): array - { - return [[[Permission::ACTION_ALLOCATION_CREATE]], [[]]]; - } -} diff --git a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php b/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php deleted file mode 100644 index 07f101a62e..0000000000 --- a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php +++ /dev/null @@ -1,105 +0,0 @@ -generateTestAccount($permission); - $server->update(['allocation_limit' => 2]); - - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create([ - 'server_id' => $server->id, - 'node_id' => $server->node_id, - 'notes' => 'hodor', - ]); - - $this->actingAs($user)->deleteJson($this->link($allocation))->assertStatus(Response::HTTP_NO_CONTENT); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null, 'notes' => null]); - } - - /** - * Test that an error is returned if the user does not have permissiont to delete an allocation. - */ - public function testErrorIsReturnedIfUserDoesNotHavePermission(): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]); - - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create([ - 'server_id' => $server->id, - 'node_id' => $server->node_id, - 'notes' => 'hodor', - ]); - - $this->actingAs($user)->deleteJson($this->link($allocation))->assertForbidden(); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]); - } - - /** - * Test that an allocation is not deleted if it is currently marked as the primary allocation - * for the server. - */ - public function testErrorIsReturnedIfAllocationIsPrimary(): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - $server->update(['allocation_limit' => 2]); - - $this->actingAs($user)->deleteJson($this->link($server->allocation)) - ->assertStatus(Response::HTTP_BAD_REQUEST) - ->assertJsonPath('errors.0.code', 'DisplayException') - ->assertJsonPath('errors.0.detail', 'You cannot delete the primary allocation for this server.'); - } - - public function testAllocationCannotBeDeletedIfServerLimitIsNotDefined(): void - { - [$user, $server] = $this->generateTestAccount(); - - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->forServer($server)->create(['notes' => 'Test notes']); - - $this->actingAs($user)->deleteJson($this->link($allocation)) - ->assertStatus(400) - ->assertJsonPath('errors.0.detail', 'You cannot delete allocations for this server: no allocation limit is set.'); - - $allocation->refresh(); - $this->assertNotNull($allocation->notes); - $this->assertEquals($server->id, $allocation->server_id); - } - - /** - * Test that an allocation cannot be deleted if it does not belong to the server instance. - */ - public function testErrorIsReturnedIfAllocationDoesNotBelongToServer(): void - { - /** @var \App\Models\Server $server */ - [$user, $server] = $this->generateTestAccount(); - [, $server2] = $this->generateTestAccount(); - - $this->actingAs($user)->deleteJson($this->link($server2->allocation))->assertNotFound(); - $this->actingAs($user)->deleteJson($this->link($server, "/network/allocations/{$server2->allocation_id}"))->assertNotFound(); - } - - public static function permissionDataProvider(): array - { - return [[[Permission::ACTION_ALLOCATION_DELETE]], [[]]]; - } -} diff --git a/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php b/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php deleted file mode 100644 index b1e7279eba..0000000000 --- a/tests/Integration/Api/Client/Server/NetworkAllocationControllerTest.php +++ /dev/null @@ -1,140 +0,0 @@ -generateTestAccount(); - - $response = $this->actingAs($user)->getJson($this->link($server, '/network/allocations')); - - $response->assertOk(); - $response->assertJsonPath('object', 'list'); - $response->assertJsonCount(1, 'data'); - - $this->assertJsonTransformedWith($response->json('data.0.attributes'), $server->allocation); - } - - /** - * Test that allocations cannot be returned without the required user permissions. - */ - public function testServerAllocationsAreNotReturnedWithoutPermission(): void - { - [$user, $server] = $this->generateTestAccount(); - $user2 = User::factory()->create(); - - $server->owner_id = $user2->id; - $server->save(); - - $this->actingAs($user)->getJson($this->link($server, '/network/allocations')) - ->assertNotFound(); - - [$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]); - - $this->actingAs($user)->getJson($this->link($server, '/network/allocations')) - ->assertForbidden(); - } - - /** - * Tests that notes on an allocation can be set correctly. - * - * @dataProvider updatePermissionsDataProvider - */ - public function testAllocationNotesCanBeUpdated(array $permissions): void - { - [$user, $server] = $this->generateTestAccount($permissions); - $allocation = $server->allocation; - - $this->assertNull($allocation->notes); - - $this->actingAs($user)->postJson($this->link($allocation), []) - ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) - ->assertJsonPath('errors.0.meta.rule', 'present'); - - $this->actingAs($user)->postJson($this->link($allocation), ['notes' => 'Test notes']) - ->assertOk() - ->assertJsonPath('object', Allocation::RESOURCE_NAME) - ->assertJsonPath('attributes.notes', 'Test notes'); - - $allocation = $allocation->refresh(); - - $this->assertSame('Test notes', $allocation->notes); - - $this->actingAs($user)->postJson($this->link($allocation), ['notes' => null]) - ->assertOk() - ->assertJsonPath('object', Allocation::RESOURCE_NAME) - ->assertJsonPath('attributes.notes', null); - - $allocation = $allocation->refresh(); - - $this->assertNull($allocation->notes); - } - - public function testAllocationNotesCannotBeUpdatedByInvalidUsers(): void - { - [$user, $server] = $this->generateTestAccount(); - $user2 = User::factory()->create(); - - $server->owner_id = $user2->id; - $server->save(); - - $this->actingAs($user)->postJson($this->link($server->allocation))->assertNotFound(); - - [$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]); - - $this->actingAs($user)->postJson($this->link($server->allocation))->assertForbidden(); - } - - /** - * @dataProvider updatePermissionsDataProvider - */ - public function testPrimaryAllocationCanBeModified(array $permissions): void - { - [$user, $server] = $this->generateTestAccount($permissions); - $allocation = $server->allocation; - $allocation2 = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]); - - $server->allocation_id = $allocation->id; - $server->save(); - - $this->actingAs($user)->postJson($this->link($allocation2, '/primary')) - ->assertOk(); - - $server = $server->refresh(); - - $this->assertSame($allocation2->id, $server->allocation_id); - } - - public function testPrimaryAllocationCannotBeModifiedByInvalidUser(): void - { - [$user, $server] = $this->generateTestAccount(); - $user2 = User::factory()->create(); - - $server->owner_id = $user2->id; - $server->save(); - - $this->actingAs($user)->postJson($this->link($server->allocation, '/primary')) - ->assertNotFound(); - - [$user, $server] = $this->generateTestAccount([Permission::ACTION_ALLOCATION_CREATE]); - - $this->actingAs($user)->postJson($this->link($server->allocation, '/primary')) - ->assertForbidden(); - } - - public static function updatePermissionsDataProvider(): array - { - return [[[]], [[Permission::ACTION_ALLOCATION_UPDATE]]]; - } -} diff --git a/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php b/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php index 88cc6ed6f2..25f5658d2a 100644 --- a/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php +++ b/tests/Integration/Api/Client/Server/Startup/GetStartupAndVariablesTest.php @@ -40,7 +40,7 @@ public function testStartupVariablesAreReturnedForServer(array $permissions): vo $response->assertJsonPath('meta.raw_startup_command', $server->startup); $response->assertJsonPath('object', 'list'); - $response->assertJsonCount(1, 'data'); + $response->assertJsonMissing(['env_variable' => 'BUNGEE_VERSION']); $response->assertJsonPath('data.0.object', EggVariable::RESOURCE_NAME); $this->assertJsonTransformedWith($response->json('data.0.attributes'), $egg->variables[1]); } diff --git a/tests/Integration/Services/Allocations/FindAssignableAllocationServiceTest.php b/tests/Integration/Services/Allocations/FindAssignableAllocationServiceTest.php deleted file mode 100644 index 382703b036..0000000000 --- a/tests/Integration/Services/Allocations/FindAssignableAllocationServiceTest.php +++ /dev/null @@ -1,174 +0,0 @@ -set('panel.client_features.allocations.enabled', true); - config()->set('panel.client_features.allocations.range_start', 0); - config()->set('panel.client_features.allocations.range_end', 0); - } - - /** - * Test that an unassigned allocation is preferred rather than creating an entirely new - * allocation for the server. - */ - public function testExistingAllocationIsPreferred(): void - { - $server = $this->createServerModel(); - - $created = Allocation::factory()->create([ - 'node_id' => $server->node_id, - 'ip' => $server->allocation->ip, - ]); - - $response = $this->getService()->handle($server); - - $this->assertSame($created->id, $response->id); - $this->assertSame($server->allocation->ip, $response->ip); - $this->assertSame($server->node_id, $response->node_id); - $this->assertSame($server->id, $response->server_id); - $this->assertNotSame($server->allocation_id, $response->id); - } - - /** - * Test that a new allocation is created if there is not a free one available. - */ - public function testNewAllocationIsCreatedIfOneIsNotFound(): void - { - $server = $this->createServerModel(); - config()->set('panel.client_features.allocations.range_start', 5000); - config()->set('panel.client_features.allocations.range_end', 5005); - - $response = $this->getService()->handle($server); - $this->assertSame($server->id, $response->server_id); - $this->assertSame($server->allocation->ip, $response->ip); - $this->assertSame($server->node_id, $response->node_id); - $this->assertNotSame($server->allocation_id, $response->id); - $this->assertTrue($response->port >= 5000 && $response->port <= 5005); - } - - /** - * Test that a currently assigned port is never assigned to a server. - */ - public function testOnlyPortNotInUseIsCreated(): void - { - $server = $this->createServerModel(); - $server2 = $this->createServerModel(['node_id' => $server->node_id]); - - config()->set('panel.client_features.allocations.range_start', 5000); - config()->set('panel.client_features.allocations.range_end', 5001); - - Allocation::factory()->create([ - 'server_id' => $server2->id, - 'node_id' => $server->node_id, - 'ip' => $server->allocation->ip, - 'port' => 5000, - ]); - - $response = $this->getService()->handle($server); - $this->assertSame(5001, $response->port); - } - - public function testExceptionIsThrownIfNoMoreAllocationsCanBeCreatedInRange(): void - { - $server = $this->createServerModel(); - $server2 = $this->createServerModel(['node_id' => $server->node_id]); - config()->set('panel.client_features.allocations.range_start', 5000); - config()->set('panel.client_features.allocations.range_end', 5005); - - for ($i = 5000; $i <= 5005; $i++) { - Allocation::factory()->create([ - 'ip' => $server->allocation->ip, - 'port' => $i, - 'node_id' => $server->node_id, - 'server_id' => $server2->id, - ]); - } - - $this->expectException(NoAutoAllocationSpaceAvailableException::class); - $this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.'); - - $this->getService()->handle($server); - } - - /** - * Test that we only auto-allocate from the current server's IP address space, and not a random - * IP address available on that node. - */ - public function testExceptionIsThrownIfOnlyFreePortIsOnADifferentIp(): void - { - $server = $this->createServerModel(); - - Allocation::factory()->times(5)->create(['node_id' => $server->node_id]); - - $this->expectException(NoAutoAllocationSpaceAvailableException::class); - $this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.'); - - $this->getService()->handle($server); - } - - public function testExceptionIsThrownIfStartOrEndRangeIsNotDefined(): void - { - $server = $this->createServerModel(); - - $this->expectException(NoAutoAllocationSpaceAvailableException::class); - $this->expectExceptionMessage('Cannot assign additional allocation: no more space available on node.'); - - $this->getService()->handle($server); - } - - public function testExceptionIsThrownIfStartOrEndRangeIsNotNumeric(): void - { - $server = $this->createServerModel(); - config()->set('panel.client_features.allocations.range_start', 'hodor'); - config()->set('panel.client_features.allocations.range_end', 10); - - try { - $this->getService()->handle($server); - $this->fail('This assertion should not be reached.'); - } catch (\Exception $exception) { - $this->assertInstanceOf(\InvalidArgumentException::class, $exception); - $this->assertSame('Expected an integerish value. Got: string', $exception->getMessage()); - } - - config()->set('panel.client_features.allocations.range_start', 10); - config()->set('panel.client_features.allocations.range_end', 'hodor'); - - try { - $this->getService()->handle($server); - $this->fail('This assertion should not be reached.'); - } catch (\Exception $exception) { - $this->assertInstanceOf(\InvalidArgumentException::class, $exception); - $this->assertSame('Expected an integerish value. Got: string', $exception->getMessage()); - } - } - - public function testExceptionIsThrownIfFeatureIsNotEnabled(): void - { - config()->set('panel.client_features.allocations.enabled', false); - $server = $this->createServerModel(); - - $this->expectException(AutoAllocationNotEnabledException::class); - - $this->getService()->handle($server); - } - - private function getService(): FindAssignableAllocationService - { - return $this->app->make(FindAssignableAllocationService::class); - } -} diff --git a/tests/Integration/Services/Servers/BuildModificationServiceTest.php b/tests/Integration/Services/Servers/BuildModificationServiceTest.php index 152b11039f..7b269cac83 100644 --- a/tests/Integration/Services/Servers/BuildModificationServiceTest.php +++ b/tests/Integration/Services/Servers/BuildModificationServiceTest.php @@ -6,9 +6,7 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use App\Models\Server; -use App\Models\Allocation; use GuzzleHttp\Exception\RequestException; -use App\Exceptions\DisplayException; use App\Tests\Integration\IntegrationTestCase; use App\Repositories\Daemon\DaemonServerRepository; use App\Services\Servers\BuildModificationService; @@ -28,82 +26,12 @@ protected function setUp(): void $this->daemonServerRepository = $this->mock(DaemonServerRepository::class); } - /** - * Test that allocations can be added and removed from a server. Only the allocations on the - * current node and belonging to this server should be modified. - */ - public function testAllocationsCanBeModifiedForTheServer(): void - { - $server = $this->createServerModel(); - $server2 = $this->createServerModel(); - - /** @var \App\Models\Allocation[] $allocations */ - $allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id, 'notes' => 'Random notes']); - - $initialAllocationId = $server->allocation_id; - $allocations[0]->update(['server_id' => $server->id, 'notes' => 'Test notes']); - - // Some additional test allocations for the other server, not the server we are attempting - // to modify. - $allocations[2]->update(['server_id' => $server2->id]); - $allocations[3]->update(['server_id' => $server2->id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $response = $this->getService()->handle($server, [ - // Attempt to add one new allocation, and an allocation assigned to another server. The - // other server allocation should be ignored, and only the allocation for this server should - // be used. - 'add_allocations' => [$allocations[2]->id, $allocations[1]->id], - // Remove the default server allocation, ensuring that the new allocation passed through - // in the data becomes the default allocation. - 'remove_allocations' => [$server->allocation_id, $allocations[0]->id, $allocations[3]->id], - ]); - - $this->assertInstanceOf(Server::class, $response); - - // Only one allocation should exist for this server now. - $this->assertCount(1, $response->allocations); - $this->assertSame($allocations[1]->id, $response->allocation_id); - $this->assertNull($response->allocation->notes); - - // These two allocations should not have been touched. - $this->assertDatabaseHas('allocations', ['id' => $allocations[2]->id, 'server_id' => $server2->id]); - $this->assertDatabaseHas('allocations', ['id' => $allocations[3]->id, 'server_id' => $server2->id]); - - // Both of these allocations should have been removed from the server, and have had their - // notes properly reset. - $this->assertDatabaseHas('allocations', ['id' => $initialAllocationId, 'server_id' => null, 'notes' => null]); - $this->assertDatabaseHas('allocations', ['id' => $allocations[0]->id, 'server_id' => null, 'notes' => null]); - } - - /** - * Test that an exception is thrown if removing the default allocation without also assigning - * new allocations to the server. - */ - public function testExceptionIsThrownIfRemovingTheDefaultAllocation(): void - { - $server = $this->createServerModel(); - /** @var \App\Models\Allocation[] $allocations */ - $allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id]); - - $allocations[0]->update(['server_id' => $server->id]); - - $this->expectException(DisplayException::class); - $this->expectExceptionMessage('You are attempting to delete the default allocation for this server but there is no fallback allocation to use.'); - - $this->getService()->handle($server, [ - 'add_allocations' => [], - 'remove_allocations' => [$server->allocation_id, $allocations[0]->id], - ]); - } - /** * Test that the build data for the server is properly passed along to the daemon instance so that * the server data is updated in realtime. This test also ensures that only certain fields get updated * for the server, and not just any arbitrary field. */ - public function testServerBuildDataIsProperlyUpdatedOndaemon(): void + public function testServerBuildDataIsProperlyUpdatedOnDaemon(): void { $server = $this->createServerModel(); @@ -164,91 +92,6 @@ public function testConnectionExceptionIsIgnoredWhenUpdatingServerSettings(): vo $this->assertDatabaseHas('servers', ['id' => $response->id, 'memory' => 256, 'disk' => 10240]); } - /** - * Test that no exception is thrown if we are only removing an allocation. - */ - public function testNoExceptionIsThrownIfOnlyRemovingAllocation(): void - { - $server = $this->createServerModel(); - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'remove_allocations' => [$allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - } - - /** - * Test that allocations in both the add and remove arrays are only added, and not removed. - * This scenario wouldn't really happen in the UI, but it is possible to perform via the API, - * so we want to make sure that the logic being used doesn't break if the allocation exists - * in both arrays. - * - * We'll default to adding the allocation in this case. - */ - public function testAllocationInBothAddAndRemoveIsAdded(): void - { - $server = $this->createServerModel(); - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'add_allocations' => [$allocation->id], - 'remove_allocations' => [$allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]); - } - - /** - * Test that using the same allocation ID multiple times in the array does not cause an error. - */ - public function testUsingSameAllocationIdMultipleTimesDoesNotError(): void - { - $server = $this->createServerModel(); - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id, 'server_id' => $server->id]); - /** @var \App\Models\Allocation $allocation2 */ - $allocation2 = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); - - $this->getService()->handle($server, [ - 'add_allocations' => [$allocation2->id, $allocation2->id], - 'remove_allocations' => [$allocation->id, $allocation->id], - ]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - $this->assertDatabaseHas('allocations', ['id' => $allocation2->id, 'server_id' => $server->id]); - } - - /** - * Test that any changes we made to the server or allocations are rolled back if there is an - * exception while performing any action. This is different from the connection exception - * test which should properly ignore connection issues. We want any other type of exception - * to properly be thrown back to the caller. - */ - public function testThatUpdatesAreRolledBackIfExceptionIsEncountered(): void - { - $server = $this->createServerModel(); - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $server->node_id]); - - $this->daemonServerRepository->expects('setServer->sync')->andThrows(new DisplayException('Test')); - - $this->expectException(DisplayException::class); - - $this->getService()->handle($server, ['add_allocations' => [$allocation->id]]); - - $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null]); - } - private function getService(): BuildModificationService { return $this->app->make(BuildModificationService::class); diff --git a/tests/Integration/Services/Servers/ServerCreationServiceTest.php b/tests/Integration/Services/Servers/ServerCreationServiceTest.php index c0b82a285d..7589dbe0a9 100644 --- a/tests/Integration/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Integration/Services/Servers/ServerCreationServiceTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Integration\Services\Servers; +use App\Models\Objects\Endpoint; use Mockery\MockInterface; use App\Models\Egg; use GuzzleHttp\Psr7\Request; @@ -9,7 +10,6 @@ use App\Models\User; use GuzzleHttp\Psr7\Response; use App\Models\Server; -use App\Models\Allocation; use Illuminate\Foundation\Testing\WithFaker; use GuzzleHttp\Exception\BadResponseException; use Illuminate\Validation\ValidationException; @@ -59,13 +59,8 @@ public function testServerIsCreatedWithDeploymentObject(): void /** @var \App\Models\Node $node */ $node = Node::factory()->create(); - /** @var \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations */ - $allocations = Allocation::factory()->times(5)->create([ - 'node_id' => $node->id, - ]); - $deployment = (new DeploymentObject())->setDedicated(true)->setPorts([ - $allocations[0]->port, + 1234, ]); $egg = $this->cloneEggAndVariables($this->bungeecord); @@ -87,12 +82,12 @@ public function testServerIsCreatedWithDeploymentObject(): void 'startup' => 'java server2.jar', 'image' => 'java:8', 'egg_id' => $egg->id, - 'allocation_additional' => [ - $allocations[4]->id, - ], + 'ports' => [1234, 2345, 3456], + 'node_id' => $node->id, 'environment' => [ 'BUNGEE_VERSION' => '123', 'SERVER_JARFILE' => 'server2.jar', + 'SERVER_PORT' => '1234', ], 'start_on_completion' => true, ]; @@ -104,13 +99,14 @@ public function testServerIsCreatedWithDeploymentObject(): void 'environment' => [ 'BUNGEE_VERSION' => '', 'SERVER_JARFILE' => 'server2.jar', + 'SERVER_PORT' => '1234', ], ]), $deployment); $this->fail('This execution pathway should not be reached.'); } catch (ValidationException $exception) { - $this->assertCount(1, $exception->errors()); $this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors()); + $this->assertArrayNotHasKey('environment.SERVER_JARFILE', $exception->errors()); $this->assertSame('The Bungeecord Version variable field is required.', $exception->errors()['environment.BUNGEE_VERSION'][0]); } @@ -120,23 +116,24 @@ public function testServerIsCreatedWithDeploymentObject(): void $this->assertNotNull($response->uuid); $this->assertSame($response->uuid_short, substr($response->uuid, 0, 8)); $this->assertSame($egg->id, $response->egg_id); - $this->assertCount(2, $response->variables); + $this->assertCount(3, $response->variables); $this->assertSame('123', $response->variables[0]->server_value); $this->assertSame('server2.jar', $response->variables[1]->server_value); foreach ($data as $key => $value) { - if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) { + if (in_array($key, ['environment', 'start_on_completion'])) { + continue; + } + + if ($key === 'ports') { + $this->assertSame($value, $response->ports->map(fn (Endpoint $endpoint) => $endpoint->port)->all()); + continue; } $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); } - $this->assertCount(2, $response->allocations); - $this->assertSame($response->allocation_id, $response->allocations[0]->id); - $this->assertSame($allocations[0]->id, $response->allocations[0]->id); - $this->assertSame($allocations[4]->id, $response->allocations[1]->id); - $this->assertFalse($response->isSuspended()); $this->assertFalse($response->oom_killer); $this->assertSame(0, $response->database_limit); @@ -156,17 +153,11 @@ public function testErrorEncounteredByDaemonCausesServerToBeDeleted(): void /** @var \App\Models\Node $node */ $node = Node::factory()->create(); - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create([ - 'node_id' => $node->id, - ]); - $data = [ 'name' => $this->faker->name(), 'description' => $this->faker->sentence(), 'owner_id' => $user->id, - 'allocation_id' => $allocation->id, - 'node_id' => $allocation->node_id, + 'node_id' => $node->id, 'memory' => 256, 'swap' => 128, 'disk' => 100, @@ -178,6 +169,7 @@ public function testErrorEncounteredByDaemonCausesServerToBeDeleted(): void 'environment' => [ 'BUNGEE_VERSION' => '123', 'SERVER_JARFILE' => 'server2.jar', + 'SERVER_PORT' => '1234', ], ]; diff --git a/tests/Integration/Services/Servers/StartupModificationServiceTest.php b/tests/Integration/Services/Servers/StartupModificationServiceTest.php index f0d2726e1f..803a98486f 100644 --- a/tests/Integration/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Integration/Services/Servers/StartupModificationServiceTest.php @@ -29,6 +29,7 @@ public function testNonAdminCanModifyServerVariables(): void 'environment' => [ 'BUNGEE_VERSION' => '$$', 'SERVER_JARFILE' => 'server.jar', + 'SERVER_PORT' => '1234', ], ]); @@ -54,11 +55,12 @@ public function testNonAdminCanModifyServerVariables(): void 'environment' => [ 'BUNGEE_VERSION' => '1234', 'SERVER_JARFILE' => 'test.jar', + 'SERVER_PORT' => '1234', ], ]); $this->assertInstanceOf(Server::class, $result); - $this->assertCount(2, $result->variables); + $this->assertCount(3, $result->variables); $this->assertSame($server->startup, $result->startup); $this->assertSame('1234', $result->variables[0]->server_value); $this->assertSame('test.jar', $result->variables[1]->server_value); @@ -125,7 +127,7 @@ public function testEnvironmentVariablesCanBeUpdatedByAdmin(): void ], ]); - $this->assertCount(2, $response->variables); + $this->assertCount(3, $response->variables); $this->assertSame('EXIST', $response->variables[0]->server_value); $this->assertSame('test.jar', $response->variables[1]->server_value); @@ -135,12 +137,14 @@ public function testEnvironmentVariablesCanBeUpdatedByAdmin(): void 'environment' => [ 'BUNGEE_VERSION' => '1234', 'SERVER_JARFILE' => 'test.jar', + 'SERVER_PORT' => '1111', ], ]); - $this->assertCount(2, $response->variables); + $this->assertCount(3, $response->variables); $this->assertSame('1234', $response->variables[0]->server_value); $this->assertSame('test.jar', $response->variables[1]->server_value); + $this->assertSame('1111', $response->variables[2]->server_value); } /** diff --git a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php b/tests/Integration/Services/Servers/VariableValidatorServiceTest.php index 6f49b97fb2..9f70f8b0d1 100644 --- a/tests/Integration/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Integration/Services/Servers/VariableValidatorServiceTest.php @@ -94,25 +94,28 @@ public function testEnvironmentVariablesCanBeUpdatedAsAdmin(): void $this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [ 'BUNGEE_VERSION' => '1.2.3', 'SERVER_JARFILE' => 'server.jar', + 'SERVER_PORT' => '1234', ]); $this->fail('This statement should not be reached.'); } catch (ValidationException $exception) { - $this->assertCount(1, $exception->errors()); $this->assertArrayHasKey('environment.BUNGEE_VERSION', $exception->errors()); } $response = $this->getService()->setUserLevel(User::USER_LEVEL_ADMIN)->handle($egg->id, [ 'BUNGEE_VERSION' => '123', 'SERVER_JARFILE' => 'server.jar', + 'SERVER_PORT' => '1234', ]); $this->assertInstanceOf(Collection::class, $response); - $this->assertCount(2, $response); + $this->assertCount(3, $response); $this->assertSame('BUNGEE_VERSION', $response->get(0)->key); $this->assertSame('123', $response->get(0)->value); $this->assertSame('SERVER_JARFILE', $response->get(1)->key); $this->assertSame('server.jar', $response->get(1)->value); + $this->assertSame('SERVER_PORT', $response->get(2)->key); + $this->assertSame('1234', $response->get(2)->value); } public function testNullableEnvironmentVariablesCanBeUsedCorrectly(): void diff --git a/tests/Traits/Integration/CreatesTestModels.php b/tests/Traits/Integration/CreatesTestModels.php index 0e28cb40e7..f33fc63aa4 100644 --- a/tests/Traits/Integration/CreatesTestModels.php +++ b/tests/Traits/Integration/CreatesTestModels.php @@ -8,7 +8,6 @@ use App\Models\User; use App\Models\Server; use App\Models\Subuser; -use App\Models\Allocation; trait CreatesTestModels { @@ -37,12 +36,6 @@ public function createServerModel(array $attributes = []): Server $attributes['node_id'] = $node->id; } - if (!isset($attributes['allocation_id'])) { - /** @var \App\Models\Allocation $allocation */ - $allocation = Allocation::factory()->create(['node_id' => $attributes['node_id']]); - $attributes['allocation_id'] = $allocation->id; - } - if (empty($attributes['egg_id'])) { $egg = $this->getBungeecordEgg(); @@ -54,10 +47,8 @@ public function createServerModel(array $attributes = []): Server /** @var \App\Models\Server $server */ $server = Server::factory()->create($attributes); - Allocation::query()->where('id', $server->allocation_id)->update(['server_id' => $server->id]); - return $server->fresh([ - 'user', 'node', 'allocation', 'egg', + 'user', 'node', 'egg', ]); }