server creation wip

This commit is contained in:
2025-03-28 17:10:36 +00:00
parent 7d2bc3ca5e
commit 350cf6e240
16 changed files with 3180 additions and 30 deletions

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Actions;
class GenerateRandomSlug
{
public function execute($adjectiveCount = 1): string
{
$adjectives = explode("\n", file_get_contents(resource_path('text/english-adjectives.txt')));
$nouns = explode("\n", file_get_contents(resource_path('text/english-nouns.txt')));
$slug = '';
for ($i = 0; $i < $adjectiveCount; $i++) {
$slug .= $adjectives[array_rand($adjectives)] . '-';
}
$slug .= $nouns[array_rand($nouns)];
return $slug;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Actions;
use App\Services\ServerProviders\HetznerService;
use App\Services\ServerProviders\ServerProviderService;
class GetProviderService
{
public function execute(string $provider): ServerProviderService|null
{
return match ($provider) {
'hetzner' => new HetznerService(),
default => null,
};
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Data\ServerProviders;
class CreatedServer
{
public function __construct(
public string $name,
public string $rootPassword,
public string $id,
public string $status,
public string $ipv4,
public string $ipv6,
) {}
}

View File

@@ -7,7 +7,7 @@ class ServerType
/**
* @param string $name The name of the server type
* @param int $cores The number of cores
* @param int $memory The amount of memory in MB
* @param int $memory The amount of memory in GB
* @param int $disk The amount of disk space in GB
*/
public function __construct(

View File

@@ -2,16 +2,100 @@
namespace App\Http\Controllers;
use App\Actions\GenerateRandomSlug;
use App\Actions\GetProviderService;
use App\Enums\ServerProvider;
use App\Enums\ServerStatus;
use App\Models\Organisation;
use App\Services\ServerProviders\HetznerService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use NunoMaduro\Collision\Provider;
class ServerController extends Controller
{
public function index(Request $request)
{
$organisation = Organisation::findOrFail($request->route('organisation'));
return inertia('servers/Index', [
'servers' => $organisation->servers()->paginate(30),
]);
}
public function create(Request $request)
{
$locations = null;
$serverTypes = null;
$images = null;
if ($request->has('provider')) {
$providerService = app(GetProviderService::class)->execute($request->provider);
if ($providerService) {
$locations = Cache::remember($request->provider . '.locations', now()->addHour(), function () use ($providerService) {
return $providerService->getLocations();
});
$serverTypes = Cache::remember($request->provider . '.serverTypes', now()->addHour(), function () use ($providerService) {
return $providerService->getServerTypes();
});
$images = Cache::remember($request->provider . '.images', now()->addHour(), function () use ($providerService) {
return $providerService->getImages();
});
}
}
return inertia('servers/Create', [
'locations' => $locations,
'serverTypes' => $serverTypes,
'images' => $images,
]);
}
public function store(Request $request)
{
$rootPassword = Str::random(32);
$providerService = app(GetProviderService::class)->execute($request->provider);
if (!$providerService) {
return back()->with('error', 'Invalid provider');
}
$createdServer = $providerService->createServer(
name: app(GenerateRandomSlug::class)->execute(), // @todo allow custom name
serverType: $request->server_type,
location: $request->location,
image: $request->image,
rootPassword: $rootPassword,
);
$organisation = Organisation::findOrFail($request->route('organisation'));
$server = $organisation->servers()->create([
'name' => $createdServer->name,
'provider' => ServerProvider::tryFrom($request->provider),
'provider_id' => $createdServer->id,
'ipv4' => $createdServer->ipv4,
'ipv6' => $createdServer->ipv6,
'provider_status' => $createdServer->status,
'status' => ServerStatus::PENDING,
'region' => $request->location,
'os' => $request->image,
'plan' => $request->server_type,
'user' => '',
]);
return redirect()->route('servers.show', ['organisation' => $organisation->id, 'server' => $server->id]);
}
public function show(Request $request)
{
$organisation = Organisation::findOrFail($request->route('organisation'));
$server = $organisation->servers()->findOrFail($request->route('server'));
return inertia('servers/Show', [
'server' => $server,
]);
}
}

View File

@@ -6,7 +6,7 @@ use Saloon\Enums\Method;
use Saloon\Http\Request;
use Saloon\Traits\Body\HasJsonBody;
class ListImagesRequest extends Request
class GetImagesRequest extends Request
{
protected Method $method = Method::GET;

View File

@@ -5,7 +5,7 @@ namespace App\Http\Integrations\Requests\Hetzner\Locations;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class ListLocationsRequest extends Request
class GetLocationsRequest extends Request
{
protected Method $method = Method::GET;

View File

@@ -5,7 +5,7 @@ namespace App\Http\Integrations\Requests\Hetzner\ServerTypes;
use Saloon\Enums\Method;
use Saloon\Http\Request;
class ListServerTypesRequest extends Request
class GetServerTypesRequest extends Request
{
protected Method $method = Method::GET;

View File

@@ -18,6 +18,7 @@ class CreateServerRequest extends Request implements HasBody
protected ?string $name = null,
protected ?string $serverType = null,
protected ?string $location = null,
protected ?string $rootPassword = null,
) {}
protected function defaultBody(): array
@@ -27,6 +28,7 @@ class CreateServerRequest extends Request implements HasBody
'name' => $this->name,
'server_type' => $this->serverType,
'location' => $this->location,
'root_password' => $this->rootPassword,
];
}

View File

@@ -2,17 +2,19 @@
namespace App\Services\ServerProviders;
use App\Data\ServerProviders\CreatedServer;
use App\Data\ServerProviders\Image;
use App\Data\ServerProviders\Location;
use App\Data\ServerProviders\ServerType;
use App\Http\Integrations\Connectors\HetznerConnector;
use App\Http\Integrations\Requests\Hetzner\Images\ListImagesRequest;
use App\Http\Integrations\Requests\Hetzner\Locations\ListLocationsRequest;
use App\Http\Integrations\Requests\Hetzner\ServerTypes\ListServerTypesRequest;
use App\Http\Integrations\Requests\Hetzner\Images\GetImagesRequest;
use App\Http\Integrations\Requests\Hetzner\Locations\GetLocationsRequest;
use App\Http\Integrations\Requests\Hetzner\Servers\CreateServerRequest;
use App\Http\Integrations\Requests\Hetzner\ServerTypes\GetServerTypesRequest;
use Exception;
use Illuminate\Support\Collection;
class HetznerService implements ServerProviderService
class HetznerService extends ServerProviderService
{
public function __construct()
{
@@ -24,13 +26,33 @@ class HetznerService implements ServerProviderService
string $serverType,
string $location,
string $image,
): bool {
return false;
string $rootPassword,
): CreatedServer {
$response = $this->connector->send(new CreateServerRequest(
image: $image,
name: $name,
serverType: $serverType,
location: $location,
rootPassword: $rootPassword,
));
if ($response->status() !== 201) {
throw new Exception('Failed to create server on Hetzner');
}
return new CreatedServer(
id: $response->json('server.id'),
name: $name,
rootPassword: $rootPassword,
status: $response->json('server.status')['status'],
ipv4: $response->json('server.public_net.ipv4.ip'),
ipv6: $response->json('server.public_net.ipv6.ip'),
);
}
public function listServerTypes(): Collection
public function getServerTypes(): Collection
{
$response = $this->connector->send(new ListServerTypesRequest);
$response = $this->connector->send(new GetServerTypesRequest);
if ($response->status() !== 200) {
throw new Exception('Failed to fetch server types from Hetzner');
@@ -41,17 +63,17 @@ class HetznerService implements ServerProviderService
id: $serverType['id'],
name: $serverType['name'],
cores: $serverType['cores'],
memory: $serverType['memory'] * 1024,
memory: $serverType['memory'],
disk: $serverType['disk'],
priceMonthly: $serverType['prices'][0]['monthly']['gross'] ?? 0,
priceHourly: $serverType['prices'][0]['hourly']['gross'] ?? 0,
);
});
})->values();
}
public function listLocations(): Collection
public function getLocations(): Collection
{
$response = $this->connector->send(new ListLocationsRequest);
$response = $this->connector->send(new GetLocationsRequest);
if ($response->status() !== 200) {
throw new Exception('Failed to fetch locations from Hetzner');
@@ -64,12 +86,12 @@ class HetznerService implements ServerProviderService
country: $location['country'],
city: $location['city'],
);
});
})->values();
}
public function listImages(): Collection
public function getImages(): Collection
{
$response = $this->connector->send(new ListImagesRequest(
$response = $this->connector->send(new GetImagesRequest(
architecture: 'x86',
));
@@ -84,6 +106,6 @@ class HetznerService implements ServerProviderService
osFlavor: $image['os_flavor'],
osVersion: $image['os_version'],
);
});
})->values();
}
}

View File

@@ -2,23 +2,25 @@
namespace App\Services\ServerProviders;
use App\Data\ServerProviders\CreatedServer;
use Illuminate\Support\Collection;
use Saloon\Http\Connector;
interface ServerProviderService
abstract class ServerProviderService
{
protected Connector $connector;
public function createServer(
abstract public function createServer(
string $name,
string $serverType,
string $location,
string $image,
): bool;
string $rootPassword,
): CreatedServer;
public function listServerTypes(): Collection;
abstract public function getServerTypes(): Collection;
public function listLocations(): Collection;
abstract public function getLocations(): Collection;
public function listImages(): Collection;
abstract public function getImages(): Collection;
}