Skip to content

Commit e81a2c6

Browse files
authored
feat(cloud-provider): Add support for Hetzner firewalls, internal networks and backups when creating the Server (#9646)
2 parents 5dc697c + a8000ac commit e81a2c6

12 files changed

Lines changed: 1287 additions & 88 deletions

File tree

app/Http/Controllers/Api/HetznerController.php

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,195 @@ public function sshKeys(Request $request)
460460
}
461461
}
462462

463+
#[OA\Get(
464+
summary: 'Get Hetzner Firewalls',
465+
description: 'Get all existing Hetzner firewalls for the current project.',
466+
path: '/hetzner/firewalls',
467+
operationId: 'get-hetzner-firewalls',
468+
security: [
469+
['bearerAuth' => []],
470+
],
471+
tags: ['Hetzner'],
472+
parameters: [
473+
new OA\Parameter(
474+
name: 'cloud_provider_token_uuid',
475+
in: 'query',
476+
required: false,
477+
description: 'Cloud provider token UUID. Required if cloud_provider_token_id is not provided.',
478+
schema: new OA\Schema(type: 'string')
479+
),
480+
new OA\Parameter(
481+
name: 'cloud_provider_token_id',
482+
in: 'query',
483+
required: false,
484+
deprecated: true,
485+
description: 'Deprecated: Use cloud_provider_token_uuid instead. Cloud provider token UUID.',
486+
schema: new OA\Schema(type: 'string')
487+
),
488+
],
489+
responses: [
490+
new OA\Response(
491+
response: 200,
492+
description: 'List of Hetzner firewalls.',
493+
content: [
494+
new OA\MediaType(
495+
mediaType: 'application/json',
496+
schema: new OA\Schema(
497+
type: 'array',
498+
items: new OA\Items(
499+
type: 'object',
500+
properties: [
501+
'id' => ['type' => 'integer'],
502+
'name' => ['type' => 'string'],
503+
]
504+
)
505+
)
506+
),
507+
]),
508+
new OA\Response(
509+
response: 401,
510+
ref: '#/components/responses/401',
511+
),
512+
new OA\Response(
513+
response: 404,
514+
ref: '#/components/responses/404',
515+
),
516+
]
517+
)]
518+
public function firewalls(Request $request)
519+
{
520+
$teamId = getTeamIdFromToken();
521+
if (is_null($teamId)) {
522+
return invalidTokenResponse();
523+
}
524+
525+
$validator = customApiValidator($request->all(), [
526+
'cloud_provider_token_uuid' => 'required_without:cloud_provider_token_id|string',
527+
'cloud_provider_token_id' => 'required_without:cloud_provider_token_uuid|string',
528+
]);
529+
530+
if ($validator->fails()) {
531+
return response()->json([
532+
'message' => 'Validation failed.',
533+
'errors' => $validator->errors(),
534+
], 422);
535+
}
536+
537+
$tokenUuid = $this->getCloudProviderTokenUuid($request);
538+
$token = CloudProviderToken::whereTeamId($teamId)
539+
->whereUuid($tokenUuid)
540+
->where('provider', 'hetzner')
541+
->first();
542+
543+
if (! $token) {
544+
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
545+
}
546+
$this->authorize('view', $token);
547+
548+
try {
549+
$hetznerService = new HetznerService($token->token);
550+
551+
return response()->json($hetznerService->getFirewalls());
552+
} catch (\Throwable $e) {
553+
return response()->json(['message' => 'Failed to fetch Hetzner firewalls.'], 500);
554+
}
555+
}
556+
557+
#[OA\Get(
558+
summary: 'Get Hetzner Networks',
559+
description: 'Get all existing Hetzner private networks for the current project.',
560+
path: '/hetzner/networks',
561+
operationId: 'get-hetzner-networks',
562+
security: [
563+
['bearerAuth' => []],
564+
],
565+
tags: ['Hetzner'],
566+
parameters: [
567+
new OA\Parameter(
568+
name: 'cloud_provider_token_uuid',
569+
in: 'query',
570+
required: false,
571+
description: 'Cloud provider token UUID. Required if cloud_provider_token_id is not provided.',
572+
schema: new OA\Schema(type: 'string')
573+
),
574+
new OA\Parameter(
575+
name: 'cloud_provider_token_id',
576+
in: 'query',
577+
required: false,
578+
deprecated: true,
579+
description: 'Deprecated: Use cloud_provider_token_uuid instead. Cloud provider token UUID.',
580+
schema: new OA\Schema(type: 'string')
581+
),
582+
],
583+
responses: [
584+
new OA\Response(
585+
response: 200,
586+
description: 'List of Hetzner networks.',
587+
content: [
588+
new OA\MediaType(
589+
mediaType: 'application/json',
590+
schema: new OA\Schema(
591+
type: 'array',
592+
items: new OA\Items(
593+
type: 'object',
594+
properties: [
595+
'id' => ['type' => 'integer'],
596+
'name' => ['type' => 'string'],
597+
'ip_range' => ['type' => 'string'],
598+
]
599+
)
600+
)
601+
),
602+
]),
603+
new OA\Response(
604+
response: 401,
605+
ref: '#/components/responses/401',
606+
),
607+
new OA\Response(
608+
response: 404,
609+
ref: '#/components/responses/404',
610+
),
611+
]
612+
)]
613+
public function networks(Request $request)
614+
{
615+
$teamId = getTeamIdFromToken();
616+
if (is_null($teamId)) {
617+
return invalidTokenResponse();
618+
}
619+
620+
$validator = customApiValidator($request->all(), [
621+
'cloud_provider_token_uuid' => 'required_without:cloud_provider_token_id|string',
622+
'cloud_provider_token_id' => 'required_without:cloud_provider_token_uuid|string',
623+
]);
624+
625+
if ($validator->fails()) {
626+
return response()->json([
627+
'message' => 'Validation failed.',
628+
'errors' => $validator->errors(),
629+
], 422);
630+
}
631+
632+
$tokenUuid = $this->getCloudProviderTokenUuid($request);
633+
$token = CloudProviderToken::whereTeamId($teamId)
634+
->whereUuid($tokenUuid)
635+
->where('provider', 'hetzner')
636+
->first();
637+
638+
if (! $token) {
639+
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
640+
}
641+
$this->authorize('view', $token);
642+
643+
try {
644+
$hetznerService = new HetznerService($token->token);
645+
646+
return response()->json($hetznerService->getNetworks());
647+
} catch (\Throwable $e) {
648+
return response()->json(['message' => 'Failed to fetch Hetzner networks.'], 500);
649+
}
650+
}
651+
463652
#[OA\Post(
464653
summary: 'Create Hetzner Server',
465654
description: 'Create a new server on Hetzner and register it in Coolify.',
@@ -487,7 +676,10 @@ public function sshKeys(Request $request)
487676
'private_key_uuid' => ['type' => 'string', 'example' => 'xyz789', 'description' => 'Private key UUID'],
488677
'enable_ipv4' => ['type' => 'boolean', 'example' => true, 'description' => 'Enable IPv4 (default: true)'],
489678
'enable_ipv6' => ['type' => 'boolean', 'example' => true, 'description' => 'Enable IPv6 (default: true)'],
679+
'enable_backups' => ['type' => 'boolean', 'example' => false, 'description' => 'Enable Hetzner server backups after creation (adds 20% to the monthly server fee)'],
490680
'hetzner_ssh_key_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Additional Hetzner SSH key IDs'],
681+
'hetzner_firewall_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Existing Hetzner firewall IDs to apply during server creation'],
682+
'hetzner_network_ids' => ['type' => 'array', 'items' => ['type' => 'integer'], 'description' => 'Existing Hetzner network IDs to attach during server creation'],
491683
'cloud_init_script' => ['type' => 'string', 'description' => 'Cloud-init YAML script (optional)'],
492684
'instant_validate' => ['type' => 'boolean', 'example' => false, 'description' => 'Validate server immediately after creation'],
493685
],
@@ -545,7 +737,10 @@ public function createServer(Request $request)
545737
'private_key_uuid',
546738
'enable_ipv4',
547739
'enable_ipv6',
740+
'enable_backups',
548741
'hetzner_ssh_key_ids',
742+
'hetzner_firewall_ids',
743+
'hetzner_network_ids',
549744
'cloud_init_script',
550745
'instant_validate',
551746
];
@@ -571,8 +766,13 @@ public function createServer(Request $request)
571766
'private_key_uuid' => 'required|string',
572767
'enable_ipv4' => 'nullable|boolean',
573768
'enable_ipv6' => 'nullable|boolean',
769+
'enable_backups' => 'nullable|boolean',
574770
'hetzner_ssh_key_ids' => 'nullable|array',
575771
'hetzner_ssh_key_ids.*' => 'integer',
772+
'hetzner_firewall_ids' => 'nullable|array',
773+
'hetzner_firewall_ids.*' => 'integer',
774+
'hetzner_network_ids' => 'nullable|array',
775+
'hetzner_network_ids.*' => 'integer',
576776
'cloud_init_script' => ['nullable', 'string', new ValidCloudInitYaml],
577777
'instant_validate' => 'nullable|boolean',
578778
]);
@@ -608,13 +808,32 @@ public function createServer(Request $request)
608808
if (is_null($request->enable_ipv6)) {
609809
$request->offsetSet('enable_ipv6', true);
610810
}
811+
if (is_null($request->enable_backups)) {
812+
$request->offsetSet('enable_backups', false);
813+
}
611814
if (is_null($request->hetzner_ssh_key_ids)) {
612815
$request->offsetSet('hetzner_ssh_key_ids', []);
613816
}
817+
if (is_null($request->hetzner_firewall_ids)) {
818+
$request->offsetSet('hetzner_firewall_ids', []);
819+
}
820+
if (is_null($request->hetzner_network_ids)) {
821+
$request->offsetSet('hetzner_network_ids', []);
822+
}
614823
if (is_null($request->instant_validate)) {
615824
$request->offsetSet('instant_validate', false);
616825
}
617826

827+
if (! $request->boolean('enable_ipv4') && ! $request->boolean('enable_ipv6')) {
828+
return response()->json([
829+
'message' => 'Validation failed.',
830+
'errors' => [
831+
'enable_ipv4' => ['Enable at least one public IP protocol.'],
832+
'enable_ipv6' => ['Enable at least one public IP protocol.'],
833+
],
834+
], 422);
835+
}
836+
618837
// Validate cloud provider token
619838
$tokenUuid = $this->getCloudProviderTokenUuid($request);
620839
$token = CloudProviderToken::whereTeamId($teamId)
@@ -687,6 +906,18 @@ public function createServer(Request $request)
687906
],
688907
];
689908

909+
$firewallIds = array_values(array_unique($request->hetzner_firewall_ids));
910+
if ($firewallIds !== []) {
911+
$params['firewalls'] = array_map(function (int $firewallId): array {
912+
return ['firewall' => $firewallId];
913+
}, $firewallIds);
914+
}
915+
916+
$networkIds = array_values(array_unique($request->hetzner_network_ids));
917+
if ($networkIds !== []) {
918+
$params['networks'] = $networkIds;
919+
}
920+
690921
// Add cloud-init script if provided
691922
if (! empty($request->cloud_init_script)) {
692923
$params['user_data'] = $request->cloud_init_script;
@@ -723,6 +954,14 @@ public function createServer(Request $request)
723954
$server->proxy->set('type', ProxyTypes::TRAEFIK->value);
724955
$server->save();
725956

957+
if ($request->enable_backups) {
958+
try {
959+
$hetznerService->enableServerBackup((int) $hetznerServer['id']);
960+
} catch (\Throwable $e) {
961+
report($e);
962+
}
963+
}
964+
726965
// Validate server if requested
727966
if ($request->instant_validate) {
728967
ValidateServer::dispatch($server);

0 commit comments

Comments
 (0)