From 7069236714055571f6d90a56a513e368683919d7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:22:25 +0200 Subject: [PATCH] feat: add IPv4/IPv6 network configuration for Hetzner server creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for configuring IPv4 and IPv6 public network interfaces when creating servers through the Hetzner integration. Users can now enable or disable IPv4 and IPv6 independently, with both enabled by default. Features: - Added enable_ipv4 and enable_ipv6 checkboxes in the server creation form - Both options are enabled by default as per Hetzner best practices - IPv4 is preferred when both are enabled - Fallback to IPv6 when only IPv6 is enabled - Proper validation and error handling for network configuration - Comprehensive test coverage for IP address selection logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/Livewire/Server/New/ByHetzner.php | 24 ++++- .../livewire/server/new/by-hetzner.blade.php | 11 +++ tests/Feature/HetznerServerCreationTest.php | 89 ++++++++++++++++++- 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/app/Livewire/Server/New/ByHetzner.php b/app/Livewire/Server/New/ByHetzner.php index 0d65bc603..047d84d93 100644 --- a/app/Livewire/Server/New/ByHetzner.php +++ b/app/Livewire/Server/New/ByHetzner.php @@ -58,6 +58,10 @@ class ByHetzner extends Component public bool $loading_data = false; + public bool $enable_ipv4 = true; + + public bool $enable_ipv6 = true; + public function mount() { $this->authorize('viewAny', CloudProviderToken::class); @@ -129,6 +133,8 @@ protected function rules(): array 'private_key_id' => 'required|integer|exists:private_keys,id,team_id,'.currentTeam()->id, 'selectedHetznerSshKeyIds' => 'nullable|array', 'selectedHetznerSshKeyIds.*' => 'integer', + 'enable_ipv4' => 'required|boolean', + 'enable_ipv6' => 'required|boolean', ]); } @@ -410,6 +416,10 @@ private function createHetznerServer(string $token): array 'location' => $this->selected_location, 'start_after_create' => true, 'ssh_keys' => $sshKeys, + 'public_net' => [ + 'enable_ipv4' => $this->enable_ipv4, + 'enable_ipv6' => $this->enable_ipv6, + ], ]; ray('Server creation parameters', $params); @@ -438,10 +448,22 @@ public function submit() // Create server on Hetzner $hetznerServer = $this->createHetznerServer($hetznerToken); + // Determine IP address to use (prefer IPv4, fallback to IPv6) + $ipAddress = null; + if ($this->enable_ipv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($this->enable_ipv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + if (! $ipAddress) { + throw new \Exception('No public IP address available. Enable at least one of IPv4 or IPv6.'); + } + // Create server in Coolify database $server = Server::create([ 'name' => $this->server_name, - 'ip' => $hetznerServer['public_net']['ipv4']['ip'], + 'ip' => $ipAddress, 'user' => 'root', 'port' => 22, 'team_id' => currentTeam()->id, diff --git a/resources/views/livewire/server/new/by-hetzner.blade.php b/resources/views/livewire/server/new/by-hetzner.blade.php index ea1934b68..d53286c53 100644 --- a/resources/views/livewire/server/new/by-hetzner.blade.php +++ b/resources/views/livewire/server/new/by-hetzner.blade.php @@ -145,6 +145,17 @@ class="p-4 border border-yellow-500 dark:border-yellow-600 rounded bg-yellow-50 @endforeach + +
+ +
+ + +
+
+
Back diff --git a/tests/Feature/HetznerServerCreationTest.php b/tests/Feature/HetznerServerCreationTest.php index 815462ffa..c939c0041 100644 --- a/tests/Feature/HetznerServerCreationTest.php +++ b/tests/Feature/HetznerServerCreationTest.php @@ -1,7 +1,94 @@ $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => true, + 'enable_ipv6' => true, + ]); +}); + +it('validates public_net configuration with IPv4 only', function () { + $enableIpv4 = true; + $enableIpv6 = false; + + $publicNet = [ + 'enable_ipv4' => $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => true, + 'enable_ipv6' => false, + ]); +}); + +it('validates public_net configuration with IPv6 only', function () { + $enableIpv4 = false; + $enableIpv6 = true; + + $publicNet = [ + 'enable_ipv4' => $enableIpv4, + 'enable_ipv6' => $enableIpv6, + ]; + + expect($publicNet)->toBe([ + 'enable_ipv4' => false, + 'enable_ipv6' => true, + ]); +}); + +it('validates IP address selection prefers IPv4 when both are enabled', function () { + $enableIpv4 = true; + $enableIpv6 = true; + + $hetznerServer = [ + 'public_net' => [ + 'ipv4' => ['ip' => '1.2.3.4'], + 'ipv6' => ['ip' => '2001:db8::1'], + ], + ]; + + $ipAddress = null; + if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + expect($ipAddress)->toBe('1.2.3.4'); +}); + +it('validates IP address selection uses IPv6 when only IPv6 is enabled', function () { + $enableIpv4 = false; + $enableIpv6 = true; + + $hetznerServer = [ + 'public_net' => [ + 'ipv4' => ['ip' => '1.2.3.4'], + 'ipv6' => ['ip' => '2001:db8::1'], + ], + ]; + + $ipAddress = null; + if ($enableIpv4 && isset($hetznerServer['public_net']['ipv4']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv4']['ip']; + } elseif ($enableIpv6 && isset($hetznerServer['public_net']['ipv6']['ip'])) { + $ipAddress = $hetznerServer['public_net']['ipv6']['ip']; + } + + expect($ipAddress)->toBe('2001:db8::1'); +}); it('validates SSH key array merging logic with Coolify key', function () { $coolifyKeyId = 123;