Upstream pelican/panel state of the touched files before 2a80f9bf21
This commit is contained in:
1 parent
8860a5c5e5
commit
72779c7ea6
6 files changed
+1204
No files matched your search
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Admin\Resources\Servers\RelationManagers;
|
||||
|
||||
use App\Enums\TablerIcon;
|
||||
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use BackedEnum;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\AssociateAction;
|
||||
use Filament\Actions\CreateAction;
|
||||
use Filament\Actions\DissociateAction;
|
||||
use Filament\Actions\DissociateBulkAction;
|
||||
use Filament\Forms\Components\Hidden;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Schemas\Components\Utilities\Get;
|
||||
use Filament\Schemas\Components\Utilities\Set;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Columns\ToggleColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
|
||||
protected static string|BackedEnum|null $icon = TablerIcon::Network;
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->heading(null)
|
||||
->selectCurrentPageOnly()
|
||||
->recordTitleAttribute('address')
|
||||
->recordTitle(fn (Allocation $allocation) => $allocation->address)
|
||||
->inverseRelationship('server')
|
||||
->columns([
|
||||
TextColumn::make('ip')
|
||||
->label(trans('admin/server.ip_address'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
TextColumn::make('port')
|
||||
->label(trans('admin/server.port'))
|
||||
->searchable()
|
||||
->sortable(),
|
||||
ToggleColumn::make('show_port')
|
||||
->label(trans('admin/server.show_port')),
|
||||
TextInputColumn::make('ip_alias')
|
||||
->label(trans('admin/server.alias'))
|
||||
->placeholder(trans('admin/server.no_alias'))
|
||||
->searchable()
|
||||
->sortable()
|
||||
->disabled(fn () => $this->isReadOnly()),
|
||||
TextInputColumn::make('notes')
|
||||
->label(trans('admin/server.notes'))
|
||||
->placeholder(trans('admin/server.no_notes'))
|
||||
->disabled(fn () => $this->isReadOnly()),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => TablerIcon::StarFilled,
|
||||
default => TablerIcon::Star,
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->tooltip(fn (Allocation $allocation) => trans('admin/server.' . ($allocation->id === $this->getOwnerRecord()->allocation_id ? 'already' : 'make') . '_primary'))
|
||||
->action(function (Allocation $allocation) {
|
||||
if ($this->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords();
|
||||
})
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
|
||||
->label(trans('admin/server.primary')),
|
||||
IconColumn::make('is_locked')
|
||||
->label(trans('admin/server.locked'))
|
||||
->tooltip(trans('admin/server.locked_helper'))
|
||||
->trueIcon(TablerIcon::Lock)
|
||||
->falseIcon(TablerIcon::LockOpen),
|
||||
])
|
||||
->emptyStateHeading(trans('admin/server.no_allocations'))
|
||||
->recordActions([
|
||||
Action::make('make-primary')
|
||||
->authorize(fn (Allocation $allocation) => user()?->can('update', $allocation))
|
||||
->tooltip(trans('admin/server.make_primary'))
|
||||
->icon(TablerIcon::Star)
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]) && $this->deselectAllTableRecords())
|
||||
->hidden(fn (Allocation $allocation) => $this->isReadOnly() || $allocation->id === $this->getOwnerRecord()->allocation_id),
|
||||
Action::make('lock')
|
||||
->authorize(fn (Allocation $allocation) => user()?->can('update', $allocation))
|
||||
->tooltip(trans('admin/server.lock'))
|
||||
->icon(TablerIcon::Lock)
|
||||
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => true]) && $this->deselectAllTableRecords())
|
||||
->hidden(fn (Allocation $allocation) => $this->isReadOnly() || $allocation->is_locked),
|
||||
Action::make('unlock')
|
||||
->authorize(fn (Allocation $allocation) => user()?->can('update', $allocation))
|
||||
->tooltip(trans('admin/server.unlock'))
|
||||
->icon(TablerIcon::LockOpen)
|
||||
->action(fn (Allocation $allocation) => $allocation->update(['is_locked' => false]) && $this->deselectAllTableRecords())
|
||||
->visible(fn (Allocation $allocation) => !$this->isReadOnly() && $allocation->is_locked),
|
||||
DissociateAction::make()
|
||||
->authorize(fn (Allocation $allocation) => user()?->can('update', $allocation))
|
||||
->tooltip(trans('admin/server.remove_allocation'))
|
||||
->hidden(fn () => $this->isReadOnly())
|
||||
->after(function (Allocation $allocation) {
|
||||
$allocation->update(Allocation::RELEASE_ATTRIBUTES);
|
||||
|
||||
if (!$this->getOwnerRecord()->allocation_id) {
|
||||
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
|
||||
}
|
||||
}),
|
||||
])
|
||||
->toolbarActions([
|
||||
DissociateBulkAction::make()
|
||||
->hidden(fn () => $this->isReadOnly())
|
||||
->after(function (Collection $records) {
|
||||
Allocation::whereIn('id', $records->pluck('id'))->update(Allocation::RELEASE_ATTRIBUTES);
|
||||
|
||||
if (!$this->getOwnerRecord()->allocation_id) {
|
||||
$this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]);
|
||||
}
|
||||
}),
|
||||
CreateAction::make()
|
||||
->hidden(fn () => $this->isReadOnly())
|
||||
->hiddenLabel()
|
||||
->tooltip(trans('admin/server.create_allocation'))
|
||||
->icon(TablerIcon::WorldPlus)
|
||||
->createAnother(false)
|
||||
->schema(fn () => [
|
||||
Select::make('allocation_ip')
|
||||
->options(fn (Get $get) => collect($this->getOwnerRecord()->node->ipAddresses())
|
||||
->when($get('allocation_ip'), fn ($ips, $current) => $ips->push($current))
|
||||
->unique()
|
||||
->mapWithKeys(fn (string $ip) => [$ip => $ip]))
|
||||
->label(trans('admin/server.ip_address'))
|
||||
->inlineLabel()
|
||||
->ip()
|
||||
->live()
|
||||
->hintAction(
|
||||
Action::make('refresh')
|
||||
->icon(TablerIcon::Refresh)
|
||||
->tooltip(trans('admin/node.refresh'))
|
||||
->action(function () {
|
||||
cache()->forget("nodes.{$this->getOwnerRecord()->node->id}.ips");
|
||||
})
|
||||
)
|
||||
->suffixAction(
|
||||
Action::make('custom_ip')
|
||||
->icon(TablerIcon::Keyboard)
|
||||
->tooltip(trans('admin/node.custom_ip'))
|
||||
->schema([
|
||||
TextInput::make('custom_ip')
|
||||
->label(trans('admin/node.ip_address'))
|
||||
->ip()
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, Set $set) => $set('allocation_ip', $data['custom_ip']))
|
||||
)
|
||||
->afterStateUpdated(fn (Set $set) => $set('allocation_ports', []))
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label(trans('admin/server.alias'))
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText(trans('admin/server.alias_helper')),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('27015, 27017-27019')
|
||||
->label(trans('admin/server.ports'))
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->disabled(fn (Get $get) => empty($get('allocation_ip')))
|
||||
->afterStateUpdated(fn ($state, Set $set, Get $get) => $set('allocation_ports', CreateServer::retrieveValidPorts($this->getOwnerRecord()->node, $state, $get('allocation_ip'))))
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
Hidden::make('is_locked')
|
||||
->default(true),
|
||||
])
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
AssociateAction::make()
|
||||
->hidden(fn () => $this->isReadOnly())
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node)->whereNull('server_id'))
|
||||
->recordSelectSearchColumns(['ip', 'port'])
|
||||
->tooltip(trans('admin/server.add_allocation'))
|
||||
->after(function (array $data) {
|
||||
Allocation::whereIn('id', array_values(array_unique($data['recordId'])))->update(['is_locked' => true]);
|
||||
|
||||
if (!$this->getOwnerRecord()->allocation_id) {
|
||||
$this->getOwnerRecord()->update(['allocation_id' => $data['recordId'][0]]);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Data\Api\Client\AllocationData;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Exceptions\Model\DataValidationException;
|
||||
use App\Facades\Activity;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\DeleteAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\GetNetworkRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\NewAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
use Dedoc\Scramble\Attributes\Group;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
#[Group('Server - Allocation')]
|
||||
class NetworkAllocationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* NetworkAllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private FindAssignableAllocationService $assignableAllocationService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* List allocations
|
||||
*
|
||||
* Lists all the allocations available to a server and whether
|
||||
* they are currently assigned as the primary for this server.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
return $this->response->collection($server->allocations)
|
||||
->transformWith(AllocationData::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update allocation
|
||||
*
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @throws DataValidationException
|
||||
*/
|
||||
public function update(UpdateAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$original = $allocation->notes;
|
||||
|
||||
$allocation->forceFill(['notes' => $request->input('notes')])->save();
|
||||
|
||||
if ($original !== $allocation->notes) {
|
||||
Activity::event('server:allocation.notes')
|
||||
->subject($allocation)
|
||||
->property(['allocation' => $allocation->address, 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
return $this->response->item($allocation)
|
||||
->transformWith(AllocationData::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set primary allocation
|
||||
*
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @throws DataValidationException
|
||||
*/
|
||||
public function setPrimary(SetPrimaryAllocationRequest $request, Server $server, Allocation $allocation): array
|
||||
{
|
||||
$server->allocation()->associate($allocation);
|
||||
$server->save();
|
||||
|
||||
Activity::event('server:allocation.primary')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return $this->response->item($allocation)
|
||||
->transformWith(AllocationData::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create allocation
|
||||
*
|
||||
* Set the notes for the allocation for a server.
|
||||
*
|
||||
* @return array<array-key, mixed>
|
||||
*
|
||||
* @throws DisplayException
|
||||
*/
|
||||
public function store(NewAllocationRequest $request, Server $server): array
|
||||
{
|
||||
$allocation = Activity::event('server:allocation.create')->transaction(function ($log) use ($server) {
|
||||
$server->allocations()->lockForUpdate();
|
||||
|
||||
throw_if($server->allocations->count() >= $server->allocation_limit, new DisplayException('Cannot assign additional allocations to this server: limit has been reached.'));
|
||||
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
$log->subject($allocation)->property('allocation', $allocation->address);
|
||||
|
||||
return $allocation;
|
||||
});
|
||||
|
||||
return $this->response->item($allocation)
|
||||
->transformWith(AllocationData::class)
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete allocation
|
||||
*
|
||||
* Delete an allocation from a server.
|
||||
*
|
||||
* @throws DisplayException
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Server $server, Allocation $allocation): JsonResponse
|
||||
{
|
||||
// Don't allow the deletion of allocations if the server does not have an
|
||||
// allocation limit set.
|
||||
throw_if(empty($server->allocation_limit), new DisplayException('You cannot delete allocations for this server: no allocation limit is set.'));
|
||||
|
||||
Allocation::query()->where('id', $allocation->id)->update(Allocation::RELEASE_ATTRIBUTES);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->address)
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,572 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Contracts\Validatable;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerResourceType;
|
||||
use App\Enums\ServerState;
|
||||
use App\Enums\WebhookScope;
|
||||
use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
use App\Models\Traits\HasIcon;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Subusers\SubuserDeletionService;
|
||||
use App\Traits\HasValidation;
|
||||
use Carbon\CarbonInterface;
|
||||
use Exception;
|
||||
use Filament\Models\Contracts\HasAvatar;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use Illuminate\Notifications\DatabaseNotification;
|
||||
use Illuminate\Notifications\DatabaseNotificationCollection;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* \App\Models\Server.
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $uuid
|
||||
* @property string $uuid_short
|
||||
* @property int $node_id
|
||||
* @property string $name
|
||||
* @property int $owner_id
|
||||
* @property int $memory
|
||||
* @property int $swap
|
||||
* @property int $disk
|
||||
* @property int $io
|
||||
* @property int $cpu
|
||||
* @property int $egg_id
|
||||
* @property string $startup
|
||||
* @property Carbon|null $created_at
|
||||
* @property Carbon|null $updated_at
|
||||
* @property int|null $allocation_id
|
||||
* @property string $image
|
||||
* @property string|null $description
|
||||
* @property bool $skip_scripts
|
||||
* @property string|null $external_id
|
||||
* @property int|null $database_limit
|
||||
* @property int|null $allocation_limit
|
||||
* @property string|null $threads
|
||||
* @property int $backup_limit
|
||||
* @property ServerState|null $status
|
||||
* @property Carbon|null $installed_at
|
||||
* @property bool $oom_killer
|
||||
* @property array<array-key, mixed>|null $docker_labels
|
||||
* @property-read Collection<int, ActivityLog> $activity
|
||||
* @property-read int|null $activity_count
|
||||
* @property-read Allocation|null $allocation
|
||||
* @property-read Collection<int, Allocation> $allocations
|
||||
* @property-read int|null $allocations_count
|
||||
* @property-read Collection<int, Backup> $backups
|
||||
* @property-read int|null $backups_count
|
||||
* @property-read ServerState|ContainerStatus $condition
|
||||
* @property-read Collection<int, Database> $databases
|
||||
* @property-read int|null $databases_count
|
||||
* @property-read Egg $egg
|
||||
* @property-read Collection<int, EggVariable> $eggVariables
|
||||
* @property-read int|null $egg_variables_count
|
||||
* @property-read string|null $icon
|
||||
* @property-read Collection<int, Mount> $mounts
|
||||
* @property-read int|null $mounts_count
|
||||
* @property-read Node $node
|
||||
* @property-read DatabaseNotificationCollection<int, DatabaseNotification> $notifications
|
||||
* @property-read int|null $notifications_count
|
||||
* @property-read Collection<int, Schedule> $schedules
|
||||
* @property-read int|null $schedules_count
|
||||
* @property-read Collection<int, ServerVariable> $serverVariables
|
||||
* @property-read int|null $server_variables_count
|
||||
* @property-read Collection<int, Subuser> $subusers
|
||||
* @property-read int|null $subusers_count
|
||||
* @property-read Collection<int, ServerUserSettings> $userSettings
|
||||
* @property-read int|null $user_settings_count
|
||||
* @property-read ServerTransfer|null $transfer
|
||||
* @property-read User $user
|
||||
* @property-read Collection<int, EggVariable> $variables
|
||||
* @property-read int|null $variables_count
|
||||
*
|
||||
* @method static \Database\Factories\ServerFactory factory($count = null, $state = [])
|
||||
* @method static Builder<static>|Server newModelQuery()
|
||||
* @method static Builder<static>|Server newQuery()
|
||||
* @method static Builder<static>|Server query()
|
||||
* @method static Builder<static>|Server whereAllocationId($value)
|
||||
* @method static Builder<static>|Server whereAllocationLimit($value)
|
||||
* @method static Builder<static>|Server whereBackupLimit($value)
|
||||
* @method static Builder<static>|Server whereCpu($value)
|
||||
* @method static Builder<static>|Server whereCreatedAt($value)
|
||||
* @method static Builder<static>|Server whereDatabaseLimit($value)
|
||||
* @method static Builder<static>|Server whereDescription($value)
|
||||
* @method static Builder<static>|Server whereDisk($value)
|
||||
* @method static Builder<static>|Server whereDockerLabels($value)
|
||||
* @method static Builder<static>|Server whereEggId($value)
|
||||
* @method static Builder<static>|Server whereExternalId($value)
|
||||
* @method static Builder<static>|Server whereId($value)
|
||||
* @method static Builder<static>|Server whereImage($value)
|
||||
* @method static Builder<static>|Server whereInstalledAt($value)
|
||||
* @method static Builder<static>|Server whereIo($value)
|
||||
* @method static Builder<static>|Server whereMemory($value)
|
||||
* @method static Builder<static>|Server whereName($value)
|
||||
* @method static Builder<static>|Server whereNodeId($value)
|
||||
* @method static Builder<static>|Server whereOomKiller($value)
|
||||
* @method static Builder<static>|Server whereOwnerId($value)
|
||||
* @method static Builder<static>|Server whereSkipScripts($value)
|
||||
* @method static Builder<static>|Server whereStartup($value)
|
||||
* @method static Builder<static>|Server whereStatus($value)
|
||||
* @method static Builder<static>|Server whereSwap($value)
|
||||
* @method static Builder<static>|Server whereThreads($value)
|
||||
* @method static Builder<static>|Server whereUpdatedAt($value)
|
||||
* @method static Builder<static>|Server whereUuid($value)
|
||||
* @method static Builder<static>|Server whereUuidShort($value)
|
||||
*/
|
||||
class Server extends Model implements HasAvatar, Validatable
|
||||
{
|
||||
use HasFactory;
|
||||
use HasIcon;
|
||||
use HasValidation;
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal. Also used as name for api key permissions.
|
||||
*/
|
||||
public const RESOURCE_NAME = 'server';
|
||||
|
||||
/**
|
||||
* Default values when creating the model. We want to switch to disabling OOM killer
|
||||
* on server instances unless the user specifies otherwise in the request.
|
||||
*/
|
||||
protected $attributes = [
|
||||
'status' => ServerState::Installing,
|
||||
'oom_killer' => false,
|
||||
'installed_at' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* The default relationships to load for all server models.
|
||||
*/
|
||||
protected $with = ['allocation'];
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $guarded = ['id', self::CREATED_AT, self::UPDATED_AT, 'installed_at'];
|
||||
|
||||
/** @var array<array-key, string[]> */
|
||||
public static array $validationRules = [
|
||||
'external_id' => ['sometimes', 'nullable', 'string', 'between:1,255', 'unique:servers'],
|
||||
'owner_id' => ['required', 'integer', 'exists:users,id'],
|
||||
'name' => ['required', 'string', 'min:1', 'max:255'],
|
||||
'node_id' => ['required', 'exists:nodes,id'],
|
||||
'description' => ['string'],
|
||||
'status' => ['nullable', 'string'],
|
||||
'memory' => ['required', 'numeric', 'min:0'],
|
||||
'swap' => ['required', 'numeric', 'min:-1'],
|
||||
'io' => ['required', 'numeric', 'between:0,1000'],
|
||||
'cpu' => ['required', 'numeric', 'min:0'],
|
||||
'threads' => ['nullable', 'regex:/^[0-9-,]+$/'],
|
||||
'oom_killer' => ['sometimes', 'boolean'],
|
||||
'disk' => ['required', 'numeric', 'min:0'],
|
||||
'allocation_id' => ['sometimes', 'nullable', 'unique:servers', 'exists:allocations,id'],
|
||||
'egg_id' => ['required', 'exists:eggs,id'],
|
||||
'startup' => ['required', 'string'],
|
||||
'skip_scripts' => ['sometimes', 'boolean'],
|
||||
'image' => ['required', 'string', 'max:255'],
|
||||
'database_limit' => ['present', 'nullable', 'integer', 'min:0'],
|
||||
'allocation_limit' => ['sometimes', 'nullable', 'integer', 'min:0'],
|
||||
'backup_limit' => ['present', 'nullable', 'integer', 'min:0'],
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'node_id' => 'integer',
|
||||
'status' => ServerState::class,
|
||||
'skip_scripts' => 'boolean',
|
||||
'owner_id' => 'integer',
|
||||
'memory' => 'integer',
|
||||
'swap' => 'integer',
|
||||
'disk' => 'integer',
|
||||
'io' => 'integer',
|
||||
'cpu' => 'integer',
|
||||
'oom_killer' => 'boolean',
|
||||
'allocation_id' => 'integer',
|
||||
'egg_id' => 'integer',
|
||||
'database_limit' => 'integer',
|
||||
'allocation_limit' => 'integer',
|
||||
'backup_limit' => 'integer',
|
||||
self::CREATED_AT => 'datetime',
|
||||
self::UPDATED_AT => 'datetime',
|
||||
'installed_at' => 'datetime',
|
||||
'docker_labels' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(function (self $server) {
|
||||
$subuser = $server->subusers()->where('user_id', $server->owner_id)->first();
|
||||
if ($subuser) {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
app(SubuserDeletionService::class)->handle($subuser, $server);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format for server allocations when communicating with the Daemon.
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
public function isInstalled(): bool
|
||||
{
|
||||
return $this->status !== ServerState::Installing && !$this->isFailedInstall();
|
||||
}
|
||||
|
||||
public function isFailedInstall(): bool
|
||||
{
|
||||
return $this->status === ServerState::InstallFailed || $this->status === ServerState::ReinstallFailed;
|
||||
}
|
||||
|
||||
public function isSuspended(): bool
|
||||
{
|
||||
return $this->status === ServerState::Suspended;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user who owns the server.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subusers associated with a server.
|
||||
*
|
||||
* @return HasMany<Subuser, $this>
|
||||
*/
|
||||
public function subusers(): HasMany
|
||||
{
|
||||
return $this->hasMany(Subuser::class, 'server_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the per-user settings associated with a server.
|
||||
*
|
||||
* @return HasMany<ServerUserSettings, $this>
|
||||
*/
|
||||
public function userSettings(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServerUserSettings::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default allocation for a server.
|
||||
*/
|
||||
public function allocation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all allocations associated with this server.
|
||||
*
|
||||
* @return HasMany<Allocation, $this>
|
||||
*/
|
||||
public function allocations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the egg associated with this server.
|
||||
*/
|
||||
public function egg(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Egg::class);
|
||||
}
|
||||
|
||||
public function eggVariables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the egg variables associated with this server.
|
||||
*
|
||||
* @return HasMany<EggVariable, $this>
|
||||
*/
|
||||
public function variables(): HasMany
|
||||
{
|
||||
return $this->hasMany(EggVariable::class, 'egg_id', 'egg_id')
|
||||
->select(['egg_variables.*', 'server_variables.variable_value as server_value'])
|
||||
->leftJoin('server_variables', function (JoinClause $join) {
|
||||
// Don't forget to join against the server ID as well since the way we're using this relationship
|
||||
// would actually return all the variables and their values for _all_ servers using that egg,
|
||||
// rather than only the server for this model.
|
||||
$join->on('server_variables.variable_id', 'egg_variables.id')
|
||||
->where('server_variables.server_id', $this->id);
|
||||
});
|
||||
}
|
||||
|
||||
public function serverVariables(): HasMany
|
||||
{
|
||||
return $this->hasMany(ServerVariable::class);
|
||||
}
|
||||
|
||||
public function ensureVariablesExist(): void
|
||||
{
|
||||
foreach ($this->eggVariables as $variable) {
|
||||
ServerVariable::firstOrCreate([
|
||||
'server_id' => $this->id,
|
||||
'variable_id' => $variable->id,
|
||||
], [
|
||||
'variable_value' => $variable->default_value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the node associated with this server.
|
||||
*/
|
||||
public function node(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Node::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the tasks associated with this server.
|
||||
*/
|
||||
public function schedules(): HasMany
|
||||
{
|
||||
return $this->hasMany(Schedule::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all databases associated with a server.
|
||||
*/
|
||||
public function databases(): HasMany
|
||||
{
|
||||
return $this->hasMany(Database::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the associated server transfer.
|
||||
*/
|
||||
public function transfer(): HasOne
|
||||
{
|
||||
return $this->hasOne(ServerTransfer::class)->whereNull('successful')->orderByDesc('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<Backup, $this>
|
||||
*/
|
||||
public function backups(): HasMany
|
||||
{
|
||||
return $this->hasMany(Backup::class);
|
||||
}
|
||||
|
||||
public function mounts(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(Mount::class, 'mountable');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<WebhookConfiguration, $this>
|
||||
*/
|
||||
public function webhookConfigurations(): HasMany
|
||||
{
|
||||
return $this->hasMany(WebhookConfiguration::class, 'server_id', 'id')
|
||||
->where('scope', WebhookScope::Server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the activity log entries where the server is the subject.
|
||||
*/
|
||||
public function activity(): MorphToMany
|
||||
{
|
||||
return $this->morphToMany(ActivityLog::class, 'subject', 'activity_log_subjects');
|
||||
}
|
||||
|
||||
public function resolveRouteBinding($value, $field = null): ?self
|
||||
{
|
||||
return match ($field) {
|
||||
'uuid', 'uuid_short' => $this->where('uuid_short', $value)->orWhere('uuid', $value)->firstOrFail(),
|
||||
default => $this->where('id', $value)->firstOrFail(),
|
||||
};
|
||||
}
|
||||
|
||||
public function resolveChildRouteBinding($childType, $value, $field)
|
||||
{
|
||||
return match ($childType) {
|
||||
'user' => $this->subusers()->whereRelation('user', 'uuid', $value)->firstOrFail()->user,
|
||||
default => parent::resolveChildRouteBinding($childType, $value, $field),
|
||||
};
|
||||
}
|
||||
|
||||
public function isInConflictState(): bool
|
||||
{
|
||||
return $this->isSuspended() || $this->node->isUnderMaintenance() || !$this->isInstalled() || $this->status === ServerState::RestoringBackup || !is_null($this->transfer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is currently in a user-accessible state. If not, an
|
||||
* exception is raised. This should be called whenever something needs to make
|
||||
* sure the server is not in a weird state that should block user access.
|
||||
*
|
||||
* @throws ServerStateConflictException
|
||||
*/
|
||||
public function validateCurrentState(): void
|
||||
{
|
||||
throw_if($this->isInConflictState(), new ServerStateConflictException($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the server is currently in a transferable state. If not, an
|
||||
* exception is raised. This should be called whenever something needs to make
|
||||
* sure the server can be transferred and is not currently being transferred
|
||||
* or installed.
|
||||
*/
|
||||
public function validateTransferState(): void
|
||||
{
|
||||
if (!$this->isInstalled() ||
|
||||
$this->status === ServerState::RestoringBackup ||
|
||||
!is_null($this->transfer)) {
|
||||
throw new ServerStateConflictException($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a command or multiple commands to a running server instance.
|
||||
*
|
||||
* @param string[]|string $command
|
||||
*
|
||||
* @throws ConnectionException
|
||||
*/
|
||||
public function send(array|string $command): ResponseInterface
|
||||
{
|
||||
return Http::daemon($this->node)->post("/api/servers/{$this->uuid}/commands", [
|
||||
'commands' => is_array($command) ? $command : [$command],
|
||||
])->toPsrResponse();
|
||||
}
|
||||
|
||||
public function retrieveStatus(): ContainerStatus
|
||||
{
|
||||
if ($this->node->isUnderMaintenance()) {
|
||||
return ContainerStatus::Missing;
|
||||
}
|
||||
|
||||
return cache()->remember("servers.$this->uuid.status", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
|
||||
return ContainerStatus::tryFrom(Arr::get($details, 'state')) ?? ContainerStatus::Missing;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
public function retrieveResources(): array
|
||||
{
|
||||
if ($this->node->isUnderMaintenance()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return cache()->remember("servers.$this->uuid.resources", now()->addSeconds(15), function () {
|
||||
// @phpstan-ignore myCustomRules.forbiddenGlobalFunctions
|
||||
$details = app(DaemonServerRepository::class)->setServer($this)->getDetails();
|
||||
|
||||
return Arr::get($details, 'utilization', []);
|
||||
});
|
||||
}
|
||||
|
||||
public function formatResource(ServerResourceType $resourceType, int $precision = 2): string
|
||||
{
|
||||
$resourceAmount = $resourceType->getResourceAmount($this);
|
||||
|
||||
if ($resourceType->isTime()) {
|
||||
if (!is_null($this->status)) {
|
||||
return $this->status->getLabel();
|
||||
}
|
||||
|
||||
if ($resourceAmount === 0) {
|
||||
return ContainerStatus::Offline->getLabel();
|
||||
}
|
||||
|
||||
return now()->subMillis($resourceAmount)->diffForHumans(syntax: CarbonInterface::DIFF_ABSOLUTE, short: true, parts: 4);
|
||||
}
|
||||
|
||||
if ($resourceAmount === 0 & $resourceType->isLimit()) {
|
||||
// Unlimited symbol
|
||||
return "\u{221E}";
|
||||
}
|
||||
|
||||
if ($resourceType->isPercentage()) {
|
||||
return format_number($resourceAmount, precision: $precision) . '%';
|
||||
}
|
||||
|
||||
return convert_bytes_to_readable($resourceAmount, decimals: $precision, base: 3);
|
||||
}
|
||||
|
||||
public function condition(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->status ?? $this->retrieveStatus(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getFilamentAvatarUrl(): ?string
|
||||
{
|
||||
return $this->icon ?? $this->egg->icon;
|
||||
}
|
||||
|
||||
public function getSftpUrl(?string $username = null, ?string $directory = null): string
|
||||
{
|
||||
$username ??= user()?->username;
|
||||
|
||||
if (!is_null($directory)) {
|
||||
$directory = explode('/', trim($directory, '/'));
|
||||
$directory = array_map(fn (string $part) => rawurlencode($part), $directory);
|
||||
$directory = '/' . implode('/', $directory);
|
||||
|
||||
// SFTP clients only treat a URL path as a directory when it ends with a
|
||||
// slash, otherwise they prompt to download the path as a file instead of
|
||||
// opening it. See #2577.
|
||||
if ($directory !== '/') {
|
||||
$directory .= '/';
|
||||
}
|
||||
}
|
||||
|
||||
if ($directory === '/') {
|
||||
$directory = null;
|
||||
}
|
||||
|
||||
$fqdn = $this->node->daemon_sftp_alias ?? $this->node->fqdn;
|
||||
|
||||
return 'sftp://' . rawurlencode($username) . '.' . $this->uuid_short . '@' . $fqdn . ':' . $this->node->daemon_sftp . $directory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\RolePermissionModels;
|
||||
use App\Filament\Admin\Resources\Servers\Pages\CreateServer;
|
||||
use App\Filament\Admin\Resources\Servers\Pages\EditServer;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Role;
|
||||
use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Filament\Actions\Testing\TestAction;
|
||||
use Filament\Facades\Filament;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
use function Pest\Livewire\livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
Filament::setCurrentPanel(Filament::getPanel('admin'));
|
||||
|
||||
[$this->admin] = generateTestAccount();
|
||||
$this->admin->syncRoles(Role::getRootAdmin());
|
||||
$this->actingAs($this->admin);
|
||||
});
|
||||
afterEach(fn () => Filament::setCurrentPanel(null));
|
||||
|
||||
it('can create a server', function () {
|
||||
$daemon = Mockery::mock(DaemonServerRepository::class);
|
||||
$daemon->expects('setServer')->andReturnSelf();
|
||||
$daemon->expects('create')->andReturnUndefined();
|
||||
$this->swap(DaemonServerRepository::class, $daemon);
|
||||
|
||||
$node = Node::factory()->create();
|
||||
$allocation = Allocation::factory()->create(['node_id' => $node->id, 'server_id' => null]);
|
||||
// A factory egg has no variables, so no required environment values get in the way.
|
||||
$egg = Egg::factory()->create();
|
||||
|
||||
livewire(CreateServer::class)
|
||||
->fillForm([
|
||||
'name' => 'test-server',
|
||||
'node_id' => $node->id,
|
||||
'owner_id' => $this->admin->id,
|
||||
'allocation_id' => $allocation->id,
|
||||
'egg_id' => $egg->id,
|
||||
'startup' => 'java -jar test.jar',
|
||||
'image' => 'ghcr.io/pelican-eggs/yolks:java_21',
|
||||
])
|
||||
->call('create')
|
||||
->assertHasNoFormErrors();
|
||||
|
||||
$this->assertDatabaseHas('servers', [
|
||||
'name' => 'test-server',
|
||||
'node_id' => $node->id,
|
||||
'owner_id' => $this->admin->id,
|
||||
'egg_id' => $egg->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can edit a server', function () {
|
||||
$server = Server::factory()->withNode()->create();
|
||||
|
||||
// Renaming is excluded from the daemon sync diff, so no daemon call happens.
|
||||
livewire(EditServer::class, ['record' => $server->getKey()])
|
||||
->fillForm([
|
||||
'name' => 'renamed-server',
|
||||
'allocation_limit' => 0,
|
||||
'database_limit' => 0,
|
||||
])
|
||||
->call('save')
|
||||
->assertHasNoFormErrors();
|
||||
|
||||
$this->assertDatabaseHas('servers', ['id' => $server->id, 'name' => 'renamed-server']);
|
||||
});
|
||||
|
||||
it('can delete a server', function () {
|
||||
$daemon = Mockery::mock(DaemonServerRepository::class);
|
||||
$daemon->shouldReceive('setServer')->andReturnSelf();
|
||||
// Rendering the edit page polls the server's container status.
|
||||
$daemon->shouldReceive('getDetails')->andReturn(['state' => 'offline']);
|
||||
$daemon->shouldReceive('delete');
|
||||
$this->swap(DaemonServerRepository::class, $daemon);
|
||||
|
||||
$server = Server::factory()->withNode()->create();
|
||||
|
||||
livewire(EditServer::class, ['record' => $server->getKey()])
|
||||
->callAction(TestAction::make('Delete'));
|
||||
|
||||
$this->assertDatabaseMissing('servers', ['id' => $server->id]);
|
||||
});
|
||||
|
||||
it('non root admin without permission cannot create servers', function () {
|
||||
$role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']);
|
||||
// Egg permission is on purpose, we check the wrong permissions.
|
||||
$role->givePermissionTo(Permission::findOrCreate(RolePermissionModels::Egg->viewAny(), 'web'));
|
||||
[$user] = generateTestAccount();
|
||||
$user->syncRoles($role);
|
||||
|
||||
$this->actingAs($user);
|
||||
livewire(CreateServer::class)->assertForbidden();
|
||||
});
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Integration\Api\Client\Server\Allocation;
|
||||
|
||||
use App\Enums\SubuserPermission;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Tests\Integration\Api\Client\ClientApiIntegrationTestCase;
|
||||
use Illuminate\Http\Response;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class DeleteAllocationTest extends ClientApiIntegrationTestCase
|
||||
{
|
||||
/**
|
||||
* Test that an allocation is deleted from the server and the notes are properly reset
|
||||
* to an empty value on assignment.
|
||||
*/
|
||||
#[DataProvider('permissionDataProvider')]
|
||||
public function test_allocation_can_be_deleted_from_server(array $permission): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
[$user, $server] = $this->generateTestAccount($permission);
|
||||
$server->update(['allocation_limit' => 2]);
|
||||
|
||||
/** @var Allocation $allocation */
|
||||
$allocation = Allocation::factory()->create([
|
||||
'server_id' => $server->id,
|
||||
'node_id' => $server->node_id,
|
||||
'is_locked' => true,
|
||||
'notes' => 'hodor',
|
||||
'show_port' => false,
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->deleteJson($this->link($allocation))->assertStatus(Response::HTTP_NO_CONTENT);
|
||||
|
||||
$this->assertDatabaseHas('allocations', [
|
||||
'id' => $allocation->id,
|
||||
'is_locked' => false,
|
||||
'server_id' => null,
|
||||
'notes' => null,
|
||||
'show_port' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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.
|
||||
*/
|
||||
public function test_error_is_returned_if_user_does_not_have_permission(): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
[$user, $server] = $this->generateTestAccount([SubuserPermission::AllocationCreate]);
|
||||
|
||||
/** @var Allocation $allocation */
|
||||
$allocation = Allocation::factory()->create([
|
||||
'server_id' => $server->id,
|
||||
'node_id' => $server->node_id,
|
||||
'notes' => 'hodor',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->deleteJson($this->link($allocation))->assertForbidden();
|
||||
|
||||
$this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => $server->id]);
|
||||
}
|
||||
|
||||
public function test_allocation_cannot_be_deleted_if_server_limit_is_not_defined(): void
|
||||
{
|
||||
[$user, $server] = $this->generateTestAccount();
|
||||
|
||||
/** @var Allocation $allocation */
|
||||
$allocation = Allocation::factory()->forServer($server)->create(['notes' => 'Test notes']);
|
||||
|
||||
$this->actingAs($user)->deleteJson($this->link($allocation))
|
||||
->assertStatus(400)
|
||||
->assertJsonPath('errors.0.detail', 'You cannot delete allocations for this server: no allocation limit is set.');
|
||||
|
||||
$allocation->refresh();
|
||||
$this->assertNotNull($allocation->notes);
|
||||
$this->assertEquals($server->id, $allocation->server_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that an allocation cannot be deleted if it does not belong to the server instance.
|
||||
*/
|
||||
public function test_error_is_returned_if_allocation_does_not_belong_to_server(): void
|
||||
{
|
||||
/** @var Server $server */
|
||||
[$user, $server] = $this->generateTestAccount();
|
||||
[, $server2] = $this->generateTestAccount();
|
||||
|
||||
$this->actingAs($user)->deleteJson($this->link($server2->allocation))->assertNotFound();
|
||||
$this->actingAs($user)->deleteJson($this->link($server, "/network/allocations/{$server2->allocation_id}"))->assertNotFound();
|
||||
}
|
||||
|
||||
public static function permissionDataProvider(): array
|
||||
{
|
||||
return [[[SubuserPermission::AllocationDelete]], [[]]];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Unit\Models;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Tests\TestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
class ServerTest extends TestCase
|
||||
{
|
||||
#[DataProvider('sftpUrlDataProvider')]
|
||||
public function test_sftp_url_points_at_the_given_directory(?string $directory, string $expectedPath): void
|
||||
{
|
||||
$server = $this->serverWithNode();
|
||||
|
||||
$this->assertSame("sftp://user.abcdefgh@node.example.com:2022{$expectedPath}", $server->getSftpUrl('user', $directory));
|
||||
}
|
||||
|
||||
public static function sftpUrlDataProvider(): array
|
||||
{
|
||||
return [
|
||||
'no directory' => [null, ''],
|
||||
'root' => ['/', ''],
|
||||
'single directory' => ['/data', '/data/'],
|
||||
'directory without a leading slash' => ['data', '/data/'],
|
||||
'directory with a trailing slash' => ['data/', '/data/'],
|
||||
'nested directory' => ['/data/world', '/data/world/'],
|
||||
'directory with encoded parts' => ['/data/my world', '/data/my%20world/'],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_sftp_url_uses_the_node_sftp_alias_when_set(): void
|
||||
{
|
||||
$server = $this->serverWithNode(sftpAlias: 'sftp.example.com');
|
||||
|
||||
$this->assertSame('sftp://user.abcdefgh@sftp.example.com:2022/data/', $server->getSftpUrl('user', '/data'));
|
||||
}
|
||||
|
||||
public function test_sftp_url_encodes_the_username(): void
|
||||
{
|
||||
$server = $this->serverWithNode();
|
||||
|
||||
$this->assertSame('sftp://user%20name.abcdefgh@node.example.com:2022/data/', $server->getSftpUrl('user name', '/data'));
|
||||
}
|
||||
|
||||
private function serverWithNode(string $fqdn = 'node.example.com', ?string $sftpAlias = null): Server
|
||||
{
|
||||
$node = new Node();
|
||||
$node->fqdn = $fqdn;
|
||||
$node->daemon_sftp_alias = $sftpAlias;
|
||||
$node->daemon_sftp = 2022;
|
||||
|
||||
$server = new Server();
|
||||
$server->uuid_short = 'abcdefgh';
|
||||
$server->setRelation('node', $node);
|
||||
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user