diff --git a/app/Casts/EndpointCollection.php b/app/Casts/EndpointCollection.php index 701f10b28..7fca4a1ce 100644 --- a/app/Casts/EndpointCollection.php +++ b/app/Casts/EndpointCollection.php @@ -28,10 +28,14 @@ class EndpointCollection implements Castable public function set($model, $key, $value, $attributes) { - if (!$value instanceof Collection) { + if (!is_array($value) && !$value instanceof Collection) { return new Collection(); } + if (!$value instanceof Collection) { + $value = new Collection($value); + } + return $value->map(fn ($endpoint) => (string) $endpoint); } }; diff --git a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php index 0bc31a369..a6c2e5da7 100644 --- a/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php +++ b/app/Http/Requests/Api/Application/Servers/StoreServerRequest.php @@ -110,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; + }); } /** @@ -125,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; } diff --git a/app/Models/Objects/DeploymentObject.php b/app/Models/Objects/DeploymentObject.php index b7a4ebaf3..b51881910 100644 --- a/app/Models/Objects/DeploymentObject.php +++ b/app/Models/Objects/DeploymentObject.php @@ -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; @@ -46,4 +50,13 @@ class DeploymentObject return $this; } + public function getNode(): Node + { + return $this->node; + } + + public function setNode(Node $node): self + { + $this->node = $node; + } } diff --git a/app/Models/Objects/Endpoint.php b/app/Models/Objects/Endpoint.php index 428898f74..c4c67542b 100644 --- a/app/Models/Objects/Endpoint.php +++ b/app/Models/Objects/Endpoint.php @@ -2,9 +2,10 @@ namespace App\Models\Objects; +use Illuminate\Contracts\Support\Jsonable; use InvalidArgumentException; -class Endpoint +class Endpoint implements Jsonable { public const CIDR_MAX_BITS = 27; public const CIDR_MIN_BITS = 32; @@ -32,7 +33,7 @@ class Endpoint throw_unless($this->port < self::PORT_CEIL, "Port $this->port must be less than " . self::PORT_CEIL); } - public function __toString() + public function __toString(): string { if ($this->ip === self::INADDR_ANY) { return (string) $this->port; @@ -40,4 +41,9 @@ class Endpoint return "$this->ip:$this->port"; } + + public function toJson($options = 0): string + { + return json_encode($this->__toString()); + } } diff --git a/app/Services/Servers/ServerConfigurationStructureService.php b/app/Services/Servers/ServerConfigurationStructureService.php index a29d80bff..a8e761847 100644 --- a/app/Services/Servers/ServerConfigurationStructureService.php +++ b/app/Services/Servers/ServerConfigurationStructureService.php @@ -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,8 +66,8 @@ class ServerConfigurationStructureService 'allocations' => [ 'force_outgoing_ip' => $server->egg->force_outgoing_ip, 'default' => [ - 'ip' => $server->getPrimaryEndpoint()->ip, - 'port' => $server->getPrimaryEndpoint()->port, + 'ip' => $server->getPrimaryEndpoint()?->ip, + 'port' => $server->getPrimaryEndpoint()?->port, ], 'mappings' => $server->getPortMappings(), ], diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index ae2fb12d8..34a50a26d 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -51,13 +51,6 @@ class ServerCreationService $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) { - // Todo: Refactor - // $allocation = $this->configureDeployment($data, $deployment); - } - Assert::false(empty($data['node_id'])); $eggVariableData = $this->validatorService @@ -118,7 +111,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'), diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index a55e27bf6..bfa986007 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -25,7 +25,7 @@ class VariableValidatorService * * @throws \Illuminate\Validation\ValidationException */ - public function handle(int $egg, array $fields = [], $validate = false): Collection + public function handle(int $egg, array $fields = [], $validate = true): Collection { $query = EggVariable::query()->where('egg_id', $egg); if (!$this->isUserLevel(User::USER_LEVEL_ADMIN)) { diff --git a/app/Transformers/Api/Client/ServerTransformer.php b/app/Transformers/Api/Client/ServerTransformer.php index b5c410f4a..6021b5900 100644 --- a/app/Transformers/Api/Client/ServerTransformer.php +++ b/app/Transformers/Api/Client/ServerTransformer.php @@ -3,6 +3,7 @@ namespace App\Transformers\Api\Client; use App\Models\Egg; +use App\Models\Objects\Endpoint; use App\Models\Server; use App\Models\Subuser; use League\Fractal\Resource\Item; @@ -74,6 +75,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(), ]; } diff --git a/database/migrations/2024_06_06_043350_modify_allocations.php b/database/migrations/2024_06_06_043350_modify_allocations.php index e6d2ce12d..ceeb51666 100644 --- a/database/migrations/2024_06_06_043350_modify_allocations.php +++ b/database/migrations/2024_06_06_043350_modify_allocations.php @@ -20,12 +20,6 @@ return new class extends Migration $table->json('ports')->nullable(); }); - DB::table('servers')->update(['ports' => '[]']); - - Schema::table('servers', function (Blueprint $table) { - $table->json('ports')->change(); - }); - $portMappings = []; foreach (DB::table('allocations')->get() as $allocation) { $portMappings[$allocation->server_id][] = "$allocation->ip:$allocation->port"; @@ -37,13 +31,15 @@ return new class extends Migration ->update(['ports' => json_encode($ports)]); } - Schema::table('servers', function (Blueprint $table) { - try { + try { + Schema::table('servers', function (Blueprint $table) { $table->dropForeign(['allocation_id']); - } catch (Exception) { - // pass - } + }); + } catch (Throwable) { + } + + Schema::table('servers', function (Blueprint $table) { $table->dropUnique(['allocation_id']); $table->dropColumn(['allocation_id']); }); diff --git a/tests/Integration/Api/Client/ClientControllerTest.php b/tests/Integration/Api/Client/ClientControllerTest.php index cc2d1d38d..aed0fe8f7 100644 --- a/tests/Integration/Api/Client/ClientControllerTest.php +++ b/tests/Integration/Api/Client/ClientControllerTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Integration\Api\Client; +use App\Models\Objects\Endpoint; use App\Models\User; use App\Models\Server; use App\Models\Subuser; @@ -260,13 +261,16 @@ class ClientControllerTest extends ClientApiIntegrationTestCase } /** - * Test that a subuser without the allocation.read permission is only able to see the primary - * allocation for the server. + * Test that a subuser without the allocation.read permission cannot see any ports */ - public function testOnlyPrimaryAllocationIsReturnedToSubuser(): void + public function testNoPortsAreReturnedToSubuser(): void { /** @var \App\Models\Server $server */ [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + $server->ports->add(new Endpoint(1234)); + $server->ports->add(new Endpoint(2345, '1.2.3.4')); + $server->ports->add(new Endpoint(3456)); + $server->save(); $server->refresh(); $response = $this->actingAs($user)->getJson('/api/client'); @@ -275,7 +279,7 @@ class ClientControllerTest extends ClientApiIntegrationTestCase $response->assertJsonCount(1, 'data'); $response->assertJsonPath('data.0.attributes.server_owner', false); $response->assertJsonPath('data.0.attributes.uuid', $server->uuid); - $response->assertJsonCount(1, 'data.0.attributes.relationships.allocations.data'); + $response->assertJsonCount(0, 'data.0.attributes.ports'); } public static function filterTypeDataProvider(): array diff --git a/tests/Integration/Services/Servers/BuildModificationServiceTest.php b/tests/Integration/Services/Servers/BuildModificationServiceTest.php index e633a5d2a..7b269cac8 100644 --- a/tests/Integration/Services/Servers/BuildModificationServiceTest.php +++ b/tests/Integration/Services/Servers/BuildModificationServiceTest.php @@ -31,7 +31,7 @@ class BuildModificationServiceTest extends IntegrationTestCase * the server data is updated in realtime. This test also ensures that only certain fields get updated * for the server, and not just any arbitrary field. */ - public function testServerBuildDataIsProperlyUpdatedOndaemon(): void + public function testServerBuildDataIsProperlyUpdatedOnDaemon(): void { $server = $this->createServerModel(); diff --git a/tests/Integration/Services/Servers/ServerCreationServiceTest.php b/tests/Integration/Services/Servers/ServerCreationServiceTest.php index e6a008b83..9cedbfea2 100644 --- a/tests/Integration/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Integration/Services/Servers/ServerCreationServiceTest.php @@ -2,6 +2,7 @@ namespace App\Tests\Integration\Services\Servers; +use App\Models\Objects\Endpoint; use Mockery\MockInterface; use App\Models\Egg; use GuzzleHttp\Psr7\Request; @@ -82,6 +83,7 @@ class ServerCreationServiceTest extends IntegrationTestCase 'image' => 'java:8', 'egg_id' => $egg->id, 'ports' => [1234, 2345, 3456], + 'node_id' => $node->id, 'environment' => [ 'BUNGEE_VERSION' => '123', 'SERVER_JARFILE' => 'server2.jar', @@ -121,6 +123,12 @@ class ServerCreationServiceTest extends IntegrationTestCase continue; } + if ($key === 'ports') { + $this->assertSame($value, $response->ports->map(fn (Endpoint $endpoint) => $endpoint->port)->all()); + + continue; + } + $this->assertSame($value, $response->{$key}, "Failed asserting equality of '$key' in server response. Got: [{$response->{$key}}] Expected: [$value]"); }