fix: harden Vultr create, Gmail identity, and provider retries

Wrap Vultr server creation in DB transactions and delete the remote
instance when local persistence fails (API and Livewire). Scope
plus/dot email normalization to gmail.com/googlemail.com only.
Use throw:false on DigitalOcean/Vultr HTTP retries, and normalize
service log line counts via normalizeLogLines.
This commit is contained in:
Andras Bacsai committed 2026-07-16 13:42:53 +02:00
1 parent 2719d66042
commit 7d699818e8
15 files changed
+429 -63

No files matched your search

@@ -490,7 +490,7 @@ class ServiceApplicationsController extends Controller
], 400);
}
$lines = (int) ($request->query('lines', 100) ?: 100);
$lines = normalizeLogLines($request->query('lines'));
$logs = getContainerLogs($server, $containerName, $lines);
return response()->json([
@@ -311,7 +311,7 @@ class ServiceDatabasesController extends Controller
return response()->json(['message' => 'Service database container is not running.'], 400);
}
$lines = (int) ($request->query('lines', 100) ?: 100);
$lines = normalizeLogLines($request->query('lines'));
return response()->json([
'logs' => getContainerLogs($server, $containerName, $lines),
+62 -28
View File
@@ -15,6 +15,7 @@ use App\Rules\ValidHostname;
use App\Services\VultrService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use OpenApi\Attributes as OA;
class VultrController extends Controller
@@ -286,6 +287,10 @@ class VultrController extends Controller
return response()->json(['message' => 'Private key not found.'], 404);
}
$vultrService = null;
$vultrInstanceId = null;
$server = null;
try {
$vultrService = new VultrService($token->token);
$publicKey = $privateKey->getPublicKey();
@@ -317,33 +322,41 @@ class VultrController extends Controller
}
$vultrInstance = $vultrService->createInstance($params);
$vultrInstanceId = (string) $vultrInstance['id'];
$ipAddress = $vultrService->getPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6) ?? Server::PLACEHOLDER_IP;
$server = Server::create([
'name' => $normalizedServerName,
'ip' => $ipAddress,
'user' => 'root',
'port' => 22,
'team_id' => $teamId,
'private_key_id' => $privateKey->id,
'cloud_provider_token_id' => $token->id,
'vultr_instance_id' => $vultrInstance['id'],
'vultr_instance_status' => $vultrInstance['status'] ?? null,
]);
$vultrInstance = $vultrService->waitForPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6);
$assignedIpAddress = $vultrService->getPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6);
if ($assignedIpAddress && $assignedIpAddress !== $server->ip) {
$ipAddress = $assignedIpAddress;
$server->update([
'ip' => $assignedIpAddress,
'vultr_instance_status' => $vultrInstance['status'] ?? $server->vultr_instance_status,
$server = DB::transaction(function () use ($normalizedServerName, $ipAddress, $teamId, $privateKey, $token, $vultrInstanceId, $vultrInstance): Server {
$server = Server::create([
'name' => $normalizedServerName,
'ip' => $ipAddress,
'user' => 'root',
'port' => 22,
'team_id' => $teamId,
'private_key_id' => $privateKey->id,
'cloud_provider_token_id' => $token->id,
'vultr_instance_id' => $vultrInstanceId,
'vultr_instance_status' => $vultrInstance['status'] ?? null,
]);
}
$server->proxy->set('status', 'exited');
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
$server->save();
$server->proxy->set('status', 'exited');
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
$server->save();
return $server;
});
try {
$vultrInstance = $vultrService->waitForPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6);
$assignedIpAddress = $vultrService->getPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6);
if ($assignedIpAddress && $assignedIpAddress !== $server->ip) {
$server->update([
'ip' => $assignedIpAddress,
'vultr_instance_status' => $vultrInstance['status'] ?? $server->vultr_instance_status,
]);
}
} catch (\Throwable $e) {
report($e);
}
if ($request->instant_validate) {
ValidateServer::dispatch($server);
@@ -353,27 +366,48 @@ class VultrController extends Controller
'team_id' => $teamId,
'server_uuid' => $server->uuid,
'server_name' => $server->name,
'vultr_instance_id' => $vultrInstance['id'],
'ip' => $ipAddress,
'vultr_instance_id' => $vultrInstanceId,
'ip' => $server->ip,
]);
return response()->json([
'uuid' => $server->uuid,
'vultr_instance_id' => $vultrInstance['id'],
'ip' => $ipAddress,
'vultr_instance_id' => $vultrInstanceId,
'ip' => $server->ip,
])->setStatusCode(201);
} catch (RateLimitException $e) {
$this->deleteUntrackedInstance($vultrService, $vultrInstanceId, $server);
$response = response()->json(['message' => $e->getMessage()], 429);
if ($e->retryAfter !== null) {
$response->header('Retry-After', $e->retryAfter);
}
return $response;
} catch (\Throwable) {
} catch (\Throwable $e) {
$this->deleteUntrackedInstance($vultrService, $vultrInstanceId, $server);
logger()->error('Failed to create Vultr server', [
'error' => $e->getMessage(),
]);
return response()->json(['message' => 'Failed to create Vultr server.'], 500);
}
}
private function deleteUntrackedInstance(?VultrService $vultrService, ?string $vultrInstanceId, ?Server $server): void
{
if (! $vultrService || ! $vultrInstanceId || $server) {
return;
}
try {
$vultrService->deleteInstance($vultrInstanceId);
} catch (\Throwable $e) {
report($e);
}
}
private function findMatchingSshKey(array $sshKeys, string $publicKey): ?array
{
$normalizedPublicKey = $this->normalizePublicKey($publicKey);
+42 -18
View File
@@ -14,6 +14,7 @@ use App\Services\VultrService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
@@ -377,9 +378,8 @@ class ByVultr extends Component
return "{$providerName} API error: {$details}";
}
private function createVultrServer(string $token): array
private function createVultrServer(VultrService $vultrService): array
{
$vultrService = new VultrService($token);
$privateKey = PrivateKey::ownedByCurrentTeam()->findOrFail($this->private_key_id);
$publicKey = $privateKey->getPublicKey();
$existingKey = $this->findMatchingSshKey($vultrService->getSshKeys(), $publicKey);
@@ -419,6 +419,10 @@ class ByVultr extends Component
return null;
}
$vultrService = null;
$vultrInstanceId = null;
$server = null;
try {
$this->authorize('create', Server::class);
@@ -437,20 +441,29 @@ class ByVultr extends Component
}
$vultrService = new VultrService($this->getVultrToken());
$vultrInstance = $this->createVultrServer($this->getVultrToken());
$vultrInstance = $this->createVultrServer($vultrService);
$vultrInstanceId = (string) $vultrInstance['id'];
$ipAddress = $vultrService->getPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6) ?? Server::PLACEHOLDER_IP;
$server = Server::create([
'name' => strtolower(trim($this->server_name)),
'ip' => $ipAddress,
'user' => 'root',
'port' => 22,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
'cloud_provider_token_id' => $this->selected_token_id,
'vultr_instance_id' => $vultrInstance['id'],
'vultr_instance_status' => $vultrInstance['status'] ?? null,
]);
$server = DB::transaction(function () use ($ipAddress, $vultrInstanceId, $vultrInstance): Server {
$server = Server::create([
'name' => strtolower(trim($this->server_name)),
'ip' => $ipAddress,
'user' => 'root',
'port' => 22,
'team_id' => currentTeam()->id,
'private_key_id' => $this->private_key_id,
'cloud_provider_token_id' => $this->selected_token_id,
'vultr_instance_id' => $vultrInstanceId,
'vultr_instance_status' => $vultrInstance['status'] ?? null,
]);
$server->proxy->set('status', 'exited');
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
$server->save();
return $server;
});
try {
$vultrInstance = $vultrService->waitForPublicIp($vultrInstance, $this->disable_public_ipv4, $this->enable_ipv6);
@@ -466,10 +479,6 @@ class ByVultr extends Component
report($e);
}
$server->proxy->set('status', 'exited');
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
$server->save();
if ($this->from_onboarding) {
currentTeam()->update([
'show_boarding' => false,
@@ -479,10 +488,25 @@ class ByVultr extends Component
return redirectRoute($this, 'server.show', [$server->uuid]);
} catch (\Throwable $e) {
$this->deleteUntrackedInstance($vultrService, $vultrInstanceId, $server);
return handleError($e, $this);
}
}
private function deleteUntrackedInstance(?VultrService $vultrService, ?string $vultrInstanceId, ?Server $server): void
{
if (! $vultrService || ! $vultrInstanceId || $server) {
return;
}
try {
$vultrService->deleteInstance($vultrInstanceId);
} catch (\Throwable $e) {
report($e);
}
}
public function render()
{
return view('livewire.server.new.by-vultr');
+1 -1
View File
@@ -28,7 +28,7 @@ class DigitalOceanService
}
return $attempt * 100;
})
}, throw: false)
->{$method}($this->baseUrl.$endpoint, $data);
if (! $response->successful()) {
+1 -1
View File
@@ -17,7 +17,7 @@ class VultrService
'Authorization' => 'Bearer '.$this->token,
])
->timeout(30)
->retry(3, fn (int $attempt) => $attempt * 100)
->retry(3, fn (int $attempt) => $attempt * 100, throw: false)
->{$method}($this->baseUrl.$endpoint, $data);
if (! $response->successful()) {