Merge branch 'main' into vehikl/singleton

This commit is contained in:
Vehikl 2025-06-26 17:06:19 -04:00
commit 6ef4e32960
47 changed files with 649 additions and 315 deletions

View File

@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands\Egg;
use Exception;
use Illuminate\Console\Command;
class UpdateEggIndexCommand extends Command
{
protected $signature = 'p:egg:update-index';
public function handle(): int
{
try {
$data = file_get_contents('https://raw.githubusercontent.com/pelican-eggs/pelican-eggs.github.io/refs/heads/main/content/pelican.json');
$data = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (Exception $exception) {
$this->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;
}
}

View File

@ -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.

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Repository;
use Exception;
class FileExistsException extends Exception {}

View File

@ -0,0 +1,7 @@
<?php
namespace App\Exceptions\Service\Deployment;
use App\Exceptions\DisplayException;
class NoViableNodeException extends DisplayException {}

View File

@ -38,8 +38,9 @@ class ServersRelationManager extends RelationManager
->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(),
]);
}

View File

@ -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'))

View File

@ -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'),

View File

@ -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);

View File

@ -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')),

View File

@ -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')

View File

@ -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]);
}),
]);
}

View File

@ -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')

View File

@ -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')

View File

@ -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'))

View File

@ -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'))

View File

@ -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])),
]);
}

View File

@ -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)

View File

@ -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('<code>' . $path . '</code> 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('<code>' . $path . '</code> already exists!')
->danger()
->closable()
->send();
$this->redirect(self::getUrl(['path' => dirname($path)]));
}
})
->form([
TextInput::make('name')

View File

@ -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),

View File

@ -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);

View File

@ -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,

View File

@ -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);

View File

@ -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;
}

View File

@ -24,7 +24,7 @@ class ServerEntry extends Component
style="background-color: #D97706;">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex-1 dark:bg-gray-850 dark:text-white rounded-lg overflow-hidden p-2">
<div class="flex items-center mb-5 gap-2">
<x-filament::loading-indicator class="h-5 w-5" />
<h2 class="text-xl font-bold">
@ -54,7 +54,7 @@ class ServerEntry extends Component
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
<p class="text-md font-semibold">{{ $server->allocation?->address ?? 'None' }} </p>
</div>
</div>
</div>

View File

@ -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

View File

@ -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<int|string> */
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;

View File

@ -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<int>
* @return array<string, array<int>>
*/
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<Allocation, $this>
*/
public function allocations(): HasMany
{

View File

@ -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<int>|null $old_additional_allocations array of allocation.id's
* @property array<int>|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'],

View File

@ -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;
}
/**

View File

@ -57,6 +57,9 @@ abstract class DaemonRepository
if (is_bool($condition)) {
return $condition;
}
if ($condition->clientError()) {
return false;
}
$header = $condition->header('User-Agent');
if (

View File

@ -107,6 +107,10 @@ class AssignmentService
}
}
if ($server && !$server->allocation_id) {
$server->update(['allocation_id' => $ids[0]]);
}
$this->connection->commit();
return $ids;

View File

@ -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();

View File

@ -80,12 +80,10 @@ class BuildModificationService
* @param array{
* add_allocations?: array<int>,
* remove_allocations?: array<int>,
* 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,

View File

@ -58,7 +58,7 @@ class ServerConfigurationStructureService
* allocations: array{
* force_outgoing_ip: bool,
* default: array{ip: string, port: int},
* mappings: array<int>,
* mappings: array<string, array<int>>,
* },
* 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(),
],

View File

@ -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();
}

View File

@ -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 . '}}';

View File

@ -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))

View File

@ -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
}
}

241
composer.lock generated
View File

@ -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",

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('servers', function (Blueprint $table) {
$table->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
}
};

View File

@ -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',

View File

@ -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',
];

View File

@ -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',
];

View File

@ -5,7 +5,7 @@
style="background-color: {{ $server->condition->getColor(true) }};">
</div>
<div class="flex-1 dark:bg-gray-800 dark:text-white rounded-lg overflow-hidden p-3">
<div class="flex-1 dark:bg-gray-850 dark:text-white rounded-t-lg overflow-hidden p-2">
<div class="flex items-center mb-5 gap-2">
<x-filament::icon-button
:icon="$server->condition->getIcon()"
@ -41,15 +41,17 @@
<div class="hidden sm:block">
<p class="text-sm dark:text-gray-400">Network</p>
<hr class="p-0.5">
<p class="text-md font-semibold">{{ $server->allocation->address }} </p>
<p class="text-md font-semibold">{{ $server->allocation?->address ?? 'None' }} </p>
</div>
</div>
</div>
</a>
<x-filament-tables::actions
:actions="\App\Filament\App\Resources\ServerResource\Pages\ListServers::getPowerActions()"
:alignment="\Filament\Support\Enums\Alignment::Center"
:record="$server"
/>
</div>
<div class="flex-1 dark:bg-gray-850 dark:text-white rounded-b-lg overflow-hidden p-1">
<x-filament-tables::actions
:actions="\App\Filament\App\Resources\ServerResource\Pages\ListServers::getPowerActions()"
:alignment="\Filament\Support\Enums\Alignment::Center"
:record="$server"
/>
</div>
</div>

View File

@ -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();

View File

@ -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]);
}
/**

View File

@ -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);
}
/**