mirror of
https://github.com/tiennm99/coolify.git
synced 2026-09-02 06:20:19 +00:00
Add Vultr cloud provider integration
This commit is contained in:
@@ -37,6 +37,9 @@ class CloudProviderTokensController extends Controller
|
||||
'digitalocean' => Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->timeout(10)->get('https://api.digitalocean.com/v2/account'),
|
||||
'vultr' => Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->timeout(10)->get('https://api.vultr.com/v2/account'),
|
||||
default => null,
|
||||
};
|
||||
|
||||
@@ -82,7 +85,7 @@ class CloudProviderTokensController extends Controller
|
||||
properties: [
|
||||
'uuid' => ['type' => 'string'],
|
||||
'name' => ['type' => 'string'],
|
||||
'provider' => ['type' => 'string', 'enum' => ['hetzner', 'digitalocean']],
|
||||
'provider' => ['type' => 'string', 'enum' => ['hetzner', 'digitalocean', 'vultr']],
|
||||
'team_id' => ['type' => 'integer'],
|
||||
'servers_count' => ['type' => 'integer'],
|
||||
'created_at' => ['type' => 'string'],
|
||||
@@ -199,7 +202,7 @@ class CloudProviderTokensController extends Controller
|
||||
type: 'object',
|
||||
required: ['provider', 'token', 'name'],
|
||||
properties: [
|
||||
'provider' => ['type' => 'string', 'enum' => ['hetzner', 'digitalocean'], 'example' => 'hetzner', 'description' => 'The cloud provider.'],
|
||||
'provider' => ['type' => 'string', 'enum' => ['hetzner', 'digitalocean', 'vultr'], 'example' => 'hetzner', 'description' => 'The cloud provider.'],
|
||||
'token' => ['type' => 'string', 'example' => 'your-api-token-here', 'description' => 'The API token for the cloud provider.'],
|
||||
'name' => ['type' => 'string', 'example' => 'My Hetzner Token', 'description' => 'A friendly name for the token.'],
|
||||
],
|
||||
@@ -253,7 +256,7 @@ class CloudProviderTokensController extends Controller
|
||||
$body = $request->json()->all();
|
||||
|
||||
$validator = customApiValidator($body, [
|
||||
'provider' => 'required|string|in:hetzner,digitalocean',
|
||||
'provider' => 'required|string|in:hetzner,digitalocean,vultr',
|
||||
'token' => 'required|string',
|
||||
'name' => 'required|string|max:255',
|
||||
]);
|
||||
|
||||
@@ -850,7 +850,9 @@ class ServersController extends Controller
|
||||
false, // Don't delete from Hetzner via API
|
||||
$server->hetzner_server_id,
|
||||
$server->cloud_provider_token_id,
|
||||
$server->team_id
|
||||
$server->team_id,
|
||||
false, // Don't delete from Vultr via API
|
||||
$server->vultr_instance_id
|
||||
);
|
||||
|
||||
auditLog('api.server.deleted', [
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Actions\Server\ValidateServer;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Exceptions\RateLimitException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Rules\ValidCloudInitYaml;
|
||||
use App\Rules\ValidHostname;
|
||||
use App\Services\VultrService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class VultrController extends Controller
|
||||
{
|
||||
private function getCloudProviderTokenUuid(Request $request): ?string
|
||||
{
|
||||
return $request->cloud_provider_token_uuid ?? $request->cloud_provider_token_id;
|
||||
}
|
||||
|
||||
private function getVultrToken(Request $request): CloudProviderToken|JsonResponse
|
||||
{
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'cloud_provider_token_uuid' => 'required_without:cloud_provider_token_id|string',
|
||||
'cloud_provider_token_id' => 'required_without:cloud_provider_token_uuid|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$token = CloudProviderToken::whereTeamId($teamId)
|
||||
->whereUuid($this->getCloudProviderTokenUuid($request))
|
||||
->where('provider', 'vultr')
|
||||
->first();
|
||||
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Vultr cloud provider token not found.'], 404);
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function regions(Request $request)
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
try {
|
||||
return response()->json((new VultrService($token->token))->getRegions());
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'Failed to fetch Vultr regions.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function plans(Request $request)
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
try {
|
||||
return response()->json((new VultrService($token->token))->getPlans());
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'Failed to fetch Vultr plans.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function operatingSystems(Request $request)
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
try {
|
||||
return response()->json((new VultrService($token->token))->getOperatingSystems());
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'Failed to fetch Vultr operating systems.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function sshKeys(Request $request)
|
||||
{
|
||||
$token = $this->getVultrToken($request);
|
||||
if ($token instanceof JsonResponse) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
try {
|
||||
return response()->json((new VultrService($token->token))->getSshKeys());
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'Failed to fetch Vultr SSH keys.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function createServer(Request $request)
|
||||
{
|
||||
$allowedFields = [
|
||||
'cloud_provider_token_uuid',
|
||||
'cloud_provider_token_id',
|
||||
'region',
|
||||
'plan',
|
||||
'os_id',
|
||||
'name',
|
||||
'private_key_uuid',
|
||||
'enable_ipv6',
|
||||
'disable_public_ipv4',
|
||||
'vultr_ssh_key_ids',
|
||||
'cloud_init_script',
|
||||
'instant_validate',
|
||||
];
|
||||
|
||||
$teamId = getTeamIdFromToken();
|
||||
if (is_null($teamId)) {
|
||||
return invalidTokenResponse();
|
||||
}
|
||||
|
||||
$return = validateIncomingRequest($request);
|
||||
if ($return instanceof JsonResponse) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$validator = customApiValidator($request->all(), [
|
||||
'cloud_provider_token_uuid' => 'required_without:cloud_provider_token_id|string',
|
||||
'cloud_provider_token_id' => 'required_without:cloud_provider_token_uuid|string',
|
||||
'region' => 'required|string',
|
||||
'plan' => 'required|string',
|
||||
'os_id' => 'required|integer',
|
||||
'name' => ['nullable', 'string', 'max:253', new ValidHostname],
|
||||
'private_key_uuid' => 'required|string',
|
||||
'enable_ipv6' => 'nullable|boolean',
|
||||
'disable_public_ipv4' => 'nullable|boolean',
|
||||
'vultr_ssh_key_ids' => 'nullable|array',
|
||||
'vultr_ssh_key_ids.*' => 'string',
|
||||
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
|
||||
'instant_validate' => 'nullable|boolean',
|
||||
]);
|
||||
|
||||
$extraFields = array_diff(array_keys($request->all()), $allowedFields);
|
||||
if ($validator->fails() || ! empty($extraFields)) {
|
||||
$errors = $validator->errors();
|
||||
foreach ($extraFields as $field) {
|
||||
$errors->add($field, 'This field is not allowed.');
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => 'Validation failed.',
|
||||
'errors' => $errors,
|
||||
], 422);
|
||||
}
|
||||
|
||||
$team = Team::find($teamId);
|
||||
if (Team::serverLimitReached($team)) {
|
||||
return response()->json(['message' => 'Server limit reached for your subscription.'], 400);
|
||||
}
|
||||
|
||||
if (! $request->name) {
|
||||
$request->offsetSet('name', generate_random_name());
|
||||
}
|
||||
if (is_null($request->enable_ipv6)) {
|
||||
$request->offsetSet('enable_ipv6', true);
|
||||
}
|
||||
if (is_null($request->disable_public_ipv4)) {
|
||||
$request->offsetSet('disable_public_ipv4', false);
|
||||
}
|
||||
if (is_null($request->vultr_ssh_key_ids)) {
|
||||
$request->offsetSet('vultr_ssh_key_ids', []);
|
||||
}
|
||||
if (is_null($request->instant_validate)) {
|
||||
$request->offsetSet('instant_validate', false);
|
||||
}
|
||||
|
||||
$token = CloudProviderToken::whereTeamId($teamId)
|
||||
->whereUuid($this->getCloudProviderTokenUuid($request))
|
||||
->where('provider', 'vultr')
|
||||
->first();
|
||||
|
||||
if (! $token) {
|
||||
return response()->json(['message' => 'Vultr cloud provider token not found.'], 404);
|
||||
}
|
||||
|
||||
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
|
||||
if (! $privateKey) {
|
||||
return response()->json(['message' => 'Private key not found.'], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$vultrService = new VultrService($token->token);
|
||||
$publicKey = $privateKey->getPublicKey();
|
||||
$existingKey = $this->findMatchingSshKey($vultrService->getSshKeys(), $publicKey);
|
||||
|
||||
if ($existingKey) {
|
||||
$sshKeyId = $existingKey['id'];
|
||||
} else {
|
||||
$uploadedKey = $vultrService->uploadSshKey($privateKey->name, $publicKey);
|
||||
$sshKeyId = $uploadedKey['id'];
|
||||
}
|
||||
|
||||
$normalizedServerName = strtolower(trim($request->name));
|
||||
$sshKeys = array_values(array_unique(array_merge([$sshKeyId], $request->vultr_ssh_key_ids)));
|
||||
|
||||
$params = [
|
||||
'region' => $request->region,
|
||||
'plan' => $request->plan,
|
||||
'os_id' => $request->os_id,
|
||||
'label' => $normalizedServerName,
|
||||
'hostname' => $normalizedServerName,
|
||||
'sshkey_id' => $sshKeys,
|
||||
'enable_ipv6' => $request->enable_ipv6,
|
||||
'disable_public_ipv4' => $request->disable_public_ipv4,
|
||||
];
|
||||
|
||||
if (! empty($request->cloud_init_script)) {
|
||||
$params['user_data'] = $request->cloud_init_script;
|
||||
}
|
||||
|
||||
$vultrInstance = $vultrService->createInstance($params);
|
||||
$ipAddress = $vultrService->getPublicIp($vultrInstance, $request->disable_public_ipv4, $request->enable_ipv6) ?? '0.0.0.0';
|
||||
|
||||
$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->proxy->set('status', 'exited');
|
||||
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
|
||||
$server->save();
|
||||
|
||||
if ($request->instant_validate) {
|
||||
ValidateServer::dispatch($server);
|
||||
}
|
||||
|
||||
auditLog('api.vultr_server.created', [
|
||||
'team_id' => $teamId,
|
||||
'server_uuid' => $server->uuid,
|
||||
'server_name' => $server->name,
|
||||
'vultr_instance_id' => $vultrInstance['id'],
|
||||
'ip' => $ipAddress,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'uuid' => $server->uuid,
|
||||
'vultr_instance_id' => $vultrInstance['id'],
|
||||
'ip' => $ipAddress,
|
||||
])->setStatusCode(201);
|
||||
} catch (RateLimitException $e) {
|
||||
$response = response()->json(['message' => $e->getMessage()], 429);
|
||||
if ($e->retryAfter !== null) {
|
||||
$response->header('Retry-After', $e->retryAfter);
|
||||
}
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable) {
|
||||
return response()->json(['message' => 'Failed to create Vultr server.'], 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function findMatchingSshKey(array $sshKeys, string $publicKey): ?array
|
||||
{
|
||||
$normalizedPublicKey = $this->normalizePublicKey($publicKey);
|
||||
|
||||
foreach ($sshKeys as $sshKey) {
|
||||
if ($this->normalizePublicKey($sshKey['ssh_key'] ?? '') === $normalizedPublicKey) {
|
||||
return $sshKey;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function normalizePublicKey(string $publicKey): string
|
||||
{
|
||||
$parts = preg_split('/\s+/', trim($publicKey));
|
||||
|
||||
return implode(' ', array_slice($parts ?: [], 0, 2));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user