From cc79aa9fbf69ed9654ea56149b21cdd12f1a7200 Mon Sep 17 00:00:00 2001 From: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> Date: Sun, 17 Nov 2024 00:01:53 +0100 Subject: [PATCH] load AWS regions and plans dynamically via SDK (#366) --- app/Actions/Server/CreateServer.php | 8 +- app/Models/ServerProvider.php | 3 +- app/ServerProviders/AWS.php | 135 +++++++++++++++++++++++----- config/core.php | 5 ++ 4 files changed, 121 insertions(+), 30 deletions(-) diff --git a/app/Actions/Server/CreateServer.php b/app/Actions/Server/CreateServer.php index 20c2173..e292668 100755 --- a/app/Actions/Server/CreateServer.php +++ b/app/Actions/Server/CreateServer.php @@ -15,7 +15,6 @@ use Exception; use Illuminate\Database\Query\Builder; use Illuminate\Support\Facades\Bus; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Illuminate\Validation\Rule; @@ -45,7 +44,6 @@ public function create(User $creator, Project $project, array $input): Server 'progress_step' => 'Initializing', ]); - DB::beginTransaction(); try { if ($server->provider != 'custom') { $server->provider_id = $input['server_provider']; @@ -70,12 +68,10 @@ public function create(User $creator, Project $project, array $input): Server // install server $this->install($server); - DB::commit(); - return $server; } catch (Exception $e) { - $server->provider()->delete(); - DB::rollBack(); + $server->delete(); + throw ValidationException::withMessages([ 'provider' => $e->getMessage(), ]); diff --git a/app/Models/ServerProvider.php b/app/Models/ServerProvider.php index c4de2f4..66c4341 100644 --- a/app/Models/ServerProvider.php +++ b/app/Models/ServerProvider.php @@ -79,6 +79,7 @@ public static function regions(?int $id): array if (! $id) { return []; } + /** @var ?ServerProvider $profile */ $profile = self::find($id); if (! $profile) { return []; @@ -109,7 +110,7 @@ public static function plans(?int $id, ?string $region): array } $plans = $profile->provider()->plans($region); - Cache::put('plans-'.$id.'-'.$region, $plans, 600); + Cache::put('plans-'.$id.'-'.$region, $plans, 60); return $plans; } diff --git a/app/ServerProviders/AWS.php b/app/ServerProviders/AWS.php index 306d986..27f2168 100755 --- a/app/ServerProviders/AWS.php +++ b/app/ServerProviders/AWS.php @@ -20,21 +20,10 @@ class AWS extends AbstractProvider public function createRules(array $input): array { - $rules = []; - // plans - $plans = []; - foreach (config('serverproviders.aws.plans') as $plan) { - $plans[] = $plan['value']; - } - $rules['plan'] = 'required|in:'.implode(',', $plans); - // regions - $regions = []; - foreach (config('serverproviders.aws.regions') as $region) { - $regions[] = $region['value']; - } - $rules['region'] = 'required|in:'.implode(',', $regions); - - return $rules; + return [ + 'plan' => ['required'], + 'region' => ['required'], + ]; } public function credentialValidationRules(array $input): array @@ -78,18 +67,65 @@ public function connect(?array $credentials = null): bool public function plans(?string $region): array { - return collect(config('serverproviders.aws.plans')) - ->mapWithKeys(fn ($value) => [$value['value'] => $value['title']]) + $this->connectToEc2Client($region); + + $nextToken = null; + $plans = []; + + do { + $params = [ + 'Filters' => [ + [ + 'Name' => 'processor-info.supported-architecture', + 'Values' => ['x86_64', 'arm64'], // Include both x86_64 and ARM64 + ], + [ + 'Name' => 'current-generation', + 'Values' => ['true'], + ], + [ + 'Name' => 'supported-virtualization-type', + 'Values' => ['hvm'], // Ubuntu AMIs require HVM + ], + [ + 'Name' => 'bare-metal', + 'Values' => ['false'], // Skip bare-metal unless explicitly needed + ], + ], + ]; + + if ($nextToken) { + $params['NextToken'] = $nextToken; + } + + $result = $this->ec2Client->describeInstanceTypes($params); + + $plans = array_merge($plans, $result->get('InstanceTypes')); + + $nextToken = $result->get('NextToken'); + } while ($nextToken); + + return collect($plans) + ->mapWithKeys(fn ($value) => [ + $value['InstanceType'] => $value['InstanceType'].' - '.$value['VCpuInfo']['DefaultVCpus'].' vCPUs, '.$value['MemoryInfo']['SizeInMiB'].' MB RAM', + ]) ->toArray(); } public function regions(): array { - return collect(config('serverproviders.aws.regions')) - ->mapWithKeys(fn ($value) => [$value['value'] => $value['title']]) + $this->connectToEc2Client(); + + $regions = $this->ec2Client->describeRegions(); + + return collect($regions->toArray()['Regions'] ?? []) + ->mapWithKeys(fn ($value) => [$value['RegionName'] => $value['RegionName']]) ->toArray(); } + /** + * @throws Exception + */ public function create(): void { $this->connectToEc2Client(); @@ -136,12 +172,16 @@ public function delete(): void } } - private function connectToEc2Client(): void + private function connectToEc2Client(?string $region = null): void { - $credentials = $this->server->serverProvider->getCredentials(); + $credentials = $this->serverProvider->getCredentials(); + + if (! $region) { + $region = $this->server?->provider_data['region']; + } $this->ec2Client = new Ec2Client([ - 'region' => $this->server->provider_data['region'], + 'region' => $region ?? config('serverproviders.aws.regions')[0]['value'], 'version' => '2016-11-15', 'credentials' => [ 'key' => $credentials['key'], @@ -202,11 +242,14 @@ private function createSecurityGroup(): void ]); } + /** + * @throws Exception + */ private function runInstance(): void { $keyName = $groupName = $this->server->name.'-'.$this->server->id; $result = $this->ec2Client->runInstances([ - 'ImageId' => config('serverproviders.aws.images.'.$this->server->provider_data['region'].'.'.$this->server->os), + 'ImageId' => $this->getImageId($this->server->os), 'MinCount' => 1, 'MaxCount' => 1, 'InstanceType' => $this->server->provider_data['plan'], @@ -220,4 +263,50 @@ private function runInstance(): void $this->server->provider_data = $providerData; $this->server->save(); } + + /** + * @throws Exception + */ + public function getImageId(string $os): string + { + $this->connectToEc2Client(); + + $version = config('core.operating_system_versions.'.$os); + + ds($version); + + $result = $this->ec2Client->describeImages([ + 'Filters' => [ + [ + 'Name' => 'name', + 'Values' => ['ubuntu/images/*-'.$version.'-amd64-server-*'], + ], + [ + 'Name' => 'state', + 'Values' => ['available'], + ], + [ + 'Name' => 'virtualization-type', + 'Values' => ['hvm'], + ], + ], + 'Owners' => ['099720109477'], + ]); + + // Extract and display image information + $images = $result->get('Images'); + + ds($images); + + if (! empty($images)) { + // Sort images by creation date to get the latest one + usort($images, function ($a, $b) { + return strtotime($b['CreationDate']) - strtotime($a['CreationDate']); + }); + + return $images[0]['ImageId']; + } + + throw new Exception('Could not find image ID'); + } } diff --git a/config/core.php b/config/core.php index 32a50c3..9ab9d90 100755 --- a/config/core.php +++ b/config/core.php @@ -18,6 +18,11 @@ \App\Enums\OperatingSystem::UBUNTU22, \App\Enums\OperatingSystem::UBUNTU24, ], + 'operating_system_versions' => [ + \App\Enums\OperatingSystem::UBUNTU20 => '20.04', + \App\Enums\OperatingSystem::UBUNTU22 => '22.04', + \App\Enums\OperatingSystem::UBUNTU24 => '24.04', + ], 'webservers' => [ \App\Enums\Webserver::NONE, \App\Enums\Webserver::NGINX,