Compare commits

...

112 Commits

Author SHA1 Message Date
Lance Pioch
4eeefab6c6 Delete allocation exceptions 2024-10-20 15:50:40 -04:00
Lance Pioch
a2108c3d91 Various improvements 2024-10-20 15:23:46 -04:00
Lance Pioch
4f5e9a6c30 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/Pages/EditProfile.php
#	app/Models/Node.php
#	app/Models/Objects/DeploymentObject.php
#	app/Services/Allocations/AssignmentService.php
#	app/Services/Servers/ServerCreationService.php
#	app/Services/Servers/TransferServerService.php
#	pint.json
2024-10-20 15:14:08 -04:00
Lance Pioch
1aa51d15f0 Resolve this for now 2024-10-18 22:47:25 -04:00
Lance Pioch
3d8847a508 Resolve phpstan issues 2024-10-18 22:46:12 -04:00
Lance Pioch
4fa49ae915 This is now an array 2024-10-18 22:46:04 -04:00
Lance Pioch
a42cb193a3 Thank you merge conflict 2024-10-18 22:45:54 -04:00
Lance Pioch
50ffaf4d01 Fix tests 2024-10-18 22:38:06 -04:00
Lance Pioch
804ff64a71 Force register the validation rule 2024-10-18 22:27:18 -04:00
Lance Pioch
c2f6842f64 Smarter check 2024-10-18 22:17:15 -04:00
Lance Pioch
455d0543f1 Fix double test runners 2024-10-18 22:17:06 -04:00
Lance Pioch
97a4601150 Update laravel framework 2024-10-18 21:40:17 -04:00
Lance Pioch
2cc4a42905 This is now an array 2024-10-18 21:37:03 -04:00
Lance Pioch
5353d38302 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/NodeResource/Pages/CreateNode.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php
#	app/Transformers/Api/Client/ServerTransformer.php
#	composer.lock
#	config/panel.php
2024-10-18 21:18:48 -04:00
Lance Pioch
df88d33af4 Update pint 2024-09-21 15:45:22 -04:00
Lance Pioch
906d4a7d28 Use new migration logic 2024-09-19 13:19:47 -04:00
Lance Pioch
9ba8c1df9b Update Laravel framework 2024-09-18 22:22:08 -04:00
Lance Pioch
0a6b846230 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Services/Allocations/AssignmentService.php
#	database/Seeders/eggs/minecraft/egg-bungeecord.json
#	database/Seeders/eggs/minecraft/egg-forge-minecraft.json
#	database/Seeders/eggs/minecraft/egg-paper.json
#	database/Seeders/eggs/minecraft/egg-sponge-sponge-vanilla.json
#	database/Seeders/eggs/minecraft/egg-vanilla-minecraft.json
#	database/Seeders/eggs/rust/egg-rust.json
#	database/Seeders/eggs/source-engine/egg-counter-strike-global-offensive.json
#	database/Seeders/eggs/source-engine/egg-custom-source-engine-game.json
#	database/Seeders/eggs/source-engine/egg-garrys-mod.json
#	database/Seeders/eggs/source-engine/egg-insurgency.json
#	database/Seeders/eggs/source-engine/egg-team-fortress2.json
#	database/Seeders/eggs/voice-servers/egg-mumble-server.json
#	database/Seeders/eggs/voice-servers/egg-teamspeak3-server.json
2024-09-18 22:21:55 -04:00
Lance Pioch
aff9f4ea37 Don’t need this anymore 2024-07-04 18:56:28 -04:00
Lance Pioch
f2754c3cb1 Fix mappings 2024-07-04 13:50:48 -04:00
Lance Pioch
8b86707150 Wip 2024-07-04 13:11:13 -04:00
Lance Pioch
233fd50b2b Don’t include this by default 2024-07-04 10:24:46 -04:00
Lance Pioch
3a76fb1c79 Revert "Add custom component"
This reverts commit 0f798e5edb.
2024-07-03 22:23:18 -04:00
Lance Pioch
e76630b7f3 Merge branch 'issue/68' of github.com:pelican-dev/panel into issue/68 2024-07-03 12:14:09 -04:00
Lance Pioch
0f798e5edb Add custom component 2024-07-03 12:13:46 -04:00
Lance Pioch
a9c7eeddde Add the any ip address 2024-07-03 12:13:25 -04:00
Charles
11feef4f8c Update Eggs 2024-07-03 06:32:30 -04:00
notCharles
bebc410eda Update these 2024-07-02 19:36:53 -04:00
notCharles
ec0fa3c913 Revert "Also nuke this"
This reverts commit 4574821ed8.
2024-07-02 18:32:32 -04:00
notCharles
4574821ed8 Also nuke this 2024-07-02 16:50:39 -04:00
Lance Pioch
d71b1a4710 Merge branch 'issue/68' of github.com:pelican-dev/panel into issue/68 2024-07-02 14:39:57 -04:00
Charles
d9922e86f2 This has annoyed me... 2024-07-02 14:30:11 -04:00
Lance Pioch
9d9e4adbbd Don’t need this anymore 2024-07-02 14:27:19 -04:00
Lance Pioch
6b104e3331 Allow the server port to be selected if it also may only exist in the config 2024-07-02 14:03:05 -04:00
Lance Pioch
f2eca17480 Use constants 2024-07-02 11:50:46 -04:00
Lance Pioch
4c41e659b5 Add main server port and default 2024-07-01 15:13:20 -04:00
Lance Pioch
6238d6dd08 Switch to port rule 2024-07-01 15:12:33 -04:00
Lance Pioch
c45e4edcf6 Allow port rule to be optional 2024-07-01 15:12:03 -04:00
Lance Pioch
4273880126 Better helper text 2024-07-01 15:11:56 -04:00
Lance Pioch
d86843977b Run pint 2024-07-01 14:55:17 -04:00
Lance Pioch
5b468c21ae Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php
#	app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php
#	app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php
#	app/Filament/Resources/NodeResource.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
2024-07-01 14:54:12 -04:00
Lance Pioch
68ef0a1d0a Fix these 2024-06-26 22:03:26 -04:00
Lance Pioch
f6122f919a Rename this because order matters 2024-06-26 22:03:23 -04:00
Lance Pioch
4f10ec2c20 Fix name 2024-06-26 21:56:01 -04:00
Lance Pioch
45fcc2a09a Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/DatabaseHostResource/Pages/CreateDatabaseHost.php
#	app/Filament/Resources/DatabaseHostResource/Pages/EditDatabaseHost.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/EditServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Http/Requests/Admin/Node/AllocationFormRequest.php
#	app/Http/Requests/Api/Application/Allocations/StoreAllocationRequest.php
#	app/Models/AuditLog.php
#	app/Models/Server.php
2024-06-26 21:52:05 -04:00
Lance Pioch
19c7b4d044 Clean up 2024-06-26 21:44:06 -04:00
Lance Pioch
7c8b204d13 Remove network tab 2024-06-26 21:42:57 -04:00
Lance Pioch
343a5b81bc Almost done 2024-06-26 21:38:18 -04:00
Lance Pioch
755632f9d5 Wip 2024-06-23 22:32:37 -04:00
Lance Pioch
e7ee86a914 Fix whoopsie 2024-06-23 22:31:41 -04:00
Lance Pioch
eb0bad82e6 Wip 2024-06-22 11:30:08 -04:00
Lance Pioch
48fd3cc84e Add restore to simplify form 2024-06-18 10:59:56 -04:00
Lance Pioch
d4484f5254 Handle nulls 2024-06-18 10:59:50 -04:00
Lance Pioch
958e8fac8a Simplify states and statuses and resolve #362 2024-06-17 18:18:48 -04:00
Lance Pioch
7986505b99 Don’t report status anymore 2024-06-17 18:15:56 -04:00
Lance Pioch
ba5b81cf2d Show localhost 2024-06-17 18:15:45 -04:00
Lance Pioch
32018399b6 Add server io weight default 2024-06-17 18:15:35 -04:00
Lance Pioch
48f4c35d0b Can be null apparently 2024-06-17 11:55:32 -04:00
Lance Pioch
f699fd5459 Make ports into badges 2024-06-17 11:46:58 -04:00
Lance Pioch
05573f64dd Group servers 2024-06-17 11:46:40 -04:00
Lance Pioch
3fa714c7e3 Refactor 2024-06-17 10:48:05 -04:00
Lance Pioch
69acc48b5e Use constants 2024-06-17 10:47:55 -04:00
Lance Pioch
9d9720a5a2 Start servers by default 2024-06-17 10:47:43 -04:00
Lance Pioch
ff261f9c99 Realism 2024-06-16 13:07:12 -04:00
Lance Pioch
c7bea4f024 This was wrong but somehow worked in sqlite, wut 2024-06-16 12:30:42 -04:00
Lance Pioch
738707b251 Add validation rule 2024-06-16 12:30:28 -04:00
Lance Pioch
a8699704de Fix return 2024-06-16 11:56:21 -04:00
Lance Pioch
d9dc932e07 Remove unused import 2024-06-16 11:55:54 -04:00
Lance Pioch
f57232bc23 Fix tests 2024-06-16 11:50:08 -04:00
Lance Pioch
b24ff8bb26 Unwrap transaction 2024-06-15 07:27:17 -04:00
Lance Pioch
eff8e509ef Wrap in transaction 2024-06-15 07:19:01 -04:00
Lance Pioch
6976fa8989 Try again 2024-06-15 06:57:11 -04:00
Lance Pioch
2b58160da9 Drop it 2024-06-15 06:04:20 -04:00
Lance Pioch
44e0dd3e09 Remove final refs 2024-06-15 06:01:40 -04:00
Lance Pioch
8ea57bc46b Remove unused audit logs 2024-06-15 05:53:29 -04:00
Lance Pioch
459d90e8d1 Revert this 2024-06-15 05:44:53 -04:00
Lance Pioch
a97341f6f2 Cast to int 2024-06-15 05:40:25 -04:00
Lance Pioch
375a64a38e Remove these relationships 2024-06-15 05:40:21 -04:00
Lance Pioch
7c25fc2a9d Revert these doc blocks 2024-06-15 05:39:53 -04:00
Lance Pioch
1a26f5ce9e This goes first 2024-06-15 05:34:56 -04:00
Lance Pioch
b47f40bd13 Remove debug 2024-06-15 05:24:57 -04:00
Lance Pioch
bcb7240ed2 Pint fixes 2024-06-15 05:24:19 -04:00
Lance Pioch
405aa857b1 Reset these for now 2024-06-15 05:23:33 -04:00
Lance Pioch
0bd2935885 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/EggResource/RelationManagers/ServersRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/NodeResource/RelationManagers/NodesRelationManager.php
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Filament/Resources/ServerResource/Pages/ListServers.php
#	app/Filament/Resources/ServerResource/RelationManagers/AllocationsRelationManager.php
#	app/Filament/Resources/UserResource/RelationManagers/ServersRelationManager.php
#	app/Models/Allocation.php
#	app/Models/ApiKey.php
#	app/Models/Server.php
#	app/Models/User.php
2024-06-15 05:21:58 -04:00
Lance Pioch
4cba1540ac Add endpoints 2024-06-15 05:14:54 -04:00
Lance Pioch
30051ab0d7 Add tooltips for versions 2024-06-15 05:14:30 -04:00
Lance Pioch
e15d515f71 Wip 2024-06-14 07:54:07 -04:00
Lance Pioch
36e2fa8e2b Wip 2024-06-13 11:31:58 -04:00
Lance Pioch
510ae3c0df Swap this for now 2024-06-10 21:38:28 -04:00
Lance Pioch
f5edb34873 Some filament changes for ports 2024-06-10 21:32:46 -04:00
Lance Pioch
0895bd2be5 Remove relation managers 2024-06-10 20:35:36 -04:00
Lance Pioch
17bc3de0d0 Rename translation keys 2024-06-10 20:34:46 -04:00
Lance Pioch
4319f24f51 Merge branch 'main' into issue/68
# Conflicts:
#	app/Filament/Resources/ServerResource/Pages/CreateServer.php
#	app/Models/Node.php
2024-06-09 15:42:49 -04:00
Lance Pioch
9ad113bc61 Move these 2024-06-09 14:18:19 -04:00
Lance Pioch
beadce96f6 Wip 2024-06-09 08:20:31 -04:00
Lance Pioch
b1d7d210fc Allow startup command to change with server variable 2024-06-07 01:00:00 -04:00
Lance Pioch
32e96dc0a6 Wip 2024-06-06 15:49:36 -04:00
Lance Pioch
b16a11c365 No longer reserved 2024-06-06 15:49:33 -04:00
Lance Pioch
81f218ddc9 Skip port variables down below 2024-06-06 15:49:23 -04:00
Lance Pioch
be6f79521e Wip 2024-06-04 16:34:54 -04:00
Lance Pioch
551175862e Better kebab names 2024-06-04 15:38:45 -04:00
Lance Pioch
dbad5ae9c7 WIP 2024-06-04 11:40:19 -04:00
Lance Pioch
768a45bbb8 More updates 2024-06-02 21:55:13 -04:00
Lance Pioch
bbe09ced1d Add some new options for ports 2024-06-02 16:58:32 -04:00
Lance Pioch
2e7c534a3b Disable this if there’s no egg 2024-06-02 16:58:21 -04:00
Lance Pioch
71684dc517 Pint fix 2024-06-02 01:38:42 -04:00
Lance Pioch
4dbb55059d Remove auto generated properties 2024-06-02 01:38:30 -04:00
Lance Pioch
f480a271b3 Simplify logic 2024-06-02 01:27:04 -04:00
Lance Pioch
9c81c0ce18 Use public member 2024-06-02 01:25:48 -04:00
Lance Pioch
b220c582cc Rearrange these 2024-06-02 01:22:33 -04:00
Lance Pioch
29f8ac625a This is for the api 2024-06-02 01:22:21 -04:00
Lance Pioch
5b6c462943 This isn’t typically used for a brand new server 2024-06-02 01:22:12 -04:00
119 changed files with 845 additions and 4111 deletions

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

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\PanelException;
class AllocationDoesNotBelongToServerException extends PanelException
{
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
<?php
namespace App\Exceptions\Service\Allocation;
use App\Exceptions\DisplayException;
class ServerUsingAllocationException extends DisplayException
{
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +0,0 @@
<?php
namespace App\Http\Requests\Api\Client\Servers\Network;
class SetPrimaryAllocationRequest extends UpdateAllocationRequest
{
public function rules(): array
{
return [];
}
}

View File

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

View 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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -77,8 +77,6 @@ class ServerDeletionService
}
}
$server->allocations()->update(['server_id' => null]);
$server->delete();
});
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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