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/Exceptions/Repository/FileExistsException.php b/app/Exceptions/Repository/FileExistsException.php new file mode 100644 index 000000000..b71649483 --- /dev/null +++ b/app/Exceptions/Repository/FileExistsException.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/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/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/Resources/FileResource/Pages/ListFiles.php b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php index d490d1ee8..9fa1de342 100644 --- a/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php +++ b/app/Filament/Server/Resources/FileResource/Pages/ListFiles.php @@ -4,6 +4,7 @@ namespace App\Filament\Server\Resources\FileResource\Pages; use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; use App\Enums\EditorLanguages; +use App\Exceptions\Repository\FileExistsException; use App\Facades\Activity; use App\Filament\Server\Resources\FileResource; use App\Models\File; @@ -12,6 +13,7 @@ use App\Models\Server; use App\Repositories\Daemon\DaemonFileRepository; use App\Filament\Components\Tables\Columns\BytesColumn; use App\Filament\Components\Tables\Columns\DateTimeColumn; +use App\Livewire\AlertBanner; use App\Traits\Filament\CanCustomizeHeaderActions; use App\Traits\Filament\CanCustomizeHeaderWidgets; use Filament\Actions\Action as HeaderAction; @@ -419,11 +421,22 @@ class ListFiles extends ListRecords ->keyBindings('') ->modalSubmitActionLabel('Create') ->action(function ($data) { - $this->getDaemonFileRepository()->putContent(join_paths($this->path, $data['name']), $data['editor'] ?? ''); + $path = join_paths($this->path, $data['name']); + try { + $this->getDaemonFileRepository()->putContent($path, $data['editor'] ?? ''); - Activity::event('server:file.write') - ->property('file', join_paths($this->path, $data['name'])) - ->log(); + Activity::event('server:file.write') + ->property('file', join_paths($path, $data['name'])) + ->log(); + } catch (FileExistsException) { + AlertBanner::make() + ->title('' . $path . ' already exists!') + ->danger() + ->closable() + ->send(); + + $this->redirect(self::getUrl(['path' => dirname($path)])); + } }) ->form([ TextInput::make('name') @@ -448,11 +461,22 @@ class ListFiles extends ListRecords ->label('New Folder') ->color('gray') ->action(function ($data) { - $this->getDaemonFileRepository()->createDirectory($data['name'], $this->path); + try { + $this->getDaemonFileRepository()->createDirectory($data['name'], $this->path); - Activity::event('server:file.create-directory') - ->property(['directory' => $this->path, 'name' => $data['name']]) - ->log(); + Activity::event('server:file.create-directory') + ->property(['directory' => $this->path, 'name' => $data['name']]) + ->log(); + } catch (FileExistsException) { + $path = join_paths($this->path, $data['name']); + AlertBanner::make() + ->title('' . $path . ' already exists!') + ->danger() + ->closable() + ->send(); + + $this->redirect(self::getUrl(['path' => dirname($path)])); + } }) ->form([ TextInput::make('name') diff --git a/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php b/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php index 5b7b2ffe4..141818a54 100644 --- a/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php +++ b/app/Filament/Server/Resources/ScheduleResource/RelationManagers/TasksRelationManager.php @@ -6,6 +6,7 @@ use App\Facades\Activity; use App\Models\Schedule; use App\Models\Task; use Filament\Forms\Components\Field; +use Filament\Forms\Set; use Filament\Tables\Actions\DeleteAction; use Filament\Forms\Components\Select; use Filament\Forms\Components\Textarea; @@ -45,10 +46,11 @@ class TasksRelationManager extends RelationManager Select::make('action') ->required() ->live() - ->disableOptionWhen(fn (string $value): bool => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0) + ->disableOptionWhen(fn (string $value) => $value === Task::ACTION_BACKUP && $schedule->server->backup_limit === 0) ->options($this->getActionOptions()) ->selectablePlaceholder(false) - ->default(Task::ACTION_POWER), + ->default(Task::ACTION_POWER) + ->afterStateUpdated(fn ($state, Set $set) => $set('payload', $state === Task::ACTION_POWER ? 'restart' : null)), Textarea::make('payload') ->hidden(fn (Get $get) => $get('action') === Task::ACTION_POWER) ->label(fn (Get $get) => $this->getActionOptions(false)[$get('action')] ?? 'Payload'), @@ -81,7 +83,8 @@ class TasksRelationManager extends RelationManager $schedule = $this->getOwnerRecord(); return $table - ->reorderable('sequence_id', true) + ->reorderable('sequence_id') + ->defaultSort('sequence_id') ->columns([ TextColumn::make('action') ->state(fn (Task $task) => $this->getActionOptions()[$task->action] ?? $task->action), diff --git a/app/Filament/Server/Widgets/ServerOverview.php b/app/Filament/Server/Widgets/ServerOverview.php index 0f7455cb1..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()), @@ -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/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 35b1a3095..958c6bbbc 100644 --- a/app/Livewire/ServerEntry.php +++ b/app/Livewire/ServerEntry.php @@ -24,7 +24,7 @@ class ServerEntry extends Component style="background-color: #D97706;"> -
+

@@ -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/Repositories/Daemon/DaemonFileRepository.php b/app/Repositories/Daemon/DaemonFileRepository.php index f7d5c78e3..b1385fe92 100644 --- a/app/Repositories/Daemon/DaemonFileRepository.php +++ b/app/Repositories/Daemon/DaemonFileRepository.php @@ -5,6 +5,7 @@ namespace App\Repositories\Daemon; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Http\Client\Response; use App\Exceptions\Http\Server\FileSizeTooLargeException; +use App\Exceptions\Repository\FileExistsException; use App\Exceptions\Repository\FileNotEditableException; use Illuminate\Http\Client\ConnectionException; @@ -46,13 +47,20 @@ class DaemonFileRepository extends DaemonRepository * a file. * * @throws ConnectionException + * @throws FileExistsException */ public function putContent(string $path, string $content): Response { - return $this->getHttpClient() + $response = $this->getHttpClient() ->withQueryParameters(['file' => $path]) ->withBody($content) ->post("/api/servers/{$this->server->uuid}/files/write"); + + if ($response->getStatusCode() === 400) { + throw new FileExistsException(); + } + + return $response; } /** @@ -73,15 +81,22 @@ class DaemonFileRepository extends DaemonRepository * Creates a new directory for the server in the given $path. * * @throws ConnectionException + * @throws FileExistsException */ public function createDirectory(string $name, string $path): Response { - return $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/create-directory", + $response = $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/files/create-directory", [ 'name' => $name, 'path' => $path, ] ); + + if ($response->getStatusCode() === 400) { + throw new FileExistsException(); + } + + return $response; } /** diff --git a/app/Repositories/Daemon/DaemonRepository.php b/app/Repositories/Daemon/DaemonRepository.php index 6b3b553d6..6d58e7646 100644 --- a/app/Repositories/Daemon/DaemonRepository.php +++ b/app/Repositories/Daemon/DaemonRepository.php @@ -57,6 +57,9 @@ abstract class DaemonRepository if (is_bool($condition)) { return $condition; } + if ($condition->clientError()) { + return false; + } $header = $condition->header('User-Agent'); if ( 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 f62f0edd5..61e31cd1c 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -58,7 +58,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[], features: string[][]}, * labels?: string[], @@ -95,8 +95,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 542693cc8..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,20 +123,30 @@ 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), Arr::get($data, 'disk', 0), Arr::get($data, 'cpu', 0), - Arr::get($data, 'tags', []), + $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/composer.json b/composer.json index fe3ef777e..5ecff2d06 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ "doctrine/dbal": "~3.6.0", "filament/filament": "^3.3", "guzzlehttp/guzzle": "^7.9", - "laravel/framework": "^12.18", + "laravel/framework": "^12.19", "laravel/helpers": "^1.7", "laravel/sanctum": "^4.1", "laravel/socialite": "^5.21", @@ -104,4 +104,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index ff0ce5af1..44633bcbc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a006241b5687d547b51a60e6ac50ccae", + "content-hash": "ee36fd7a30d4f56f0d3019267e62f834", "packages": [ { "name": "abdelhamiderrahmouni/filament-monaco-editor", @@ -1020,16 +1020,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.344.3", + "version": "3.345.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "0cf789243c7de82be7d3f7641cab37b5bb5d937d" + "reference": "61b4675bc02db8d7f3e1ba6931dc827c5ae23aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0cf789243c7de82be7d3f7641cab37b5bb5d937d", - "reference": "0cf789243c7de82be7d3f7641cab37b5bb5d937d", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/61b4675bc02db8d7f3e1ba6931dc827c5ae23aa8", + "reference": "61b4675bc02db8d7f3e1ba6931dc827c5ae23aa8", "shasum": "" }, "require": { @@ -1111,9 +1111,9 @@ "support": { "forum": "https://github.com/aws/aws-sdk-php/discussions", "issues": "https://github.com/aws/aws-sdk-php/issues", - "source": "https://github.com/aws/aws-sdk-php/tree/3.344.3" + "source": "https://github.com/aws/aws-sdk-php/tree/3.345.0" }, - "time": "2025-06-02T18:04:47+00:00" + "time": "2025-06-17T18:09:42+00:00" }, { "name": "blade-ui-kit/blade-heroicons", @@ -1758,16 +1758,16 @@ }, { "name": "dedoc/scramble", - "version": "v0.12.22", + "version": "v0.12.23", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "3c06a756d4fc20a281638e8ba9941f6463000d78" + "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/3c06a756d4fc20a281638e8ba9941f6463000d78", - "reference": "3c06a756d4fc20a281638e8ba9941f6463000d78", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/5b650167c81c59138e844c2ae550c14dc1a249d0", + "reference": "5b650167c81c59138e844c2ae550c14dc1a249d0", "shasum": "" }, "require": { @@ -1826,7 +1826,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.12.22" + "source": "https://github.com/dedoc/scramble/tree/v0.12.23" }, "funding": [ { @@ -1834,7 +1834,7 @@ "type": "github" } ], - "time": "2025-06-03T07:50:53+00:00" + "time": "2025-06-15T09:04:49+00:00" }, { "name": "dflydev/dot-access-data", @@ -2558,16 +2558,16 @@ }, { "name": "filament/actions", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/actions.git", - "reference": "151f776552ee10d70591c2649708bc4b0a7cba91" + "reference": "67dd0da772f19e2d74e60eb53f99330faf183892" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/actions/zipball/151f776552ee10d70591c2649708bc4b0a7cba91", - "reference": "151f776552ee10d70591c2649708bc4b0a7cba91", + "url": "https://api.github.com/repos/filamentphp/actions/zipball/67dd0da772f19e2d74e60eb53f99330faf183892", + "reference": "67dd0da772f19e2d74e60eb53f99330faf183892", "shasum": "" }, "require": { @@ -2607,20 +2607,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-03T06:15:27+00:00" + "time": "2025-06-13T14:47:50+00:00" }, { "name": "filament/filament", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/panels.git", - "reference": "8d915ef313835f46f49175396de82feb0166d8a8" + "reference": "b060d2d01a969e3b6541ab4f1e24c745352f51c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/panels/zipball/8d915ef313835f46f49175396de82feb0166d8a8", - "reference": "8d915ef313835f46f49175396de82feb0166d8a8", + "url": "https://api.github.com/repos/filamentphp/panels/zipball/b060d2d01a969e3b6541ab4f1e24c745352f51c1", + "reference": "b060d2d01a969e3b6541ab4f1e24c745352f51c1", "shasum": "" }, "require": { @@ -2672,20 +2672,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-10T16:10:42+00:00" + "time": "2025-06-12T15:10:00+00:00" }, { "name": "filament/forms", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/forms.git", - "reference": "014dd23a7691dc25bb037f26df852cfec5602d01" + "reference": "586a13f9d2a6f395ffdc8557730c85a5858b4c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/forms/zipball/014dd23a7691dc25bb037f26df852cfec5602d01", - "reference": "014dd23a7691dc25bb037f26df852cfec5602d01", + "url": "https://api.github.com/repos/filamentphp/forms/zipball/586a13f9d2a6f395ffdc8557730c85a5858b4c4f", + "reference": "586a13f9d2a6f395ffdc8557730c85a5858b4c4f", "shasum": "" }, "require": { @@ -2728,11 +2728,11 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-10T16:10:45+00:00" + "time": "2025-06-12T15:10:19+00:00" }, { "name": "filament/infolists", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/infolists.git", @@ -2783,7 +2783,7 @@ }, { "name": "filament/notifications", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/notifications.git", @@ -2835,16 +2835,16 @@ }, { "name": "filament/support", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/support.git", - "reference": "5c140580d7feeabb4d2b0007c854676ae87be1b3" + "reference": "7d850347ffbd8c84d84040ffb8c5ceb0f709a9fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/support/zipball/5c140580d7feeabb4d2b0007c854676ae87be1b3", - "reference": "5c140580d7feeabb4d2b0007c854676ae87be1b3", + "url": "https://api.github.com/repos/filamentphp/support/zipball/7d850347ffbd8c84d84040ffb8c5ceb0f709a9fe", + "reference": "7d850347ffbd8c84d84040ffb8c5ceb0f709a9fe", "shasum": "" }, "require": { @@ -2890,20 +2890,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-10T16:10:55+00:00" + "time": "2025-06-12T15:02:34+00:00" }, { "name": "filament/tables", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/tables.git", - "reference": "3d52c23443f6846774a6a2ce60f6e6173ce20943" + "reference": "920204cd5ec1550209cf398fea8dba3dece979de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/tables/zipball/3d52c23443f6846774a6a2ce60f6e6173ce20943", - "reference": "3d52c23443f6846774a6a2ce60f6e6173ce20943", + "url": "https://api.github.com/repos/filamentphp/tables/zipball/920204cd5ec1550209cf398fea8dba3dece979de", + "reference": "920204cd5ec1550209cf398fea8dba3dece979de", "shasum": "" }, "require": { @@ -2942,20 +2942,20 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-06-10T16:10:40+00:00" + "time": "2025-06-12T15:01:25+00:00" }, { "name": "filament/widgets", - "version": "v3.3.21", + "version": "v3.3.26", "source": { "type": "git", "url": "https://github.com/filamentphp/widgets.git", - "reference": "048c5a4bf0477efbe2910c54a1aeb55c64cf1348" + "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filamentphp/widgets/zipball/048c5a4bf0477efbe2910c54a1aeb55c64cf1348", - "reference": "048c5a4bf0477efbe2910c54a1aeb55c64cf1348", + "url": "https://api.github.com/repos/filamentphp/widgets/zipball/5b956f884aaef479f6091463cb829e7c9f2afc2c", + "reference": "5b956f884aaef479f6091463cb829e7c9f2afc2c", "shasum": "" }, "require": { @@ -2986,7 +2986,7 @@ "issues": "https://github.com/filamentphp/filament/issues", "source": "https://github.com/filamentphp/filament" }, - "time": "2025-04-23T06:39:59+00:00" + "time": "2025-06-12T15:11:14+00:00" }, { "name": "firebase/php-jwt", @@ -3718,16 +3718,16 @@ }, { "name": "laravel/framework", - "version": "v12.18.0", + "version": "v12.19.3", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "7d264a0dad2bfc5c154240b38e8ce9b2c4cdd14d" + "reference": "4e6ec689ef704bb4bd282f29d9dd658dfb4fb262" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/7d264a0dad2bfc5c154240b38e8ce9b2c4cdd14d", - "reference": "7d264a0dad2bfc5c154240b38e8ce9b2c4cdd14d", + "url": "https://api.github.com/repos/laravel/framework/zipball/4e6ec689ef704bb4bd282f29d9dd658dfb4fb262", + "reference": "4e6ec689ef704bb4bd282f29d9dd658dfb4fb262", "shasum": "" }, "require": { @@ -3929,7 +3929,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2025-06-10T14:48:34+00:00" + "time": "2025-06-18T12:56:23+00:00" }, { "name": "laravel/helpers", @@ -5776,16 +5776,16 @@ }, { "name": "nesbot/carbon", - "version": "3.9.1", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon.git", - "reference": "ced71f79398ece168e24f7f7710462f462310d4d" + "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/ced71f79398ece168e24f7f7710462f462310d4d", - "reference": "ced71f79398ece168e24f7f7710462f462310d4d", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", + "reference": "c1397390dd0a7e0f11660f0ae20f753d88c1f3d9", "shasum": "" }, "require": { @@ -5793,9 +5793,9 @@ "ext-json": "*", "php": "^8.1", "psr/clock": "^1.0", - "symfony/clock": "^6.3 || ^7.0", + "symfony/clock": "^6.3.12 || ^7.0", "symfony/polyfill-mbstring": "^1.0", - "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" }, "provide": { "psr/clock-implementation": "1.0" @@ -5803,14 +5803,13 @@ "require-dev": { "doctrine/dbal": "^3.6.3 || ^4.0", "doctrine/orm": "^2.15.2 || ^3.0", - "friendsofphp/php-cs-fixer": "^3.57.2", + "friendsofphp/php-cs-fixer": "^3.75.0", "kylekatarnls/multi-tester": "^2.5.3", - "ondrejmirtes/better-reflection": "^6.25.0.4", "phpmd/phpmd": "^2.15.0", - "phpstan/extension-installer": "^1.3.1", - "phpstan/phpstan": "^1.11.2", - "phpunit/phpunit": "^10.5.20", - "squizlabs/php_codesniffer": "^3.9.0" + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" }, "bin": [ "bin/carbon" @@ -5878,7 +5877,7 @@ "type": "tidelift" } ], - "time": "2025-05-01T19:51:51+00:00" + "time": "2025-06-12T10:24:28+00:00" }, { "name": "nette/schema", @@ -6707,16 +6706,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.43", + "version": "3.0.44", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02" + "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02", - "reference": "709ec107af3cb2f385b9617be72af8cf62441d02", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/1d0b5e7e1434678411787c5a0535e68907cf82d9", + "reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9", "shasum": "" }, "require": { @@ -6797,7 +6796,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.43" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.44" }, "funding": [ { @@ -6813,7 +6812,7 @@ "type": "tidelift" } ], - "time": "2024-12-14T21:12:59+00:00" + "time": "2025-06-15T09:59:26+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -8461,16 +8460,16 @@ }, { "name": "spatie/laravel-data", - "version": "4.15.1", + "version": "4.15.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-data.git", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9" + "reference": "50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-data/zipball/cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", - "reference": "cb97afe6c0dadeb2e76ea1b7220cd04ed33dcca9", + "url": "https://api.github.com/repos/spatie/laravel-data/zipball/50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d", + "reference": "50f5abe716ff1ad9a3e96dcfdeb4ad00f014bf8d", "shasum": "" }, "require": { @@ -8532,7 +8531,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-data/issues", - "source": "https://github.com/spatie/laravel-data/tree/4.15.1" + "source": "https://github.com/spatie/laravel-data/tree/4.15.2" }, "funding": [ { @@ -8540,7 +8539,7 @@ "type": "github" } ], - "time": "2025-04-10T06:06:27+00:00" + "time": "2025-06-12T09:42:08+00:00" }, { "name": "spatie/laravel-fractal", @@ -8779,16 +8778,16 @@ }, { "name": "spatie/laravel-permission", - "version": "6.19.0", + "version": "6.20.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-permission.git", - "reference": "0cd412dcad066d75caf0b977716809be7e7642fd" + "reference": "31c05679102c73f3b0d05790d2400182745a5615" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/0cd412dcad066d75caf0b977716809be7e7642fd", - "reference": "0cd412dcad066d75caf0b977716809be7e7642fd", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/31c05679102c73f3b0d05790d2400182745a5615", + "reference": "31c05679102c73f3b0d05790d2400182745a5615", "shasum": "" }, "require": { @@ -8850,7 +8849,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-permission/issues", - "source": "https://github.com/spatie/laravel-permission/tree/6.19.0" + "source": "https://github.com/spatie/laravel-permission/tree/6.20.0" }, "funding": [ { @@ -8858,7 +8857,7 @@ "type": "github" } ], - "time": "2025-05-31T00:50:27+00:00" + "time": "2025-06-05T07:33:07+00:00" }, { "name": "spatie/laravel-query-builder", @@ -12668,16 +12667,16 @@ }, { "name": "filp/whoops", - "version": "2.18.1", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26", - "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -12727,7 +12726,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.1" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -12735,7 +12734,7 @@ "type": "github" } ], - "time": "2025-06-03T18:56:14+00:00" + "time": "2025-06-16T00:02:10+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -12891,16 +12890,16 @@ }, { "name": "larastan/larastan", - "version": "v3.4.1", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "dc20d24871d5a2138b292b0430d94d18da3dbc53" + "reference": "36706736a0c51d3337478fab9c919d78d2e03404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/dc20d24871d5a2138b292b0430d94d18da3dbc53", - "reference": "dc20d24871d5a2138b292b0430d94d18da3dbc53", + "url": "https://api.github.com/repos/larastan/larastan/zipball/36706736a0c51d3337478fab9c919d78d2e03404", + "reference": "36706736a0c51d3337478fab9c919d78d2e03404", "shasum": "" }, "require": { @@ -12968,7 +12967,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.4.1" + "source": "https://github.com/larastan/larastan/tree/v3.4.2" }, "funding": [ { @@ -12976,7 +12975,7 @@ "type": "github" } ], - "time": "2025-06-09T21:23:36+00:00" + "time": "2025-06-10T09:34:58+00:00" }, { "name": "laravel/pail", @@ -13271,23 +13270,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.8.0", + "version": "v8.8.1", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8" + "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/4cf9f3b47afff38b139fb79ce54fc71799022ce8", - "reference": "4cf9f3b47afff38b139fb79ce54fc71799022ce8", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/44ccb82e3e21efb5446748d2a3c81a030ac22bd5", + "reference": "44ccb82e3e21efb5446748d2a3c81a030ac22bd5", "shasum": "" }, "require": { - "filp/whoops": "^2.18.0", - "nunomaduro/termwind": "^2.3.0", + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", "php": "^8.2.0", - "symfony/console": "^7.2.5" + "symfony/console": "^7.3.0" }, "conflict": { "laravel/framework": "<11.44.2 || >=13.0.0", @@ -13295,15 +13294,15 @@ }, "require-dev": { "brianium/paratest": "^7.8.3", - "larastan/larastan": "^3.2", - "laravel/framework": "^11.44.2 || ^12.6", - "laravel/pint": "^1.21.2", - "laravel/sail": "^1.41.0", - "laravel/sanctum": "^4.0.8", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", "laravel/tinker": "^2.10.1", - "orchestra/testbench-core": "^9.12.0 || ^10.1", - "pestphp/pest": "^3.8.0", - "sebastian/environment": "^7.2.0 || ^8.0" + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", "extra": { @@ -13366,7 +13365,7 @@ "type": "patreon" } ], - "time": "2025-04-03T14:33:09+00:00" + "time": "2025-06-11T01:04:21+00:00" }, { "name": "pestphp/pest", @@ -14001,16 +14000,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "11.0.9", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/14d63fbcca18457e49c6f8bebaa91a87e8e188d7", - "reference": "14d63fbcca18457e49c6f8bebaa91a87e8e188d7", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "shasum": "" }, "require": { @@ -14067,15 +14066,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.9" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" } ], - "time": "2025-02-25T13:26:39+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", 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/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', 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 6f4b25775..39272290f 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 +
+ +
+ 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); } /**