From c599112021a9090a40e893329889c58ba6d3f664 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 30 Jan 2018 20:36:59 -0600 Subject: [PATCH] Finalize server management API --- .../Connection/DaemonConnectionException.php | 3 +- .../Application/Servers/StartupController.php | 49 +++++++++++++++++ .../Servers/UpdateServerStartupRequest.php | 55 +++++++++++++++++++ .../Servers/StartupModificationService.php | 29 ++++++++-- routes/api-application.php | 1 + .../StartupModificationServiceTest.php | 43 +++++++++++---- 6 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 app/Http/Controllers/Api/Application/Servers/StartupController.php create mode 100644 app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php diff --git a/app/Exceptions/Http/Connection/DaemonConnectionException.php b/app/Exceptions/Http/Connection/DaemonConnectionException.php index d6e0ed724..f2892789c 100644 --- a/app/Exceptions/Http/Connection/DaemonConnectionException.php +++ b/app/Exceptions/Http/Connection/DaemonConnectionException.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Exceptions\Http\Connection; +use Illuminate\Http\Response; use GuzzleHttp\Exception\GuzzleException; use Pterodactyl\Exceptions\DisplayException; @@ -10,7 +11,7 @@ class DaemonConnectionException extends DisplayException /** * @var int */ - private $statusCode = 500; + private $statusCode = Response::HTTP_GATEWAY_TIMEOUT; /** * Throw a displayable exception caused by a daemon connection error. diff --git a/app/Http/Controllers/Api/Application/Servers/StartupController.php b/app/Http/Controllers/Api/Application/Servers/StartupController.php new file mode 100644 index 000000000..e6b8015d8 --- /dev/null +++ b/app/Http/Controllers/Api/Application/Servers/StartupController.php @@ -0,0 +1,49 @@ +modificationService = $modificationService; + } + + /** + * Update the startup and environment settings for a specific server. + * + * @param \Pterodactyl\Http\Requests\Api\Application\Servers\UpdateServerStartupRequest $request + * @return array + * + * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function index(UpdateServerStartupRequest $request): array + { + $server = $this->modificationService->handle($request->getModel(Server::class), $request->validated()); + + return $this->fractal->item($server) + ->transformWith($this->getTransformer(ServerTransformer::class)) + ->toArray(); + } +} diff --git a/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php new file mode 100644 index 000000000..d337cb4dd --- /dev/null +++ b/app/Http/Requests/Api/Application/Servers/UpdateServerStartupRequest.php @@ -0,0 +1,55 @@ +getModel(Server::class)->id); + + return [ + 'startup' => $data['startup'], + 'environment' => 'present|array', + 'egg' => $data['egg_id'], + 'pack' => $data['pack_id'], + 'image' => $data['image'], + 'skip_scripts' => 'present|boolean', + ]; + } + + /** + * Return the validated data in a format that is expected by the service. + * + * @return array + */ + public function validated() + { + $data = parent::validated(); + + return collect($data)->only(['startup', 'environment', 'skip_scripts'])->merge([ + 'egg_id' => array_get($data, 'egg'), + 'pack_id' => array_get($data, 'pack'), + 'docker_image' => array_get($data, 'image'), + ])->toArray(); + } +} diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 76a10ad0d..4e954ae1f 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -7,6 +7,7 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Traits\Services\HasUserLevels; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -26,6 +27,11 @@ class StartupModificationService */ private $connection; + /** + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface + */ + private $eggRepository; + /** * @var \Pterodactyl\Services\Servers\EnvironmentService */ @@ -51,6 +57,7 @@ class StartupModificationService * * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\EggRepositoryInterface $eggRepository * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository @@ -59,6 +66,7 @@ class StartupModificationService public function __construct( ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, + EggRepositoryInterface $eggRepository, EnvironmentService $environmentService, ServerRepositoryInterface $repository, ServerVariableRepositoryInterface $serverVariableRepository, @@ -66,6 +74,7 @@ class StartupModificationService ) { $this->daemonServerRepository = $daemonServerRepository; $this->connection = $connection; + $this->eggRepository = $eggRepository; $this->environmentService = $environmentService; $this->repository = $repository; $this->serverVariableRepository = $serverVariableRepository; @@ -77,13 +86,14 @@ class StartupModificationService * * @param \Pterodactyl\Models\Server $server * @param array $data + * @return \Pterodactyl\Models\Server * * @throws \Illuminate\Validation\ValidationException + * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ - public function handle(Server $server, array $data) + public function handle(Server $server, array $data): Server { $this->connection->beginTransaction(); if (! is_null(array_get($data, 'environment'))) { @@ -119,6 +129,8 @@ class StartupModificationService } $this->connection->commit(); + + return $server; } /** @@ -133,13 +145,22 @@ class StartupModificationService */ private function updateAdministrativeSettings(array $data, Server &$server, array &$daemonData) { + if ( + is_digit(array_get($data, 'egg_id')) + && $data['egg_id'] != $server->egg_id + && is_null(array_get($data, 'nest_id')) + ) { + $egg = $this->eggRepository->setColumns(['id', 'nest_id'])->find($data['egg_id']); + $data['nest_id'] = $egg->nest_id; + } + $server = $this->repository->update($server->id, [ 'installed' => 0, 'startup' => array_get($data, 'startup', $server->startup), 'nest_id' => array_get($data, 'nest_id', $server->nest_id), 'egg_id' => array_get($data, 'egg_id', $server->egg_id), 'pack_id' => array_get($data, 'pack_id', $server->pack_id) > 0 ? array_get($data, 'pack_id', $server->pack_id) : null, - 'skip_scripts' => isset($data['skip_scripts']), + 'skip_scripts' => array_get($data, 'skip_scripts') ?? isset($data['skip_scripts']), 'image' => array_get($data, 'docker_image', $server->image), ]); @@ -147,7 +168,7 @@ class StartupModificationService 'build' => ['image' => $server->image], 'service' => array_merge( $this->repository->getDaemonServiceData($server, true), - ['skip_scripts' => isset($data['skip_scripts'])] + ['skip_scripts' => $server->skip_scripts] ), ]); } diff --git a/routes/api-application.php b/routes/api-application.php index 35cda6fe8..049ba391b 100644 --- a/routes/api-application.php +++ b/routes/api-application.php @@ -76,6 +76,7 @@ Route::group(['prefix' => '/servers'], function () { Route::patch('/{server}/details', 'Servers\ServerDetailsController@details')->name('api.application.servers.details'); Route::patch('/{server}/build', 'Servers\ServerDetailsController@build')->name('api.application.servers.build'); + Route::patch('/{server}/startup', 'Servers\StartupController@index')->name('api.application.servers.startup'); Route::post('/', 'Servers\ServerController@store'); Route::post('/{server}/suspend', 'Servers\ServerManagementController@suspend')->name('api.application.servers.suspend'); diff --git a/tests/Unit/Services/Servers/StartupModificationServiceTest.php b/tests/Unit/Services/Servers/StartupModificationServiceTest.php index fe8392cfe..99453e515 100644 --- a/tests/Unit/Services/Servers/StartupModificationServiceTest.php +++ b/tests/Unit/Services/Servers/StartupModificationServiceTest.php @@ -1,22 +1,17 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; +use Pterodactyl\Models\Egg; use Pterodactyl\Models\User; use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Contracts\Repository\EggRepositoryInterface; use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; @@ -34,6 +29,11 @@ class StartupModificationServiceTest extends TestCase */ private $connection; + /** + * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock + */ + private $eggRepository; + /** * @var \Pterodactyl\Services\Servers\EnvironmentService|\Mockery\Mock */ @@ -63,6 +63,7 @@ class StartupModificationServiceTest extends TestCase $this->daemonServerRepository = m::mock(DaemonServerRepository::class); $this->connection = m::mock(ConnectionInterface::class); + $this->eggRepository = m::mock(EggRepositoryInterface::class); $this->environmentService = m::mock(EnvironmentService::class); $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); @@ -96,8 +97,10 @@ class StartupModificationServiceTest extends TestCase $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); - $this->assertTrue(true); + $response = $this->getService()->handle($model, ['egg_id' => 123, 'environment' => ['test' => 'abcd1234']]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame($model, $response); } /** @@ -110,6 +113,11 @@ class StartupModificationServiceTest extends TestCase 'image' => 'docker:image', ]); + $eggModel = factory(Egg::class)->make([ + 'id' => 456, + 'nest_id' => 12345, + ]); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->validatorService->shouldReceive('setUserLevel')->with(User::USER_LEVEL_ADMIN)->once()->andReturnNull(); $this->validatorService->shouldReceive('handle')->with(456, ['test' => 'abcd1234'])->once()->andReturn( @@ -122,9 +130,12 @@ class StartupModificationServiceTest extends TestCase 'variable_id' => 1, ], ['variable_value' => 'stored-value'])->once()->andReturnNull(); + $this->eggRepository->shouldReceive('setColumns->find')->once()->with($eggModel->id)->andReturn($eggModel); + $this->repository->shouldReceive('update')->with($model->id, m::subset([ 'installed' => 0, - 'egg_id' => 456, + 'nest_id' => $eggModel->nest_id, + 'egg_id' => $eggModel->id, 'pack_id' => 789, 'image' => 'docker:image', ]))->once()->andReturn($model); @@ -152,8 +163,15 @@ class StartupModificationServiceTest extends TestCase $service = $this->getService(); $service->setUserLevel(User::USER_LEVEL_ADMIN); - $service->handle($model, ['docker_image' => 'docker:image', 'egg_id' => 456, 'pack_id' => 789, 'environment' => ['test' => 'abcd1234']]); - $this->assertTrue(true); + $response = $service->handle($model, [ + 'docker_image' => 'docker:image', + 'egg_id' => $eggModel->id, + 'pack_id' => 789, + 'environment' => ['test' => 'abcd1234'], + ]); + + $this->assertInstanceOf(Server::class, $response); + $this->assertSame($model, $response); } /** @@ -166,6 +184,7 @@ class StartupModificationServiceTest extends TestCase return new StartupModificationService( $this->connection, $this->daemonServerRepository, + $this->eggRepository, $this->environmentService, $this->repository, $this->serverVariableRepository,