Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 213 additions & 0 deletions app/Http/Controllers/Api/HetznerController.php
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,193 @@ public function sshKeys(Request $request)
}
}

#[OA\Get(
summary: 'Get Hetzner Firewalls',
description: 'Get all existing Hetzner firewalls for the current project.',
path: '/hetzner/firewalls',
operationId: 'get-hetzner-firewalls',
security: [
['bearerAuth' => []],
],
tags: ['Hetzner'],
parameters: [
new OA\Parameter(
name: 'cloud_provider_token_uuid',
in: 'query',
required: false,
description: 'Cloud provider token UUID. Required if cloud_provider_token_id is not provided.',
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'cloud_provider_token_id',
in: 'query',
required: false,
deprecated: true,
description: 'Deprecated: Use cloud_provider_token_uuid instead. Cloud provider token UUID.',
schema: new OA\Schema(type: 'string')
),
],
responses: [
new OA\Response(
response: 200,
description: 'List of Hetzner firewalls.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
]
)
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function firewalls(Request $request)
{
$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);
}

$tokenUuid = $this->getCloudProviderTokenUuid($request);
$token = CloudProviderToken::whereTeamId($teamId)
->whereUuid($tokenUuid)
->where('provider', 'hetzner')
->first();

if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}

try {
$hetznerService = new HetznerService($token->token);

return response()->json($hetznerService->getFirewalls());
} catch (\Throwable $e) {
return response()->json(['message' => 'Failed to fetch firewalls: '.$e->getMessage()], 500);
}
}

#[OA\Get(
summary: 'Get Hetzner Networks',
description: 'Get all existing Hetzner private networks for the current project.',
path: '/hetzner/networks',
operationId: 'get-hetzner-networks',
security: [
['bearerAuth' => []],
],
tags: ['Hetzner'],
parameters: [
new OA\Parameter(
name: 'cloud_provider_token_uuid',
in: 'query',
required: false,
description: 'Cloud provider token UUID. Required if cloud_provider_token_id is not provided.',
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'cloud_provider_token_id',
in: 'query',
required: false,
deprecated: true,
description: 'Deprecated: Use cloud_provider_token_uuid instead. Cloud provider token UUID.',
schema: new OA\Schema(type: 'string')
),
],
responses: [
new OA\Response(
response: 200,
description: 'List of Hetzner networks.',
content: [
new OA\MediaType(
mediaType: 'application/json',
schema: new OA\Schema(
type: 'array',
items: new OA\Items(
type: 'object',
properties: [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
'ip_range' => ['type' => 'string'],
]
)
)
),
]),
new OA\Response(
response: 401,
ref: '#/components/responses/401',
),
new OA\Response(
response: 404,
ref: '#/components/responses/404',
),
]
)]
public function networks(Request $request)
{
$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);
}

$tokenUuid = $this->getCloudProviderTokenUuid($request);
$token = CloudProviderToken::whereTeamId($teamId)
->whereUuid($tokenUuid)
->where('provider', 'hetzner')
->first();

if (! $token) {
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
}

try {
$hetznerService = new HetznerService($token->token);

return response()->json($hetznerService->getNetworks());
} catch (\Throwable $e) {
return response()->json(['message' => 'Failed to fetch networks: '.$e->getMessage()], 500);
}
}

#[OA\Post(
summary: 'Create Hetzner Server',
description: 'Create a new server on Hetzner and register it in Coolify.',
Expand Down Expand Up @@ -482,6 +669,8 @@ public function sshKeys(Request $request)
'enable_ipv4' => ['type' => 'boolean', 'example' => true, 'description' => 'Enable IPv4 (default: true)'],
'enable_ipv6' => ['type' => 'boolean', 'example' => true, 'description' => 'Enable IPv6 (default: true)'],
'hetzner_ssh_key_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Additional Hetzner SSH key IDs'],
'hetzner_firewall_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Existing Hetzner firewall IDs to apply during server creation'],
'hetzner_network_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Existing Hetzner network IDs to attach during server creation'],
'cloud_init_script' => ['type' => 'string', 'description' => 'Cloud-init YAML script (optional)'],
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Validate server immediately after creation'],
],
Expand Down Expand Up @@ -540,6 +729,8 @@ public function createServer(Request $request)
'enable_ipv4',
'enable_ipv6',
'hetzner_ssh_key_ids',
'hetzner_firewall_ids',
'hetzner_network_ids',
'cloud_init_script',
'instant_validate',
];
Expand All @@ -566,6 +757,10 @@ public function createServer(Request $request)
'enable_ipv6' => 'nullable|boolean',
'hetzner_ssh_key_ids' => 'nullable|array',
'hetzner_ssh_key_ids.*' => 'integer',
'hetzner_firewall_ids' => 'nullable|array',
'hetzner_firewall_ids.*' => 'integer',
'hetzner_network_ids' => 'nullable|array',
'hetzner_network_ids.*' => 'integer',
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
'instant_validate' => 'nullable|boolean',
]);
Expand Down Expand Up @@ -604,6 +799,12 @@ public function createServer(Request $request)
if (is_null($request->hetzner_ssh_key_ids)) {
$request->offsetSet('hetzner_ssh_key_ids', []);
}
if (is_null($request->hetzner_firewall_ids)) {
$request->offsetSet('hetzner_firewall_ids', []);
}
if (is_null($request->hetzner_network_ids)) {
$request->offsetSet('hetzner_network_ids', []);
}
if (is_null($request->instant_validate)) {
$request->offsetSet('instant_validate', false);
}
Expand Down Expand Up @@ -679,6 +880,18 @@ public function createServer(Request $request)
],
];

$firewallIds = array_values(array_unique($request->hetzner_firewall_ids));
if ($firewallIds !== []) {
$params['firewalls'] = array_map(function (int $firewallId): array {
return ['firewall' => $firewallId];
}, $firewallIds);
}

$networkIds = array_values(array_unique($request->hetzner_network_ids));
if ($networkIds !== []) {
$params['networks'] = $networkIds;
}

// Add cloud-init script if provided
if (! empty($request->cloud_init_script)) {
$params['user_data'] = $request->cloud_init_script;
Expand Down
76 changes: 76 additions & 0 deletions app/Livewire/Server/New/ByHetzner.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ class ByHetzner extends Component

public array $hetznerSshKeys = [];

public array $hetznerFirewalls = [];

public array $hetznerNetworks = [];

public ?string $selected_location = null;

public ?int $selected_image = null;
Expand All @@ -54,6 +58,10 @@ class ByHetzner extends Component

public array $selectedHetznerSshKeyIds = [];

public array $selectedHetznerFirewallIds = [];

public array $selectedHetznerNetworkIds = [];

public string $server_name = '';

public ?int $private_key_id = null;
Expand Down Expand Up @@ -112,6 +120,9 @@ public function resetSelection()
$this->save_cloud_init_script = false;
$this->cloud_init_script_name = null;
$this->selected_cloud_init_script_id = null;
$this->selectedHetznerSshKeyIds = [];
$this->selectedHetznerFirewallIds = [];
$this->selectedHetznerNetworkIds = [];
}

public function loadTokens()
Expand Down Expand Up @@ -160,6 +171,10 @@ protected function rules(): array
'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id,
'selectedHetznerSshKeyIds' => 'nullable|array',
'selectedHetznerSshKeyIds.*' => 'integer',
'selectedHetznerFirewallIds' => 'nullable|array',
'selectedHetznerFirewallIds.*' => 'integer',
'selectedHetznerNetworkIds' => 'nullable|array',
'selectedHetznerNetworkIds.*' => 'integer',
'enable_ipv4' => 'required|boolean',
'enable_ipv6' => 'required|boolean',
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
Expand Down Expand Up @@ -241,6 +256,9 @@ public function previousStep()
private function loadHetznerData(string $token)
{
$this->loading_data = true;
$this->selectedHetznerSshKeyIds = [];
$this->selectedHetznerFirewallIds = [];
$this->selectedHetznerNetworkIds = [];

try {
$hetznerService = new HetznerService($token);
Expand Down Expand Up @@ -270,6 +288,14 @@ private function loadHetznerData(string $token)
->toArray();
// Load SSH keys from Hetzner
$this->hetznerSshKeys = $hetznerService->getSshKeys();
$this->hetznerFirewalls = collect($hetznerService->getFirewalls())
->sortBy('name')
->values()
->toArray();
$this->hetznerNetworks = collect($hetznerService->getNetworks())
->sortBy('name')
->values()
->toArray();
$this->loading_data = false;
} catch (\Throwable $e) {
$this->loading_data = false;
Expand Down Expand Up @@ -345,6 +371,37 @@ public function getAvailableImagesProperty()
return $filtered;
}

public function getAvailableNetworksProperty(): array
{
$attachableNetworks = collect($this->hetznerNetworks)
->filter(function (array $network) {
return collect($network['subnets'] ?? [])->contains(function (array $subnet) {
return in_array($subnet['type'] ?? null, ['cloud', 'server'], true);
});
});

if (! $this->selected_location) {
return $attachableNetworks->values()->toArray();
}

$location = collect($this->locations)->firstWhere('name', $this->selected_location);
$networkZone = $location['network_zone'] ?? null;

if (! $networkZone) {
return $attachableNetworks->values()->toArray();
}

return $attachableNetworks
->filter(function (array $network) use ($networkZone) {
return collect($network['subnets'] ?? [])->contains(function (array $subnet) use ($networkZone) {
return in_array($subnet['type'] ?? null, ['cloud', 'server'], true)
&& ($subnet['network_zone'] ?? null) === $networkZone;
});
})
->values()
->toArray();
}

public function getSelectedServerPriceProperty(): ?string
{
if (! $this->selected_server_type) {
Expand All @@ -367,6 +424,13 @@ public function updatedSelectedLocation($value)
// Reset server type and image when location changes
$this->selected_server_type = null;
$this->selected_image = null;

$this->selectedHetznerNetworkIds = array_values(array_filter(
$this->selectedHetznerNetworkIds,
function (int $selectedNetworkId): bool {
return collect($this->availableNetworks)->contains('id', $selectedNetworkId);
}
));
}

public function updatedSelectedServerType($value)
Expand Down Expand Up @@ -454,6 +518,18 @@ private function createHetznerServer(string $token): array
],
];

$firewallIds = array_values(array_unique($this->selectedHetznerFirewallIds));
if ($firewallIds !== []) {
$params['firewalls'] = array_map(function (int $firewallId): array {
return ['firewall' => $firewallId];
}, $firewallIds);
}

$networkIds = array_values(array_unique($this->selectedHetznerNetworkIds));
if ($networkIds !== []) {
$params['networks'] = $networkIds;
}

// Add cloud-init script if provided
if (! empty($this->cloud_init_script)) {
$params['user_data'] = $this->cloud_init_script;
Expand Down
Loading
Loading