mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
fix(hetzner): hide server types that are out of stock or deprecated in a location
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1242353511
commit
18a85d37ba
@@ -140,7 +140,7 @@ class HetznerController extends Controller
|
||||
|
||||
#[OA\Get(
|
||||
summary: 'Get Hetzner Server Types',
|
||||
description: 'Get all available Hetzner server types (instance sizes).',
|
||||
description: 'Get the Hetzner server types (instance sizes) that can be ordered now. Each type lists only the locations where it is in stock and not deprecated.',
|
||||
path: '/hetzner/server-types',
|
||||
operationId: 'get-hetzner-server-types',
|
||||
security: [
|
||||
@@ -148,6 +148,13 @@ class HetznerController extends Controller
|
||||
],
|
||||
tags: ['Hetzner'],
|
||||
parameters: [
|
||||
new OA\Parameter(
|
||||
name: 'location',
|
||||
in: 'query',
|
||||
required: false,
|
||||
description: 'Return only the server types that can be ordered in this location, for example nbg1.',
|
||||
schema: new OA\Schema(type: 'string')
|
||||
),
|
||||
new OA\Parameter(
|
||||
name: 'cloud_provider_token_uuid',
|
||||
in: 'query',
|
||||
@@ -231,6 +238,7 @@ class HetznerController extends Controller
|
||||
$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',
|
||||
'location' => 'nullable|string',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
@@ -253,7 +261,7 @@ class HetznerController extends Controller
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
$serverTypes = $hetznerService->getServerTypes();
|
||||
$serverTypes = $hetznerService->getServerTypes($request->query('location'));
|
||||
|
||||
return response()->json($serverTypes);
|
||||
} catch (CloudProviderApiException $e) {
|
||||
|
||||
@@ -118,16 +118,29 @@ class HetznerService
|
||||
]);
|
||||
}
|
||||
|
||||
public function getServerTypes(): array
|
||||
/**
|
||||
* Server types that can be ordered now. Each type keeps only the locations
|
||||
* where it is in stock and not deprecated, and types with no such location are removed.
|
||||
*/
|
||||
public function getServerTypes(?string $location = null): array
|
||||
{
|
||||
$types = $this->requestPaginated('get', '/server_types', 'server_types');
|
||||
|
||||
// Filter out entries where "deprecated" is explicitly true
|
||||
$filtered = array_filter($types, function ($type) {
|
||||
return ! (isset($type['deprecated']) && $type['deprecated'] === true);
|
||||
});
|
||||
return collect($types)
|
||||
->reject(fn (array $type) => ($type['deprecated'] ?? false) === true)
|
||||
->map(function (array $type) {
|
||||
$type['locations'] = collect($type['locations'] ?? [])
|
||||
->filter(fn (array $typeLocation) => ($typeLocation['available'] ?? true) === true && empty($typeLocation['deprecation']))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return array_values($filtered);
|
||||
return $type;
|
||||
})
|
||||
->filter(fn (array $type) => $location === null
|
||||
? $type['locations'] !== []
|
||||
: in_array($location, array_column($type['locations'], 'name'), true))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getSshKeys(): array
|
||||
|
||||
+10
-1
@@ -12588,9 +12588,18 @@
|
||||
"Hetzner"
|
||||
],
|
||||
"summary": "Get Hetzner Server Types",
|
||||
"description": "Get all available Hetzner server types (instance sizes).",
|
||||
"description": "Get the Hetzner server types (instance sizes) that can be ordered now. Each type lists only the locations where it is in stock and not deprecated.",
|
||||
"operationId": "get-hetzner-server-types",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "location",
|
||||
"in": "query",
|
||||
"description": "Return only the server types that can be ordered in this location, for example nbg1.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cloud_provider_token_uuid",
|
||||
"in": "query",
|
||||
|
||||
+8
-1
@@ -8129,9 +8129,16 @@ paths:
|
||||
tags:
|
||||
- Hetzner
|
||||
summary: 'Get Hetzner Server Types'
|
||||
description: 'Get all available Hetzner server types (instance sizes).'
|
||||
description: 'Get the Hetzner server types (instance sizes) that can be ordered now. Each type lists only the locations where it is in stock and not deprecated.'
|
||||
operationId: get-hetzner-server-types
|
||||
parameters:
|
||||
-
|
||||
name: location
|
||||
in: query
|
||||
description: 'Return only the server types that can be ordered in this location, for example nbg1.'
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
-
|
||||
name: cloud_provider_token_uuid
|
||||
in: query
|
||||
|
||||
@@ -127,8 +127,8 @@ describe('GET /api/v1/hetzner/server-types', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
||||
'server_types' => [
|
||||
['id' => 1, 'name' => 'cx11', 'description' => 'CX11', 'cores' => 1, 'memory' => 2.0, 'disk' => 20],
|
||||
['id' => 2, 'name' => 'cx21', 'description' => 'CX21', 'cores' => 2, 'memory' => 4.0, 'disk' => 40],
|
||||
['id' => 1, 'name' => 'cx11', 'description' => 'CX11', 'cores' => 1, 'memory' => 2.0, 'disk' => 20, 'locations' => [['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null]]],
|
||||
['id' => 2, 'name' => 'cx21', 'description' => 'CX21', 'cores' => 2, 'memory' => 4.0, 'disk' => 40, 'locations' => [['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null]]],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
@@ -148,8 +148,8 @@ describe('GET /api/v1/hetzner/server-types', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
||||
'server_types' => [
|
||||
['id' => 1, 'name' => 'cx11', 'deprecated' => false],
|
||||
['id' => 2, 'name' => 'cx21', 'deprecated' => true],
|
||||
['id' => 1, 'name' => 'cx11', 'deprecated' => false, 'locations' => [['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null]]],
|
||||
['id' => 2, 'name' => 'cx21', 'deprecated' => true, 'locations' => [['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null]]],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
@@ -165,6 +165,57 @@ describe('GET /api/v1/hetzner/server-types', function () {
|
||||
$response->assertJsonFragment(['name' => 'cx11']);
|
||||
$response->assertJsonMissing(['name' => 'cx21']);
|
||||
});
|
||||
|
||||
test('removes locations where a server type is out of stock or deprecated', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
||||
'server_types' => [
|
||||
['id' => 1, 'name' => 'cx22', 'locations' => [
|
||||
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null],
|
||||
['id' => 2, 'name' => 'fsn1', 'available' => false, 'deprecation' => null],
|
||||
['id' => 3, 'name' => 'hel1', 'available' => true, 'deprecation' => ['announced' => '2025-09-24T00:00:00+00:00', 'unavailable_after' => '2025-12-31T00:00:00+00:00']],
|
||||
]],
|
||||
['id' => 2, 'name' => 'cpx11', 'locations' => [
|
||||
['id' => 2, 'name' => 'fsn1', 'available' => false, 'deprecation' => null],
|
||||
]],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->getJson('/api/v1/hetzner/server-types?cloud_provider_token_uuid='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertStatus(200);
|
||||
expect($response->json())->toHaveCount(1)
|
||||
->and($response->json('0.name'))->toBe('cx22')
|
||||
->and(collect($response->json('0.locations'))->pluck('name')->all())->toBe(['nbg1']);
|
||||
});
|
||||
|
||||
test('returns only server types that can be ordered in the given location', function () {
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/server_types*' => Http::response([
|
||||
'server_types' => [
|
||||
['id' => 1, 'name' => 'cx22', 'locations' => [
|
||||
['id' => 1, 'name' => 'nbg1', 'available' => true, 'deprecation' => null],
|
||||
['id' => 2, 'name' => 'fsn1', 'available' => false, 'deprecation' => null],
|
||||
]],
|
||||
['id' => 2, 'name' => 'cpx11', 'locations' => [
|
||||
['id' => 2, 'name' => 'fsn1', 'available' => true, 'deprecation' => null],
|
||||
]],
|
||||
],
|
||||
'meta' => ['pagination' => ['next_page' => null]],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
])->getJson('/api/v1/hetzner/server-types?location=fsn1&cloud_provider_token_uuid='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertStatus(200);
|
||||
expect(collect($response->json())->pluck('name')->all())->toBe(['cpx11']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/hetzner/images', function () {
|
||||
|
||||
Reference in New Issue
Block a user