mirror of
https://github.com/pelican-dev/panel.git
synced 2026-02-08 03:10:34 +03:00
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eeefab6c6 | ||
|
|
a2108c3d91 | ||
|
|
4f5e9a6c30 | ||
|
|
1aa51d15f0 | ||
|
|
3d8847a508 | ||
|
|
4fa49ae915 | ||
|
|
a42cb193a3 | ||
|
|
50ffaf4d01 | ||
|
|
804ff64a71 | ||
|
|
c2f6842f64 | ||
|
|
455d0543f1 | ||
|
|
97a4601150 | ||
|
|
2cc4a42905 | ||
|
|
5353d38302 | ||
|
|
df88d33af4 | ||
|
|
906d4a7d28 | ||
|
|
9ba8c1df9b | ||
|
|
0a6b846230 | ||
|
|
aff9f4ea37 | ||
|
|
f2754c3cb1 | ||
|
|
8b86707150 | ||
|
|
233fd50b2b | ||
|
|
3a76fb1c79 | ||
|
|
e76630b7f3 | ||
|
|
0f798e5edb | ||
|
|
a9c7eeddde | ||
|
|
11feef4f8c | ||
|
|
bebc410eda | ||
|
|
ec0fa3c913 | ||
|
|
4574821ed8 | ||
|
|
d71b1a4710 | ||
|
|
d9922e86f2 | ||
|
|
9d9e4adbbd | ||
|
|
6b104e3331 | ||
|
|
f2eca17480 | ||
|
|
4c41e659b5 | ||
|
|
6238d6dd08 | ||
|
|
c45e4edcf6 | ||
|
|
4273880126 | ||
|
|
d86843977b | ||
|
|
5b468c21ae | ||
|
|
68ef0a1d0a | ||
|
|
f6122f919a | ||
|
|
4f10ec2c20 | ||
|
|
45fcc2a09a | ||
|
|
19c7b4d044 | ||
|
|
7c8b204d13 | ||
|
|
343a5b81bc | ||
|
|
755632f9d5 | ||
|
|
e7ee86a914 | ||
|
|
eb0bad82e6 | ||
|
|
48fd3cc84e | ||
|
|
d4484f5254 | ||
|
|
958e8fac8a | ||
|
|
7986505b99 | ||
|
|
ba5b81cf2d | ||
|
|
32018399b6 | ||
|
|
48f4c35d0b | ||
|
|
f699fd5459 | ||
|
|
05573f64dd | ||
|
|
3fa714c7e3 | ||
|
|
69acc48b5e | ||
|
|
9d9720a5a2 | ||
|
|
ff261f9c99 | ||
|
|
c7bea4f024 | ||
|
|
738707b251 | ||
|
|
a8699704de | ||
|
|
d9dc932e07 | ||
|
|
f57232bc23 | ||
|
|
b24ff8bb26 | ||
|
|
eff8e509ef | ||
|
|
6976fa8989 | ||
|
|
2b58160da9 | ||
|
|
44e0dd3e09 | ||
|
|
8ea57bc46b | ||
|
|
459d90e8d1 | ||
|
|
a97341f6f2 | ||
|
|
375a64a38e | ||
|
|
7c25fc2a9d | ||
|
|
1a26f5ce9e | ||
|
|
b47f40bd13 | ||
|
|
bcb7240ed2 | ||
|
|
405aa857b1 | ||
|
|
0bd2935885 | ||
|
|
4cba1540ac | ||
|
|
30051ab0d7 | ||
|
|
e15d515f71 | ||
|
|
36e2fa8e2b | ||
|
|
510ae3c0df | ||
|
|
f5edb34873 | ||
|
|
0895bd2be5 | ||
|
|
17bc3de0d0 | ||
|
|
4319f24f51 | ||
|
|
9ad113bc61 | ||
|
|
beadce96f6 | ||
|
|
b1d7d210fc | ||
|
|
32e96dc0a6 | ||
|
|
b16a11c365 | ||
|
|
81f218ddc9 | ||
|
|
be6f79521e | ||
|
|
551175862e | ||
|
|
dbad5ae9c7 | ||
|
|
768a45bbb8 | ||
|
|
bbe09ced1d | ||
|
|
2e7c534a3b | ||
|
|
71684dc517 | ||
|
|
4dbb55059d | ||
|
|
f480a271b3 | ||
|
|
9c81c0ce18 | ||
|
|
b220c582cc | ||
|
|
29f8ac625a | ||
|
|
5b6c462943 |
45
app/Casts/EndpointCollection.php
Normal file
45
app/Casts/EndpointCollection.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Casts;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Illuminate\Contracts\Database\Eloquent\Castable;
|
||||
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class EndpointCollection implements Castable
|
||||
{
|
||||
public static function castUsing(array $arguments)
|
||||
{
|
||||
return new class() implements CastsAttributes
|
||||
{
|
||||
public function get($model, $key, $value, $attributes)
|
||||
{
|
||||
if (!isset($attributes[$key])) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
$data = json_decode($attributes[$key], true);
|
||||
|
||||
return (new Collection($data))->map(function ($value) {
|
||||
return new Endpoint($value);
|
||||
});
|
||||
}
|
||||
|
||||
public function set($model, $key, $value, $attributes)
|
||||
{
|
||||
if (!is_array($value) && !$value instanceof Collection) {
|
||||
return new Collection();
|
||||
}
|
||||
|
||||
if (!$value instanceof Collection) {
|
||||
$value = new Collection($value);
|
||||
}
|
||||
|
||||
return [
|
||||
'ports' => $value->toJson(),
|
||||
];
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\PanelException;
|
||||
|
||||
class AllocationDoesNotBelongToServerException extends PanelException
|
||||
{
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class AutoAllocationNotEnabledException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* AutoAllocationNotEnabledException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Server auto-allocation is not enabled for this instance.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class CidrOutOfRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* CidrOutOfRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.cidr_out_of_range'));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class InvalidPortMappingException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* InvalidPortMappingException constructor.
|
||||
*/
|
||||
public function __construct(mixed $port)
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.invalid_mapping', ['port' => $port]));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoAutoAllocationSpaceAvailableException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* NoAutoAllocationSpaceAvailableException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(
|
||||
'Cannot assign additional allocation: no more space available on node.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class PortOutOfRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* PortOutOfRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.port_out_of_range'));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class ServerUsingAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Allocation;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class TooManyPortsInRangeException extends DisplayException
|
||||
{
|
||||
/**
|
||||
* TooManyPortsInRangeException constructor.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(trans('exceptions.allocations.too_many_ports'));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Exceptions\Service\Deployment;
|
||||
|
||||
use App\Exceptions\DisplayException;
|
||||
|
||||
class NoViableAllocationException extends DisplayException
|
||||
{
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Services\Databases\Hosts\HostCreationService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
@@ -59,7 +60,7 @@ class CreateDatabaseHost extends CreateRecord
|
||||
->numeric()
|
||||
->default(3306)
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
->maxValue(Endpoint::PORT_CEIL),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Filament\Resources\DatabaseHostResource\Pages;
|
||||
use App\Filament\Resources\DatabaseHostResource;
|
||||
use App\Filament\Resources\DatabaseHostResource\RelationManagers\DatabasesRelationManager;
|
||||
use App\Models\DatabaseHost;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Services\Databases\Hosts\HostUpdateService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
@@ -55,7 +56,7 @@ class EditDatabaseHost extends EditRecord
|
||||
->required()
|
||||
->numeric()
|
||||
->minValue(0)
|
||||
->maxValue(65535),
|
||||
->maxValue(Endpoint::PORT_CEIL),
|
||||
TextInput::make('max_databases')
|
||||
->label('Max databases')
|
||||
->helpertext('Blank is unlimited.')
|
||||
|
||||
@@ -65,7 +65,7 @@ class CreateEgg extends CreateRecord
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 2, 'lg' => 2]),
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary endpoint.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Hidden::make('script_is_privileged')
|
||||
|
||||
@@ -83,7 +83,7 @@ class EditEgg extends EditRecord
|
||||
Toggle::make('force_outgoing_ip')
|
||||
->inline(false)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's primary allocation IP.
|
||||
->hintIconTooltip("Forces all outgoing network traffic to have its Source IP NATed to the IP of the server's endpoint.
|
||||
Required for certain games to work properly when the Node has multiple public IP addresses.
|
||||
Enabling this option will disable internal networking for any servers using this egg, causing them to be unable to internally access other servers on the same node."),
|
||||
Hidden::make('script_is_privileged')
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\EggResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -33,11 +32,6 @@ class ServersRelationManager extends RelationManager
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.nodes.edit', ['record' => $server->node])),
|
||||
TextColumn::make('image')
|
||||
->label('Docker Image'),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Filament\Resources\NodeResource\Pages;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\AllocationsRelationManager;
|
||||
use App\Filament\Resources\NodeResource\RelationManagers\NodesRelationManager;
|
||||
use App\Models\Node;
|
||||
use Filament\Resources\Resource;
|
||||
@@ -24,7 +23,6 @@ class NodeResource extends Resource
|
||||
public static function getRelations(): array
|
||||
{
|
||||
return [
|
||||
AllocationsRelationManager::class,
|
||||
NodesRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Grid;
|
||||
@@ -139,7 +140,7 @@ class CreateNode extends CreateRecord
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
@@ -244,7 +245,7 @@ class CreateNode extends CreateRecord
|
||||
->columnSpan(1)
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Filament\Resources\NodeResource\Pages;
|
||||
|
||||
use App\Filament\Resources\NodeResource;
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Services\Nodes\NodeUpdateService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
@@ -165,7 +166,7 @@ class EditNode extends EditRecord
|
||||
->label(trans('strings.port'))
|
||||
->helperText('If you are running the daemon behind Cloudflare you should set the daemon port to 8443 to allow websocket proxying over SSL.')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(8080)
|
||||
->required()
|
||||
->integer(),
|
||||
@@ -243,7 +244,7 @@ class EditNode extends EditRecord
|
||||
->columnSpan(['default' => 1, 'sm' => 1, 'md' => 1, 'lg' => 3])
|
||||
->label('SFTP Port')
|
||||
->minValue(1)
|
||||
->maxValue(65535)
|
||||
->maxValue(Endpoint::PORT_CEIL)
|
||||
->default(2022)
|
||||
->required()
|
||||
->integer(),
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Node getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
|
||||
protected static ?string $icon = 'tabler-plug-connected';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('ip')
|
||||
|
||||
// Non Primary Allocations
|
||||
// ->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->id !== $allocation->server?->allocation_id)
|
||||
|
||||
// All assigned allocations
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $allocation) => $allocation->server_id === null)
|
||||
->searchable()
|
||||
->columns([
|
||||
TextColumn::make('id'),
|
||||
TextColumn::make('port')
|
||||
->searchable()
|
||||
->label('Port'),
|
||||
TextColumn::make('server.name')
|
||||
->label('Server')
|
||||
->icon('tabler-brand-docker')
|
||||
->searchable()
|
||||
->url(fn (Allocation $allocation): string => $allocation->server ? route('filament.admin.resources.servers.edit', ['record' => $allocation->server]) : ''),
|
||||
TextInputColumn::make('ip_alias')
|
||||
->searchable()
|
||||
->label('Alias'),
|
||||
TextInputColumn::make('ip')
|
||||
->searchable()
|
||||
->label('IP'),
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
])
|
||||
->actions([
|
||||
//
|
||||
])
|
||||
->headerActions([
|
||||
Tables\Actions\Action::make('create new allocation')->label('Create Allocations')
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord(), $data)),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make()
|
||||
->authorize(fn () => auth()->user()->can('delete allocation')),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ namespace App\Filament\Resources\NodeResource\RelationManagers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -33,11 +32,6 @@ class NodesRelationManager extends RelationManager
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->user]))
|
||||
->sortable(),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('memory')->icon('tabler-device-desktop-analytics'),
|
||||
TextColumn::make('cpu')->icon('tabler-cpu'),
|
||||
TextColumn::make('databases_count')
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Models\User;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ServerCreationService;
|
||||
use App\Services\Users\UserCreationService;
|
||||
@@ -25,7 +24,6 @@ use Filament\Forms\Components\Placeholder;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Section;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
@@ -35,7 +33,6 @@ use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -50,6 +47,12 @@ class CreateServer extends CreateRecord
|
||||
|
||||
public ?Node $node = null;
|
||||
|
||||
public ?Egg $egg = null;
|
||||
|
||||
public array $ports = [];
|
||||
|
||||
public array $eggDefaultPorts = [];
|
||||
|
||||
private ServerCreationService $serverCreationService;
|
||||
|
||||
public function boot(ServerCreationService $serverCreationService): void
|
||||
@@ -147,175 +150,11 @@ class CreateServer extends CreateRecord
|
||||
->relationship('node', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->afterStateUpdated(function (Set $set, $state) {
|
||||
$set('allocation_id', null);
|
||||
->afterStateUpdated(function (Forms\Set $set, $state) {
|
||||
$this->node = Node::find($state);
|
||||
})
|
||||
->required(),
|
||||
|
||||
Select::make('allocation_id')
|
||||
->preload()
|
||||
->live()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Primary Allocation')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 2,
|
||||
'lg' => 3,
|
||||
])
|
||||
->disabled(fn (Get $get) => $get('node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->afterStateUpdated(function (Set $set) {
|
||||
$set('allocation_additional', null);
|
||||
$set('allocation_additional.needstobeastringhere.extra_allocations', null);
|
||||
})
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder(function (Get $get) {
|
||||
$node = Node::find($get('node_id'));
|
||||
|
||||
if ($node?->allocations) {
|
||||
return 'Select an Allocation';
|
||||
}
|
||||
|
||||
return 'Create a New Allocation';
|
||||
})
|
||||
->relationship(
|
||||
'allocation',
|
||||
'ip',
|
||||
fn (Builder $query, Get $get) => $query
|
||||
->where('node_id', $get('node_id'))
|
||||
->whereNull('server_id'),
|
||||
)
|
||||
->createOptionForm(fn (Get $get) => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist(Node::find($get('node_id'))?->ipAddresses() ?? [])
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
// ->selectablePlaceholder(false)
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->datalist([
|
||||
$get('name'),
|
||||
Egg::find($get('egg_id'))?->name,
|
||||
])
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non-numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
$range = $start <= $end ? range($start, $end) : range($end, $start);
|
||||
foreach ($range as $i) {
|
||||
if ($i > 1024 && $i <= 65535) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->createOptionUsing(function (array $data, Get $get, AssignmentService $assignmentService): int {
|
||||
return collect(
|
||||
$assignmentService->handle(Node::find($get('node_id')), $data)
|
||||
)->first();
|
||||
})
|
||||
->required(),
|
||||
|
||||
Repeater::make('allocation_additional')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 3,
|
||||
'md' => 3,
|
||||
'lg' => 3,
|
||||
])
|
||||
->addActionLabel('Add Allocation')
|
||||
->disabled(fn (Get $get) => $get('allocation_id') === null)
|
||||
// ->addable() TODO disable when all allocations are taken
|
||||
// ->addable() TODO disable until first additional allocation is selected
|
||||
->simple(
|
||||
Select::make('extra_allocations')
|
||||
->live()
|
||||
->preload()
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->prefixIcon('tabler-network')
|
||||
->label('Additional Allocations')
|
||||
->columnSpan(2)
|
||||
->disabled(fn (Get $get) => $get('../../node_id') === null)
|
||||
->searchable(['ip', 'port', 'ip_alias'])
|
||||
->getOptionLabelFromRecordUsing(
|
||||
fn (Allocation $allocation) => "$allocation->ip:$allocation->port" .
|
||||
($allocation->ip_alias ? " ($allocation->ip_alias)" : '')
|
||||
)
|
||||
->placeholder('Select additional Allocations')
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->relationship(
|
||||
'allocations',
|
||||
'ip',
|
||||
fn (Builder $query, Get $get, Select $component, $state) => $query
|
||||
->where('node_id', $get('../../node_id'))
|
||||
->whereNot('id', $get('../../allocation_id'))
|
||||
->whereNull('server_id'),
|
||||
),
|
||||
),
|
||||
|
||||
Textarea::make('description')
|
||||
->placeholder('Description')
|
||||
->rows(3)
|
||||
@@ -341,40 +180,26 @@ class CreateServer extends CreateRecord
|
||||
->schema([
|
||||
Select::make('egg_id')
|
||||
->prefixIcon('tabler-egg')
|
||||
->relationship('egg', 'name')
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'default' => 2,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->relationship('egg', 'name')
|
||||
->searchable()
|
||||
->preload()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set, Get $get, $old) {
|
||||
$egg = Egg::query()->find($state);
|
||||
$set('startup', $egg->startup ?? '');
|
||||
$this->egg = Egg::query()->find($state);
|
||||
$set('startup', $this->egg?->startup);
|
||||
$set('image', '');
|
||||
|
||||
$variables = $egg->variables ?? [];
|
||||
$serverVariables = collect();
|
||||
foreach ($variables as $variable) {
|
||||
$serverVariables->add($variable->toArray());
|
||||
}
|
||||
|
||||
$variables = [];
|
||||
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
|
||||
for ($i = 0; $i < $serverVariables->count(); $i++) {
|
||||
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
|
||||
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
|
||||
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
|
||||
}
|
||||
|
||||
$set('environment', $variables);
|
||||
$this->resetEggVariables($set, $get);
|
||||
|
||||
$previousEgg = Egg::query()->find($old);
|
||||
if (!$get('name') || $previousEgg?->getKebabName() === $get('name')) {
|
||||
$set('name', $egg->getKebabName());
|
||||
$set('name', $this->egg->getKebabName());
|
||||
}
|
||||
})
|
||||
->required(),
|
||||
@@ -430,13 +255,21 @@ class CreateServer extends CreateRecord
|
||||
Textarea::make('startup')
|
||||
->hintIcon('tabler-code')
|
||||
->label('Startup Command')
|
||||
->hidden(fn (Get $get) => $get('egg_id') === null)
|
||||
->hidden(fn () => !$this->egg)
|
||||
->required()
|
||||
->live()
|
||||
->disabled(fn (Forms\Get $get) => $this->egg === null)
|
||||
->afterStateUpdated($this->resetEggVariables(...))
|
||||
->columnSpan([
|
||||
'default' => 1,
|
||||
'sm' => 2,
|
||||
'md' => 2,
|
||||
'lg' => 4,
|
||||
])
|
||||
->rows(function ($state) {
|
||||
return str($state)->explode("\n")->reduce(
|
||||
fn (int $carry, $line) => $carry + floor(strlen($line) / 125),
|
||||
1
|
||||
0
|
||||
);
|
||||
})
|
||||
->columnSpan([
|
||||
@@ -520,6 +353,70 @@ class CreateServer extends CreateRecord
|
||||
->columnSpan(2),
|
||||
]),
|
||||
]),
|
||||
|
||||
Wizard\Step::make('Allocation')
|
||||
->label('Allocation')
|
||||
->icon('tabler-transfer-in')
|
||||
->completedIcon('tabler-check')
|
||||
->columns(4)
|
||||
->schema([
|
||||
|
||||
Forms\Components\TagsInput::make('ports')
|
||||
->columnSpan(2)
|
||||
->hintIcon('tabler-question-mark')
|
||||
->hintIconTooltip('Ports are limited from 1025 to 65535')
|
||||
->placeholder('Example: 25565, 8080, 1337-1340')
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
You would typically port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->afterStateUpdated(self::ports(...))
|
||||
->live(),
|
||||
|
||||
Forms\Components\Repeater::make('assignments')
|
||||
->columnSpan(2)
|
||||
->defaultItems(fn () => count($this->eggDefaultPorts))
|
||||
->label('Port Assignments')
|
||||
->helperText(function (Forms\Get $get) {
|
||||
if (empty($this->eggDefaultPorts)) {
|
||||
return "This egg doesn't have any ports defined.";
|
||||
}
|
||||
|
||||
if (empty($get('ports'))) {
|
||||
return 'You must add ports to assign them!';
|
||||
}
|
||||
|
||||
return '';
|
||||
})
|
||||
->live()
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
->reorderable(false)
|
||||
->simple(
|
||||
Forms\Components\Select::make('port')
|
||||
->live()
|
||||
->placeholder('Select a Port')
|
||||
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
|
||||
->prefix(function (Forms\Components\Component $component) {
|
||||
$key = str($component->getStatePath())->beforeLast('.')->afterLast('.')->toString();
|
||||
|
||||
return $key;
|
||||
})
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->options(fn (Forms\Get $get) => $this->ports)
|
||||
->required(),
|
||||
),
|
||||
|
||||
Forms\Components\Select::make('ip')
|
||||
->label('IP Address')
|
||||
->options(fn () => collect($this->node?->ipAddresses())->mapWithKeys(fn ($ip) => [$ip => $ip]))
|
||||
->placeholder('Any')
|
||||
->columnSpan(1),
|
||||
|
||||
]),
|
||||
|
||||
Step::make('Environment Configuration')
|
||||
->label('Environment Configuration')
|
||||
->icon('tabler-brand-docker')
|
||||
@@ -679,7 +576,7 @@ class CreateServer extends CreateRecord
|
||||
Hidden::make('io')
|
||||
->helperText('The IO performance relative to other running containers')
|
||||
->label('Block IO Proportion')
|
||||
->default(500),
|
||||
->default(config('panel.default_io_weight')),
|
||||
|
||||
Grid::make()
|
||||
->columns(4)
|
||||
@@ -832,9 +729,16 @@ class CreateServer extends CreateRecord
|
||||
|
||||
protected function handleRecordCreation(array $data): Model
|
||||
{
|
||||
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
|
||||
$ipAddress = $data['ip'] ?? Endpoint::INADDR_ANY;
|
||||
foreach ($data['ports'] ?? [] as $i => $port) {
|
||||
$data['ports'][$i] = (string) new Endpoint($port, $ipAddress);
|
||||
}
|
||||
|
||||
return $this->serverCreationService->handle($data);
|
||||
foreach (array_keys($this->eggDefaultPorts) as $i => $env) {
|
||||
$data['environment'][$env] = $data['ports'][$data['assignments'][$i]];
|
||||
}
|
||||
|
||||
return $this->serverCreationService->handle($data, validateVariables: false);
|
||||
}
|
||||
|
||||
private function shouldHideComponent(Get $get, Component $component): bool
|
||||
@@ -867,4 +771,79 @@ class CreateServer extends CreateRecord
|
||||
->mapWithKeys(fn ($value) => [$value => $value])
|
||||
->all();
|
||||
}
|
||||
|
||||
public function ports(array $state, Forms\Set $set): void
|
||||
{
|
||||
$ports = collect();
|
||||
foreach ($state as $portEntry) {
|
||||
if (str_contains($portEntry, '-')) {
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, Endpoint::PORT_FLOOR);
|
||||
$end = min((int) $end, Endpoint::PORT_CEIL);
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_numeric($portEntry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ports->push((int) $portEntry);
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > Endpoint::PORT_FLOOR && $port < Endpoint::PORT_CEIL)->values();
|
||||
|
||||
$set('ports', $ports->all());
|
||||
$this->ports = $ports->all();
|
||||
}
|
||||
|
||||
public function resetEggVariables(Forms\Set $set, Forms\Get $get): void
|
||||
{
|
||||
$set('assignments', []);
|
||||
|
||||
$i = 0;
|
||||
$this->eggDefaultPorts = [];
|
||||
if (str_contains($get('startup'), '{{SERVER_PORT}}') || str_contains($this->egg->config_files, '{{server.allocations.default.port}}')) {
|
||||
$this->eggDefaultPorts['SERVER_PORT'] = null;
|
||||
$set('assignments.SERVER_PORT', ['port' => null]);
|
||||
}
|
||||
|
||||
$variables = $this->egg->variables ?? [];
|
||||
$serverVariables = collect();
|
||||
$this->ports = [];
|
||||
foreach ($variables as $variable) {
|
||||
if (in_array('port', $variable->rules)) {
|
||||
$this->eggDefaultPorts[$variable->env_variable] = $variable->default_value;
|
||||
$this->ports[] = (int) $variable->default_value;
|
||||
|
||||
$set("assignments.$variable->env_variable", ['port' => $i++]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$serverVariables->add($variable->toArray());
|
||||
}
|
||||
|
||||
$set('ports', $this->ports);
|
||||
|
||||
$variables = [];
|
||||
$set($path = 'server_variables', $serverVariables->sortBy(['sort'])->all());
|
||||
for ($i = 0; $i < $serverVariables->count(); $i++) {
|
||||
$set("$path.$i.variable_value", $serverVariables[$i]['default_value']);
|
||||
$set("$path.$i.variable_id", $serverVariables[$i]['id']);
|
||||
$variables[$serverVariables[$i]['env_variable']] = $serverVariables[$i]['default_value'];
|
||||
}
|
||||
|
||||
$set('environment', $variables);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,29 +2,36 @@
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\Pages;
|
||||
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Models\Node;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\HtmlString;
|
||||
use App\Models\Database;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use LogicException;
|
||||
use App\Filament\Resources\ServerResource;
|
||||
use App\Http\Controllers\Admin\ServersController;
|
||||
use App\Models\Database;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Models\Egg;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerVariable;
|
||||
use App\Services\Databases\DatabaseManagementService;
|
||||
use App\Services\Databases\DatabasePasswordService;
|
||||
use App\Services\Servers\RandomWordService;
|
||||
use App\Services\Servers\ServerDeletionService;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use App\Services\Servers\TransferServerService;
|
||||
use Closure;
|
||||
use Exception;
|
||||
use Filament\Actions;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Actions\Action;
|
||||
use Filament\Forms\Components\CheckboxList;
|
||||
use Filament\Forms\Components\Fieldset;
|
||||
use Filament\Forms\Components\Grid;
|
||||
use Filament\Forms\Components\Repeater;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Forms\Components\Tabs;
|
||||
use Filament\Forms\Components\Tabs\Tab;
|
||||
@@ -32,17 +39,21 @@ use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Get;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Notifications\Notification;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use LogicException;
|
||||
use Webbingbrasil\FilamentCopyActions\Forms\Actions\CopyAction;
|
||||
|
||||
class EditServer extends EditRecord
|
||||
{
|
||||
public ?Node $node = null;
|
||||
|
||||
public ?Egg $egg = null;
|
||||
|
||||
public array $ports = [];
|
||||
|
||||
public array $eggDefaultPorts = [];
|
||||
|
||||
protected static string $resource = ServerResource::class;
|
||||
|
||||
public function form(Form $form): Form
|
||||
@@ -62,6 +73,26 @@ class EditServer extends EditRecord
|
||||
Tab::make('Information')
|
||||
->icon('tabler-info-circle')
|
||||
->schema([
|
||||
Forms\Components\ToggleButtons::make('condition')
|
||||
->label('Status')
|
||||
->formatStateUsing(fn (Server $server) => $server->condition)
|
||||
->options(fn ($state) => collect(array_merge(ContainerStatus::cases(), ServerState::cases()))
|
||||
->filter(fn ($condition) => $condition->value === $state)
|
||||
->mapWithKeys(fn ($state) => [$state->value => str($state->value)->replace('_', ' ')->ucwords()])
|
||||
)
|
||||
->colors(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
|
||||
fn ($status) => [$status->value => $status->color()]
|
||||
))
|
||||
->icons(collect(array_merge(ContainerStatus::cases(), ServerState::cases()))->mapWithKeys(
|
||||
fn ($status) => [$status->value => $status->icon()]
|
||||
))
|
||||
->columnSpan([
|
||||
'default' => 2,
|
||||
'sm' => 1,
|
||||
'md' => 1,
|
||||
'lg' => 1,
|
||||
]),
|
||||
|
||||
TextInput::make('name')
|
||||
->prefixIcon('tabler-server')
|
||||
->label('Display Name')
|
||||
@@ -119,7 +150,7 @@ class EditServer extends EditRecord
|
||||
]),
|
||||
|
||||
Textarea::make('description')
|
||||
->label('Description')
|
||||
->label('Notes')
|
||||
->columnSpanFull(),
|
||||
|
||||
TextInput::make('uuid')
|
||||
@@ -163,6 +194,7 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->disabled(),
|
||||
]),
|
||||
|
||||
Tab::make('Environment')
|
||||
->icon('tabler-brand-docker')
|
||||
->schema([
|
||||
@@ -460,6 +492,66 @@ class EditServer extends EditRecord
|
||||
])
|
||||
->required(),
|
||||
|
||||
Forms\Components\TagsInput::make('ports')
|
||||
->columnSpan(3)
|
||||
->placeholder('Example: 25565, 8080, 1337-1340')
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would typically port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->formatStateUsing(fn (Server $server) => $server->ports->map(fn ($port) => (string) $port)->all())
|
||||
->afterStateUpdated(self::ports(...))
|
||||
->live(),
|
||||
|
||||
Forms\Components\Repeater::make('portVariables')
|
||||
->label('Port Assignments')
|
||||
->columnSpan(3)
|
||||
->addable(false)
|
||||
->deletable(false)
|
||||
|
||||
->mutateRelationshipDataBeforeSaveUsing(function ($data) {
|
||||
$portIndex = $data['port'];
|
||||
unset($data['port']);
|
||||
|
||||
return [
|
||||
'variable_value' => (string) $this->ports[$portIndex],
|
||||
];
|
||||
})
|
||||
|
||||
->relationship('serverVariables', function (Builder $query) {
|
||||
$query->whereHas('variable', function (Builder $query) {
|
||||
$query->where('rules', 'like', '%port%');
|
||||
});
|
||||
})
|
||||
|
||||
->simple(
|
||||
Forms\Components\Select::make('port')
|
||||
->live()
|
||||
->disabled(fn (Forms\Get $get) => empty($get('../../ports')) || empty($get('../../assignments')))
|
||||
->prefix(function (Forms\Components\Component $component, ServerVariable $serverVariable) {
|
||||
return $serverVariable->variable->env_variable;
|
||||
})
|
||||
|
||||
->formatStateUsing(function (ServerVariable $serverVariable, Forms\Get $get) {
|
||||
return array_search($serverVariable->variable_value, array_values($get('../../ports')));
|
||||
})
|
||||
|
||||
->disableOptionsWhenSelectedInSiblingRepeaterItems()
|
||||
->options(fn (Forms\Get $get) => $this->ports)
|
||||
->required(),
|
||||
)
|
||||
|
||||
->afterStateHydrated(function (Forms\Set $set, Forms\Get $get, Server $server) {
|
||||
$this->ports($ports = $get('ports'), $set);
|
||||
|
||||
foreach ($this->portOptions($server->egg) as $key => $port) {
|
||||
$set("assignments.$key", ['port' => $portIndex = array_search($port, array_values($ports))]);
|
||||
}
|
||||
}),
|
||||
|
||||
Textarea::make('startup')
|
||||
->label('Startup Command')
|
||||
->required()
|
||||
@@ -542,7 +634,7 @@ class EditServer extends EditRecord
|
||||
->label(fn (ServerVariable $serverVariable) => $serverVariable->variable->name)
|
||||
->hintIconTooltip(fn (ServerVariable $serverVariable) => implode('|', $serverVariable->variable->rules))
|
||||
->prefix(fn (ServerVariable $serverVariable) => '{{' . $serverVariable->variable->env_variable . '}}')
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable->description) ? '—' : $serverVariable->variable->description);
|
||||
->helperText(fn (ServerVariable $serverVariable) => empty($serverVariable->variable?->description) ? '—' : $serverVariable->variable->description);
|
||||
}
|
||||
|
||||
return $components;
|
||||
@@ -781,17 +873,14 @@ class EditServer extends EditRecord
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getRelationManagers(): array
|
||||
{
|
||||
return [
|
||||
ServerResource\RelationManagers\AllocationsRelationManager::class,
|
||||
];
|
||||
}
|
||||
|
||||
private function shouldHideComponent(ServerVariable $serverVariable, Forms\Components\Component $component): bool
|
||||
{
|
||||
$containsRuleIn = array_first($serverVariable->variable->rules, fn ($value) => str($value)->startsWith('in:'), false);
|
||||
|
||||
if (collect($serverVariable->variable->rules)->contains('port')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($component instanceof Select) {
|
||||
return !$containsRuleIn;
|
||||
}
|
||||
@@ -815,6 +904,76 @@ class EditServer extends EditRecord
|
||||
->all();
|
||||
}
|
||||
|
||||
public function ports(array $state, Forms\Set $set): void
|
||||
{
|
||||
$ports = collect();
|
||||
|
||||
foreach ($state as $portEntry) {
|
||||
if (str_contains($portEntry, '-')) {
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
|
||||
try {
|
||||
$startEndpoint = new Endpoint($start);
|
||||
$endEndpoint = new Endpoint($end);
|
||||
} catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($startEndpoint->ip !== $endEndpoint->ip) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (range($startEndpoint->port, $endEndpoint->port) as $port) {
|
||||
$ports->push(new Endpoint($port, $startEndpoint->ip));
|
||||
}
|
||||
|
||||
for ($i = $start; $i <= $end; $i++) {
|
||||
$ports->push($i);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$ports->push(new Endpoint($portEntry));
|
||||
} catch (Exception) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$ports = $ports->map(fn ($endpoint) => (string) $endpoint);
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$set('ports', $ports->all());
|
||||
$this->ports = $ports->all();
|
||||
}
|
||||
|
||||
public function portOptions(Egg $egg, ?string $startup = null): array
|
||||
{
|
||||
if (empty($startup)) {
|
||||
$startup = $egg->startup;
|
||||
}
|
||||
|
||||
$options = [];
|
||||
if (str_contains($startup, '{{SERVER_PORT}}')) {
|
||||
$options['SERVER_PORT'] = null;
|
||||
}
|
||||
|
||||
foreach ($egg->variables as $variable) {
|
||||
if (!in_array('port', $variable->rules)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[$variable->env_variable] = $variable->default_value;
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
protected function rotatePassword(DatabasePasswordService $service, Database $record, Set $set, Get $get): void
|
||||
{
|
||||
$newPassword = $service->handle($record);
|
||||
|
||||
@@ -9,7 +9,6 @@ use Filament\Resources\Pages\ListRecords;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Grouping\Group;
|
||||
use Filament\Tables\Table;
|
||||
@@ -61,16 +60,6 @@ class ListServers extends ListRecords
|
||||
->hidden(fn (Table $table) => $table->getGrouping()?->getId() === 'user.username')
|
||||
->sortable()
|
||||
->searchable(),
|
||||
SelectColumn::make('allocation_id')
|
||||
->label('Primary Allocation')
|
||||
->hidden(!auth()->user()->can('update server'))
|
||||
->options(fn (Server $server) => $server->allocations->mapWithKeys(fn ($allocation) => [$allocation->id => $allocation->address]))
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('allocation_id_readonly')
|
||||
->label('Primary Allocation')
|
||||
->hidden(auth()->user()->can('update server'))
|
||||
->state(fn (Server $server) => $server->allocation->address),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('backups_count')
|
||||
->counts('backups')
|
||||
@@ -78,6 +67,9 @@ class ListServers extends ListRecords
|
||||
->icon('tabler-file-download')
|
||||
->numeric()
|
||||
->sortable(),
|
||||
TextColumn::make('ports')
|
||||
->badge()
|
||||
->separator(),
|
||||
])
|
||||
->actions([
|
||||
Action::make('View')
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ServerResource\RelationManagers;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Server;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use Filament\Forms\Components\TagsInput;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Forms\Set;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\AssociateAction;
|
||||
use Filament\Tables\Actions\CreateAction;
|
||||
use Filament\Tables\Columns\IconColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
/**
|
||||
* @method Server getOwnerRecord()
|
||||
*/
|
||||
class AllocationsRelationManager extends RelationManager
|
||||
{
|
||||
protected static string $relationship = 'allocations';
|
||||
|
||||
public function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
TextInput::make('ip')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
]);
|
||||
}
|
||||
|
||||
public function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->recordTitleAttribute('ip')
|
||||
->recordTitle(fn (Allocation $allocation) => "$allocation->ip:$allocation->port")
|
||||
->checkIfRecordIsSelectableUsing(fn (Allocation $record) => $record->id !== $this->getOwnerRecord()->allocation_id)
|
||||
->inverseRelationship('server')
|
||||
->columns([
|
||||
TextColumn::make('ip')->label('IP'),
|
||||
TextColumn::make('port')->label('Port'),
|
||||
TextInputColumn::make('ip_alias')->label('Alias'),
|
||||
IconColumn::make('primary')
|
||||
->icon(fn ($state) => match ($state) {
|
||||
true => 'tabler-star-filled',
|
||||
default => 'tabler-star',
|
||||
})
|
||||
->color(fn ($state) => match ($state) {
|
||||
true => 'warning',
|
||||
default => 'gray',
|
||||
})
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
->default(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id)
|
||||
->label('Primary'),
|
||||
])
|
||||
->actions([
|
||||
Action::make('make-primary')
|
||||
->action(fn (Allocation $allocation) => $this->getOwnerRecord()->update(['allocation_id' => $allocation->id]))
|
||||
->label(fn (Allocation $allocation) => $allocation->id === $this->getOwnerRecord()->allocation_id ? '' : 'Make Primary'),
|
||||
])
|
||||
->headerActions([
|
||||
CreateAction::make()->label('Create Allocation')
|
||||
->createAnother(false)
|
||||
->form(fn () => [
|
||||
TextInput::make('allocation_ip')
|
||||
->datalist($this->getOwnerRecord()->node->ipAddresses())
|
||||
->label('IP Address')
|
||||
->inlineLabel()
|
||||
->ipv4()
|
||||
->helperText("Usually your machine's public IP unless you are port forwarding.")
|
||||
->required(),
|
||||
TextInput::make('allocation_alias')
|
||||
->label('Alias')
|
||||
->inlineLabel()
|
||||
->default(null)
|
||||
->helperText('Optional display name to help you remember what these are.')
|
||||
->required(false),
|
||||
TagsInput::make('allocation_ports')
|
||||
->placeholder('Examples: 27015, 27017-27019')
|
||||
->helperText(new HtmlString('
|
||||
These are the ports that users can connect to this Server through.
|
||||
<br />
|
||||
You would have to port forward these on your home network.
|
||||
'))
|
||||
->label('Ports')
|
||||
->inlineLabel()
|
||||
->live()
|
||||
->afterStateUpdated(function ($state, Set $set) {
|
||||
$ports = collect();
|
||||
$update = false;
|
||||
foreach ($state as $portEntry) {
|
||||
if (!str_contains($portEntry, '-')) {
|
||||
if (is_numeric($portEntry)) {
|
||||
$ports->push((int) $portEntry);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not add non numerical ports
|
||||
$update = true;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$update = true;
|
||||
[$start, $end] = explode('-', $portEntry);
|
||||
if (!is_numeric($start) || !is_numeric($end)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$start = max((int) $start, 0);
|
||||
$end = min((int) $end, 2 ** 16 - 1);
|
||||
foreach (range($start, $end) as $i) {
|
||||
$ports->push($i);
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePorts = $ports->unique()->values();
|
||||
if ($ports->count() > $uniquePorts->count()) {
|
||||
$update = true;
|
||||
$ports = $uniquePorts;
|
||||
}
|
||||
|
||||
$sortedPorts = $ports->sort()->values();
|
||||
if ($sortedPorts->all() !== $ports->all()) {
|
||||
$update = true;
|
||||
$ports = $sortedPorts;
|
||||
}
|
||||
|
||||
$ports = $ports->filter(fn ($port) => $port > 1024 && $port < 65535)->values();
|
||||
|
||||
if ($update) {
|
||||
$set('allocation_ports', $ports->all());
|
||||
}
|
||||
})
|
||||
->splitKeys(['Tab', ' ', ','])
|
||||
->required(),
|
||||
])
|
||||
->action(fn (array $data, AssignmentService $service) => $service->handle($this->getOwnerRecord()->node, $data, $this->getOwnerRecord())),
|
||||
AssociateAction::make()
|
||||
->multiple()
|
||||
->associateAnother(false)
|
||||
->preloadRecordSelect()
|
||||
->recordSelectOptionsQuery(fn ($query) => $query->whereBelongsTo($this->getOwnerRecord()->node))
|
||||
->label('Add Allocation'),
|
||||
])
|
||||
->bulkActions([
|
||||
Tables\Actions\BulkActionGroup::make([
|
||||
Tables\Actions\DissociateBulkAction::make(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
Tab::make('API Keys')
|
||||
->icon('tabler-key')
|
||||
->schema([
|
||||
Grid::make('asdf')->columns(5)->schema([
|
||||
Grid::make(5)->schema([
|
||||
Section::make('Create API Key')->columnSpan(3)->schema([
|
||||
TextInput::make('description')
|
||||
->live(),
|
||||
@@ -289,7 +289,7 @@ class EditProfile extends \Filament\Pages\Auth\EditProfile
|
||||
|
||||
if ($token = $data['2facode'] ?? null) {
|
||||
$tokens = $this->toggleTwoFactorService->handle($record, $token, true);
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), now()->addSeconds(15));
|
||||
cache()->set("users.$record->id.2fa.tokens", implode("\n", $tokens), 15);
|
||||
|
||||
$this->redirectRoute('filament.admin.auth.profile', ['tab' => '-2fa-tab']);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use App\Models\User;
|
||||
use App\Services\Servers\SuspensionService;
|
||||
use Filament\Resources\RelationManagers\RelationManager;
|
||||
use Filament\Tables\Actions;
|
||||
use Filament\Tables\Columns\SelectColumn;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Table;
|
||||
|
||||
@@ -66,11 +65,6 @@ class ServersRelationManager extends RelationManager
|
||||
->icon('tabler-egg')
|
||||
->url(fn (Server $server): string => route('filament.admin.resources.eggs.edit', ['record' => $server->egg]))
|
||||
->sortable(),
|
||||
SelectColumn::make('allocation.id')
|
||||
->label('Primary Allocation')
|
||||
->options(fn (Server $server) => [$server->allocation->id => $server->allocation->address])
|
||||
->selectablePlaceholder(false)
|
||||
->sortable(),
|
||||
TextColumn::make('image')->hidden(),
|
||||
TextColumn::make('databases_count')
|
||||
->counts('databases')
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin\Nodes;
|
||||
use Illuminate\View\View;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Traits\Controllers\JavascriptInjection;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
@@ -57,32 +56,6 @@ class NodeViewController extends Controller
|
||||
return view('admin.nodes.view.configuration', compact('node'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the node allocation management page.
|
||||
*/
|
||||
public function allocations(Node $node): View
|
||||
{
|
||||
$node->setRelation(
|
||||
'allocations',
|
||||
$node->allocations()
|
||||
->orderByRaw('server_id IS NOT NULL DESC, server_id IS NULL')
|
||||
->orderByRaw('INET_ATON(ip) ASC')
|
||||
->orderBy('port')
|
||||
->with('server:id,name')
|
||||
->paginate(50)
|
||||
);
|
||||
|
||||
$this->plainInject(['node' => Collection::wrap($node)->only(['id'])]);
|
||||
|
||||
return view('admin.nodes.view.allocation', [
|
||||
'node' => $node,
|
||||
'allocations' => Allocation::query()->where('node_id', $node->id)
|
||||
->groupBy('ip')
|
||||
->orderByRaw('INET_ATON(ip) ASC')
|
||||
->get(['ip']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a listing of servers that exist for this specific node.
|
||||
*/
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\Node;
|
||||
use Illuminate\Http\Response;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Prologue\Alerts\AlertsMessageBag;
|
||||
use Illuminate\View\Factory as ViewFactory;
|
||||
@@ -15,11 +12,8 @@ use App\Services\Nodes\NodeUpdateService;
|
||||
use Illuminate\Cache\Repository as CacheRepository;
|
||||
use App\Services\Nodes\NodeCreationService;
|
||||
use App\Services\Nodes\NodeDeletionService;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Services\Helpers\SoftwareVersionService;
|
||||
use App\Http\Requests\Admin\Node\NodeFormRequest;
|
||||
use App\Http\Requests\Admin\Node\AllocationFormRequest;
|
||||
use App\Http\Requests\Admin\Node\AllocationAliasFormRequest;
|
||||
|
||||
class NodesController extends Controller
|
||||
{
|
||||
@@ -28,7 +22,6 @@ class NodesController extends Controller
|
||||
*/
|
||||
public function __construct(
|
||||
protected AlertsMessageBag $alert,
|
||||
protected AssignmentService $assignmentService,
|
||||
protected CacheRepository $cache,
|
||||
protected NodeCreationService $creationService,
|
||||
protected NodeDeletionService $deletionService,
|
||||
@@ -46,19 +39,6 @@ class NodesController extends Controller
|
||||
return view('admin.nodes.new');
|
||||
}
|
||||
|
||||
/**
|
||||
* Post controller to create a new node on the system.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(NodeFormRequest $request): RedirectResponse
|
||||
{
|
||||
$node = $this->creationService->handle($request->normalize());
|
||||
$this->alert->info(trans('admin/node.notices.node_created'))->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates settings for a node.
|
||||
*
|
||||
@@ -73,83 +53,6 @@ class NodesController extends Controller
|
||||
return redirect()->route('admin.nodes.view.settings', $node->id)->withInput();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single allocation from a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
|
||||
*/
|
||||
public function allocationRemoveSingle(int $node, Allocation $allocation): Response
|
||||
{
|
||||
$allocation->delete();
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes multiple individual allocations from a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\ServerUsingAllocationException
|
||||
*/
|
||||
public function allocationRemoveMultiple(Request $request, int $node): Response
|
||||
{
|
||||
$allocations = $request->input('allocations');
|
||||
foreach ($allocations as $rawAllocation) {
|
||||
$allocation = new Allocation();
|
||||
$allocation->id = $rawAllocation['id'];
|
||||
$this->allocationRemoveSingle($node, $allocation);
|
||||
}
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all allocations for a specific IP at once on a node.
|
||||
*/
|
||||
public function allocationRemoveBlock(Request $request, int $node): RedirectResponse
|
||||
{
|
||||
/** @var Node $node */
|
||||
$node = Node::query()->findOrFail($node);
|
||||
$node->allocations()
|
||||
->where('ip', $request->input('ip'))
|
||||
->whereNull('server_id')
|
||||
->delete();
|
||||
|
||||
$this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')]))
|
||||
->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an alias for a specific allocation on a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function allocationSetAlias(AllocationAliasFormRequest $request): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$allocation = Allocation::query()->findOrFail($request->input('allocation_id'));
|
||||
$alias = (empty($request->input('alias'))) ? null : $request->input('alias');
|
||||
$allocation->update(['ip_alias' => $alias]);
|
||||
|
||||
return response('', 204);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new allocations on a node.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function createAllocation(AllocationFormRequest $request, Node $node): RedirectResponse
|
||||
{
|
||||
$this->assignmentService->handle($node, $request->normalize());
|
||||
$this->alert->success(trans('admin/node.notices.allocations_added'))->flash();
|
||||
|
||||
return redirect()->route('admin.nodes.view.allocation', $node->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a node from the system.
|
||||
*
|
||||
|
||||
@@ -36,11 +36,6 @@ class CreateServerController extends Controller
|
||||
|
||||
$eggs = Egg::with('variables')->get();
|
||||
|
||||
\JavaScript::put([
|
||||
'nodeData' => Node::getForServerCreation(),
|
||||
'eggs' => $eggs->keyBy('id'),
|
||||
]);
|
||||
|
||||
return view('admin.servers.new', [
|
||||
'eggs' => $eggs,
|
||||
'nodes' => Node::all(),
|
||||
@@ -52,7 +47,6 @@ class CreateServerController extends Controller
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function store(ServerFormRequest $request): RedirectResponse
|
||||
|
||||
@@ -29,8 +29,6 @@ class ServerTransferController extends Controller
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
|
||||
@@ -47,12 +47,8 @@ class ServerViewController extends Controller
|
||||
*/
|
||||
public function build(Server $server): View
|
||||
{
|
||||
$allocations = $server->node->allocations->toBase();
|
||||
|
||||
return view('admin.servers.view.build', [
|
||||
'server' => $server,
|
||||
'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'),
|
||||
'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -121,10 +117,6 @@ class ServerViewController extends Controller
|
||||
$canTransfer = true;
|
||||
}
|
||||
|
||||
\JavaScript::put([
|
||||
'nodeData' => Node::getForServerCreation(),
|
||||
]);
|
||||
|
||||
return view('admin.servers.view.manage', [
|
||||
'nodes' => Node::all(),
|
||||
'server' => $server,
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Application\Nodes;
|
||||
|
||||
use App\Models\Node;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Allocation;
|
||||
use Spatie\QueryBuilder\QueryBuilder;
|
||||
use Spatie\QueryBuilder\AllowedFilter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Transformers\Api\Application\AllocationTransformer;
|
||||
use App\Http\Controllers\Api\Application\ApplicationApiController;
|
||||
use App\Http\Requests\Api\Application\Allocations\GetAllocationsRequest;
|
||||
use App\Http\Requests\Api\Application\Allocations\StoreAllocationRequest;
|
||||
use App\Http\Requests\Api\Application\Allocations\DeleteAllocationRequest;
|
||||
|
||||
class AllocationController extends ApplicationApiController
|
||||
{
|
||||
/**
|
||||
* AllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AssignmentService $assignmentService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the allocations that exist for a given node.
|
||||
*/
|
||||
public function index(GetAllocationsRequest $request, Node $node): array
|
||||
{
|
||||
$allocations = QueryBuilder::for($node->allocations())
|
||||
->allowedFilters([
|
||||
AllowedFilter::exact('ip'),
|
||||
AllowedFilter::exact('port'),
|
||||
'ip_alias',
|
||||
AllowedFilter::callback('server_id', function (Builder $builder, $value) {
|
||||
if (empty($value) || is_bool($value) || !ctype_digit((string) $value)) {
|
||||
return $builder->whereNull('server_id');
|
||||
}
|
||||
|
||||
return $builder->where('server_id', $value);
|
||||
}),
|
||||
])
|
||||
->paginate($request->query('per_page') ?? 50);
|
||||
|
||||
return $this->fractal->collection($allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new allocations for a given node.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function store(StoreAllocationRequest $request, Node $node): JsonResponse
|
||||
{
|
||||
$this->assignmentService->handle($node, $request->validated());
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific allocation from the Panel.
|
||||
*/
|
||||
public function delete(DeleteAllocationRequest $request, Node $node, Allocation $allocation): JsonResponse
|
||||
{
|
||||
$allocation->delete();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ class ServerController extends ApplicationApiController
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Model\DataValidationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
public function store(StoreServerRequest $request): JsonResponse
|
||||
{
|
||||
|
||||
@@ -69,8 +69,6 @@ class ServerManagementController extends ApplicationApiController
|
||||
{
|
||||
$validatedData = $request->validate([
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'allocation_additional' => 'nullable',
|
||||
]);
|
||||
|
||||
if ($this->transferServerService->handle($server, $validatedData)) {
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Client\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Facades\Activity;
|
||||
use App\Models\Allocation;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Transformers\Api\Client\AllocationTransformer;
|
||||
use App\Http\Controllers\Api\Client\ClientApiController;
|
||||
use App\Services\Allocations\FindAssignableAllocationService;
|
||||
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\DeleteAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\UpdateAllocationRequest;
|
||||
use App\Http\Requests\Api\Client\Servers\Network\SetPrimaryAllocationRequest;
|
||||
|
||||
class NetworkAllocationController extends ClientApiController
|
||||
{
|
||||
/**
|
||||
* NetworkAllocationController constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private FindAssignableAllocationService $assignableAllocationService,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all the allocations available to a server and whether
|
||||
* they are currently assigned as the primary for this server.
|
||||
*/
|
||||
public function index(GetNetworkRequest $request, Server $server): array
|
||||
{
|
||||
return $this->fractal->collection($server->allocations)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\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->toString(), 'old' => $original, 'new' => $allocation->notes])
|
||||
->log();
|
||||
}
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the primary allocation for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\Model\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->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the notes for the allocation for a server.
|
||||
*s.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
public function store(NewAllocationRequest $request, Server $server): array
|
||||
{
|
||||
if ($server->allocations()->count() >= $server->allocation_limit) {
|
||||
throw new DisplayException('Cannot assign additional allocations to this server: limit has been reached.');
|
||||
}
|
||||
|
||||
$allocation = $this->assignableAllocationService->handle($server);
|
||||
|
||||
Activity::event('server:allocation.create')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return $this->fractal->item($allocation)
|
||||
->transformWith($this->getTransformer(AllocationTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an allocation from a server.
|
||||
*
|
||||
* @throws \App\Exceptions\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.
|
||||
if (empty($server->allocation_limit)) {
|
||||
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,
|
||||
]);
|
||||
|
||||
Activity::event('server:allocation.delete')
|
||||
->subject($allocation)
|
||||
->property('allocation', $allocation->toString())
|
||||
->log();
|
||||
|
||||
return new JsonResponse([], JsonResponse::HTTP_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ class ServerContainersController extends Controller
|
||||
{
|
||||
$status = fluent($request->json()->all())->get('data.new_state');
|
||||
|
||||
cache()->set("servers.$server->uuid.container.status", $status, now()->addHour());
|
||||
cache()->set("servers.$server->uuid.container.status", $status, 3600);
|
||||
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class ServerDetailsController extends Controller
|
||||
|
||||
// Avoid run-away N+1 SQL queries by preloading the relationships that are used
|
||||
// within each of the services called below.
|
||||
$servers = Server::query()->with('allocations', 'egg', 'mounts', 'variables')
|
||||
$servers = Server::query()->with('egg', 'mounts', 'variables')
|
||||
->where('node_id', $node->id)
|
||||
// If you don't cast this to a string you'll end up with a stringified per_page returned in
|
||||
// the metadata, and then daemon will panic crash as a result.
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Models\Server;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\ServerTransfer;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Http\Controllers\Controller;
|
||||
@@ -53,13 +52,7 @@ class ServerTransferController extends Controller
|
||||
|
||||
/** @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,
|
||||
]);
|
||||
|
||||
@@ -93,9 +86,6 @@ 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]);
|
||||
});
|
||||
|
||||
return new JsonResponse([], Response::HTTP_NO_CONTENT);
|
||||
|
||||
@@ -10,7 +10,6 @@ use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Models\Database;
|
||||
use App\Models\Schedule;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@@ -48,7 +47,6 @@ class ResourceBelongsToServer
|
||||
switch (get_class($model)) {
|
||||
// All of these models use "server_id" as the field key for the server
|
||||
// they are assigned to, so the logic is identical for them all.
|
||||
case Allocation::class:
|
||||
case Backup::class:
|
||||
case Database::class:
|
||||
case Schedule::class:
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Node;
|
||||
|
||||
use App\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class AllocationAliasFormRequest extends AdminFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'alias' => 'present|nullable|string',
|
||||
'allocation_id' => 'required|numeric|exists:allocations,id',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\Node;
|
||||
|
||||
use App\Http\Requests\Admin\AdminFormRequest;
|
||||
|
||||
class AllocationFormRequest extends AdminFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'allocation_ip' => 'required|string',
|
||||
'allocation_alias' => 'sometimes|nullable|string|max:255',
|
||||
'allocation_ports' => 'required|array',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
|
||||
class ServerFormRequest extends AdminFormRequest
|
||||
@@ -25,34 +24,10 @@ class ServerFormRequest extends AdminFormRequest
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function ($validator) {
|
||||
$validator->after(function (Validator $validator) {
|
||||
$validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) {
|
||||
return !$input->auto_deploy;
|
||||
});
|
||||
|
||||
$validator->sometimes('allocation_id', [
|
||||
'required',
|
||||
'numeric',
|
||||
'bail',
|
||||
Rule::exists('allocations', 'id')->where(function ($query) {
|
||||
$query->where('node_id', $this->input('node_id'));
|
||||
$query->whereNull('server_id');
|
||||
}),
|
||||
], function ($input) {
|
||||
return !$input->auto_deploy;
|
||||
});
|
||||
|
||||
$validator->sometimes('allocation_additional.*', [
|
||||
'sometimes',
|
||||
'required',
|
||||
'numeric',
|
||||
Rule::exists('allocations', 'id')->where(function ($query) {
|
||||
$query->where('node_id', $this->input('node_id'));
|
||||
$query->whereNull('server_id');
|
||||
}),
|
||||
], function ($input) {
|
||||
return !$input->auto_deploy;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Allocations;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class DeleteAllocationRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Allocations;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class GetAllocationsRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
|
||||
|
||||
protected int $permission = AdminAcl::READ;
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Application\Allocations;
|
||||
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Http\Requests\Api\Application\ApplicationApiRequest;
|
||||
|
||||
class StoreAllocationRequest extends ApplicationApiRequest
|
||||
{
|
||||
protected ?string $resource = AdminAcl::RESOURCE_ALLOCATIONS;
|
||||
|
||||
protected int $permission = AdminAcl::WRITE;
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'ip' => 'required|string',
|
||||
'alias' => 'sometimes|nullable|string|max:255',
|
||||
'ports' => 'required|array',
|
||||
'ports.*' => 'string',
|
||||
];
|
||||
}
|
||||
|
||||
public function validated($key = null, $default = null): array
|
||||
{
|
||||
$data = parent::validated();
|
||||
|
||||
return [
|
||||
'allocation_ip' => $data['ip'],
|
||||
'allocation_ports' => $data['ports'],
|
||||
'allocation_alias' => $data['alias'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Http\Requests\Api\Application\Servers;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\Validator;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
use App\Models\Objects\DeploymentObject;
|
||||
@@ -49,10 +48,6 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'feature_limits.allocations' => $rules['allocation_limit'],
|
||||
'feature_limits.backups' => $rules['backup_limit'],
|
||||
|
||||
// Placeholders for rules added in withValidator() function.
|
||||
'allocation.default' => '',
|
||||
'allocation.additional.*' => '',
|
||||
|
||||
// Automatic deployment rules
|
||||
'deploy' => 'sometimes|required|array',
|
||||
'deploy.locations' => 'array',
|
||||
@@ -87,8 +82,7 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
'cpu' => array_get($data, 'limits.cpu'),
|
||||
'threads' => array_get($data, 'limits.threads'),
|
||||
'skip_scripts' => array_get($data, 'skip_scripts', false),
|
||||
'allocation_id' => array_get($data, 'allocation.default'),
|
||||
'allocation_additional' => array_get($data, 'allocation.additional'),
|
||||
'ports' => array_get($data, 'ports'),
|
||||
'start_on_completion' => array_get($data, 'start_on_completion', false),
|
||||
'database_limit' => array_get($data, 'feature_limits.databases'),
|
||||
'allocation_limit' => array_get($data, 'feature_limits.allocations'),
|
||||
@@ -104,24 +98,6 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
*/
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->sometimes('allocation.default', [
|
||||
'required', 'integer', 'bail',
|
||||
Rule::exists('allocations', 'id')->where(function ($query) {
|
||||
$query->whereNull('server_id');
|
||||
}),
|
||||
], function ($input) {
|
||||
return !$input->deploy;
|
||||
});
|
||||
|
||||
$validator->sometimes('allocation.additional.*', [
|
||||
'integer',
|
||||
Rule::exists('allocations', 'id')->where(function ($query) {
|
||||
$query->whereNull('server_id');
|
||||
}),
|
||||
], function ($input) {
|
||||
return !$input->deploy;
|
||||
});
|
||||
|
||||
/** @deprecated use tags instead */
|
||||
$validator->sometimes('deploy.locations', 'present', function ($input) {
|
||||
return $input->deploy;
|
||||
@@ -134,6 +110,10 @@ class StoreServerRequest extends ApplicationApiRequest
|
||||
$validator->sometimes('deploy.port_range', 'present', function ($input) {
|
||||
return $input->deploy;
|
||||
});
|
||||
|
||||
$validator->sometimes('deploy.node_id', 'present', function ($input) {
|
||||
return $input->deploy;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -149,6 +129,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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
$rules = Server::getRulesForUpdate($this->parameter('server', Server::class));
|
||||
|
||||
return [
|
||||
'allocation' => $rules['allocation_id'],
|
||||
'oom_killer' => $rules['oom_killer'],
|
||||
|
||||
'limits' => 'sometimes|array',
|
||||
@@ -54,7 +53,6 @@ class UpdateServerBuildConfigurationRequest extends ServerWriteRequest
|
||||
{
|
||||
$data = parent::validated();
|
||||
|
||||
$data['allocation_id'] = $data['allocation'];
|
||||
$data['database_limit'] = $data['feature_limits']['databases'] ?? null;
|
||||
$data['allocation_limit'] = $data['feature_limits']['allocations'] ?? null;
|
||||
$data['backup_limit'] = $data['feature_limits']['backups'] ?? null;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class DeleteAllocationRequest extends ClientApiRequest
|
||||
{
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_DELETE;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class GetNetworkRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* Check that the user has permission to view the allocations for
|
||||
* this server.
|
||||
*/
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_READ;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use App\Models\Permission;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class NewAllocationRequest extends ClientApiRequest
|
||||
{
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_CREATE;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
class SetPrimaryAllocationRequest extends UpdateAllocationRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Api\Client\Servers\Network;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Permission;
|
||||
use App\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class UpdateAllocationRequest extends ClientApiRequest
|
||||
{
|
||||
public function permission(): string
|
||||
{
|
||||
return Permission::ACTION_ALLOCATION_UPDATE;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
$rules = Allocation::getRules();
|
||||
|
||||
return [
|
||||
'notes' => array_merge($rules['notes'], ['present']),
|
||||
];
|
||||
}
|
||||
}
|
||||
31
app/Livewire/EndpointSynth.php
Normal file
31
app/Livewire/EndpointSynth.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth;
|
||||
use Stringable;
|
||||
|
||||
class EndpointSynth extends Synth
|
||||
{
|
||||
public static string $key = 'endpoint';
|
||||
|
||||
public static function match(mixed $target): bool
|
||||
{
|
||||
return $target instanceof Endpoint;
|
||||
}
|
||||
|
||||
public function dehydrate(Stringable $target): string
|
||||
{
|
||||
return (string) $target;
|
||||
}
|
||||
|
||||
public function hydrate(mixed $value): ?Endpoint
|
||||
{
|
||||
if (!is_string($value) && !is_int($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Endpoint($value);
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\Service\Allocation\ServerUsingAllocationException;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* App\Models\Allocation.
|
||||
*
|
||||
* @property int $id
|
||||
* @property int $node_id
|
||||
* @property string $ip
|
||||
* @property string|null $ip_alias
|
||||
* @property int $port
|
||||
* @property int|null $server_id
|
||||
* @property string|null $notes
|
||||
* @property \Carbon\Carbon|null $created_at
|
||||
* @property \Carbon\Carbon|null $updated_at
|
||||
* @property string $alias
|
||||
* @property bool $has_alias
|
||||
* @property \App\Models\Server|null $server
|
||||
* @property \App\Models\Node $node
|
||||
*
|
||||
* @method static \Database\Factories\AllocationFactory factory(...$parameters)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereIp($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereIpAlias($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereNodeId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereNotes($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation wherePort($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereServerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Allocation whereUpdatedAt($value)
|
||||
*
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class Allocation extends Model
|
||||
{
|
||||
/**
|
||||
* The resource name for this model when it is transformed into an
|
||||
* API representation using fractal.
|
||||
*/
|
||||
public const RESOURCE_NAME = 'allocation';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*/
|
||||
protected $table = 'allocations';
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
protected $guarded = ['id', 'created_at', 'updated_at'];
|
||||
|
||||
public static array $validationRules = [
|
||||
'node_id' => 'required|exists:nodes,id',
|
||||
'ip' => 'required|ip',
|
||||
'port' => 'required|numeric|between:1024,65535',
|
||||
'ip_alias' => 'nullable|string',
|
||||
'server_id' => 'nullable|exists:servers,id',
|
||||
'notes' => 'nullable|string|max:256',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::deleting(function (self $allocation) {
|
||||
throw_if($allocation->server_id, new ServerUsingAllocationException(trans('exceptions.allocations.server_using')));
|
||||
});
|
||||
}
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'node_id' => 'integer',
|
||||
'port' => 'integer',
|
||||
'server_id' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
public function getRouteKeyName(): string
|
||||
{
|
||||
return $this->getKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to automatically provide the IP alias if defined.
|
||||
*/
|
||||
public function getAliasAttribute(?string $value): string
|
||||
{
|
||||
return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor to quickly determine if this allocation has an alias.
|
||||
*/
|
||||
public function getHasAliasAttribute(?string $value): bool
|
||||
{
|
||||
return !is_null($this->ip_alias);
|
||||
}
|
||||
|
||||
/** @return Attribute<string, never> */
|
||||
protected function address(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => "$this->ip:$this->port",
|
||||
);
|
||||
}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the server associated with this allocation.
|
||||
*/
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Node model associated with this allocation.
|
||||
*/
|
||||
public function node(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Node::class);
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Container\Container;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* @deprecated — this class will be dropped in a future version, use the activity log
|
||||
*/
|
||||
class AuditLog extends Model
|
||||
{
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
public static array $validationRules = [
|
||||
'uuid' => 'required|uuid',
|
||||
'action' => 'required|string|max:255',
|
||||
'subaction' => 'nullable|string|max:255',
|
||||
'device' => 'array',
|
||||
'device.ip_address' => 'ip',
|
||||
'device.user_agent' => 'string',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
protected $table = 'audit_logs';
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_system' => 'bool',
|
||||
'device' => 'array',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'immutable_datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function server(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AuditLog model and returns it, attaching device information and the
|
||||
* currently authenticated user if available. This model is not saved at this point, so
|
||||
* you can always make modifications to it as needed before saving.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public static function instance(string $action, array $metadata, bool $isSystem = false): self
|
||||
{
|
||||
/** @var \Illuminate\Http\Request $request */
|
||||
$request = Container::getInstance()->make('request');
|
||||
if ($isSystem || !$request instanceof Request) {
|
||||
$request = null;
|
||||
}
|
||||
|
||||
return (new self())->fill([
|
||||
'uuid' => Uuid::uuid4()->toString(),
|
||||
'is_system' => $isSystem,
|
||||
'user_id' => ($request && $request->user()) ? $request->user()->id : null,
|
||||
'server_id' => null,
|
||||
'action' => $action,
|
||||
'device' => $request ? [
|
||||
'ip_address' => $request->getClientIp() ?? '127.0.0.1',
|
||||
'user_agent' => $request->userAgent() ?? '',
|
||||
] : [],
|
||||
'metadata' => $metadata,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
* @property \Carbon\CarbonImmutable $updated_at
|
||||
* @property \Carbon\CarbonImmutable|null $deleted_at
|
||||
* @property \App\Models\Server $server
|
||||
* @property \App\Models\AuditLog[] $audits
|
||||
*/
|
||||
class Backup extends Model
|
||||
{
|
||||
|
||||
@@ -322,6 +322,12 @@ class Egg extends Model
|
||||
|
||||
public function getKebabName(): string
|
||||
{
|
||||
return str($this->name)->kebab()->lower()->trim()->split('/[^\w\-]/')->join('');
|
||||
return str($this->name)
|
||||
->kebab()
|
||||
->replace('--', '-')
|
||||
->lower()
|
||||
->trim()
|
||||
->split('/[^\w\-]/')
|
||||
->join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class EggVariable extends Model
|
||||
/**
|
||||
* Reserved environment variable names.
|
||||
*/
|
||||
public const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID';
|
||||
public const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID';
|
||||
|
||||
/**
|
||||
* The table associated with the model.
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Models\Filters;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Spatie\QueryBuilder\Filters\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
@@ -32,26 +31,6 @@ class MultiFieldServerFilter implements Filter
|
||||
// Only select the server values, otherwise you'll end up merging the allocation and
|
||||
// server objects together, resulting in incorrect behavior and returned values.
|
||||
->select('servers.*')
|
||||
->join('allocations', 'allocations.server_id', '=', 'servers.id')
|
||||
->where(function (Builder $builder) use ($value) {
|
||||
$parts = explode(':', $value);
|
||||
|
||||
$builder->when(
|
||||
!Str::startsWith($value, ':'),
|
||||
// When the string does not start with a ":" it means we're looking for an IP or IP:Port
|
||||
// combo, so use a query to handle that.
|
||||
function (Builder $builder) use ($parts) {
|
||||
$builder->orWhere('allocations.ip', $parts[0]);
|
||||
if (!is_null($parts[1] ?? null)) {
|
||||
$builder->where('allocations.port', 'LIKE', "{$parts[1]}%");
|
||||
}
|
||||
},
|
||||
// Otherwise, just try to search for that specific port in the allocations.
|
||||
function (Builder $builder) use ($value) {
|
||||
$builder->orWhere('allocations.port', 'LIKE', substr($value, 1) . '%');
|
||||
}
|
||||
);
|
||||
})
|
||||
->groupBy('servers.id');
|
||||
|
||||
return;
|
||||
|
||||
@@ -8,7 +8,6 @@ use Exception;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@@ -41,7 +40,6 @@ use Symfony\Component\Yaml\Yaml;
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property \App\Models\Mount[]|\Illuminate\Database\Eloquent\Collection $mounts
|
||||
* @property \App\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers
|
||||
* @property \App\Models\Allocation[]|\Illuminate\Database\Eloquent\Collection $allocations
|
||||
*/
|
||||
class Node extends Model
|
||||
{
|
||||
@@ -235,14 +233,6 @@ class Node extends Model
|
||||
return $this->hasMany(Server::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the allocations associated with a node.
|
||||
*/
|
||||
public function allocations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean if the node is viable for an additional server to be placed on it.
|
||||
*/
|
||||
@@ -272,28 +262,6 @@ class Node extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function getForServerCreation(): Collection
|
||||
{
|
||||
return self::with('allocations')->get()->map(function (Node $item) {
|
||||
$filtered = $item->getRelation('allocations')->where('server_id', null)->map(function ($map) {
|
||||
return collect($map)->only(['id', 'ip', 'port']);
|
||||
});
|
||||
|
||||
$ports = $filtered->map(function ($map) {
|
||||
return [
|
||||
'id' => $map['id'],
|
||||
'text' => sprintf('%s:%s', $map['ip'], $map['port']),
|
||||
];
|
||||
})->values();
|
||||
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'text' => $item->name,
|
||||
'allocations' => $ports,
|
||||
];
|
||||
})->values();
|
||||
}
|
||||
|
||||
public function systemInformation(): array
|
||||
{
|
||||
return once(function () {
|
||||
@@ -320,11 +288,10 @@ class Node extends Model
|
||||
|
||||
public function serverStatuses(): array
|
||||
{
|
||||
$statuses = [];
|
||||
try {
|
||||
$statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->get('/api/servers')->json() ?? [];
|
||||
} catch (Exception $exception) {
|
||||
report($exception);
|
||||
$statuses = Http::daemon($this)->connectTimeout(1)->timeout(1)->throw()->get('/api/servers')->json() ?? [];
|
||||
} catch (Exception) {
|
||||
$statuses = [];
|
||||
}
|
||||
|
||||
foreach ($statuses as $status) {
|
||||
@@ -378,7 +345,10 @@ class Node extends Model
|
||||
// pass
|
||||
}
|
||||
|
||||
return $ips->all();
|
||||
return $ips
|
||||
->filter(fn ($ip) => preg_match('/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/', $ip))
|
||||
->unique()
|
||||
->all();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Models\Objects;
|
||||
|
||||
use App\Models\Node;
|
||||
|
||||
class DeploymentObject
|
||||
{
|
||||
private bool $dedicated = false;
|
||||
@@ -10,6 +12,8 @@ class DeploymentObject
|
||||
|
||||
private array $ports = [];
|
||||
|
||||
private Node $node;
|
||||
|
||||
public function isDedicated(): bool
|
||||
{
|
||||
return $this->dedicated;
|
||||
@@ -45,4 +49,16 @@ class DeploymentObject
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNode(): Node
|
||||
{
|
||||
return $this->node;
|
||||
}
|
||||
|
||||
public function setNode(Node $node): self
|
||||
{
|
||||
$this->node = $node;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Models/Objects/Endpoint.php
Normal file
64
app/Models/Objects/Endpoint.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Objects;
|
||||
|
||||
use Illuminate\Contracts\Support\Jsonable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
class Endpoint implements Jsonable
|
||||
{
|
||||
public const CIDR_MAX_BITS = 27;
|
||||
|
||||
public const CIDR_MIN_BITS = 32;
|
||||
|
||||
public const PORT_FLOOR = 1024;
|
||||
|
||||
public const PORT_CEIL = 65535;
|
||||
|
||||
public const PORT_RANGE_LIMIT = 1000;
|
||||
|
||||
public const PORT_RANGE_REGEX = '/^(\d{4,5})-(\d{4,5})$/';
|
||||
|
||||
public const INADDR_ANY = '0.0.0.0';
|
||||
|
||||
public const INADDR_LOOPBACK = '127.0.0.1';
|
||||
|
||||
public int $port;
|
||||
|
||||
public string $ip;
|
||||
|
||||
public function __construct(string|int $port, ?string $ip = null)
|
||||
{
|
||||
$this->ip = $ip ?? self::INADDR_ANY;
|
||||
$this->port = (int) $port;
|
||||
|
||||
if (str_contains($port, ':')) {
|
||||
[$this->ip, $port] = explode(':', $port);
|
||||
$this->port = (int) $port;
|
||||
}
|
||||
|
||||
throw_unless(filter_var($this->ip, FILTER_VALIDATE_IP) !== false, new InvalidArgumentException("$this->ip is an invalid IP address"));
|
||||
throw_unless($this->port > self::PORT_FLOOR, "Port $this->port must be greater than " . self::PORT_FLOOR);
|
||||
throw_unless($this->port < self::PORT_CEIL, "Port $this->port must be less than " . self::PORT_CEIL);
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
$ip = $this->ip;
|
||||
|
||||
if ($ip === self::INADDR_ANY) {
|
||||
return (string) $this->port;
|
||||
}
|
||||
|
||||
if ($ip === self::INADDR_LOOPBACK) {
|
||||
$ip = 'localhost';
|
||||
}
|
||||
|
||||
return "$ip:$this->port";
|
||||
}
|
||||
|
||||
public function toJson($options = 0): string
|
||||
{
|
||||
return json_encode($this->__toString());
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Casts\EndpointCollection;
|
||||
use App\Enums\ContainerStatus;
|
||||
use App\Enums\ServerState;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Objects\Endpoint;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
@@ -38,7 +41,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @property int $cpu
|
||||
* @property string|null $threads
|
||||
* @property bool $oom_killer
|
||||
* @property int $allocation_id
|
||||
* @property int $egg_id
|
||||
* @property string $startup
|
||||
* @property string $image
|
||||
@@ -50,7 +52,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @property \Illuminate\Support\Carbon|null $installed_at
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\ActivityLog[] $activity
|
||||
* @property int|null $activity_count
|
||||
* @property \App\Models\Allocation|null $allocation
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Allocation[] $allocations
|
||||
* @property int|null $allocations_count
|
||||
* @property \Illuminate\Database\Eloquent\Collection|\App\Models\Backup[] $backups
|
||||
@@ -76,7 +77,6 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereAllocationLimit($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereBackupLimit($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereCpu($value)
|
||||
@@ -104,7 +104,7 @@ use App\Exceptions\Http\Server\ServerStateConflictException;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|Server whereuuid_short($value)
|
||||
*
|
||||
* @property array|null $docker_labels
|
||||
* @property string|null $ports
|
||||
* @property Collection<Endpoint>|null $ports
|
||||
* @property-read mixed $condition
|
||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\EggVariable> $eggVariables
|
||||
* @property-read int|null $egg_variables_count
|
||||
@@ -143,11 +143,6 @@ class Server extends Model
|
||||
'installed_at' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* The default relationships to load for all server models.
|
||||
*/
|
||||
protected $with = ['allocation'];
|
||||
|
||||
/**
|
||||
* Fields that are not mass assignable.
|
||||
*/
|
||||
@@ -167,7 +162,6 @@ class Server extends Model
|
||||
'threads' => 'nullable|regex:/^[0-9-,]+$/',
|
||||
'oom_killer' => 'sometimes|boolean',
|
||||
'disk' => 'required|numeric|min:0',
|
||||
'allocation_id' => 'required|bail|unique:servers|exists:allocations,id',
|
||||
'egg_id' => 'required|exists:eggs,id',
|
||||
'startup' => 'required|string',
|
||||
'skip_scripts' => 'sometimes|boolean',
|
||||
@@ -175,6 +169,7 @@ class Server extends Model
|
||||
'database_limit' => 'present|nullable|integer|min:0',
|
||||
'allocation_limit' => 'sometimes|nullable|integer|min:0',
|
||||
'backup_limit' => 'present|nullable|integer|min:0',
|
||||
'ports' => 'nullable|array',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -190,27 +185,24 @@ class Server extends Model
|
||||
'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',
|
||||
'deleted_at' => 'datetime',
|
||||
'installed_at' => 'datetime',
|
||||
'docker_labels' => 'array',
|
||||
'ports' => EndpointCollection::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the format for server allocations when communicating with the Daemon.
|
||||
* Returns the format for server's ports when communicating with the Daemon.
|
||||
*/
|
||||
public function getAllocationMappings(): array
|
||||
public function getPortMappings(): array
|
||||
{
|
||||
return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) {
|
||||
return $item->pluck('port');
|
||||
})->toArray();
|
||||
return $this->ports->mapToGroups(fn (Endpoint $endpoint) => [$endpoint->ip => $endpoint->port]
|
||||
)->toArray();
|
||||
}
|
||||
|
||||
public function isInstalled(): bool
|
||||
@@ -239,22 +231,6 @@ class Server extends Model
|
||||
return $this->hasMany(Subuser::class, 'server_id', 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default allocation for a server.
|
||||
*/
|
||||
public function allocation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all allocations associated with this server.
|
||||
*/
|
||||
public function allocations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Allocation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information for the egg associated with this server.
|
||||
*/
|
||||
@@ -454,4 +430,21 @@ class Server extends Model
|
||||
|
||||
return $this->status->color();
|
||||
}
|
||||
|
||||
public function getPrimaryEndpoint(): ?Endpoint
|
||||
{
|
||||
$endpoint = $this->ports->first();
|
||||
|
||||
$portEggVariable = $this->variables->firstWhere('env_variable', 'SERVER_PORT');
|
||||
if ($portEggVariable) {
|
||||
$portServerVariable = $this->serverVariables->firstWhere('variable_id', $portEggVariable->id);
|
||||
if (!$portServerVariable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$endpoint = new Endpoint($portServerVariable->variable_value);
|
||||
}
|
||||
|
||||
return $endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Extensions\Themes\Theme;
|
||||
use App\Livewire\EndpointSynth;
|
||||
use App\Models;
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\Node;
|
||||
use App\Models\User;
|
||||
use App\Rules\Port;
|
||||
use Dedoc\Scramble\Scramble;
|
||||
use Dedoc\Scramble\Support\Generator\OpenApi;
|
||||
use Dedoc\Scramble\Support\Generator\SecurityScheme;
|
||||
@@ -20,10 +22,13 @@ use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\InvokableValidationRule;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Livewire\Livewire;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
@@ -46,7 +51,6 @@ class AppServiceProvider extends ServiceProvider
|
||||
}
|
||||
|
||||
Relation::enforceMorphMap([
|
||||
'allocation' => Models\Allocation::class,
|
||||
'api_key' => Models\ApiKey::class,
|
||||
'backup' => Models\Backup::class,
|
||||
'database' => Models\Database::class,
|
||||
@@ -74,6 +78,22 @@ class AppServiceProvider extends ServiceProvider
|
||||
$this->bootAuth();
|
||||
$this->bootBroadcast();
|
||||
|
||||
Livewire::propertySynthesizer(EndpointSynth::class);
|
||||
|
||||
// Assign custom validation rules
|
||||
Validator::extend('port', function ($attribute, $value, $parameters, $validator) {
|
||||
$rule = InvokableValidationRule::make(new Port());
|
||||
$rule->setValidator($validator); // @phpstan-ignore-line
|
||||
$rule->setData($validator->getData()); // @phpstan-ignore-line
|
||||
|
||||
$result = $rule->passes($attribute, $value);
|
||||
if (!$result) {
|
||||
$validator->customMessages[$attribute] = $rule->message();
|
||||
}
|
||||
|
||||
return $result;
|
||||
});
|
||||
|
||||
$bearerTokens = fn (OpenApi $openApi) => $openApi->secure(SecurityScheme::http('bearer'));
|
||||
Gate::define('viewApiDocs', fn () => true);
|
||||
Scramble::registerApi('application', ['api_path' => 'api/application', 'info' => ['version' => '1.0']]);
|
||||
|
||||
@@ -2,33 +2,38 @@
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
class Port implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Run the validation rule.
|
||||
*
|
||||
* @param \Closure(string): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||
*/
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// Allow port to be optional
|
||||
if (empty($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Require port to be a number
|
||||
if (!is_numeric($value)) {
|
||||
$fail('The :attribute must be numeric.');
|
||||
}
|
||||
|
||||
// Require port to be an integer
|
||||
$value = intval($value);
|
||||
if (floatval($value) !== (float) $value) {
|
||||
$fail('The :attribute must be an integer.');
|
||||
}
|
||||
|
||||
if ($value < 0) {
|
||||
$fail('The :attribute must be greater or equal to 0.');
|
||||
// Require minimum valid port
|
||||
if ($value <= Endpoint::PORT_FLOOR) {
|
||||
$fail('The :attribute must be greater than 1024.');
|
||||
}
|
||||
|
||||
if ($value > 65535) {
|
||||
$fail('The :attribute must be less or equal to 65535.');
|
||||
// Require maximum valid port
|
||||
if ($value > Endpoint::PORT_CEIL) {
|
||||
$fail('The :attribute must be less than 65535.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Allocations;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use IPTools\Network;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Exceptions\Service\Allocation\CidrOutOfRangeException;
|
||||
use App\Exceptions\Service\Allocation\PortOutOfRangeException;
|
||||
use App\Exceptions\Service\Allocation\InvalidPortMappingException;
|
||||
use App\Exceptions\Service\Allocation\TooManyPortsInRangeException;
|
||||
|
||||
class AssignmentService
|
||||
{
|
||||
public const CIDR_MAX_BITS = 25;
|
||||
|
||||
public const CIDR_MIN_BITS = 32;
|
||||
|
||||
public const PORT_FLOOR = 1024;
|
||||
|
||||
public const PORT_CEIL = 65535;
|
||||
|
||||
public const PORT_RANGE_LIMIT = 1000;
|
||||
|
||||
public const PORT_RANGE_REGEX = '/^(\d{4,5})-(\d{4,5})$/';
|
||||
|
||||
/**
|
||||
* AssignmentService constructor.
|
||||
*/
|
||||
public function __construct(protected ConnectionInterface $connection)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert allocations into the database and link them to a specific node.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function handle(Node $node, array $data, ?Server $server = null): array
|
||||
{
|
||||
$explode = explode('/', $data['allocation_ip']);
|
||||
if (count($explode) !== 1) {
|
||||
if (!ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) {
|
||||
throw new CidrOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: how should we approach supporting IPv6 with this?
|
||||
// gethostbyname only supports IPv4, but the alternative (dns_get_record) returns
|
||||
// an array of records, which is not ideal for this use case, we need a SINGLE
|
||||
// IP to use, not multiple.
|
||||
$underlying = gethostbyname($data['allocation_ip']);
|
||||
$parsed = Network::parse($underlying);
|
||||
} catch (\Exception $exception) {
|
||||
throw new DisplayException("Could not parse provided allocation IP address ({$data['allocation_ip']}): {$exception->getMessage()}", $exception);
|
||||
}
|
||||
|
||||
$this->connection->beginTransaction();
|
||||
|
||||
$ids = [];
|
||||
foreach ($parsed as $ip) {
|
||||
foreach ($data['allocation_ports'] as $port) {
|
||||
if (!is_digit($port) && !preg_match(self::PORT_RANGE_REGEX, $port)) {
|
||||
throw new InvalidPortMappingException($port);
|
||||
}
|
||||
|
||||
$insertData = [];
|
||||
if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) {
|
||||
$block = range($matches[1], $matches[2]);
|
||||
|
||||
if (count($block) > self::PORT_RANGE_LIMIT) {
|
||||
throw new TooManyPortsInRangeException();
|
||||
}
|
||||
|
||||
if ((int) $matches[1] < self::PORT_FLOOR || (int) $matches[2] > self::PORT_CEIL) {
|
||||
throw new PortOutOfRangeException();
|
||||
}
|
||||
|
||||
foreach ($block as $unit) {
|
||||
$insertData[] = [
|
||||
'node_id' => $node->id,
|
||||
'ip' => $ip->__toString(),
|
||||
'port' => (int) $unit,
|
||||
'ip_alias' => array_get($data, 'allocation_alias'),
|
||||
'server_id' => $server->id ?? null,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if ((int) $port < self::PORT_FLOOR || (int) $port > self::PORT_CEIL) {
|
||||
throw new PortOutOfRangeException();
|
||||
}
|
||||
|
||||
$insertData[] = [
|
||||
'node_id' => $node->id,
|
||||
'ip' => $ip->__toString(),
|
||||
'port' => (int) $port,
|
||||
'ip_alias' => array_get($data, 'allocation_alias'),
|
||||
'server_id' => $server->id ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($insertData as $insert) {
|
||||
$allocation = Allocation::query()->create($insert);
|
||||
$ids[] = $allocation->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->connection->commit();
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -2,110 +2,48 @@
|
||||
|
||||
namespace App\Services\Allocations;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use Illuminate\Support\Collection;
|
||||
use Webmozart\Assert\Assert;
|
||||
use App\Models\Server;
|
||||
use App\Models\Allocation;
|
||||
use App\Exceptions\Service\Allocation\AutoAllocationNotEnabledException;
|
||||
use App\Exceptions\Service\Allocation\NoAutoAllocationSpaceAvailableException;
|
||||
|
||||
class FindAssignableAllocationService
|
||||
{
|
||||
/**
|
||||
* FindAssignableAllocationService constructor.
|
||||
*/
|
||||
public function __construct(private AssignmentService $service)
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an existing unassigned allocation and attempts to assign it to the given server. If
|
||||
* no allocation can be found, a new one will be created with a random port between the defined
|
||||
* range from the configuration.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
public function handle(Server $server): Allocation
|
||||
public function handle(Server $server): int
|
||||
{
|
||||
if (!config('panel.client_features.allocations.enabled')) {
|
||||
throw new AutoAllocationNotEnabledException();
|
||||
}
|
||||
abort_unless(config('panel.client_features.allocations.enabled'), 403, 'Auto Allocation is not enabled');
|
||||
|
||||
// Attempt to find a given available allocation for a server. If one cannot be found
|
||||
// we will fall back to attempting to create a new allocation that can be used for the
|
||||
// server.
|
||||
/** @var \App\Models\Allocation|null $allocation */
|
||||
$allocation = $server->node->allocations()
|
||||
->where('ip', $server->allocation->ip)
|
||||
->whereNull('server_id')
|
||||
->inRandomOrder()
|
||||
->first();
|
||||
|
||||
$allocation = $allocation ?? $this->createNewAllocation($server);
|
||||
|
||||
$allocation->update(['server_id' => $server->id]);
|
||||
|
||||
return $allocation->refresh();
|
||||
return $this->createNewAllocation($server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new allocation on the server's node with a random port from the defined range
|
||||
* in the settings. If there are no matches in that range, or something is wrong with the
|
||||
* range information provided an exception will be raised.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Allocation\CidrOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\InvalidPortMappingException
|
||||
* @throws \App\Exceptions\Service\Allocation\PortOutOfRangeException
|
||||
* @throws \App\Exceptions\Service\Allocation\TooManyPortsInRangeException
|
||||
*/
|
||||
protected function createNewAllocation(Server $server): Allocation
|
||||
protected function createNewAllocation(Server $server): int
|
||||
{
|
||||
$start = config('panel.client_features.allocations.range_start', null);
|
||||
$end = config('panel.client_features.allocations.range_end', null);
|
||||
|
||||
if (!$start || !$end) {
|
||||
throw new NoAutoAllocationSpaceAvailableException();
|
||||
}
|
||||
$start = config('panel.client_features.allocations.range_start');
|
||||
$end = config('panel.client_features.allocations.range_end');
|
||||
|
||||
Assert::integerish($start);
|
||||
Assert::integerish($end);
|
||||
|
||||
// Get all of the currently allocated ports for the node so that we can figure out
|
||||
// which port might be available.
|
||||
$ports = $server->node->allocations()
|
||||
->where('ip', $server->allocation->ip)
|
||||
->whereBetween('port', [$start, $end])
|
||||
->pluck('port');
|
||||
$ports = $server->node->servers
|
||||
->reduce(fn (Collection $result, $value) => $result->merge($value), collect())
|
||||
->map(fn (Endpoint $endpoint) => $endpoint->port)
|
||||
->filter(fn (int $port): bool => $port >= $start && $port <= $end);
|
||||
|
||||
// Compute the difference of the range and the currently created ports, finding
|
||||
// any port that does not already exist in the database. We will then use this
|
||||
// array of ports to create a new allocation to assign to the server.
|
||||
$available = array_diff(range($start, $end), $ports->toArray());
|
||||
|
||||
// If we've already allocated all of the ports, just abort.
|
||||
if (empty($available)) {
|
||||
throw new NoAutoAllocationSpaceAvailableException();
|
||||
}
|
||||
|
||||
// Pick a random port out of the remaining available ports.
|
||||
/** @var int $port */
|
||||
$port = $available[array_rand($available)];
|
||||
|
||||
$this->service->handle($server->node, [
|
||||
'allocation_ip' => $server->allocation->ip,
|
||||
'allocation_ports' => [$port],
|
||||
]);
|
||||
|
||||
/** @var \App\Models\Allocation $allocation */
|
||||
$allocation = $server->node->allocations()
|
||||
->where('ip', $server->allocation->ip)
|
||||
->where('port', $port)
|
||||
->firstOrFail();
|
||||
|
||||
return $allocation;
|
||||
return $available[array_rand($available)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Deployment;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Services\Allocations\AssignmentService;
|
||||
use App\Exceptions\Service\Deployment\NoViableAllocationException;
|
||||
|
||||
class AllocationSelectionService
|
||||
{
|
||||
protected bool $dedicated = false;
|
||||
|
||||
protected array $nodes = [];
|
||||
|
||||
protected array $ports = [];
|
||||
|
||||
/**
|
||||
* Toggle if the selected allocation should be the only allocation belonging
|
||||
* to the given IP address. If true an allocation will not be selected if an IP
|
||||
* already has another server set to use on if its allocations.
|
||||
*/
|
||||
public function setDedicated(bool $dedicated): self
|
||||
{
|
||||
$this->dedicated = $dedicated;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of node IDs that should be used when selecting an allocation. If empty, all
|
||||
* nodes will be used to filter with.
|
||||
*/
|
||||
public function setNodes(array $nodes): self
|
||||
{
|
||||
$this->nodes = $nodes;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of individual ports or port ranges to use when selecting an allocation. If
|
||||
* empty, all ports will be considered when finding an allocation. If set, only ports appearing
|
||||
* in the array or range will be used.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
public function setPorts(array $ports): self
|
||||
{
|
||||
$stored = [];
|
||||
foreach ($ports as $port) {
|
||||
if (is_digit($port)) {
|
||||
$stored[] = $port;
|
||||
}
|
||||
|
||||
// Ranges are stored in the ports array as an array which can be
|
||||
// better processed in the repository.
|
||||
if (preg_match(AssignmentService::PORT_RANGE_REGEX, $port, $matches)) {
|
||||
if (abs((int) $matches[2] - (int) $matches[1]) > AssignmentService::PORT_RANGE_LIMIT) {
|
||||
throw new DisplayException(trans('exceptions.allocations.too_many_ports'));
|
||||
}
|
||||
|
||||
$stored[] = [$matches[1], $matches[2]];
|
||||
}
|
||||
}
|
||||
|
||||
$this->ports = $stored;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single allocation that should be used as the default allocation for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
public function handle(): Allocation
|
||||
{
|
||||
$allocation = $this->getRandomAllocation($this->nodes, $this->ports, $this->dedicated);
|
||||
|
||||
if (is_null($allocation)) {
|
||||
throw new NoViableAllocationException(trans('exceptions.deployment.no_viable_allocations'));
|
||||
}
|
||||
|
||||
return $allocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a single allocation from those meeting the requirements.
|
||||
*/
|
||||
private function getRandomAllocation(array $nodes = [], array $ports = [], bool $dedicated = false): ?Allocation
|
||||
{
|
||||
$query = Allocation::query()
|
||||
->whereNull('server_id')
|
||||
->whereIn('node_id', $nodes);
|
||||
|
||||
if (!empty($ports)) {
|
||||
$query->where(function ($inner) use ($ports) {
|
||||
$whereIn = [];
|
||||
foreach ($ports as $port) {
|
||||
if (is_array($port)) {
|
||||
$inner->orWhereBetween('port', $port);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$whereIn[] = $port;
|
||||
}
|
||||
|
||||
if (!empty($whereIn)) {
|
||||
$inner->orWhereIn('port', $whereIn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// If this allocation should not be shared with any other servers get
|
||||
// the data and modify the query as necessary,
|
||||
if ($dedicated) {
|
||||
$discard = $this->getDiscardableDedicatedAllocations($nodes);
|
||||
|
||||
if (!empty($discard)) {
|
||||
$query->whereNotIn('ip', $discard);
|
||||
}
|
||||
}
|
||||
|
||||
return $query->inRandomOrder()->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a result set of node ips that already have at least one
|
||||
* server assigned to that IP. This allows for filtering out sets for
|
||||
* dedicated allocation IPs.
|
||||
*
|
||||
* If an array of nodes is passed the results will be limited to allocations
|
||||
* in those nodes.
|
||||
*/
|
||||
private function getDiscardableDedicatedAllocations(array $nodes = []): array
|
||||
{
|
||||
$query = Allocation::query()->whereNotNull('server_id');
|
||||
|
||||
if (!empty($nodes)) {
|
||||
$query->whereIn('node_id', $nodes);
|
||||
}
|
||||
|
||||
return $query->groupBy('ip')
|
||||
->get()
|
||||
->pluck('ip')
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,7 @@ use Illuminate\Support\Collection;
|
||||
class FindViableNodesService
|
||||
{
|
||||
/**
|
||||
* Returns a collection of nodes that meet the provided requirements and can then
|
||||
* be passed to the AllocationSelectionService to return a single allocation.
|
||||
* Returns a collection of nodes that meet the provided requirements
|
||||
*
|
||||
* This functionality is used for automatic deployments of servers and will
|
||||
* attempt to find all nodes in the defined locations that meet the memory, disk
|
||||
|
||||
@@ -4,9 +4,7 @@ namespace App\Services\Servers;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use App\Models\Server;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Exceptions\DisplayException;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
|
||||
@@ -32,20 +30,12 @@ class BuildModificationService
|
||||
{
|
||||
/** @var \App\Models\Server $server */
|
||||
$server = $this->connection->transaction(function () use ($server, $data) {
|
||||
$this->processAllocations($server, $data);
|
||||
|
||||
if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) {
|
||||
$existingAllocation = $server->allocations()->findOrFail($data['allocation_id']);
|
||||
|
||||
throw_unless($existingAllocation, new DisplayException('The requested default allocation is not currently assigned to this server.'));
|
||||
}
|
||||
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
// If any of these values are passed through in the data array go ahead and set them correctly on the server model.
|
||||
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'allocation_id']);
|
||||
$merge = Arr::only($data, ['oom_killer', 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', 'ports']);
|
||||
|
||||
$server->forceFill(array_merge($merge, [
|
||||
'database_limit' => Arr::get($data, 'database_limit', 0) ?? null,
|
||||
@@ -72,59 +62,4 @@ class BuildModificationService
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the allocations being assigned in the data and ensure they are available for a server.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
*/
|
||||
private function processAllocations(Server $server, array &$data): void
|
||||
{
|
||||
if (empty($data['add_allocations']) && empty($data['remove_allocations'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the addition of allocations to this server. Only assign allocations that are not currently
|
||||
// assigned to a different server, and only allocations on the same node as the server.
|
||||
if (!empty($data['add_allocations'])) {
|
||||
$query = Allocation::query()
|
||||
->where('node_id', $server->node_id)
|
||||
->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]);
|
||||
}
|
||||
|
||||
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.');
|
||||
}
|
||||
|
||||
// Update the default allocation to be the first allocation that we are creating.
|
||||
$data['allocation_id'] = $freshlyAllocated;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'] ?? []))
|
||||
->update([
|
||||
'notes' => null,
|
||||
'server_id' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ class ServerConfigurationStructureService
|
||||
/**
|
||||
* Return a configuration array for a specific server when passed a server model.
|
||||
*
|
||||
* DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for the new daemon
|
||||
* daemon, if you modify the structure eggs will break unexpectedly.
|
||||
* DO NOT MODIFY THIS FUNCTION. This powers legacy code handling for wings
|
||||
* if you modify the structure eggs will break unexpectedly.
|
||||
*/
|
||||
public function handle(Server $server, array $override = []): array
|
||||
{
|
||||
@@ -66,10 +66,10 @@ class ServerConfigurationStructureService
|
||||
'allocations' => [
|
||||
'force_outgoing_ip' => $server->egg->force_outgoing_ip,
|
||||
'default' => [
|
||||
'ip' => $server->allocation->ip,
|
||||
'port' => $server->allocation->port,
|
||||
'ip' => $server->getPrimaryEndpoint()?->ip,
|
||||
'port' => $server->getPrimaryEndpoint()?->port,
|
||||
],
|
||||
'mappings' => $server->getAllocationMappings(),
|
||||
'mappings' => $server->getPortMappings(),
|
||||
],
|
||||
'egg' => [
|
||||
'id' => $server->egg->uuid,
|
||||
|
||||
@@ -10,85 +10,54 @@ use App\Models\User;
|
||||
use Webmozart\Assert\Assert;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use App\Models\Objects\DeploymentObject;
|
||||
use App\Repositories\Daemon\DaemonServerRepository;
|
||||
use App\Services\Deployment\FindViableNodesService;
|
||||
use App\Services\Deployment\AllocationSelectionService;
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Egg;
|
||||
|
||||
class ServerCreationService
|
||||
{
|
||||
/**
|
||||
* ServerCreationService constructor.
|
||||
*/
|
||||
public function __construct(
|
||||
private AllocationSelectionService $allocationSelectionService,
|
||||
private ConnectionInterface $connection,
|
||||
private DaemonServerRepository $daemonServerRepository,
|
||||
private FindViableNodesService $findViableNodesService,
|
||||
private ServerDeletionService $serverDeletionService,
|
||||
private VariableValidatorService $validatorService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server on the Panel and trigger a request to the Daemon to begin the server
|
||||
* creation process. This function will attempt to set as many additional values
|
||||
* as possible given the input data. For example, if an allocation_id is passed with
|
||||
* no node_id the node_is will be picked from the allocation.
|
||||
*
|
||||
* @throws \Throwable
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
* Create a server on the Panel and trigger a request to the Daemon to begin the server creation process.
|
||||
* This function will attempt to set as many additional values as possible given the input data.
|
||||
*/
|
||||
public function handle(array $data, ?DeploymentObject $deployment = null): Server
|
||||
public function handle(array $data, ?DeploymentObject $deployment = null, bool $validateVariables = true): Server
|
||||
{
|
||||
if (!isset($data['oom_killer']) && isset($data['oom_disabled'])) {
|
||||
$data['oom_killer'] = !$data['oom_disabled'];
|
||||
}
|
||||
|
||||
/** @var Egg $egg */
|
||||
$egg = Egg::query()->findOrFail($data['egg_id']);
|
||||
|
||||
// Fill missing fields from egg
|
||||
$data['image'] = $data['image'] ?? collect($egg->docker_images)->first();
|
||||
$data['startup'] = $data['startup'] ?? $egg->startup;
|
||||
|
||||
// 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) {
|
||||
$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;
|
||||
}
|
||||
Assert::false(empty($data['node_id']));
|
||||
|
||||
$eggVariableData = $this->validatorService
|
||||
->setUserLevel(User::USER_LEVEL_ADMIN)
|
||||
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []));
|
||||
->handle(Arr::get($data, 'egg_id'), Arr::get($data, 'environment', []), $validateVariables);
|
||||
|
||||
// Due to the design of the Daemon, we need to persist this server to the disk
|
||||
// before we can actually create it on the Daemon.
|
||||
//
|
||||
// If that connection fails out we will attempt to perform a cleanup by just
|
||||
// deleting the server itself from the system.
|
||||
/** @var \App\Models\Server $server */
|
||||
/** @var Server $server */
|
||||
$server = $this->connection->transaction(function () use ($data, $eggVariableData) {
|
||||
// Create the server and assign any additional allocations to it.
|
||||
$server = $this->createModel($data);
|
||||
|
||||
$this->storeAssignedAllocations($server, $data);
|
||||
$this->storeEggVariables($server, $eggVariableData);
|
||||
|
||||
return $server;
|
||||
@@ -96,7 +65,7 @@ class ServerCreationService
|
||||
|
||||
try {
|
||||
$this->daemonServerRepository->setServer($server)->create(
|
||||
Arr::get($data, 'start_on_completion', false) ?? false
|
||||
Arr::get($data, 'start_on_completion', true) ?? true,
|
||||
);
|
||||
} catch (DaemonConnectionException $exception) {
|
||||
$this->serverDeletionService->withForce()->handle($server);
|
||||
@@ -107,28 +76,6 @@ class ServerCreationService
|
||||
return $server;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an allocation to use for automatic deployment.
|
||||
*
|
||||
* @throws \App\Exceptions\DisplayException
|
||||
* @throws \App\Exceptions\Service\Deployment\NoViableAllocationException
|
||||
*/
|
||||
private function configureDeployment(array $data, DeploymentObject $deployment): Allocation
|
||||
{
|
||||
/** @var Collection<\App\Models\Node> $nodes */
|
||||
$nodes = $this->findViableNodesService->handle(
|
||||
Arr::get($data, 'memory', 0),
|
||||
Arr::get($data, 'disk', 0),
|
||||
Arr::get($data, 'cpu', 0),
|
||||
Arr::get($data, 'tags', []),
|
||||
);
|
||||
|
||||
return $this->allocationSelectionService->setDedicated($deployment->isDedicated())
|
||||
->setNodes($nodes->pluck('id')->toArray())
|
||||
->setPorts($deployment->getPorts())
|
||||
->handle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the server in the database and return the model.
|
||||
*
|
||||
@@ -155,7 +102,7 @@ class ServerCreationService
|
||||
'cpu' => Arr::get($data, 'cpu'),
|
||||
'threads' => Arr::get($data, 'threads'),
|
||||
'oom_killer' => Arr::get($data, 'oom_killer') ?? false,
|
||||
'allocation_id' => Arr::get($data, 'allocation_id'),
|
||||
'ports' => Arr::get($data, 'ports') ?? [],
|
||||
'egg_id' => Arr::get($data, 'egg_id'),
|
||||
'startup' => Arr::get($data, 'startup'),
|
||||
'image' => Arr::get($data, 'image'),
|
||||
@@ -166,21 +113,6 @@ class ServerCreationService
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the allocations assigned to this server.
|
||||
*/
|
||||
private function storeAssignedAllocations(Server $server, array $data): void
|
||||
{
|
||||
$records = [$data['allocation_id']];
|
||||
if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) {
|
||||
$records = array_merge($records, $data['allocation_additional']);
|
||||
}
|
||||
|
||||
Allocation::query()->whereIn('id', $records)->update([
|
||||
'server_id' => $server->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process environment variables passed for this server and store them in the database.
|
||||
*/
|
||||
|
||||
@@ -77,8 +77,6 @@ class ServerDeletionService
|
||||
}
|
||||
}
|
||||
|
||||
$server->allocations()->update(['server_id' => null]);
|
||||
|
||||
$server->delete();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Models\Server;
|
||||
|
||||
class StartupCommandService
|
||||
@@ -11,8 +12,10 @@ class StartupCommandService
|
||||
*/
|
||||
public function handle(Server $server, bool $hideAllValues = false): string
|
||||
{
|
||||
$endpoint = $server->getPrimaryEndpoint();
|
||||
|
||||
$find = ['{{SERVER_MEMORY}}', '{{SERVER_IP}}', '{{SERVER_PORT}}'];
|
||||
$replace = [$server->memory, $server->allocation->ip, $server->allocation->port];
|
||||
$replace = [$server->memory, $endpoint->ip ?? Endpoint::INADDR_ANY, $endpoint->port ?? ''];
|
||||
|
||||
foreach ($server->variables as $variable) {
|
||||
$find[] = '{{' . $variable->env_variable . '}}';
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\Services\Servers;
|
||||
|
||||
use App\Exceptions\Http\Connection\DaemonConnectionException;
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerTransfer;
|
||||
@@ -52,8 +51,6 @@ class TransferServerService
|
||||
public function handle(Server $server, array $data): bool
|
||||
{
|
||||
$node_id = $data['node_id'];
|
||||
$allocation_id = intval($data['allocation_id']);
|
||||
$additional_allocations = array_map(intval(...), $data['allocation_additional'] ?? []);
|
||||
|
||||
// Check if the node is viable for the transfer.
|
||||
$node = Node::query()
|
||||
@@ -71,23 +68,15 @@ class TransferServerService
|
||||
|
||||
$server->validateTransferState();
|
||||
|
||||
$this->connection->transaction(function () use ($server, $node_id, $allocation_id, $additional_allocations) {
|
||||
// Create a new ServerTransfer entry.
|
||||
$this->connection->transaction(function () use ($server, $node_id) {
|
||||
$transfer = new ServerTransfer();
|
||||
|
||||
$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;
|
||||
|
||||
$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))
|
||||
@@ -102,32 +91,4 @@ class TransferServerService
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the specified allocations to the specified server.
|
||||
*/
|
||||
private function assignAllocationsToServer(Server $server, int $node_id, int $allocation_id, array $additional_allocations): void
|
||||
{
|
||||
$allocations = $additional_allocations;
|
||||
$allocations[] = $allocation_id;
|
||||
|
||||
$node = Node::query()->findOrFail($node_id);
|
||||
$unassigned = $node->allocations()
|
||||
->whereNull('server_id')
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
|
||||
$updateIds = [];
|
||||
foreach ($allocations as $allocation) {
|
||||
if (!in_array($allocation, $unassigned)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$updateIds[] = $allocation;
|
||||
}
|
||||
|
||||
if (!empty($updateIds)) {
|
||||
Allocation::query()->whereIn('id', $updateIds)->update(['server_id' => $server->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class VariableValidatorService
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function handle(int $egg, array $fields = []): Collection
|
||||
public function handle(int $egg, array $fields = [], bool $validate = true): Collection
|
||||
{
|
||||
$query = EggVariable::query()->where('egg_id', $egg);
|
||||
if (!$this->isUserLevel(User::USER_LEVEL_ADMIN)) {
|
||||
@@ -44,9 +44,11 @@ class VariableValidatorService
|
||||
$customAttributes['environment.' . $variable->env_variable] = trans('validation.internal.variable_value', ['env' => $variable->name]);
|
||||
}
|
||||
|
||||
$validator = $this->validator->make($data, $rules, [], $customAttributes);
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
if ($validate) {
|
||||
$validator = $this->validator->make($data, $rules, [], $customAttributes);
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
}
|
||||
|
||||
return Collection::make($variables)->map(function ($item) use ($fields) {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Application;
|
||||
|
||||
use App\Models\Node;
|
||||
use App\Models\Server;
|
||||
use League\Fractal\Resource\Item;
|
||||
use App\Models\Allocation;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use App\Services\Acl\Api\AdminAcl;
|
||||
|
||||
class AllocationTransformer extends BaseTransformer
|
||||
{
|
||||
/**
|
||||
* Relationships that can be loaded onto allocation transformations.
|
||||
*/
|
||||
protected array $availableIncludes = ['node', 'server'];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return Allocation::RESOURCE_NAME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generic transformed allocation array.
|
||||
*/
|
||||
public function transform(Allocation $allocation): array
|
||||
{
|
||||
return [
|
||||
'id' => $allocation->id,
|
||||
'ip' => $allocation->ip,
|
||||
'alias' => $allocation->ip_alias,
|
||||
'port' => $allocation->port,
|
||||
'notes' => $allocation->notes,
|
||||
'assigned' => !is_null($allocation->server_id),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the node relationship onto a given transformation.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeNode(Allocation $allocation): Item|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_NODES)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
return $this->item(
|
||||
$allocation->node,
|
||||
$this->makeTransformer(NodeTransformer::class),
|
||||
Node::RESOURCE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the server relationship onto a given transformation.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeServer(Allocation $allocation): Item|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_SERVERS) || !$allocation->server) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
return $this->item(
|
||||
$allocation->server,
|
||||
$this->makeTransformer(ServerTransformer::class),
|
||||
Server::RESOURCE_NAME
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class NodeTransformer extends BaseTransformer
|
||||
/**
|
||||
* List of resources that can be included.
|
||||
*/
|
||||
protected array $availableIncludes = ['allocations', 'servers'];
|
||||
protected array $availableIncludes = ['servers'];
|
||||
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
@@ -45,26 +45,6 @@ class NodeTransformer extends BaseTransformer
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the nodes associated with this location.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Node $node): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$node->loadMissing('allocations');
|
||||
|
||||
return $this->collection(
|
||||
$node->getRelation('allocations'),
|
||||
$this->makeTransformer(AllocationTransformer::class),
|
||||
'allocation'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the nodes associated with this location.
|
||||
*
|
||||
|
||||
@@ -17,7 +17,6 @@ class ServerTransformer extends BaseTransformer
|
||||
* List of resources that can be included.
|
||||
*/
|
||||
protected array $availableIncludes = [
|
||||
'allocations',
|
||||
'user',
|
||||
'subusers',
|
||||
'egg',
|
||||
@@ -76,7 +75,6 @@ class ServerTransformer extends BaseTransformer
|
||||
],
|
||||
'user' => $server->owner_id,
|
||||
'node' => $server->node_id,
|
||||
'allocation' => $server->allocation_id,
|
||||
'egg' => $server->egg_id,
|
||||
'container' => [
|
||||
'startup_command' => $server->startup,
|
||||
@@ -87,25 +85,25 @@ class ServerTransformer extends BaseTransformer
|
||||
],
|
||||
$server->getUpdatedAtColumn() => $this->formatTimestamp($server->updated_at),
|
||||
$server->getCreatedAtColumn() => $this->formatTimestamp($server->created_at),
|
||||
|
||||
'allocations' => collect($server->ports)->map(function ($port) {
|
||||
$ip = '0.0.0.0';
|
||||
if (str_contains($port, ':')) {
|
||||
[$ip, $port] = explode(':', $port);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => random_int(1, PHP_INT_MAX),
|
||||
'ip' => $ip,
|
||||
'alias' => null,
|
||||
'port' => (int) $port,
|
||||
'notes' => null,
|
||||
'assigned' => false,
|
||||
];
|
||||
})->all(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generic array of allocations for this server.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Server $server): Collection|NullResource
|
||||
{
|
||||
if (!$this->authorize(AdminAcl::RESOURCE_ALLOCATIONS)) {
|
||||
return $this->null();
|
||||
}
|
||||
|
||||
$server->loadMissing('allocations');
|
||||
|
||||
return $this->collection($server->getRelation('allocations'), $this->makeTransformer(AllocationTransformer::class), 'allocation');
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generic array of data about subusers for this server.
|
||||
*
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Transformers\Api\Client;
|
||||
|
||||
use App\Models\Allocation;
|
||||
|
||||
class AllocationTransformer extends BaseClientTransformer
|
||||
{
|
||||
/**
|
||||
* Return the resource name for the JSONAPI output.
|
||||
*/
|
||||
public function getResourceName(): string
|
||||
{
|
||||
return 'allocation';
|
||||
}
|
||||
|
||||
public function transform(Allocation $model): array
|
||||
{
|
||||
return [
|
||||
'id' => $model->id,
|
||||
'ip' => $model->ip,
|
||||
'ip_alias' => $model->ip_alias,
|
||||
'port' => $model->port,
|
||||
'notes' => $model->notes,
|
||||
'is_default' => $model->server->allocation_id === $model->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,21 +2,20 @@
|
||||
|
||||
namespace App\Transformers\Api\Client;
|
||||
|
||||
use App\Models\Allocation;
|
||||
use App\Models\Egg;
|
||||
use App\Models\EggVariable;
|
||||
use App\Models\Permission;
|
||||
use App\Models\Server;
|
||||
use App\Models\Subuser;
|
||||
use App\Services\Servers\StartupCommandService;
|
||||
use Illuminate\Container\Container;
|
||||
use League\Fractal\Resource\Collection;
|
||||
use League\Fractal\Resource\Item;
|
||||
use League\Fractal\Resource\NullResource;
|
||||
use League\Fractal\Resource\Item;
|
||||
use Illuminate\Container\Container;
|
||||
use App\Services\Servers\StartupCommandService;
|
||||
|
||||
class ServerTransformer extends BaseClientTransformer
|
||||
{
|
||||
protected array $defaultIncludes = ['allocations', 'variables'];
|
||||
protected array $defaultIncludes = ['variables'];
|
||||
|
||||
protected array $availableIncludes = ['egg', 'subusers'];
|
||||
|
||||
@@ -75,6 +74,7 @@ class ServerTransformer extends BaseClientTransformer
|
||||
// This field is deprecated, please use "status".
|
||||
'is_installing' => !$server->isInstalled(),
|
||||
'is_transferring' => !is_null($server->transfer),
|
||||
'ports' => $user->can(Permission::ACTION_ALLOCATION_READ, $server) ? $server->ports : collect(),
|
||||
];
|
||||
|
||||
if (!config('panel.editable_server_descriptions')) {
|
||||
@@ -84,33 +84,6 @@ class ServerTransformer extends BaseClientTransformer
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the allocations associated with this server.
|
||||
*
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
public function includeAllocations(Server $server): Collection
|
||||
{
|
||||
$transformer = $this->makeTransformer(AllocationTransformer::class);
|
||||
|
||||
$user = $this->request->user();
|
||||
// While we include this permission, we do need to actually handle it slightly different here
|
||||
// for the purpose of keeping things functionally working. If the user doesn't have read permissions
|
||||
// for the allocations we'll only return the primary server allocation, and any notes associated
|
||||
// with it will be hidden.
|
||||
//
|
||||
// This allows us to avoid too much permission regression, without also hiding information that
|
||||
// is generally needed for the frontend to make sense when browsing or searching results.
|
||||
if (!$user->can(Permission::ACTION_ALLOCATION_READ, $server)) {
|
||||
$primary = clone $server->allocation;
|
||||
$primary->notes = null;
|
||||
|
||||
return $this->collection([$primary], $transformer, Allocation::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
return $this->collection($server->allocations, $transformer, Allocation::RESOURCE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \App\Exceptions\Transformer\InvalidTransformerLevelException
|
||||
*/
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"filament/filament": "^3.2",
|
||||
"guzzlehttp/guzzle": "^7.8.1",
|
||||
"laracasts/utilities": "~3.2.2",
|
||||
"laravel/framework": "^11.7",
|
||||
"laravel/framework": "^11.28.1",
|
||||
"laravel/helpers": "^1.7",
|
||||
"laravel/sanctum": "^4.0.2",
|
||||
"laravel/socialite": "^5.14",
|
||||
|
||||
20
composer.lock
generated
20
composer.lock
generated
@@ -2802,16 +2802,16 @@
|
||||
},
|
||||
{
|
||||
"name": "laravel/framework",
|
||||
"version": "v11.10.0",
|
||||
"version": "v11.28.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/laravel/framework.git",
|
||||
"reference": "99b4255194912044b75ab72329f8c19e6345720e"
|
||||
"reference": "3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/99b4255194912044b75ab72329f8c19e6345720e",
|
||||
"reference": "99b4255194912044b75ab72329f8c19e6345720e",
|
||||
"url": "https://api.github.com/repos/laravel/framework/zipball/3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c",
|
||||
"reference": "3ef5c8a85b4c598d5ffaf98afd72f6a5d6a0be2c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2830,7 +2830,7 @@
|
||||
"fruitcake/php-cors": "^1.3",
|
||||
"guzzlehttp/guzzle": "^7.8",
|
||||
"guzzlehttp/uri-template": "^1.0",
|
||||
"laravel/prompts": "^0.1.18",
|
||||
"laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
|
||||
"laravel/serializable-closure": "^1.3",
|
||||
"league/commonmark": "^2.2.1",
|
||||
"league/flysystem": "^3.8.0",
|
||||
@@ -2864,6 +2864,7 @@
|
||||
},
|
||||
"provide": {
|
||||
"psr/container-implementation": "1.1|2.0",
|
||||
"psr/log-implementation": "1.0|2.0|3.0",
|
||||
"psr/simple-cache-implementation": "1.0|2.0|3.0"
|
||||
},
|
||||
"replace": {
|
||||
@@ -2872,6 +2873,7 @@
|
||||
"illuminate/bus": "self.version",
|
||||
"illuminate/cache": "self.version",
|
||||
"illuminate/collections": "self.version",
|
||||
"illuminate/concurrency": "self.version",
|
||||
"illuminate/conditionable": "self.version",
|
||||
"illuminate/config": "self.version",
|
||||
"illuminate/console": "self.version",
|
||||
@@ -2914,9 +2916,9 @@
|
||||
"league/flysystem-sftp-v3": "^3.0",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nyholm/psr7": "^1.2",
|
||||
"orchestra/testbench-core": "^9.0.15",
|
||||
"orchestra/testbench-core": "^9.5",
|
||||
"pda/pheanstalk": "^5.0",
|
||||
"phpstan/phpstan": "^1.4.7",
|
||||
"phpstan/phpstan": "^1.11.5",
|
||||
"phpunit/phpunit": "^10.5|^11.0",
|
||||
"predis/predis": "^2.0.2",
|
||||
"resend/resend-php": "^0.10.0",
|
||||
@@ -2972,6 +2974,8 @@
|
||||
"src/Illuminate/Events/functions.php",
|
||||
"src/Illuminate/Filesystem/functions.php",
|
||||
"src/Illuminate/Foundation/helpers.php",
|
||||
"src/Illuminate/Log/functions.php",
|
||||
"src/Illuminate/Support/functions.php",
|
||||
"src/Illuminate/Support/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
@@ -3003,7 +3007,7 @@
|
||||
"issues": "https://github.com/laravel/framework/issues",
|
||||
"source": "https://github.com/laravel/framework"
|
||||
},
|
||||
"time": "2024-06-04T13:45:55+00:00"
|
||||
"time": "2024-10-16T16:32:21+00:00"
|
||||
},
|
||||
{
|
||||
"name": "laravel/helpers",
|
||||
|
||||
@@ -166,5 +166,7 @@ return [
|
||||
|
||||
'use_binary_prefix' => env('PANEL_USE_BINARY_PREFIX', true),
|
||||
|
||||
'default_io_weight' => env('PANEL_IO_WEIGHT', 500),
|
||||
|
||||
'editable_server_descriptions' => env('PANEL_EDITABLE_SERVER_DESCRIPTIONS', true),
|
||||
];
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Allocation;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class AllocationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The name of the factory's corresponding model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $model = Allocation::class;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'ip' => $this->faker->unique()->ipv4(),
|
||||
'port' => $this->faker->unique()->numberBetween(1024, 65535),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the allocation to a specific server model.
|
||||
*/
|
||||
public function forServer(Server $server): self
|
||||
{
|
||||
return $this->for($server)->for($server->node);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,17 @@
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Server Port",
|
||||
"description": "",
|
||||
"env_variable": "SERVER_PORT",
|
||||
"default_value": "25565",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|port",
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,6 +94,17 @@
|
||||
],
|
||||
"sort": 4,
|
||||
"field_type": "text"
|
||||
},
|
||||
{
|
||||
"name": "Server Port",
|
||||
"description": "",
|
||||
"env_variable": "SERVER_PORT",
|
||||
"default_value": "25565",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|port",
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
"port"
|
||||
],
|
||||
"sort": 10,
|
||||
"field_type": "text"
|
||||
@@ -182,7 +182,7 @@
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
"port"
|
||||
],
|
||||
"sort": 11,
|
||||
"field_type": "text"
|
||||
@@ -239,7 +239,7 @@
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"required",
|
||||
"integer"
|
||||
"port"
|
||||
],
|
||||
"sort": 15,
|
||||
"field_type": "text"
|
||||
@@ -288,4 +288,4 @@
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"between:1025,65535"
|
||||
"port"
|
||||
],
|
||||
"sort": 2,
|
||||
"field_type": "text"
|
||||
@@ -69,7 +68,7 @@
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"between:1025,65535"
|
||||
"port"
|
||||
],
|
||||
"sort": 3,
|
||||
"field_type": "text"
|
||||
@@ -96,10 +95,10 @@
|
||||
"default_value": "10022",
|
||||
"user_viewable": true,
|
||||
"user_editable": false,
|
||||
"rules": "required|port",
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"between:1025,65535"
|
||||
"port"
|
||||
],
|
||||
"sort": 5,
|
||||
"field_type": "text"
|
||||
@@ -113,11 +112,10 @@
|
||||
"user_editable": false,
|
||||
"rules": [
|
||||
"required",
|
||||
"integer",
|
||||
"between:1025,65535"
|
||||
"port",
|
||||
],
|
||||
"sort": 6,
|
||||
"field_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,9 @@ return new class extends Migration
|
||||
$table->dropIndex('permissions_server_id_foreign');
|
||||
$table->dropForeign('permissions_user_id_foreign');
|
||||
$table->dropIndex('permissions_user_id_foreign');
|
||||
} else {
|
||||
$table->dropForeign(['server_id']);
|
||||
$table->dropForeign(['user_id']);
|
||||
}
|
||||
|
||||
$table->dropColumn('server_id');
|
||||
|
||||
@@ -12,10 +12,7 @@ return new class extends Migration
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->dropForeign(['pack_id']);
|
||||
}
|
||||
|
||||
$table->dropForeign(['pack_id']);
|
||||
$table->dropColumn('pack_id');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,14 +30,15 @@ return new class extends Migration
|
||||
Schema::table('eggs', function (Blueprint $table) {
|
||||
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->dropForeign('service_options_nest_id_foreign');
|
||||
} else {
|
||||
$table->dropForeign(['nest_id']);
|
||||
}
|
||||
|
||||
$table->dropColumn('nest_id');
|
||||
});
|
||||
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->dropForeign('servers_nest_id_foreign');
|
||||
}
|
||||
$table->dropForeign(['nest_id']);
|
||||
$table->dropColumn('nest_id');
|
||||
});
|
||||
|
||||
|
||||
@@ -27,10 +27,7 @@ return new class extends Migration
|
||||
}
|
||||
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
if (Schema::getConnection()->getDriverName() !== 'sqlite') {
|
||||
$table->dropForeign('nodes_location_id_foreign');
|
||||
}
|
||||
|
||||
$table->dropForeign(['location_id']);
|
||||
$table->dropColumn('location_id');
|
||||
});
|
||||
|
||||
|
||||
66
database/migrations/2024_09_18_043350_modify_allocations.php
Normal file
66
database/migrations/2024_09_18_043350_modify_allocations.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Objects\Endpoint;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('server_transfers', function (Blueprint $table) {
|
||||
$table->dropColumn(['old_allocation', 'new_allocation', 'old_additional_allocations', 'new_additional_allocations']);
|
||||
});
|
||||
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->json('ports')->nullable();
|
||||
});
|
||||
|
||||
$portMappings = [];
|
||||
foreach (DB::table('allocations')->get() as $allocation) {
|
||||
$portMappings[$allocation->server_id][] = "$allocation->ip:$allocation->port";
|
||||
}
|
||||
|
||||
foreach ($portMappings as $serverId => $ports) {
|
||||
/** @var Server $server */
|
||||
$server = Server::find($serverId);
|
||||
if (!$server) {
|
||||
// Orphaned Allocations
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($ports as $port) {
|
||||
$server->ports ??= collect();
|
||||
$server->ports->add(new Endpoint($port));
|
||||
}
|
||||
$server->save();
|
||||
}
|
||||
|
||||
Schema::table('servers', function (Blueprint $table) {
|
||||
$table->dropForeign(['allocation_id']);
|
||||
$table->dropUnique(['allocation_id']);
|
||||
$table->dropColumn(['allocation_id']);
|
||||
});
|
||||
|
||||
Schema::dropIfExists('allocations');
|
||||
|
||||
Schema::table('nodes', function (Blueprint $table) {
|
||||
$table->boolean('strict_ports')->default(true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
// Too much time to ensure this works correctly, please take a backup if necessary
|
||||
}
|
||||
};
|
||||
@@ -1,119 +0,0 @@
|
||||
import React, { memo, useCallback, useState } from 'react';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import tw from 'twin.macro';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faNetworkWired } from '@fortawesome/free-solid-svg-icons';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import { Textarea } from '@/components/elements/Input';
|
||||
import Can from '@/components/elements/Can';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import { Allocation } from '@/api/server/getServer';
|
||||
import styled from 'styled-components/macro';
|
||||
import { debounce } from 'debounce';
|
||||
import setServerAllocationNotes from '@/api/server/network/setServerAllocationNotes';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import DeleteAllocationButton from '@/components/server/network/DeleteAllocationButton';
|
||||
import setPrimaryServerAllocation from '@/api/server/network/setPrimaryServerAllocation';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
import { ip } from '@/lib/formatters';
|
||||
import Code from '@/components/elements/Code';
|
||||
|
||||
const Label = styled.label`
|
||||
${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
allocation: Allocation;
|
||||
}
|
||||
|
||||
const AllocationRow = ({ allocation }: Props) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:network');
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const { mutate } = getServerAllocations();
|
||||
|
||||
const onNotesChanged = useCallback((id: number, notes: string) => {
|
||||
mutate((data) => data?.map((a) => (a.id === id ? { ...a, notes } : a)), false);
|
||||
}, []);
|
||||
|
||||
const setAllocationNotes = debounce((notes: string) => {
|
||||
setLoading(true);
|
||||
clearFlashes();
|
||||
|
||||
setServerAllocationNotes(uuid, allocation.id, notes)
|
||||
.then(() => onNotesChanged(allocation.id, notes))
|
||||
.catch((error) => clearAndAddHttpError(error))
|
||||
.then(() => setLoading(false));
|
||||
}, 750);
|
||||
|
||||
const setPrimaryAllocation = () => {
|
||||
clearFlashes();
|
||||
mutate((data) => data?.map((a) => ({ ...a, isDefault: a.id === allocation.id })), false);
|
||||
|
||||
setPrimaryServerAllocation(uuid, allocation.id).catch((error) => {
|
||||
clearAndAddHttpError(error);
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<GreyRowBox $hoverable={false} className={'flex-wrap md:flex-nowrap mt-2'}>
|
||||
<div className={'flex items-center w-full md:w-auto'}>
|
||||
<div className={'pl-4 pr-6 text-neutral-400'}>
|
||||
<FontAwesomeIcon icon={faNetworkWired} />
|
||||
</div>
|
||||
<div className={'mr-4 flex-1 md:w-40'}>
|
||||
{allocation.alias ? (
|
||||
<CopyOnClick text={allocation.alias}>
|
||||
<Code dark className={'w-40 truncate'}>
|
||||
{allocation.alias}
|
||||
</Code>
|
||||
</CopyOnClick>
|
||||
) : (
|
||||
<CopyOnClick text={ip(allocation.ip)}>
|
||||
<Code dark>{ip(allocation.ip)}</Code>
|
||||
</CopyOnClick>
|
||||
)}
|
||||
<Label>{allocation.alias ? 'Hostname' : 'IP Address'}</Label>
|
||||
</div>
|
||||
<div className={'w-16 md:w-24 overflow-hidden'}>
|
||||
<Code dark>{allocation.port}</Code>
|
||||
<Label>Port</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-4 w-full md:mt-0 md:flex-1 md:w-auto'}>
|
||||
<InputSpinner visible={loading}>
|
||||
<Textarea
|
||||
className={'bg-neutral-800 hover:border-neutral-600 border-transparent'}
|
||||
placeholder={'Notes'}
|
||||
defaultValue={allocation.notes || undefined}
|
||||
onChange={(e) => setAllocationNotes(e.currentTarget.value)}
|
||||
/>
|
||||
</InputSpinner>
|
||||
</div>
|
||||
<div className={'flex justify-end space-x-4 mt-4 w-full md:mt-0 md:w-48'}>
|
||||
{allocation.isDefault ? (
|
||||
<Button size={Button.Sizes.Small} className={'!text-gray-50 !bg-blue-600'} disabled>
|
||||
Primary
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Can action={'allocation.delete'}>
|
||||
<DeleteAllocationButton allocation={allocation.id} />
|
||||
</Can>
|
||||
<Can action={'allocation.update'}>
|
||||
<Button.Text size={Button.Sizes.Small} onClick={setPrimaryAllocation}>
|
||||
Make Primary
|
||||
</Button.Text>
|
||||
</Can>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</GreyRowBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AllocationRow, isEqual);
|
||||
@@ -1,61 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import tw from 'twin.macro';
|
||||
import Icon from '@/components/elements/Icon';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import deleteServerAllocation from '@/api/server/network/deleteServerAllocation';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import { Dialog } from '@/components/elements/dialog';
|
||||
import { Button } from '@/components/elements/button/index';
|
||||
|
||||
interface Props {
|
||||
allocation: number;
|
||||
}
|
||||
|
||||
const DeleteAllocationButton = ({ allocation }: Props) => {
|
||||
const [confirm, setConfirm] = useState(false);
|
||||
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
|
||||
|
||||
const { mutate } = getServerAllocations();
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:network');
|
||||
|
||||
const deleteAllocation = () => {
|
||||
clearFlashes();
|
||||
|
||||
mutate((data) => data?.filter((a) => a.id !== allocation), false);
|
||||
setServerFromState((s) => ({ ...s, allocations: s.allocations.filter((a) => a.id !== allocation) }));
|
||||
|
||||
deleteServerAllocation(uuid, allocation).catch((error) => {
|
||||
clearAndAddHttpError(error);
|
||||
mutate();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog.Confirm
|
||||
open={confirm}
|
||||
onClose={() => setConfirm(false)}
|
||||
title={'Remove Allocation'}
|
||||
confirm={'Delete'}
|
||||
onConfirmed={deleteAllocation}
|
||||
>
|
||||
This allocation will be immediately removed from your server.
|
||||
</Dialog.Confirm>
|
||||
<Button.Danger
|
||||
variant={Button.Variants.Secondary}
|
||||
size={Button.Sizes.Small}
|
||||
shape={Button.Shapes.IconSquare}
|
||||
type={'button'}
|
||||
onClick={() => setConfirm(true)}
|
||||
>
|
||||
<Icon icon={faTrashAlt} css={tw`w-3 h-auto`} />
|
||||
</Button.Danger>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteAllocationButton;
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import { useFlashKey } from '@/plugins/useFlash';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import AllocationRow from '@/components/server/network/AllocationRow';
|
||||
import Button from '@/components/elements/Button';
|
||||
import createServerAllocation from '@/api/server/network/createServerAllocation';
|
||||
import tw from 'twin.macro';
|
||||
import Can from '@/components/elements/Can';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import getServerAllocations from '@/api/swr/getServerAllocations';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';
|
||||
|
||||
const NetworkContainer = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const uuid = ServerContext.useStoreState((state) => state.server.data!.uuid);
|
||||
const allocationLimit = ServerContext.useStoreState((state) => state.server.data!.featureLimits.allocations);
|
||||
const allocations = ServerContext.useStoreState((state) => state.server.data!.allocations, isEqual);
|
||||
const setServerFromState = ServerContext.useStoreActions((actions) => actions.server.setServerFromState);
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useFlashKey('server:network');
|
||||
const { data, error, mutate } = getServerAllocations();
|
||||
|
||||
useEffect(() => {
|
||||
mutate(allocations);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
clearAndAddHttpError(error);
|
||||
}, [error]);
|
||||
|
||||
useDeepCompareEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
setServerFromState((state) => ({ ...state, allocations: data }));
|
||||
}, [data]);
|
||||
|
||||
const onCreateAllocation = () => {
|
||||
clearFlashes();
|
||||
|
||||
setLoading(true);
|
||||
createServerAllocation(uuid)
|
||||
.then((allocation) => {
|
||||
setServerFromState((s) => ({ ...s, allocations: s.allocations.concat(allocation) }));
|
||||
return mutate(data?.concat(allocation), false);
|
||||
})
|
||||
.catch((error) => clearAndAddHttpError(error))
|
||||
.then(() => setLoading(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<ServerContentBlock showFlashKey={'server:network'} title={'Network'}>
|
||||
{!data ? (
|
||||
<Spinner size={'large'} centered />
|
||||
) : (
|
||||
<>
|
||||
{data.map((allocation) => (
|
||||
<AllocationRow key={`${allocation.ip}:${allocation.port}`} allocation={allocation} />
|
||||
))}
|
||||
{allocationLimit > 0 && (
|
||||
<Can action={'allocation.create'}>
|
||||
<SpinnerOverlay visible={loading} />
|
||||
<div css={tw`mt-6 sm:flex items-center justify-end`}>
|
||||
<p css={tw`text-sm text-neutral-300 mb-4 sm:mr-6 sm:mb-0`}>
|
||||
You are currently using {data.length} of {allocationLimit} allowed allocations for
|
||||
this server.
|
||||
</p>
|
||||
{allocationLimit > data.length && (
|
||||
<Button css={tw`w-full sm:w-auto`} color={'primary'} onClick={onCreateAllocation}>
|
||||
Create Allocation
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Can>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default NetworkContainer;
|
||||
@@ -4,7 +4,6 @@ import DatabasesContainer from '@/components/server/databases/DatabasesContainer
|
||||
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
|
||||
import UsersContainer from '@/components/server/users/UsersContainer';
|
||||
import BackupContainer from '@/components/server/backups/BackupContainer';
|
||||
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||
import StartupContainer from '@/components/server/startup/StartupContainer';
|
||||
import FileManagerContainer from '@/components/server/files/FileManagerContainer';
|
||||
import SettingsContainer from '@/components/server/settings/SettingsContainer';
|
||||
@@ -116,12 +115,6 @@ export default {
|
||||
name: 'Backups',
|
||||
component: BackupContainer,
|
||||
},
|
||||
{
|
||||
path: '/network',
|
||||
permission: 'allocation.*',
|
||||
name: 'Network',
|
||||
component: NetworkContainer,
|
||||
},
|
||||
{
|
||||
path: '/startup',
|
||||
permission: 'startup.*',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user