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
Network
-
{{ $server->allocation->address }}
+
{{ $server->allocation?->address ?? 'None' }}
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 @@
Network
-
{{ $server->allocation->address }}
+
{{ $server->allocation?->address ?? 'None' }}
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);
}
/**