diff --git a/internal/webhook/proxmoxcluster_webhook.go b/internal/webhook/proxmoxcluster_webhook.go index 924db909..6448b482 100644 --- a/internal/webhook/proxmoxcluster_webhook.go +++ b/internal/webhook/proxmoxcluster_webhook.go @@ -20,6 +20,7 @@ package webhook import ( "context" "fmt" + "net" "net/netip" "strings" @@ -98,6 +99,11 @@ func validateIPs(cluster *infrav1.ProxmoxCluster) error { endpointHostIP := ep.Host + if isValidFQDN(endpointHostIP) { + ipAddr, _ := net.ResolveIPAddr("ip", endpointHostIP) + endpointHostIP = ipAddr.String() + } + addr, err := netip.ParseAddr(endpointHostIP) if err != nil { return apierrors.NewInvalid( @@ -105,12 +111,12 @@ func validateIPs(cluster *infrav1.ProxmoxCluster) error { name, field.ErrorList{ field.Invalid( - field.NewPath("spec", "controlplaneEndpoint"), endpointHostIP, "provided endpoint address is not a valid IP"), + field.NewPath("spec", "controlplaneEndpoint"), endpointHostIP, "provided endpoint address is not a valid IP or FQDN"), }) } // If the passed control-plane endppoint is an IPv6 address, wrap it in [], so it can properly pass ParseAddrPort validation if addr.Is6() { - endpointHostIP = fmt.Sprintf("[%s]", ep.Host) + endpointHostIP = fmt.Sprintf("[%s]", endpointHostIP) } ipAddr, err := netip.ParseAddrPort(fmt.Sprintf("%s:%d", endpointHostIP, ep.Port)) @@ -213,3 +219,8 @@ func buildSetFromAddresses(addresses []string) (*netipx.IPSet, error) { func hasNoIPPoolConfig(cluster *infrav1.ProxmoxCluster) bool { return cluster.Spec.IPv4Config == nil && cluster.Spec.IPv6Config == nil } + +func isValidFQDN(fqdn string) bool { + _, err := net.ResolveIPAddr("ip", fqdn) + return err == nil +} diff --git a/internal/webhook/proxmoxcluster_webhook_test.go b/internal/webhook/proxmoxcluster_webhook_test.go index 4bb7f982..1e1e5d14 100644 --- a/internal/webhook/proxmoxcluster_webhook_test.go +++ b/internal/webhook/proxmoxcluster_webhook_test.go @@ -43,12 +43,24 @@ var _ = Describe("Controller Test", func() { g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("at least one ip config must be set"))) }) + It("should disallow invalid/non-existing endpoint FQDN", func() { + cluster := invalidProxmoxCluster("test-cluster") + cluster.Spec.ControlPlaneEndpoint.Host = "this.does.not.exist.ionos.com" + g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("provided endpoint address is not a valid IP or FQDN"))) + }) + It("should disallow invalid endpoint IP", func() { cluster := invalidProxmoxCluster("test-cluster") cluster.Spec.ControlPlaneEndpoint.Host = "invalid" g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("provided endpoint address is not a valid IP"))) }) + It("should allow valid endpoint from FQDN", func() { + cluster := validProxmoxCluster("succeed-test-cluster-with-fqdn") + cluster.Spec.ControlPlaneEndpoint.Host = "ionos.com" + g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(Succeed()) + }) + It("should disallow invalid endpoint IP + port combination", func() { cluster := invalidProxmoxCluster("test-cluster") cluster.Spec.ControlPlaneEndpoint.Host = "127.0.0.1"