@@ -21,6 +21,7 @@ import (
2121 "context"
2222 "fmt"
2323 "net/netip"
24+ "regexp"
2425 "strings"
2526
2627 infrav1 "github.com/ionos-cloud/cluster-api-provider-proxmox/api/v1alpha1"
@@ -63,7 +64,7 @@ func (*ProxmoxCluster) ValidateCreate(_ context.Context, obj runtime.Object) (wa
6364 return warnings , err
6465 }
6566
66- if err := validateIPs (cluster ); err != nil {
67+ if err := validateControlPlaneEndpoint (cluster ); err != nil {
6768 warnings = append (warnings , fmt .Sprintf ("cannot create proxmox cluster %s" , cluster .GetName ()))
6869 return warnings , err
6970 }
@@ -83,44 +84,62 @@ func (*ProxmoxCluster) ValidateUpdate(_ context.Context, _ runtime.Object, newOb
8384 return warnings , apierrors .NewBadRequest (fmt .Sprintf ("expected a ProxmoxCluster but got %T" , newCluster ))
8485 }
8586
86- if err := validateIPs (newCluster ); err != nil {
87+ if err := validateControlPlaneEndpoint (newCluster ); err != nil {
8788 warnings = append (warnings , fmt .Sprintf ("cannot update proxmox cluster %s" , newCluster .GetName ()))
8889 return warnings , err
8990 }
9091
9192 return warnings , nil
9293}
9394
94- func validateIPs (cluster * infrav1.ProxmoxCluster ) error {
95+ func validateControlPlaneEndpoint (cluster * infrav1.ProxmoxCluster ) error {
9596 ep := cluster .Spec .ControlPlaneEndpoint
9697
9798 gk , name := cluster .GroupVersionKind ().GroupKind (), cluster .GetName ()
9899
99- endpointHostIP := ep .Host
100+ endpoint := ep .Host
101+
102+ addr , err := netip .ParseAddr (endpoint )
103+
104+ /*
105+ No further validation is done on hostnames. Checking DNS records
106+ incures a lot of complexity. To list a few of the problems:
107+ - DNS TTL will lead to incorrect results
108+ - IP addresses can be PTR records
109+ - Both A and AAAA records would need checking
110+ - A record can have multiple entries, each of which need to be checked
111+ - A valid record can start with _, but that is not a valid hostname
112+ - ...
113+ Most importantly, cluster-api does not validate controlPlaneEndpoint
114+ at all.
115+ */
116+ match := isHostname (endpoint )
117+ if match {
118+ return nil
119+ }
100120
101- addr , err := netip .ParseAddr (endpointHostIP )
102121 if err != nil {
103122 return apierrors .NewInvalid (
104123 gk ,
105124 name ,
106125 field.ErrorList {
107126 field .Invalid (
108- field .NewPath ("spec" , "controlplaneEndpoint" ), endpointHostIP , "provided endpoint address is not a valid IP" ),
127+ field .NewPath ("spec" , "controlplaneEndpoint" ), endpoint , "provided endpoint address is not a valid IP or FQDN " ),
109128 })
110129 }
111130 // If the passed control-plane endppoint is an IPv6 address, wrap it in [], so it can properly pass ParseAddrPort validation
112131 if addr .Is6 () {
113- endpointHostIP = fmt .Sprintf ("[%s]" , ep . Host )
132+ endpoint = fmt .Sprintf ("[%s]" , endpoint )
114133 }
115134
116- ipAddr , err := netip .ParseAddrPort (fmt .Sprintf ("%s:%d" , endpointHostIP , ep .Port ))
135+ ipAddr , err := netip .ParseAddrPort (fmt .Sprintf ("%s:%d" , endpoint , ep .Port ))
117136 if err != nil {
118137 return apierrors .NewInvalid (
119138 gk ,
120139 name ,
121140 field.ErrorList {
122141 field .Invalid (
123- field .NewPath ("spec" , "controlplaneEndpoint" ), fmt .Sprintf ("%s:%d" , endpointHostIP , ep .Port ), "provided endpoint is not in a valid IP and port format" ),
142+ field .NewPath ("spec" , "controlplaneEndpoint" ), fmt .Sprintf ("%s:%d" , endpoint , ep .Port ), "provided endpoint is not in a valid IP and port format" ),
124143 })
125144 }
126145
@@ -213,3 +232,17 @@ func buildSetFromAddresses(addresses []string) (*netipx.IPSet, error) {
213232func hasNoIPPoolConfig (cluster * infrav1.ProxmoxCluster ) bool {
214233 return cluster .Spec .IPv4Config == nil && cluster .Spec .IPv6Config == nil
215234}
235+
236+ func isHostname (h string ) bool {
237+ // shortname is up to 253 bytes long
238+ shortname := `([a-z0-9]{1,253}|[a-z0-9][a-z0-9-]{1,251}[a-z0-9])`
239+ // hostname is optional in a domain
240+ hostname := `([a-z0-9]{1,63}|[a-z0-9][a-z0-9-]{1,61}[a-z0-9]\.)?`
241+ domain := `((([a-z0-9]{1,63}|[a-z0-9][a-z0-9-]{1,61}[a-z0-9])\.)+?[a-z]{2,63})`
242+
243+ // make hostname match case insensitive, match complete string
244+ hostmatch := `(?i)^(` + shortname + `|` + hostname + domain + `)$`
245+
246+ match , _ := regexp .Match (hostmatch , []byte (h ))
247+ return match
248+ }
0 commit comments