Skip to content

Commit 99d6379

Browse files
committed
Enhance node controller to assign pod prefix to node object
1 parent 46b95fc commit 99d6379

File tree

3 files changed

+160
-2
lines changed

3 files changed

+160
-2
lines changed

pkg/cloudprovider/metal/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,13 @@ type CloudConfig struct {
4141
var (
4242
MetalKubeconfigPath string
4343
MetalNamespace string
44+
PodPrefixSize int
4445
)
4546

4647
func AddExtraFlags(fs *pflag.FlagSet) {
4748
fs.StringVar(&MetalKubeconfigPath, "metal-kubeconfig", "", "Path to the metal cluster kubeconfig.")
4849
fs.StringVar(&MetalNamespace, "metal-namespace", "", "Override metal cluster namespace.")
50+
fs.IntVar(&PodPrefixSize, "pod-prefix-size", 112, "Prefix size for the pod prefix, zero or less disables pod prefix assignment.")
4951
}
5052

5153
func LoadCloudProviderConfig(f io.Reader) (*CloudProviderConfig, error) {

pkg/cloudprovider/metal/node_controller.go

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"net"
1011
"reflect"
1112
"strings"
1213

@@ -125,10 +126,52 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) error
125126
} else {
126127
delete(claim.Labels, metalv1alpha1.ServerMaintenanceApprovalKey)
127128
}
128-
if reflect.DeepEqual(claim, originalClaim) {
129+
if !reflect.DeepEqual(claim, originalClaim) {
130+
if err = r.metalClient.Patch(ctx, claim, client.MergeFrom(originalClaim)); err != nil {
131+
return err
132+
}
133+
}
134+
135+
if PodPrefixSize <= 0 {
136+
// <= 0 disables automatic assignment of pod CIDR.
137+
return nil
138+
}
139+
140+
if node.Spec.PodCIDR != "" {
141+
klog.InfoS("PodCIDR is already populated; patch was not done", "Node", node.Name, "PodCIDR", node.Spec.PodCIDR)
129142
return nil
130143
}
131-
return r.metalClient.Patch(ctx, claim, client.MergeFrom(originalClaim))
144+
145+
for _, addr := range node.Status.Addresses {
146+
if addr.Type == corev1.NodeInternalIP {
147+
ip := net.ParseIP(addr.Address)
148+
if ip == nil {
149+
return fmt.Errorf("invalid IP address format")
150+
}
151+
152+
maskedIP := zeroHostBits(ip, PodPrefixSize)
153+
podCIDR := fmt.Sprintf("%s/%d", maskedIP, PodPrefixSize)
154+
155+
nodeBase := node.DeepCopy()
156+
node.Spec.PodCIDR = podCIDR
157+
if node.Spec.PodCIDRs == nil {
158+
node.Spec.PodCIDRs = []string{}
159+
}
160+
node.Spec.PodCIDRs = append(node.Spec.PodCIDRs, podCIDR)
161+
162+
if err := r.targetClient.Patch(ctx, node, client.MergeFrom(nodeBase)); err != nil {
163+
return fmt.Errorf("failed to patch Node's PodCIDR with error %w", err)
164+
}
165+
166+
klog.Info("Patched Node's PodCIDR and PodCIDRs", "Node", node.Name, "PodCIDR", podCIDR)
167+
168+
return nil
169+
}
170+
}
171+
172+
klog.Info("Node does not have a NodeInternalIP, not setting podCIDR")
173+
174+
return nil
132175
}
133176

134177
func parseProviderID(providerID string) (types.NamespacedName, error) {
@@ -145,3 +188,13 @@ func parseProviderID(providerID string) (types.NamespacedName, error) {
145188
}
146189
return types.NamespacedName{Namespace: parts[0], Name: parts[1]}, nil
147190
}
191+
192+
func zeroHostBits(ip net.IP, maskSize int) net.IP {
193+
if ip.To4() != nil {
194+
mask := net.CIDRMask(maskSize, 32)
195+
return ip.Mask(mask)
196+
} else {
197+
mask := net.CIDRMask(maskSize, 128)
198+
return ip.Mask(mask)
199+
}
200+
}

pkg/cloudprovider/metal/node_controller_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package metal
55

66
import (
7+
"net"
8+
79
metalv1alpha1 "github.com/ironcore-dev/metal-operator/api/v1alpha1"
810
. "github.com/onsi/ginkgo/v2"
911
. "github.com/onsi/gomega"
@@ -116,4 +118,105 @@ var _ = Describe("NodeReconciler", func() {
116118
Eventually(Object(serverClaim)).Should(HaveField("Labels", HaveKeyWithValue(metalv1alpha1.ServerMaintenanceApprovalKey, TrueStr)))
117119
})
118120

121+
Context("PodCIDR assignment", func() {
122+
BeforeEach(func() {
123+
PodPrefixSize = 24
124+
})
125+
126+
AfterEach(func() {
127+
PodPrefixSize = 0
128+
})
129+
130+
It("should assign PodCIDR to a node with NodeInternalIP", func(ctx SpecContext) {
131+
By("Setting the NodeInternalIP address on the node")
132+
Eventually(UpdateStatus(node, func() {
133+
node.Status.Addresses = append(node.Status.Addresses, corev1.NodeAddress{
134+
Type: corev1.NodeInternalIP,
135+
Address: "10.0.5.42",
136+
})
137+
})).Should(Succeed())
138+
139+
By("Verifying PodCIDR is assigned to the node")
140+
Eventually(Object(node)).Should(HaveField("Spec.PodCIDR", "10.0.5.0/24"))
141+
Eventually(Object(node)).Should(HaveField("Spec.PodCIDRs", ContainElement("10.0.5.0/24")))
142+
})
143+
144+
It("should not overwrite existing PodCIDR", func(ctx SpecContext) {
145+
By("Setting the PodCIDR on the node")
146+
originalNode := node.DeepCopy()
147+
node.Spec.PodCIDR = "192.168.0.0/24"
148+
Expect(k8sClient.Patch(ctx, node, client.MergeFrom(originalNode))).To(Succeed())
149+
150+
By("Setting the NodeInternalIP address on the node")
151+
Eventually(UpdateStatus(node, func() {
152+
node.Status.Addresses = append(node.Status.Addresses, corev1.NodeAddress{
153+
Type: corev1.NodeInternalIP,
154+
Address: "10.0.5.42",
155+
})
156+
})).Should(Succeed())
157+
158+
By("Verifying PodCIDR was not overwritten")
159+
Consistently(Object(node)).Should(HaveField("Spec.PodCIDR", "192.168.0.0/24"))
160+
})
161+
162+
It("should not assign PodCIDR if node has no NodeInternalIP", func(ctx SpecContext) {
163+
By("Triggering reconciliation by patching the claim")
164+
originalServerClaim := serverClaim.DeepCopy()
165+
serverClaim.Labels = map[string]string{
166+
metalv1alpha1.ServerMaintenanceNeededLabelKey: TrueStr,
167+
}
168+
Expect(k8sClient.Patch(ctx, serverClaim, client.MergeFrom(originalServerClaim))).To(Succeed())
169+
170+
By("Verifying PodCIDR remains empty")
171+
Consistently(Object(node)).Should(HaveField("Spec.PodCIDR", ""))
172+
})
173+
174+
It("should not assign PodCIDR if PodPrefixSize is disabled", func(ctx SpecContext) {
175+
By("Disabling PodCIDR assignment")
176+
PodPrefixSize = 0
177+
178+
By("Setting the NodeInternalIP address on the node")
179+
Eventually(UpdateStatus(node, func() {
180+
node.Status.Addresses = append(node.Status.Addresses, corev1.NodeAddress{
181+
Type: corev1.NodeInternalIP,
182+
Address: "10.0.5.42",
183+
})
184+
})).Should(Succeed())
185+
186+
By("Triggering reconciliation by patching the claim")
187+
originalServerClaim := serverClaim.DeepCopy()
188+
serverClaim.Labels = map[string]string{
189+
metalv1alpha1.ServerMaintenanceNeededLabelKey: TrueStr,
190+
}
191+
Expect(k8sClient.Patch(ctx, serverClaim, client.MergeFrom(originalServerClaim))).To(Succeed())
192+
193+
By("Verifying PodCIDR remains empty despite having NodeInternalIP")
194+
Consistently(Object(node)).Should(HaveField("Spec.PodCIDR", ""))
195+
})
196+
})
197+
})
198+
199+
var _ = Describe("zeroHostBits", func() {
200+
DescribeTable("should correctly mask IPv4 addresses",
201+
func(ip string, maskSize int, expected string) {
202+
result := zeroHostBits(net.ParseIP(ip), maskSize)
203+
Expect(result.String()).To(Equal(expected))
204+
},
205+
Entry("mask /24", "10.0.5.42", 24, "10.0.5.0"),
206+
Entry("mask /16", "10.0.5.42", 16, "10.0.0.0"),
207+
Entry("mask /8", "10.20.30.40", 8, "10.0.0.0"),
208+
Entry("mask /32", "10.0.5.42", 32, "10.0.5.42"),
209+
Entry("mask /0", "10.0.5.42", 0, "0.0.0.0"),
210+
)
211+
212+
DescribeTable("should correctly mask IPv6 addresses",
213+
func(ip string, maskSize int, expected string) {
214+
result := zeroHostBits(net.ParseIP(ip), maskSize)
215+
Expect(result.String()).To(Equal(expected))
216+
},
217+
Entry("mask /64", "2001:db8::1", 64, "2001:db8::"),
218+
Entry("mask /48", "2001:db8:1234:5678::1", 48, "2001:db8:1234::"),
219+
Entry("mask /128", "2001:db8::1", 128, "2001:db8::1"),
220+
Entry("mask /0", "2001:db8::1", 0, "::"),
221+
)
119222
})

0 commit comments

Comments
 (0)