From 7731f16b0fea1a3c159267fb60668e0865c1ac78 Mon Sep 17 00:00:00 2001 From: Walter van der Broek <113507212+wbvanderbroek@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:48:39 +0200 Subject: [PATCH 1/4] Fix: Search for tags in correct variable (#1461) --- app/Services/Servers/ServerCreationService.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index 542693cc8..ef959036b 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -128,7 +128,7 @@ class ServerCreationService Arr::get($data, 'memory', 0), Arr::get($data, 'disk', 0), Arr::get($data, 'cpu', 0), - Arr::get($data, 'tags', []), + $deployment->getTags(), ); return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) From 6a088d0c4f3fb9468046d56f2c2f37ea13ff8d8b Mon Sep 17 00:00:00 2001 From: Charles Date: Wed, 25 Jun 2025 19:49:00 -0400 Subject: [PATCH 2/4] Tweak Grid View, Use Memory Limit, not wings reported allocation (#1462) --- app/Filament/Server/Widgets/ServerOverview.php | 2 +- app/Livewire/ServerEntry.php | 2 +- resources/views/livewire/server-entry.blade.php | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/Filament/Server/Widgets/ServerOverview.php b/app/Filament/Server/Widgets/ServerOverview.php index 0f7455cb1..72d620d26 100644 --- a/app/Filament/Server/Widgets/ServerOverview.php +++ b/app/Filament/Server/Widgets/ServerOverview.php @@ -68,7 +68,7 @@ class ServerOverview extends StatsOverviewWidget } $latestMemoryUsed = collect(cache()->get("servers.{$this->server->id}.memory_bytes"))->last(default: 0); - $totalMemory = collect(cache()->get("servers.{$this->server->id}.memory_limit_bytes"))->last(default: 0); + $totalMemory = $this->server->memory * 2 ** 20; $used = convert_bytes_to_readable($latestMemoryUsed); $total = convert_bytes_to_readable($totalMemory); diff --git a/app/Livewire/ServerEntry.php b/app/Livewire/ServerEntry.php index 35b1a3095..4b864f8ba 100644 --- a/app/Livewire/ServerEntry.php +++ b/app/Livewire/ServerEntry.php @@ -24,7 +24,7 @@ class ServerEntry extends Component style="background-color: #D97706;"> -
+

diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index 6f4b25775..ac64d1a5d 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -5,7 +5,7 @@ style="background-color: {{ $server->condition->getColor(true) }};">

-
+
- -
\ No newline at end of file +
+ +
+
From dca37ccc95ca6425fcc7613490adf17d17698e97 Mon Sep 17 00:00:00 2001 From: JoanFo <161775222+JoanFo1456@users.noreply.github.com> Date: Thu, 26 Jun 2025 01:49:43 +0200 Subject: [PATCH 3/4] Server Without Allocations (#1432) Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com> --- .../Deployment/NoViableNodeException.php | 7 ++ .../ServersRelationManager.php | 5 +- .../AllocationsRelationManager.php | 6 +- .../RelationManagers/NodesRelationManager.php | 6 +- .../ServerResource/Pages/CreateServer.php | 16 ++-- .../ServerResource/Pages/EditServer.php | 9 +- .../ServerResource/Pages/ListServers.php | 11 ++- .../AllocationsRelationManager.php | 38 +++----- .../ServersRelationManager.php | 5 +- .../ServerResource/Pages/ListServers.php | 3 +- .../Server/Resources/AllocationResource.php | 11 +-- .../Pages/ListAllocations.php | 4 + .../Server/Widgets/ServerOverview.php | 2 +- .../Servers/NetworkAllocationController.php | 4 - .../Servers/ServerTransferController.php | 27 +++--- .../Servers/StoreServerRequest.php | 1 + app/Livewire/ServerEntry.php | 2 +- app/Models/Allocation.php | 14 --- app/Models/Objects/DeploymentObject.php | 16 ++++ app/Models/Server.php | 12 ++- app/Models/ServerTransfer.php | 8 +- .../Allocations/AssignmentService.php | 4 + .../FindAssignableAllocationService.php | 4 +- .../Servers/BuildModificationService.php | 35 +++----- .../ServerConfigurationStructureService.php | 6 +- .../Servers/ServerCreationService.php | 42 ++++++--- .../Servers/StartupCommandService.php | 6 +- .../Servers/TransferServerService.php | 18 ++-- ...3_171448_nullable_server_allocation_id.php | 34 ++++++++ lang/en/admin/node.php | 3 + lang/en/admin/server.php | 4 +- .../views/livewire/server-entry.blade.php | 2 +- .../Allocation/DeleteAllocationTest.php | 33 +++---- .../Servers/BuildModificationServiceTest.php | 36 +++++--- .../Servers/ServerCreationServiceTest.php | 86 +++++++++++++++++++ 35 files changed, 346 insertions(+), 174 deletions(-) create mode 100644 app/Exceptions/Service/Deployment/NoViableNodeException.php create mode 100644 database/migrations/2025_06_03_171448_nullable_server_allocation_id.php diff --git a/app/Exceptions/Service/Deployment/NoViableNodeException.php b/app/Exceptions/Service/Deployment/NoViableNodeException.php new file mode 100644 index 000000000..432c42703 --- /dev/null +++ b/app/Exceptions/Service/Deployment/NoViableNodeException.php @@ -0,0 +1,7 @@ +label(trans('admin/server.docker_image')), SelectColumn::make('allocation.id') ->label(trans('admin/server.primary_allocation')) - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) + ->disabled() + ->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) + ->placeholder('None') ->sortable(), ]); } diff --git a/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php index 10a3566ca..020d4eb39 100644 --- a/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php @@ -58,6 +58,9 @@ class AllocationsRelationManager extends RelationManager TextInputColumn::make('ip_alias') ->searchable() ->label(trans('admin/node.table.alias')), + TextInputColumn::make('notes') + ->label(trans('admin/node.table.allocation_notes')) + ->placeholder(trans('admin/node.table.no_notes')), SelectColumn::make('ip') ->options(fn (Allocation $allocation) => collect($this->getOwnerRecord()->ipAddresses())->merge([$allocation->ip])->mapWithKeys(fn (string $ip) => [$ip => $ip])) ->selectablePlaceholder(false) @@ -81,8 +84,7 @@ class AllocationsRelationManager extends RelationManager ->label(trans('admin/node.table.alias')) ->inlineLabel() ->default(null) - ->helperText(trans('admin/node.alias_help')) - ->required(false), + ->helperText(trans('admin/node.alias_help')), TagsInput::make('allocation_ports') ->placeholder('27015, 27017-27019') ->label(trans('admin/node.ports')) diff --git a/app/Filament/Admin/Resources/NodeResource/RelationManagers/NodesRelationManager.php b/app/Filament/Admin/Resources/NodeResource/RelationManagers/NodesRelationManager.php index 716c80c6c..b2768a3a2 100644 --- a/app/Filament/Admin/Resources/NodeResource/RelationManagers/NodesRelationManager.php +++ b/app/Filament/Admin/Resources/NodeResource/RelationManagers/NodesRelationManager.php @@ -43,8 +43,10 @@ class NodesRelationManager extends RelationManager ->sortable(), SelectColumn::make('allocation.id') ->label(trans('admin/node.primary_allocation')) - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) + ->disabled(fn (Server $server) => $server->allocations->count() <= 1) + ->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) + ->selectablePlaceholder(fn (SelectColumn $select) => !$select->isDisabled()) + ->placeholder('None') ->sortable(), TextColumn::make('memory')->label(trans('admin/node.memory'))->icon('tabler-device-desktop-analytics'), TextColumn::make('cpu')->label(trans('admin/node.cpu'))->icon('tabler-cpu'), diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php index c0a5b2efe..878279271 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php @@ -128,12 +128,12 @@ class CreateServer extends CreateRecord ->live() ->relationship('node', 'name', fn (Builder $query) => $query->whereIn('id', auth()->user()->accessibleNodes()->pluck('id'))) ->searchable() + ->required() ->preload() ->afterStateUpdated(function (Set $set, $state) { $set('allocation_id', null); $this->node = Node::find($state); - }) - ->required(), + }), Select::make('owner_id') ->preload() @@ -194,7 +194,7 @@ class CreateServer extends CreateRecord $set('allocation_additional', null); $set('allocation_additional.needstobeastringhere.extra_allocations', null); }) - ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address) + ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address ?? '') ->placeholder(function (Get $get) { $node = Node::find($get('node_id')); @@ -248,9 +248,7 @@ class CreateServer extends CreateRecord return collect( $assignmentService->handle(Node::find($get('node_id')), $data) )->first(); - }) - ->required(), - + }), Repeater::make('allocation_additional') ->label(trans('admin/server.additional_allocations')) ->columnSpan([ @@ -270,7 +268,7 @@ class CreateServer extends CreateRecord ->prefixIcon('tabler-network') ->label('Additional Allocations') ->columnSpan(2) - ->disabled(fn (Get $get) => $get('../../node_id') === null) + ->disabled(fn (Get $get) => $get('../../allocation_id') === null || $get('../../node_id') === null) ->searchable(['ip', 'port', 'ip_alias']) ->getOptionLabelFromRecordUsing(fn (Allocation $allocation) => $allocation->address) ->placeholder(trans('admin/server.select_additional')) @@ -833,7 +831,9 @@ class CreateServer extends CreateRecord protected function handleRecordCreation(array $data): Model { - $data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all(); + if ($allocation_additional = array_get($data, 'allocation_additional')) { + $data['allocation_additional'] = collect($allocation_additional)->filter()->all(); + } try { return $this->serverCreationService->handle($data); diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php index 18ba0d732..af6b87861 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/EditServer.php @@ -1020,17 +1020,20 @@ class EditServer extends EditRecord ->options(fn (Server $server) => Node::whereNot('id', $server->node->id)->pluck('name', 'id')->all()), Select::make('allocation_id') ->label(trans('admin/server.primary_allocation')) - ->required() + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || !$server->allocation_id) + ->required(fn (Server $server) => $server->allocation_id) ->prefixIcon('tabler-network') - ->disabled(fn (Get $get) => !$get('node_id')) ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) ->searchable(['ip', 'port', 'ip_alias']) ->placeholder(trans('admin/server.select_allocation')), Select::make('allocation_additional') ->label(trans('admin/server.additional_allocations')) + ->disabled(fn (Get $get, Server $server) => !$get('node_id') || $server->allocations->count() <= 1) ->multiple() + ->minItems(fn (Select $select) => $select->getMaxItems()) + ->maxItems(fn (Select $select, Server $server) => $select->isDisabled() ? null : $server->allocations->count() - 1) ->prefixIcon('tabler-network') - ->disabled(fn (Get $get) => !$get('node_id')) + ->required(fn (Server $server) => $server->allocations->count() > 1) ->options(fn (Get $get) => Allocation::where('node_id', $get('node_id'))->whereNull('server_id')->when($get('allocation_id'), fn ($query) => $query->whereNot('id', $get('allocation_id')))->get()->mapWithKeys(fn (Allocation $allocation) => [$allocation->id => $allocation->address])) ->searchable(['ip', 'port', 'ip_alias']) ->placeholder(trans('admin/server.select_additional')), diff --git a/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php index 14de8fc4b..e1c69a98f 100644 --- a/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/Admin/Resources/ServerResource/Pages/ListServers.php @@ -73,14 +73,17 @@ class ListServers extends ListRecords ->searchable(), SelectColumn::make('allocation_id') ->label(trans('admin/server.primary_allocation')) - ->hidden(!auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) + ->hidden(fn () => !auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) + ->disabled(fn (Server $server) => $server->allocations->count() <= 1) ->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) - ->selectablePlaceholder(false) + ->selectablePlaceholder(fn (Server $server) => $server->allocations->count() <= 1) + ->placeholder('None') ->sortable(), TextColumn::make('allocation_id_readonly') ->label(trans('admin/server.primary_allocation')) - ->hidden(auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) - ->state(fn (Server $server) => $server->allocation->address), + ->hidden(fn () => auth()->user()->can('update server')) // TODO: update to policy check (fn (Server $server) --> $server is empty) + ->disabled(fn (Server $server) => $server->allocations->count() <= 1) + ->state(fn (Server $server) => $server->allocation->address ?? 'None'), TextColumn::make('image')->hidden(), TextColumn::make('backups_count') ->counts('backups') diff --git a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php index 14ca71d1d..070812053 100644 --- a/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php @@ -12,8 +12,6 @@ use Filament\Forms\Components\TextInput; use Filament\Forms\Get; use Filament\Forms\Set; use Filament\Resources\RelationManagers\RelationManager; -use Filament\Support\Exceptions\Halt; -use Filament\Tables\Actions\Action; use Filament\Tables\Actions\AssociateAction; use Filament\Tables\Actions\CreateAction; use Filament\Tables\Actions\DissociateAction; @@ -22,7 +20,6 @@ use Filament\Tables\Columns\IconColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextInputColumn; use Filament\Tables\Table; -use Illuminate\Database\Eloquent\Collection; /** * @method Server getOwnerRecord() @@ -37,7 +34,6 @@ class AllocationsRelationManager extends RelationManager ->selectCurrentPageOnly() ->recordTitleAttribute('address') ->recordTitle(fn (Allocation $allocation) => $allocation->address) - ->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id) ->inverseRelationship('server') ->heading(trans('admin/server.allocations')) ->columns([ @@ -47,6 +43,9 @@ class AllocationsRelationManager extends RelationManager ->label(trans('admin/server.port')), TextInputColumn::make('ip_alias') ->label(trans('admin/server.alias')), + TextInputColumn::make('notes') + ->label(trans('admin/server.notes')) + ->placeholder(trans('admin/server.no_notes')), IconColumn::make('primary') ->icon(fn ($state) => match ($state) { true => 'tabler-star-filled', @@ -56,17 +55,17 @@ class AllocationsRelationManager extends RelationManager true => 'warning', default => 'gray', }) + ->tooltip(fn (Allocation $allocation) => trans('admin/server.' . ($allocation->id === $this->getOwnerRecord()->allocation_id ? 'already' : 'make') . '_primary')) ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) ->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id) ->label(trans('admin/server.primary')), ]) ->actions([ - Action::make('make-primary') - ->label(trans('admin/server.make_primary')) - ->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords()) - ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), DissociateAction::make() - ->hidden(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id), + ->after(function (Allocation $allocation) { + $allocation->update(['notes' => null]); + $this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); + }), ]) ->headerActions([ CreateAction::make()->label(trans('admin/server.create_allocation')) @@ -84,8 +83,7 @@ class AllocationsRelationManager extends RelationManager ->label(trans('admin/server.alias')) ->inlineLabel() ->default(null) - ->helperText(trans('admin/server.alias_helper')) - ->required(false), + ->helperText(trans('admin/server.alias_helper')), TagsInput::make('allocation_ports') ->placeholder('27015, 27017-27019') ->label(trans('admin/server.ports')) @@ -103,22 +101,14 @@ class AllocationsRelationManager extends RelationManager ->preloadRecordSelect() ->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id')) ->recordSelectSearchColumns(['ip', 'port']) - ->label(trans('admin/server.add_allocation')), + ->label(trans('admin/server.add_allocation')) + ->after(fn (array $data) => !$this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]])), ]) ->groupedBulkActions([ DissociateBulkAction::make() - ->before(function (DissociateBulkAction $action, Collection $records) { - $records = $records->filter(function ($allocation) { - /** @var Allocation $allocation */ - return $allocation->id !== $this->getOwnerRecord()->allocation_id; - }); - - if ($records->isEmpty()) { - $action->failureNotificationTitle(trans('admin/server.notifications.dissociate_primary'))->failure(); - throw new Halt(); - } - - return $records; + ->after(function () { + Allocation::whereNull('server_id')->update(['notes' => null]); + $this->getOwnerRecord()->allocation_id && $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); }), ]); } diff --git a/app/Filament/Admin/Resources/UserResource/RelationManagers/ServersRelationManager.php b/app/Filament/Admin/Resources/UserResource/RelationManagers/ServersRelationManager.php index 5f2c7b479..9388a32ac 100644 --- a/app/Filament/Admin/Resources/UserResource/RelationManagers/ServersRelationManager.php +++ b/app/Filament/Admin/Resources/UserResource/RelationManagers/ServersRelationManager.php @@ -70,8 +70,9 @@ class ServersRelationManager extends RelationManager ->sortable(), SelectColumn::make('allocation.id') ->label(trans('admin/server.primary_allocation')) - ->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address]) - ->selectablePlaceholder(false) + ->disabled() + ->options(fn (Server $server) => $server->allocations->take(1)->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address])) + ->placeholder('None') ->sortable(), TextColumn::make('image')->hidden(), TextColumn::make('databases_count') diff --git a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php index dd78433a3..1568cbeaa 100644 --- a/app/Filament/App/Resources/ServerResource/Pages/ListServers.php +++ b/app/Filament/App/Resources/ServerResource/Pages/ListServers.php @@ -74,7 +74,8 @@ class ListServers extends ListRecords ->label('') ->badge() ->visibleFrom('md') - ->copyable(request()->isSecure()), + ->copyable(request()->isSecure()) + ->state(fn (Server $server) => $server->allocation->address ?? 'None'), TextColumn::make('cpuUsage') ->label('Resources') ->icon('tabler-cpu') diff --git a/app/Filament/Server/Resources/AllocationResource.php b/app/Filament/Server/Resources/AllocationResource.php index 26ae815ee..15fd24527 100644 --- a/app/Filament/Server/Resources/AllocationResource.php +++ b/app/Filament/Server/Resources/AllocationResource.php @@ -65,11 +65,8 @@ class AllocationResource extends Resource true => 'warning', default => 'gray', }) - ->action(function (Allocation $allocation) use ($server) { - if (auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server)) { - return $server->update(['allocation_id' => $allocation->id]); - } - }) + ->tooltip(fn (Allocation $allocation) => ($allocation->id === $server->allocation_id ? 'Already' : 'Make') . ' Primary') + ->action(fn (Allocation $allocation) => auth()->user()->can(PERMISSION::ACTION_ALLOCATION_UPDATE, $server) && $server->update(['allocation_id' => $allocation->id])) ->default(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) ->label('Primary'), ]) @@ -78,7 +75,6 @@ class AllocationResource extends Resource ->authorize(fn () => auth()->user()->can(Permission::ACTION_ALLOCATION_DELETE, $server)) ->label('Delete') ->icon('tabler-trash') - ->hidden(fn (Allocation $allocation) => $allocation->id === $server->allocation_id) ->action(function (Allocation $allocation) { Allocation::query()->where('id', $allocation->id)->update([ 'notes' => null, @@ -89,7 +85,8 @@ class AllocationResource extends Resource ->subject($allocation) ->property('allocation', $allocation->address) ->log(); - }), + }) + ->after(fn (Allocation $allocation) => $allocation->id === $server->allocation_id && $server->update(['allocation_id' => $server->allocations()->first()?->id])), ]); } diff --git a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php index 83e5ab4c7..5d1e14155 100644 --- a/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php +++ b/app/Filament/Server/Resources/AllocationResource/Pages/ListAllocations.php @@ -37,6 +37,10 @@ class ListAllocations extends ListRecords ->action(function (FindAssignableAllocationService $service) use ($server) { $allocation = $service->handle($server); + if (!$server->allocation_id) { + $server->update(['allocation_id' => $allocation->id]); + } + Activity::event('server:allocation.create') ->subject($allocation) ->property('allocation', $allocation->address) diff --git a/app/Filament/Server/Widgets/ServerOverview.php b/app/Filament/Server/Widgets/ServerOverview.php index 72d620d26..510e42e5b 100644 --- a/app/Filament/Server/Widgets/ServerOverview.php +++ b/app/Filament/Server/Widgets/ServerOverview.php @@ -23,7 +23,7 @@ class ServerOverview extends StatsOverviewWidget SmallStatBlock::make('Name', $this->server->name) ->copyOnClick(fn () => request()->isSecure()), SmallStatBlock::make('Status', $this->status()), - SmallStatBlock::make('Address', $this->server->allocation->address) + SmallStatBlock::make('Address', $this->server?->allocation->address ?? 'None') ->copyOnClick(fn () => request()->isSecure()), SmallStatBlock::make('CPU', $this->cpuUsage()), SmallStatBlock::make('Memory', $this->memoryUsage()), diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index c287addb6..2c2ad6e85 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -137,10 +137,6 @@ class NetworkAllocationController extends ClientApiController 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, diff --git a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php index 7dbce0288..514f7f60f 100644 --- a/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php +++ b/app/Http/Controllers/Api/Remote/Servers/ServerTransferController.php @@ -50,17 +50,18 @@ class ServerTransferController extends Controller throw new ConflictHttpException('Server is not being transferred.'); } + $data = []; /** @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, - ]); + $server = $this->connection->transaction(function () use ($server, $transfer, $data) { + if ($transfer->old_allocation || $transfer->old_additional_allocations) { + $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]); + $data['allocation_id'] = $transfer->new_allocation; + } + $data['node_id'] = $transfer->new_node; + $server->update($data); $server = $server->fresh(); $server->transfer->update(['successful' => true]); @@ -93,8 +94,10 @@ class ServerTransferController extends Controller $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]); + if ($transfer->new_allocation || $transfer->new_additional_allocations) { + $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/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 1c217dd68..fb94dd739 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -176,6 +176,7 @@ class StoreServerRequest extends ApplicationApiRequest $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/Livewire/ServerEntry.php b/app/Livewire/ServerEntry.php index 4b864f8ba..958c6bbbc 100644 --- a/app/Livewire/ServerEntry.php +++ b/app/Livewire/ServerEntry.php @@ -54,7 +54,7 @@ class ServerEntry extends Component
diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index bb642244f..cf6941faa 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -72,20 +72,6 @@ class Allocation extends Model static::deleting(function (self $allocation) { throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using'))); }); - - static::updating(function ($allocation) { - $originalServerId = $allocation->getOriginal('server_id'); - if (!$originalServerId) { - return; - } - $server = Server::find($originalServerId); - if (!$server) { - return; - } - if ($allocation->isDirty('server_id') && is_null($allocation->server_id) && $allocation->id === $server->allocation_id) { - return false; - } - }); } protected function casts(): array diff --git a/app/Models/Objects/DeploymentObject.php b/app/Models/Objects/DeploymentObject.php index 105cbfd72..b318e7e71 100644 --- a/app/Models/Objects/DeploymentObject.php +++ b/app/Models/Objects/DeploymentObject.php @@ -2,8 +2,12 @@ namespace App\Models\Objects; +use App\Models\Node; + class DeploymentObject { + private ?Node $node = null; + private bool $dedicated = false; /** @var string[] */ @@ -12,6 +16,18 @@ class DeploymentObject /** @var array */ private array $ports = []; + public function getNode(): ?Node + { + return $this->node; + } + + public function setNode(Node $node): self + { + $this->node = $node; + + return $this; + } + public function isDedicated(): bool { return $this->dedicated; diff --git a/app/Models/Server.php b/app/Models/Server.php index 5ba473418..58ad0e089 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -46,7 +46,7 @@ use App\Services\Subusers\SubuserDeletionService; * @property int $cpu * @property string|null $threads * @property bool $oom_killer - * @property int $allocation_id + * @property int|null $allocation_id * @property int $egg_id * @property string $startup * @property string $image @@ -171,7 +171,7 @@ class Server extends Model implements Validatable 'threads' => ['nullable', 'regex:/^[0-9-,]+$/'], 'oom_killer' => ['sometimes', 'boolean'], 'disk' => ['required', 'numeric', 'min:0'], - 'allocation_id' => ['required', 'bail', 'unique:servers', 'exists:allocations,id'], + 'allocation_id' => ['sometimes', 'nullable', 'unique:servers', 'exists:allocations,id'], 'egg_id' => ['required', 'exists:eggs,id'], 'startup' => ['required', 'string'], 'skip_scripts' => ['sometimes', 'boolean'], @@ -220,10 +220,14 @@ class Server extends Model implements Validatable /** * Returns the format for server allocations when communicating with the Daemon. * - * @return array + * @return array> */ public function getAllocationMappings(): array { + if (!$this->allocation) { + return ['' => []]; + } + return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) { return $item->pluck('port'); })->toArray(); @@ -272,6 +276,8 @@ class Server extends Model implements Validatable /** * Gets all allocations associated with this server. + * + * @return HasMany */ public function allocations(): HasMany { diff --git a/app/Models/ServerTransfer.php b/app/Models/ServerTransfer.php index 36bf08135..354b5692c 100644 --- a/app/Models/ServerTransfer.php +++ b/app/Models/ServerTransfer.php @@ -13,8 +13,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property int $server_id * @property int $old_node * @property int $new_node - * @property int $old_allocation - * @property int $new_allocation + * @property int|null $old_allocation + * @property int|null $new_allocation * @property array|null $old_additional_allocations array of allocation.id's * @property array|null $new_additional_allocations array of allocation.id's * @property bool|null $successful @@ -45,8 +45,8 @@ class ServerTransfer extends Model implements Validatable 'server_id' => ['required', 'numeric', 'exists:servers,id'], 'old_node' => ['required', 'numeric'], 'new_node' => ['required', 'numeric'], - 'old_allocation' => ['required', 'numeric'], - 'new_allocation' => ['required', 'numeric'], + 'old_allocation' => ['nullable', 'numeric'], + 'new_allocation' => ['nullable', 'numeric'], 'old_additional_allocations' => ['nullable', 'array'], 'old_additional_allocations.*' => ['numeric'], 'new_additional_allocations' => ['nullable', 'array'], diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index 39c67c37e..2ef8f4ffa 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -107,6 +107,10 @@ class AssignmentService } } + if ($server && !$server->allocation_id) { + $server->update(['allocation_id' => $ids[0]]); + } + $this->connection->commit(); return $ids; diff --git a/app/Services/Allocations/FindAssignableAllocationService.php b/app/Services/Allocations/FindAssignableAllocationService.php index 5a439799f..395b1b287 100644 --- a/app/Services/Allocations/FindAssignableAllocationService.php +++ b/app/Services/Allocations/FindAssignableAllocationService.php @@ -37,7 +37,9 @@ class FindAssignableAllocationService // server. /** @var \App\Models\Allocation|null $allocation */ $allocation = $server->node->allocations() - ->where('ip', $server->allocation->ip) + ->when($server->allocation, function ($query) use ($server) { + $query->where('ip', $server->allocation->ip); + }) ->whereNull('server_id') ->inRandomOrder() ->first(); diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 88edbb81b..6257e6cc0 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -80,12 +80,10 @@ class BuildModificationService * @param array{ * add_allocations?: array, * remove_allocations?: array, - * allocation_id?: int, + * allocation_id: ?int, * oom_killer?: bool, * oom_disabled?: bool, * } $data - * - * @throws \App\Exceptions\DisplayException */ private function processAllocations(Server $server, array &$data): void { @@ -101,35 +99,26 @@ class BuildModificationService ->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]); + $query->update(['server_id' => $server->id]); } 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.'); - } + $allocations = Allocation::query() + ->where('server_id', $server->id) + // Only use the allocations that we didn't also attempt to add to the server... + ->whereIn('id', array_diff($data['remove_allocations'], $data['add_allocations'] ?? [])); - // Update the default allocation to be the first allocation that we are creating. - $data['allocation_id'] = $freshlyAllocated; - } + // 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 ((clone $allocations)->where('id', $server->allocation_id)->exists()) { + $nonPrimaryAllocations = $server->allocations->whereNotIn('id', $data['remove_allocations']); + $data['allocation_id'] = $nonPrimaryAllocations->first()->id ?? ($data['add_allocations'][0] ?? null); } // 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'] ?? [])) + $allocations ->update([ 'notes' => null, 'server_id' => null, diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index b0c8156c3..6561b6401 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -56,7 +56,7 @@ class ServerConfigurationStructureService * allocations: array{ * force_outgoing_ip: bool, * default: array{ip: string, port: int}, - * mappings: array, + * mappings: array>, * }, * egg: array{id: string, file_denylist: string[]}, * labels?: string[], @@ -93,8 +93,8 @@ class ServerConfigurationStructureService 'allocations' => [ 'force_outgoing_ip' => $server->egg->force_outgoing_ip, 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, + 'ip' => $server->allocation->ip ?? '127.0.0.1', + 'port' => $server->allocation->port ?? 0, ], 'mappings' => $server->getAllocationMappings(), ], diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index ef959036b..c4f9a3dd4 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -3,6 +3,7 @@ namespace App\Services\Servers; use App\Enums\ServerState; +use App\Exceptions\Service\Deployment\NoViableNodeException; use Illuminate\Http\Client\ConnectionException; use Ramsey\Uuid\Uuid; use Illuminate\Support\Arr; @@ -39,6 +40,7 @@ class ServerCreationService * no node_id the node_is will be picked from the allocation. * * @param array{ + * node_id?: int, * oom_killer?: bool, * oom_disabled?: bool, * egg_id?: int, @@ -67,19 +69,18 @@ class ServerCreationService // 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) { + if ($deployment) { $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; + if ($allocation) { + $data['allocation_id'] = $allocation->id; + // Auto-configure the node based on the selected allocation + // if no node was defined. + $data['node_id'] = $allocation->node_id; + } + $data['node_id'] ??= $deployment->getNode()->id; } + Assert::false(empty($data['node_id']), 'Expected a non-empty node_id in server creation data.'); $eggVariableData = $this->validatorService ->setUserLevel(User::USER_LEVEL_ADMIN) @@ -95,7 +96,10 @@ class ServerCreationService // Create the server and assign any additional allocations to it. $server = $this->createModel($data); - $this->storeAssignedAllocations($server, $data); + if ($server->allocation_id) { + $this->storeAssignedAllocations($server, $data); + } + $this->storeEggVariables($server, $eggVariableData); return $server; @@ -119,10 +123,10 @@ class ServerCreationService * * @param array{memory?: ?int, disk?: ?int, cpu?: ?int, tags?: ?string[]} $data * - * @throws \App\Exceptions\DisplayException * @throws \App\Exceptions\Service\Deployment\NoViableAllocationException + * @throws \App\Exceptions\Service\Deployment\NoViableNodeException */ - private function configureDeployment(array $data, DeploymentObject $deployment): Allocation + private function configureDeployment(array $data, DeploymentObject $deployment): ?Allocation { $nodes = $this->findViableNodesService->handle( Arr::get($data, 'memory', 0), @@ -131,8 +135,18 @@ class ServerCreationService $deployment->getTags(), ); + $availableNodes = $nodes->pluck('id'); + + if ($availableNodes->isEmpty()) { + throw new NoViableNodeException(trans('exceptions.deployment.no_viable_nodes')); + } + + if (!$deployment->getPorts()) { + return null; + } + return $this->allocationSelectionService->setDedicated($deployment->isDedicated()) - ->setNodes($nodes->pluck('id')->toArray()) + ->setNodes($availableNodes->toArray()) ->setPorts($deployment->getPorts()) ->handle(); } diff --git a/app/Services/Servers/StartupCommandService.php b/app/Services/Servers/StartupCommandService.php index 172795fc0..c29f3e8bb 100644 --- a/app/Services/Servers/StartupCommandService.php +++ b/app/Services/Servers/StartupCommandService.php @@ -12,7 +12,11 @@ class StartupCommandService public function handle(Server $server, bool $hideAllValues = false): string { $find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}']; - $replace = [(string) $server->memory, $server->allocation->ip, (string) $server->allocation->port]; + $replace = [ + (string) $server->memory, + $server->allocation->ip ?? '127.0.0.1', + (string) ($server->allocation->port ?? '0'), + ]; foreach ($server->variables as $variable) { $find[] = '{{' . $variable->env_variable . '}}'; diff --git a/app/Services/Servers/TransferServerService.php b/app/Services/Servers/TransferServerService.php index f9df5d647..d1aabf86d 100644 --- a/app/Services/Servers/TransferServerService.php +++ b/app/Services/Servers/TransferServerService.php @@ -41,7 +41,7 @@ class TransferServerService * * @throws \Throwable */ - public function handle(Server $server, int $node_id, int $allocation_id, array $additional_allocations): bool + public function handle(Server $server, int $node_id, ?int $allocation_id = null, ?array $additional_allocations = []): bool { $additional_allocations = array_map(intval(...), $additional_allocations); @@ -68,16 +68,18 @@ class TransferServerService $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; + if ($server->allocation_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; + + // 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); + } $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)) diff --git a/database/migrations/2025_06_03_171448_nullable_server_allocation_id.php b/database/migrations/2025_06_03_171448_nullable_server_allocation_id.php new file mode 100644 index 000000000..d9d6bbd96 --- /dev/null +++ b/database/migrations/2025_06_03_171448_nullable_server_allocation_id.php @@ -0,0 +1,34 @@ +dropForeign(['allocation_id']); + $table->dropUnique(['allocation_id']); + $table->unsignedInteger('allocation_id')->nullable()->change(); + $table->foreign('allocation_id')->references('id')->on('allocations')->nullOnDelete(); + }); + + Schema::table('server_transfers', function (Blueprint $table) { + $table->unsignedInteger('old_allocation')->nullable()->change(); + $table->unsignedInteger('new_allocation')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Not needed + } +}; diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index 6aed06229..7587101df 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -20,6 +20,8 @@ return [ 'ip' => 'IP', 'egg' => 'Egg', 'owner' => 'Owner', + 'allocation_notes' => 'Notes', + 'no_notes' => 'No notes', ], 'node_info' => 'Node Information', 'wings_version' => 'Wings Version', @@ -109,4 +111,5 @@ return [ 'error_connecting' => 'Error connecting to :node', 'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.', + 'allocation' => 'Allocation', ]; diff --git a/lang/en/admin/server.php b/lang/en/admin/server.php index 4b64320f9..3fac2ef1e 100644 --- a/lang/en/admin/server.php +++ b/lang/en/admin/server.php @@ -22,6 +22,7 @@ return [ 'no' => 'No', 'skip' => 'Skip', 'primary' => 'Primary', + 'already_primary' => 'Already Primary', 'make_primary' => 'Make Primary', 'startup_cmd' => 'Startup Command', 'default_startup' => 'Default Startup Command', @@ -122,7 +123,6 @@ return [ 'too_many_ports_body' => 'The current limit is :limit number of ports at one time.', 'invalid_port' => 'Port not in valid range', 'invalid_port_body' => ':i is not in the valid port range between :portFloor-:portCeil', - 'dissociate_primary' => 'Cannot dissociate primary allocation', 'already_exists' => 'Port already in use', 'already_exists_body' => ':i is already with an allocation', 'error_connecting' => 'Error connecting to :node', @@ -133,4 +133,6 @@ return [ 'reinstall_failed' => 'Could not start reinstall', 'log_failed' => 'Could not connect to Wings to retrieve server install log.', ], + 'notes' => 'Notes', + 'no_notes' => 'No Notes', ]; diff --git a/resources/views/livewire/server-entry.blade.php b/resources/views/livewire/server-entry.blade.php index ac64d1a5d..39272290f 100644 --- a/resources/views/livewire/server-entry.blade.php +++ b/resources/views/livewire/server-entry.blade.php @@ -41,7 +41,7 @@
diff --git a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php b/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php index 070d07724..4e1dd4022 100644 --- a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php +++ b/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php @@ -33,6 +33,23 @@ class DeleteAllocationTest extends ClientApiIntegrationTestCase $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null, 'notes' => null]); } + /** + * Test that an allocation is deleted if it is currently marked as the primary allocation + * for the server. + */ + public function test_primary_allocation_can_be_deleted_from_server(): void + { + /** @var \App\Models\Server $server */ + [$user, $server] = $this->generateTestAccount(); + $server->update(['allocation_limit' => 2]); + + $allocation = $server->allocation; + + $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. */ @@ -53,22 +70,6 @@ class DeleteAllocationTest extends ClientApiIntegrationTestCase $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 test_error_is_returned_if_allocation_is_primary(): 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 test_allocation_cannot_be_deleted_if_server_limit_is_not_defined(): void { [$user, $server] = $this->generateTestAccount(); diff --git a/tests/Integration/Services/Servers/BuildModificationServiceTest.php b/tests/Integration/Services/Servers/BuildModificationServiceTest.php index 998e29c56..154fa5c1c 100644 --- a/tests/Integration/Services/Servers/BuildModificationServiceTest.php +++ b/tests/Integration/Services/Servers/BuildModificationServiceTest.php @@ -62,7 +62,7 @@ class BuildModificationServiceTest extends IntegrationTestCase // 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); + $this->assertSame('Random notes', $response->allocation->notes); // These two allocations should not have been touched. $this->assertDatabaseHas('allocations', ['id' => $allocations[2]->id, 'server_id' => $server2->id]); @@ -75,24 +75,36 @@ class BuildModificationServiceTest extends IntegrationTestCase } /** - * Test that an exception is thrown if removing the default allocation without also assigning - * new allocations to the server. + * Test that the primary allocation can be removed. */ - public function test_exception_is_thrown_if_removing_the_default_allocation(): void + public function test_primary_allocation_can_be_removed(): void { $server = $this->createServerModel(); - /** @var \App\Models\Allocation[] $allocations */ - $allocations = Allocation::factory()->times(4)->create(['node_id' => $server->node_id]); + $server2 = $this->createServerModel(); - $allocations[0]->update(['server_id' => $server->id]); + $server->allocation->update(['notes' => 'Random Notes']); + $server2->allocation->update(['notes' => 'Random Notes']); - $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.'); + $initialAllocationId = $server->allocation->id; - $this->getService()->handle($server, [ - 'add_allocations' => [], - 'remove_allocations' => [$server->allocation_id, $allocations[0]->id], + $this->daemonServerRepository->expects('setServer->sync')->andReturnUndefined(); + + $response = $this->getService()->handle($server, [ + // Remove the default server allocation, ensuring that the new allocation passed through + // in the data becomes the default allocation. + 'remove_allocations' => [$server->allocation->id, $server2->allocation->id], ]); + + // No allocation should exist for this server now. + $this->assertEmpty($response->allocations); + $this->assertNull($response->allocation_id); + + // This allocation should not have been touched. + $this->assertDatabaseHas('allocations', ['id' => $server2->allocation->id, 'server_id' => $server2->id, 'notes' => 'Random Notes']); + + // This allocation should have been removed from the server, and have had its + // notes properly reset. + $this->assertDatabaseHas('allocations', ['id' => $initialAllocationId, 'server_id' => null, 'notes' => null]); } /** diff --git a/tests/Integration/Services/Servers/ServerCreationServiceTest.php b/tests/Integration/Services/Servers/ServerCreationServiceTest.php index c51cc1b3c..bb907b60f 100644 --- a/tests/Integration/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Integration/Services/Servers/ServerCreationServiceTest.php @@ -129,16 +129,102 @@ class ServerCreationServiceTest extends IntegrationTestCase $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); } + $this->assertFalse($response->isSuspended()); + $this->assertFalse($response->oom_killer); + $this->assertSame(0, $response->database_limit); + $this->assertSame(0, $response->allocation_limit); + $this->assertSame(0, $response->backup_limit); + $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); + } + + /** + * Test that a server without allocation can be created when a deployment object is + * provided to the service. + */ + public function test_server_without_allocation_is_created_with_deployment_object(): void + { + /** @var \App\Models\User $user */ + $user = User::factory()->create(); + + /** @var \App\Models\Node $node */ + $node = Node::factory()->create(); + + $deployment = (new DeploymentObject())->setNode($node); + + $egg = $this->cloneEggAndVariables($this->bungeecord); + // We want to make sure that the validator service runs as an admin, and not as a regular + // user when saving variables. + $egg->variables()->first()->update([ + 'user_editable' => false, + ]); + + $data = [ + 'name' => $this->faker->name(), + 'description' => $this->faker->sentence(), + 'owner_id' => $user->id, + 'memory' => 256, + 'swap' => 128, + 'disk' => 100, + 'io' => 500, + 'cpu' => 0, + 'startup' => 'java server2.jar', + 'image' => 'java:8', + 'egg_id' => $egg->id, + 'allocation_additional' => [], + 'environment' => [ + 'BUNGEE_VERSION' => '123', + 'SERVER_JARFILE' => 'server2.jar', + ], + 'start_on_completion' => true, + ]; + + $this->daemonServerRepository->expects('setServer->create')->with(true)->andReturnUndefined(); + + try { + $this->getService()->handle(array_merge($data, [ + 'environment' => [ + 'BUNGEE_VERSION' => '', + 'SERVER_JARFILE' => 'server2.jar', + ], + ]), $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->assertSame('The Bungeecord Version variable field is required.', $exception->errors()['environment.BUNGEE_VERSION'][0]); + } + + $response = $this->getService()->handle($data, $deployment); + + $this->assertInstanceOf(Server::class, $response); + $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->assertSame('123', $response->variables()->firstWhere('env_variable', 'BUNGEE_VERSION')->server_value); + $this->assertSame('server2.jar', $response->variables()->firstWhere('env_variable', 'SERVER_JARFILE')->server_value); + + foreach ($data as $key => $value) { + if (in_array($key, ['allocation_additional', 'environment', 'start_on_completion'])) { + continue; + } + + $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); + } $this->assertFalse($response->isSuspended()); $this->assertFalse($response->oom_killer); $this->assertSame(0, $response->database_limit); $this->assertSame(0, $response->allocation_limit); $this->assertSame(0, $response->backup_limit); + + $this->assertEmpty($response->allocations); + $this->assertNull($response->allocation_id); } /** From 68f72b9b4d98ea687f890cff0122f839e60ce1a7 Mon Sep 17 00:00:00 2001 From: Boy132 Date: Thu, 26 Jun 2025 01:50:09 +0200 Subject: [PATCH 4/4] Add "egg index" and dropdown to egg importer (#1451) Co-authored-by: notCharles --- .../Commands/Egg/UpdateEggIndexCommand.php | 46 +++++++++++++++++++ app/Console/Kernel.php | 5 +- .../Components/Actions/ImportEggAction.php | 27 +++++++++++ .../Tables/Actions/ImportEggAction.php | 19 ++++++++ lang/en/admin/egg.php | 2 + 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/Egg/UpdateEggIndexCommand.php diff --git a/app/Console/Commands/Egg/UpdateEggIndexCommand.php b/app/Console/Commands/Egg/UpdateEggIndexCommand.php new file mode 100644 index 000000000..8ef7646b2 --- /dev/null +++ b/app/Console/Commands/Egg/UpdateEggIndexCommand.php @@ -0,0 +1,46 @@ +error($exception->getMessage()); + + return 1; + } + + $index = []; + foreach ($data['nests'] as $nest) { + $nestName = $nest['nest_type']; + + $this->info("Nest: $nestName"); + + $nestEggs = []; + foreach ($nest['Eggs'] as $egg) { + $eggName = $egg['egg']['name']; + + $this->comment("Egg: $eggName"); + + $nestEggs[$egg['download_url']] = $eggName; + } + $index[$nestName] = $nestEggs; + + $this->info(''); + } + + cache()->forever('eggs.index', $index); + + return 0; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 48fbe6b84..8421c1b82 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -3,6 +3,7 @@ namespace App\Console; use App\Console\Commands\Egg\CheckEggUpdatesCommand; +use App\Console\Commands\Egg\UpdateEggIndexCommand; use App\Console\Commands\Maintenance\CleanServiceBackupFilesCommand; use App\Console\Commands\Maintenance\PruneImagesCommand; use App\Console\Commands\Maintenance\PruneOrphanedBackupsCommand; @@ -41,7 +42,9 @@ class Kernel extends ConsoleKernel $schedule->command(CleanServiceBackupFilesCommand::class)->daily(); $schedule->command(PruneImagesCommand::class)->daily(); - $schedule->command(CheckEggUpdatesCommand::class)->hourly(); + + $schedule->command(CheckEggUpdatesCommand::class)->daily(); + $schedule->command(UpdateEggIndexCommand::class)->daily(); if (config('backups.prune_age')) { // Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted. diff --git a/app/Filament/Components/Actions/ImportEggAction.php b/app/Filament/Components/Actions/ImportEggAction.php index c6e3b8b58..615cbe0ba 100644 --- a/app/Filament/Components/Actions/ImportEggAction.php +++ b/app/Filament/Components/Actions/ImportEggAction.php @@ -2,6 +2,7 @@ namespace App\Filament\Components\Actions; +use App\Console\Commands\Egg\UpdateEggIndexCommand; use App\Models\Egg; use App\Services\Eggs\Sharing\EggImporterService; use Closure; @@ -9,11 +10,16 @@ use Exception; use Filament\Actions\Action; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Repeater; +use Filament\Forms\Components\Select; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs\Tab; use Filament\Forms\Components\TextInput; +use Filament\Forms\Get; +use Filament\Forms\Set; use Filament\Notifications\Notification; use Illuminate\Support\Arr; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Str; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; class ImportEggAction extends Action @@ -97,7 +103,28 @@ class ImportEggAction extends Action Tab::make(trans('admin/egg.import.url')) ->icon('tabler-world-upload') ->schema([ + Select::make('github') + ->label(trans('admin/egg.import.github')) + ->options(cache('eggs.index')) + ->selectablePlaceholder(false) + ->searchable() + ->preload() + ->live() + ->hintIcon('tabler-refresh') + ->hintIconTooltip(trans('admin/egg.import.refresh')) + ->hintAction(function () { + Artisan::call(UpdateEggIndexCommand::class); + }) + ->afterStateUpdated(function ($state, Set $set, Get $get) use ($isMultiple) { + if ($state) { + $urls = $isMultiple ? $get('urls') : []; + $urls[Str::uuid()->toString()] = ['url' => $state]; + $set('urls', $urls); + $set('github', null); + } + }), Repeater::make('urls') + ->label('') ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline()) ->hint(trans('admin/egg.import.url_help')) ->addActionLabel(trans('admin/egg.import.add_url')) diff --git a/app/Filament/Components/Tables/Actions/ImportEggAction.php b/app/Filament/Components/Tables/Actions/ImportEggAction.php index 426ba46b9..b51576535 100644 --- a/app/Filament/Components/Tables/Actions/ImportEggAction.php +++ b/app/Filament/Components/Tables/Actions/ImportEggAction.php @@ -8,12 +8,16 @@ use Closure; use Exception; use Filament\Forms\Components\FileUpload; use Filament\Forms\Components\Repeater; +use Filament\Forms\Components\Select; use Filament\Forms\Components\Tabs; use Filament\Forms\Components\Tabs\Tab; use Filament\Forms\Components\TextInput; +use Filament\Forms\Get; +use Filament\Forms\Set; use Filament\Notifications\Notification; use Filament\Tables\Actions\Action; use Illuminate\Support\Arr; +use Illuminate\Support\Str; use Livewire\Features\SupportFileUploads\TemporaryUploadedFile; class ImportEggAction extends Action @@ -97,6 +101,21 @@ class ImportEggAction extends Action Tab::make(trans('admin/egg.import.url')) ->icon('tabler-world-upload') ->schema([ + Select::make('github') + ->label(trans('admin/egg.import.github')) + ->options(cache('eggs.index')) + ->selectablePlaceholder(false) + ->searchable() + ->preload() + ->live() + ->afterStateUpdated(function ($state, Set $set, Get $get) use ($isMultiple) { + if ($state) { + $urls = $isMultiple ? $get('urls') : []; + $urls[Str::uuid()->toString()] = ['url' => $state]; + $set('urls', $urls); + $set('github', null); + } + }), Repeater::make('urls') ->itemLabel(fn (array $state) => str($state['url'])->afterLast('/egg-')->before('.json')->headline()) ->hint(trans('admin/egg.import.url_help')) diff --git a/lang/en/admin/egg.php b/lang/en/admin/egg.php index db7171f2b..365fa1670 100644 --- a/lang/en/admin/egg.php +++ b/lang/en/admin/egg.php @@ -18,6 +18,8 @@ return [ 'add_url' => 'New URL', 'import_failed' => 'Import Failed', 'import_success' => 'Import Success', + 'github' => 'Add from Github', + 'refresh' => 'Refresh', ], 'in_use' => 'In Use', 'servers' => 'Servers',