Replace some guzzle exceptions and fix server creation failures (#848)

* Replace guzzle exceptions

* Pint fixes

* Fix test

* Remove unused imports

* Catch & Notify the user instead of 500

* Update app/Filament/Admin/Resources/ServerResource/Pages/CreateServer.php

Co-authored-by: Boy132 <Boy132@users.noreply.github.com>

---------

Co-authored-by: RMartinOscar <40749467+RMartinOscar@users.noreply.github.com>
Co-authored-by: Boy132 <Boy132@users.noreply.github.com>
This commit is contained in:
Lance Pioch 2025-01-01 15:20:16 -05:00 committed by GitHub
parent 3a7ddfca5e
commit 133c1a511f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 81 additions and 152 deletions

View File

@ -2,8 +2,8 @@
namespace App\Exceptions\Http\Connection; namespace App\Exceptions\Http\Connection;
use Exception;
use Illuminate\Http\Response; use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException; use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context; use Illuminate\Support\Facades\Context;
@ -22,7 +22,7 @@ class DaemonConnectionException extends DisplayException
/** /**
* Throw a displayable exception caused by a daemon connection error. * Throw a displayable exception caused by a daemon connection error.
*/ */
public function __construct(GuzzleException $previous, bool $useStatusCode = true) public function __construct(?Exception $previous, bool $useStatusCode = true)
{ {
/** @var \GuzzleHttp\Psr7\Response|null $response */ /** @var \GuzzleHttp\Psr7\Response|null $response */
$response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null; $response = method_exists($previous, 'getResponse') ? $previous->getResponse() : null;

View File

@ -26,8 +26,7 @@ use Filament\Notifications\Notification;
use Filament\Pages\Concerns\InteractsWithHeaderActions; use Filament\Pages\Concerns\InteractsWithHeaderActions;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Support\Enums\MaxWidth; use Filament\Support\Enums\MaxWidth;
use GuzzleHttp\Client; use Illuminate\Http\Client\Factory;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification; use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString; use Illuminate\Support\HtmlString;
@ -164,22 +163,23 @@ class Settings extends Page implements HasForms
->label('Set to Cloudflare IPs') ->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare') ->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings')) ->authorize(fn () => auth()->user()->can('update settings'))
->action(function (Client $client, Set $set) { ->action(function (Factory $client, Set $set) {
$ips = collect(); $ips = collect();
try { try {
$response = $client->request( $response = $client
'GET', ->timeout(3)
'https://api.cloudflare.com/client/v4/ips', ->connectTimeout(3)
config('panel.guzzle') ->get('https://api.cloudflare.com/client/v4/ips');
);
if ($response->getStatusCode() === 200) { if ($response->getStatusCode() === 200) {
$result = json_decode($response->getBody(), true)['result']; $result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) { foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value)); $ips->push(...data_get($result, $value));
} }
$ips->unique(); $ips->unique();
} }
} catch (GuzzleException $e) { } catch (Exception) {
} }
$set('TRUSTED_PROXIES', $ips->values()->all()); $set('TRUSTED_PROXIES', $ips->values()->all());
@ -245,12 +245,12 @@ class Settings extends Page implements HasForms
->columnSpanFull() ->columnSpanFull()
->inline() ->inline()
->options([ ->options([
'log' => 'Print mails to Log', 'log' => '/storage/logs Directory',
'smtp' => 'SMTP Server', 'smtp' => 'SMTP Server',
'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun', 'mailgun' => 'Mailgun',
'mandrill' => 'Mandrill', 'mandrill' => 'Mandrill',
'postmark' => 'Postmark', 'postmark' => 'Postmark',
'sendmail' => 'sendmail (PHP)',
]) ])
->live() ->live()
->default(env('MAIL_MAILER', config('mail.default'))) ->default(env('MAIL_MAILER', config('mail.default')))

View File

@ -34,7 +34,9 @@ use Filament\Forms\Components\Wizard\Step;
use Filament\Forms\Form; use Filament\Forms\Form;
use Filament\Forms\Get; use Filament\Forms\Get;
use Filament\Forms\Set; use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord; use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade; use Illuminate\Support\Facades\Blade;
@ -873,7 +875,18 @@ class CreateServer extends CreateRecord
{ {
$data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all(); $data['allocation_additional'] = collect($data['allocation_additional'])->filter()->all();
return $this->serverCreationService->handle($data); try {
return $this->serverCreationService->handle($data);
} catch (Exception $exception) {
Notification::make()
->title('Could not create server')
->body($exception->getMessage())
->color('danger')
->danger()
->send();
throw new Halt();
}
} }
private function shouldHideComponent(Get $get, Component $component): bool private function shouldHideComponent(Get $get, Component $component): bool

View File

@ -5,12 +5,11 @@ namespace App\Models;
use App\Enums\ContainerStatus; use App\Enums\ContainerStatus;
use App\Enums\ServerResourceType; use App\Enums\ServerResourceType;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use Carbon\CarbonInterface; use Carbon\CarbonInterface;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Database\Query\JoinClause; use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -421,17 +420,13 @@ class Server extends Model
/** /**
* Sends a command or multiple commands to a running server instance. * Sends a command or multiple commands to a running server instance.
* *
* @throws DaemonConnectionException|GuzzleException * @throws ConnectionException
*/ */
public function send(array|string $command): ResponseInterface public function send(array|string $command): ResponseInterface
{ {
try { return Http::daemon($this->node)->post("/api/servers/{$this->uuid}/commands", [
return Http::daemon($this->node)->post("/api/servers/{$this->uuid}/commands", [ 'commands' => is_array($command) ? $command : [$command],
'commands' => is_array($command) ? $command : [$command], ])->toPsrResponse();
])->toPsrResponse();
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
public function retrieveStatus(): string public function retrieveStatus(): string

View File

@ -6,23 +6,16 @@ use App\Enums\ContainerStatus;
use App\Enums\HttpStatusCode; use App\Enums\HttpStatusCode;
use Exception; use Exception;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\RequestException;
use Webmozart\Assert\Assert;
use App\Models\Server;
use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\Http\Connection\DaemonConnectionException;
class DaemonServerRepository extends DaemonRepository class DaemonServerRepository extends DaemonRepository
{ {
/** /**
* Returns details about a server from the Daemon instance. * Returns details about a server from the Daemon instance.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/ */
public function getDetails(): array public function getDetails(): array
{ {
Assert::isInstanceOf($this->server, Server::class);
try { try {
return $this->getHttpClient()->get( return $this->getHttpClient()->get(
sprintf('/api/servers/%s', $this->server->uuid) sprintf('/api/servers/%s', $this->server->uuid)
@ -53,131 +46,85 @@ class DaemonServerRepository extends DaemonRepository
/** /**
* Creates a new server on the daemon. * Creates a new server on the daemon.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function create(bool $startOnCompletion = true): void public function create(bool $startOnCompletion = true): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()->post('/api/servers', [
'uuid' => $this->server->uuid,
try { 'start_on_completion' => $startOnCompletion,
$response = $this->getHttpClient()->post('/api/servers', [ ]);
'uuid' => $this->server->uuid,
'start_on_completion' => $startOnCompletion,
]);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
/** /**
* Triggers a server sync on daemon. * Triggers a server sync on daemon.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function sync(): void public function sync(): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
try {
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
/** /**
* Delete a server from the daemon, forcibly if passed. * Delete a server from the daemon, forcibly if passed.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function delete(): void public function delete(): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()->delete("/api/servers/{$this->server->uuid}");
try {
$this->getHttpClient()->delete('/api/servers/' . $this->server->uuid);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
/** /**
* Reinstall a server on the daemon. * Reinstall a server on the daemon.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function reinstall(): void public function reinstall(): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/reinstall");
try {
$this->getHttpClient()->post(sprintf(
'/api/servers/%s/reinstall',
$this->server->uuid
));
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
/** /**
* Requests the daemon to create a full archive of the server. Once the daemon is finished * Requests the daemon to create a full archive of the server. Once the daemon is finished
* they will send a POST request to "/api/remote/servers/{uuid}/archive" with a boolean. * they will send a POST request to "/api/remote/servers/{uuid}/archive" with a boolean.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function requestArchive(): void public function requestArchive(): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()->post("/api/servers/{$this->server->uuid}/archive");
try {
$this->getHttpClient()->post(sprintf(
'/api/servers/%s/archive',
$this->server->uuid
));
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
/** /**
* Cancels a server transfer. * Cancels a server transfer.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function cancelTransfer(): void public function cancelTransfer(): void
{ {
Assert::isInstanceOf($this->server, Server::class); $transfer = $this->server->transfer;
if (!$transfer) {
if ($transfer = $this->server->transfer) { return;
// Source node
$this->setNode($transfer->oldNode);
try {
$this->getHttpClient()->delete(sprintf(
'/api/servers/%s/transfer',
$this->server->uuid
));
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
// Destination node
$this->setNode($transfer->newNode);
try {
$this->getHttpClient()->delete('/api/transfer', [
'json' => [
'server_id' => $this->server->uuid,
'server' => [
'uuid' => $this->server->uuid,
],
],
]);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
// Source node
$this->setNode($transfer->oldNode);
$this->getHttpClient()->delete("/api/servers/{$this->server->uuid}/transfer");
// Destination node
$this->setNode($transfer->newNode);
$this->getHttpClient()->delete('/api/transfer', [
'json' => [
'server_id' => $this->server->uuid,
'server' => [
'uuid' => $this->server->uuid,
],
],
]);
} }
/** /**
@ -185,32 +132,13 @@ class DaemonServerRepository extends DaemonRepository
* make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted * make it easier to revoke tokens on the fly. This ensures that the JTI key is formatted
* correctly and avoids any costly mistakes in the codebase. * correctly and avoids any costly mistakes in the codebase.
* *
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException * @throws ConnectionException
*/ */
public function revokeUserJTI(int $id): void public function revokeUserJTI(int $id): void
{ {
Assert::isInstanceOf($this->server, Server::class); $this->getHttpClient()
->post("/api/servers/{$this->server->uuid}/ws/deny", [
$this->revokeJTIs([md5($id . $this->server->uuid)]); 'jtis' => [md5($id . $this->server->uuid)],
} ]);
/**
* Revokes an array of JWT JTI's by marking any token generated before the current time on
* the daemon instance as being invalid.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
protected function revokeJTIs(array $jtis): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()
->post(sprintf('/api/servers/%s/ws/deny', $this->server->uuid), [
'jtis' => $jtis,
]);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
} }
} }

View File

@ -4,6 +4,7 @@ namespace App\Services\Servers;
use App\Enums\ServerState; use App\Enums\ServerState;
use App\Models\ServerVariable; use App\Models\ServerVariable;
use Illuminate\Http\Client\ConnectionException;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
use App\Models\User; use App\Models\User;
@ -16,7 +17,6 @@ use App\Models\Objects\DeploymentObject;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Services\Deployment\FindViableNodesService; use App\Services\Deployment\FindViableNodesService;
use App\Services\Deployment\AllocationSelectionService; use App\Services\Deployment\AllocationSelectionService;
use App\Exceptions\Http\Connection\DaemonConnectionException;
use App\Models\Egg; use App\Models\Egg;
class ServerCreationService class ServerCreationService
@ -94,10 +94,10 @@ class ServerCreationService
}, 5); }, 5);
try { try {
$this->daemonServerRepository->setServer($server)->create( $this->daemonServerRepository
Arr::get($data, 'start_on_completion', false) ?? false ->setServer($server)
); ->create($data['start_on_completion'] ?? false);
} catch (DaemonConnectionException $exception) { } catch (ConnectionException $exception) {
$this->serverDeletionService->withForce()->handle($server); $this->serverDeletionService->withForce()->handle($server);
throw $exception; throw $exception;

View File

@ -2,22 +2,19 @@
namespace App\Tests\Integration\Services\Servers; namespace App\Tests\Integration\Services\Servers;
use Illuminate\Http\Client\ConnectionException;
use Mockery\MockInterface; use Mockery\MockInterface;
use App\Models\Egg; use App\Models\Egg;
use GuzzleHttp\Psr7\Request;
use App\Models\Node; use App\Models\Node;
use App\Models\User; use App\Models\User;
use GuzzleHttp\Psr7\Response;
use App\Models\Server; use App\Models\Server;
use App\Models\Allocation; use App\Models\Allocation;
use Illuminate\Foundation\Testing\WithFaker; use Illuminate\Foundation\Testing\WithFaker;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use App\Models\Objects\DeploymentObject; use App\Models\Objects\DeploymentObject;
use App\Tests\Integration\IntegrationTestCase; use App\Tests\Integration\IntegrationTestCase;
use App\Services\Servers\ServerCreationService; use App\Services\Servers\ServerCreationService;
use App\Repositories\Daemon\DaemonServerRepository; use App\Repositories\Daemon\DaemonServerRepository;
use App\Exceptions\Http\Connection\DaemonConnectionException;
class ServerCreationServiceTest extends IntegrationTestCase class ServerCreationServiceTest extends IntegrationTestCase
{ {
@ -181,15 +178,11 @@ class ServerCreationServiceTest extends IntegrationTestCase
], ],
]; ];
$this->daemonServerRepository->expects('setServer->create')->andThrows( $this->daemonServerRepository->expects('setServer->create')->andThrows(new ConnectionException());
new DaemonConnectionException(
new BadResponseException('Bad request', new Request('POST', '/create'), new Response(500))
)
);
$this->daemonServerRepository->expects('setServer->delete')->andReturnUndefined(); $this->daemonServerRepository->expects('setServer->delete')->andReturnUndefined();
$this->expectException(DaemonConnectionException::class); $this->expectException(ConnectionException::class);
$this->getService()->handle($data); $this->getService()->handle($data);