Skip to content

Commit 59ad710

Browse files
authored
fix: allow FQDNs as controlPlaneEndpoint (#157)
* fix: allow FQDNs as controlPlaneEndpoint * add testcases with plain ip4/ip6 endpoints
1 parent 7a8c9e0 commit 59ad710

File tree

2 files changed

+75
-12
lines changed

2 files changed

+75
-12
lines changed

internal/webhook/proxmoxcluster_webhook.go

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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) {
213232
func 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+
}

internal/webhook/proxmoxcluster_webhook_test.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,40 @@ var _ = Describe("Controller Test", func() {
4343
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("at least one ip config must be set")))
4444
})
4545

46-
It("should disallow invalid endpoint IP", func() {
46+
It("should disallow invalid endpoint FQDN", func() {
4747
cluster := invalidProxmoxCluster("test-cluster")
48-
cluster.Spec.ControlPlaneEndpoint.Host = "invalid"
49-
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("provided endpoint address is not a valid IP")))
48+
cluster.Spec.ControlPlaneEndpoint.Host = "_this.is.a.txt.record"
49+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("provided endpoint address is not a valid IP or FQDN")))
50+
})
51+
52+
It("should disallow invalid endpoint short hostname", func() {
53+
cluster := invalidProxmoxCluster("test-cluster")
54+
cluster.Spec.ControlPlaneEndpoint.Host = "invalid-"
55+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(MatchError(ContainSubstring("provided endpoint address is not a valid IP or FQDN")))
56+
})
57+
58+
It("should allow valid endpoint FQDN", func() {
59+
cluster := validProxmoxCluster("succeed-test-cluster-with-fqdn")
60+
cluster.Spec.ControlPlaneEndpoint.Host = "host.example.com"
61+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(Succeed())
62+
})
63+
64+
It("should allow valid upper case endpoint FQDN", func() {
65+
cluster := validProxmoxCluster("succeed-test-cluster-with-uppercase-fqdn")
66+
cluster.Spec.ControlPlaneEndpoint.Host = "HOST.EXAMPLE.COM"
67+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(Succeed())
68+
})
69+
70+
It("should allow valid endpoint IP4", func() {
71+
cluster := validProxmoxCluster("succeed-test-cluster-with-ip4")
72+
cluster.Spec.ControlPlaneEndpoint.Host = "127.0.0.1"
73+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(Succeed())
74+
})
75+
76+
It("should allow valid endpoint IP6", func() {
77+
cluster := validProxmoxCluster("succeed-test-cluster-with-ip6")
78+
cluster.Spec.ControlPlaneEndpoint.Host = "::1"
79+
g.Expect(k8sClient.Create(testEnv.GetContext(), &cluster)).To(Succeed())
5080
})
5181

5282
It("should disallow invalid endpoint IP + port combination", func() {

0 commit comments

Comments
 (0)