server creation wip
This commit is contained in:
22
app/Actions/GenerateRandomSlug.php
Normal file
22
app/Actions/GenerateRandomSlug.php
Normal 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;
|
||||
}
|
||||
}
|
||||
17
app/Actions/GetProviderService.php
Normal file
17
app/Actions/GetProviderService.php
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
app/Data/ServerProviders/CreatedServer.php
Normal file
15
app/Data/ServerProviders/CreatedServer.php
Normal 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,
|
||||
) {}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
32
resources/js/components/RadioButton.vue
Normal file
32
resources/js/components/RadioButton.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: String,
|
||||
disabled: Boolean,
|
||||
value: String,
|
||||
name: String,
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
function onChange(event) {
|
||||
console.log(event);
|
||||
emit('update:modelValue', event.target.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label
|
||||
class="relative rounded-lg border-2 border-white/20 px-3 py-1 has-[:checked]:border-white has-[:disabled]:opacity-40"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
:name="name"
|
||||
:value="value"
|
||||
class="invisible absolute inset-0"
|
||||
:disabled="disabled"
|
||||
:checked="modelValue === value"
|
||||
@change="onChange"
|
||||
/>
|
||||
<slot />
|
||||
</label>
|
||||
</template>
|
||||
@@ -1,6 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
<script setup>
|
||||
import RadioButton from '@/components/RadioButton.vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { Head, router, useForm } from '@inertiajs/vue3';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
locations: Array,
|
||||
serverTypes: Array,
|
||||
images: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
provider: 'hetzner',
|
||||
location: null,
|
||||
serverType: null,
|
||||
image: null,
|
||||
});
|
||||
|
||||
const serverProviders = [
|
||||
{
|
||||
name: 'Hetzner',
|
||||
value: 'hetzner',
|
||||
},
|
||||
{
|
||||
name: 'Digital Ocean',
|
||||
value: 'digital-ocean',
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
watch(
|
||||
() => form.provider,
|
||||
(provider) => {
|
||||
console.log(provider);
|
||||
},
|
||||
);
|
||||
|
||||
if (form.provider && !props.locations) {
|
||||
router.reload({
|
||||
only: ['locations'],
|
||||
data: {
|
||||
provider: form.provider,
|
||||
},
|
||||
async: true,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -8,7 +53,43 @@ import { Head } from '@inertiajs/vue3';
|
||||
|
||||
<AppLayout>
|
||||
<div class="flex h-full flex-1 flex-col gap-4 rounded-xl p-4">
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<RadioButton
|
||||
v-for="serverProvider in serverProviders"
|
||||
v-model="form.provider"
|
||||
:value="serverProvider.value"
|
||||
:disabled="serverProvider.disabled"
|
||||
name="server-provider"
|
||||
>
|
||||
{{ serverProvider.name }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.provider" class="flex flex-wrap gap-2">
|
||||
<RadioButton
|
||||
v-for="location in locations"
|
||||
v-model="form.location"
|
||||
:value="location.id"
|
||||
:disabled="location.disabled"
|
||||
name="location"
|
||||
>
|
||||
{{ location.city }}
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div v-if="form.location" class="grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
|
||||
<RadioButton
|
||||
v-for="serverType in serverTypes.sort((a, b) => a.cores - b.cores)"
|
||||
v-model="form.serverType"
|
||||
:value="serverType.id"
|
||||
:disabled="serverType.disabled"
|
||||
name="server-type"
|
||||
>
|
||||
<h5 class="text-lg font-semibold uppercase tracking-tight">{{ serverType.name }}</h5>
|
||||
<p class="text-sm opacity-60">{{ serverType.cores }} cores • {{ serverType.memory }} GB RAM • {{ serverType.disk }} GB disk</p>
|
||||
</RadioButton>
|
||||
</div>
|
||||
<div class="flex justify-end items-center">
|
||||
<Button @click="form.post(route('servers.store', { organisation: $page.props.organisation.id }))">Submit</Button>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
1347
resources/text/english-adjectives.txt
Normal file
1347
resources/text/english-adjectives.txt
Normal file
File diff suppressed because it is too large
Load Diff
1525
resources/text/english-nouns.txt
Normal file
1525
resources/text/english-nouns.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,9 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/', [OrganisationController::class, 'show'])->name('organisations.show');
|
||||
|
||||
Route::resource('servers', ServerController::class)
|
||||
->only('index', 'create', 'store')
|
||||
->only('index', 'show', 'create', 'store')
|
||||
->name('index', 'servers.index')
|
||||
->name('show', 'servers.show')
|
||||
->name('create', 'servers.create')
|
||||
->name('store', 'servers.store');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user