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;
use Exception;
use Illuminate\Http\Response;
use GuzzleHttp\Exception\GuzzleException;
use App\Exceptions\DisplayException;
use Illuminate\Support\Facades\Context;
@ -22,7 +22,7 @@ class DaemonConnectionException extends DisplayException
/**
* 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 */
$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\Page;
use Filament\Support\Enums\MaxWidth;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Illuminate\Http\Client\Factory;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Notification as MailNotification;
use Illuminate\Support\HtmlString;
@ -164,22 +163,23 @@ class Settings extends Page implements HasForms
->label('Set to Cloudflare IPs')
->icon('tabler-brand-cloudflare')
->authorize(fn () => auth()->user()->can('update settings'))
->action(function (Client $client, Set $set) {
->action(function (Factory $client, Set $set) {
$ips = collect();
try {
$response = $client->request(
'GET',
'https://api.cloudflare.com/client/v4/ips',
config('panel.guzzle')
);
$response = $client
->timeout(3)
->connectTimeout(3)
->get('https://api.cloudflare.com/client/v4/ips');
if ($response->getStatusCode() === 200) {
$result = json_decode($response->getBody(), true)['result'];
$result = $response->json('result');
foreach (['ipv4_cidrs', 'ipv6_cidrs'] as $value) {
$ips->push(...data_get($result, $value));
}
$ips->unique();
}
} catch (GuzzleException $e) {
} catch (Exception) {
}
$set('TRUSTED_PROXIES', $ips->values()->all());
@ -245,12 +245,12 @@ class Settings extends Page implements HasForms
->columnSpanFull()
->inline()
->options([
'log' => 'Print mails to Log',
'log' => '/storage/logs Directory',
'smtp' => 'SMTP Server',
'sendmail' => 'sendmail Binary',
'mailgun' => 'Mailgun',
'mandrill' => 'Mandrill',
'postmark' => 'Postmark',
'sendmail' => 'sendmail (PHP)',
])
->live()
->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\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
use Filament\Resources\Pages\CreateRecord;
use Filament\Support\Exceptions\Halt;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Blade;
@ -873,7 +875,18 @@ class CreateServer extends CreateRecord
{
$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

View File

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

View File

@ -6,23 +6,16 @@ use App\Enums\ContainerStatus;
use App\Enums\HttpStatusCode;
use Exception;
use Filament\Notifications\Notification;
use Illuminate\Http\Client\ConnectionException;
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
{
/**
* Returns details about a server from the Daemon instance.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
*/
public function getDetails(): array
{
Assert::isInstanceOf($this->server, Server::class);
try {
return $this->getHttpClient()->get(
sprintf('/api/servers/%s', $this->server->uuid)
@ -53,131 +46,85 @@ class DaemonServerRepository extends DaemonRepository
/**
* Creates a new server on the daemon.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function create(bool $startOnCompletion = true): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$response = $this->getHttpClient()->post('/api/servers', [
'uuid' => $this->server->uuid,
'start_on_completion' => $startOnCompletion,
]);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
$this->getHttpClient()->post('/api/servers', [
'uuid' => $this->server->uuid,
'start_on_completion' => $startOnCompletion,
]);
}
/**
* Triggers a server sync on daemon.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function sync(): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/sync");
}
/**
* Delete a server from the daemon, forcibly if passed.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function delete(): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->delete('/api/servers/' . $this->server->uuid);
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
$this->getHttpClient()->delete("/api/servers/{$this->server->uuid}");
}
/**
* Reinstall a server on the daemon.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function reinstall(): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->post(sprintf(
'/api/servers/%s/reinstall',
$this->server->uuid
));
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/reinstall");
}
/**
* 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.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function requestArchive(): void
{
Assert::isInstanceOf($this->server, Server::class);
try {
$this->getHttpClient()->post(sprintf(
'/api/servers/%s/archive',
$this->server->uuid
));
} catch (GuzzleException $exception) {
throw new DaemonConnectionException($exception);
}
$this->getHttpClient()->post("/api/servers/{$this->server->uuid}/archive");
}
/**
* Cancels a server transfer.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function cancelTransfer(): void
{
Assert::isInstanceOf($this->server, Server::class);
if ($transfer = $this->server->transfer) {
// 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);
}
$transfer = $this->server->transfer;
if (!$transfer) {
return;
}
// 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
* correctly and avoids any costly mistakes in the codebase.
*
* @throws \App\Exceptions\Http\Connection\DaemonConnectionException
* @throws ConnectionException
*/
public function revokeUserJTI(int $id): void
{
Assert::isInstanceOf($this->server, Server::class);
$this->revokeJTIs([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);
}
$this->getHttpClient()
->post("/api/servers/{$this->server->uuid}/ws/deny", [
'jtis' => [md5($id . $this->server->uuid)],
]);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Services\Servers;
use App\Enums\ServerState;
use App\Models\ServerVariable;
use Illuminate\Http\Client\ConnectionException;
use Ramsey\Uuid\Uuid;
use Illuminate\Support\Arr;
use App\Models\User;
@ -16,7 +17,6 @@ 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
@ -94,10 +94,10 @@ class ServerCreationService
}, 5);
try {
$this->daemonServerRepository->setServer($server)->create(
Arr::get($data, 'start_on_completion', false) ?? false
);
} catch (DaemonConnectionException $exception) {
$this->daemonServerRepository
->setServer($server)
->create($data['start_on_completion'] ?? false);
} catch (ConnectionException $exception) {
$this->serverDeletionService->withForce()->handle($server);
throw $exception;

View File

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