Skip to content

Commit d9fa7ec

Browse files
danmanoropenshift-merge-bot[bot]
authored andcommitted
MGMT-21202: Enable dual-stack
1 parent aba0462 commit d9fa7ec

10 files changed

Lines changed: 632 additions & 24 deletions

api/v1alpha1/imageclusterinstall_types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,18 @@ type ImageClusterInstallSpec struct {
9494
// MachineNetwork is the subnet provided by user for the ocp cluster.
9595
// This will be used to create the node network and choose ip address for the node.
9696
// Equivalent to install-config.yaml's machineNetwork.
97+
// Deprecated: Use MachineNetworks instead.
9798
// +optional.
9899
MachineNetwork string `json:"machineNetwork,omitempty"`
99100

101+
// MachineNetworks is the list of IP address pools for machines.
102+
// This enables dual-stack support by allowing multiple networks.
103+
// If both MachineNetwork and MachineNetworks are specified, MachineNetwork should match
104+
// the first element of MachineNetworks.
105+
// Use instead of MachineNetwork.
106+
// +optional
107+
MachineNetworks []MachineNetworkEntry `json:"machineNetworks,omitempty"`
108+
100109
// Proxy defines the proxy settings to be applied in relocated cluster
101110
// +optional
102111
Proxy *Proxy `json:"proxy,omitempty"`
@@ -162,6 +171,12 @@ type Proxy struct {
162171
NoProxy string `json:"noProxy,omitempty"`
163172
}
164173

174+
// MachineNetworkEntry is a single IP address block for node IP blocks.
175+
type MachineNetworkEntry struct {
176+
// CIDR is the IP block address pool for machines within the cluster.
177+
CIDR string `json:"cidr"`
178+
}
179+
165180
//+kubebuilder:object:root=true
166181

167182
// ImageClusterInstallList contains a list of ImageClusterInstall

api/v1alpha1/imageclusterinstall_webhook.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"reflect"
2424
"strings"
2525

26+
"github.com/go-logr/logr"
2627
"golang.org/x/crypto/ssh"
2728
"k8s.io/apimachinery/pkg/runtime"
2829
"k8s.io/apimachinery/pkg/util/validation"
@@ -90,10 +91,12 @@ func (r *ImageClusterInstall) validate() error {
9091
return fmt.Errorf("invalid hostname: %w", err)
9192
}
9293

93-
if err := isValidMachineNetwork(r.Spec.MachineNetwork); err != nil {
94+
if err := isValidMachineNetworks(r.Spec.MachineNetwork, r.Spec.MachineNetworks); err != nil {
9495
return fmt.Errorf("invalid machine network: %w", err)
9596
}
96-
if err := isValidProxy(r.Spec.Proxy, r.Spec.MachineNetwork); err != nil {
97+
98+
effectiveMachineNetworks := getEffectiveMachineNetworks(r.Spec.MachineNetwork, r.Spec.MachineNetworks, icilog)
99+
if err := isValidProxy(r.Spec.Proxy, effectiveMachineNetworks); err != nil {
97100
return fmt.Errorf("invalid proxy: %w", err)
98101
}
99102
return nil
@@ -139,20 +142,56 @@ func isValidHostname(hostname string) error {
139142
return nil
140143
}
141144

142-
func isValidMachineNetwork(machineNetwork string) error {
143-
if machineNetwork == "" {
144-
return nil
145+
// isValidMachineNetworks validates both legacy MachineNetwork and new MachineNetworks fields
146+
func isValidMachineNetworks(legacyMachineNetwork string, machineNetworks []MachineNetworkEntry) error {
147+
// If both are specified, MachineNetworks takes precedence but we still validate both
148+
if legacyMachineNetwork != "" {
149+
if err := isValidNetworkCidr(legacyMachineNetwork); err != nil {
150+
return err
151+
}
152+
}
153+
154+
for i, network := range machineNetworks {
155+
if err := isValidNetworkCidr(network.CIDR); err != nil {
156+
return fmt.Errorf("machine network %d: %w", i, err)
157+
}
158+
}
159+
160+
return nil
161+
}
162+
163+
// getEffectiveMachineNetworks returns the effective machine networks for other validations
164+
func getEffectiveMachineNetworks(legacyMachineNetwork string, machineNetworks []MachineNetworkEntry, log logr.Logger) []string {
165+
if len(machineNetworks) > 0 {
166+
networks := make([]string, len(machineNetworks))
167+
for i, network := range machineNetworks {
168+
networks[i] = network.CIDR
169+
}
170+
return networks
171+
}
172+
173+
if legacyMachineNetwork != "" {
174+
log.Info("Using legacy MachineNetwork field", "legacyMachineNetwork", legacyMachineNetwork)
175+
return []string{legacyMachineNetwork}
176+
}
177+
178+
return nil
179+
}
180+
181+
func isValidNetworkCidr(networkCIDR string) error {
182+
if networkCIDR == "" {
183+
return fmt.Errorf("network CIDR is empty")
145184
}
146185

147-
_, _, err := net.ParseCIDR(machineNetwork)
186+
_, _, err := net.ParseCIDR(networkCIDR)
148187
if err != nil {
149-
return fmt.Errorf("error parsing machine network, check that it is valid cidr: %w", err)
188+
return fmt.Errorf("error parsing network, check that it is valid CIDR: %w", err)
150189
}
151190
return nil
152191
}
153192

154-
func isValidProxy(proxy *Proxy, machineNetwork string) error {
155-
if proxy != nil && machineNetwork == "" {
193+
func isValidProxy(proxy *Proxy, machineNetworks []string) error {
194+
if proxy != nil && len(machineNetworks) == 0 {
156195
return errors.New("machine network must be set when proxy is defined")
157196
}
158197
return nil

api/v1alpha1/imageclusterinstall_webhook_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,65 @@ var _ = Describe("ValidateUpdate", func() {
192192
Expect(err.Error()).To(ContainSubstring("invalid machine network"))
193193
})
194194

195+
It("create success when dual-stack machine networks are valid", func() {
196+
newClusterInstall := &ImageClusterInstall{
197+
ObjectMeta: metav1.ObjectMeta{
198+
Name: "config",
199+
Namespace: "test-namespace",
200+
},
201+
Spec: ImageClusterInstallSpec{
202+
MachineNetworks: []MachineNetworkEntry{
203+
{CIDR: "192.0.2.0/24"},
204+
{CIDR: "2001:db8::/64"},
205+
},
206+
},
207+
}
208+
209+
warns, err := newClusterInstall.ValidateCreate()
210+
Expect(warns).To(BeNil())
211+
Expect(err).To(BeNil())
212+
})
213+
214+
It("create fail when machine networks have invalid CIDR", func() {
215+
newClusterInstall := &ImageClusterInstall{
216+
ObjectMeta: metav1.ObjectMeta{
217+
Name: "config",
218+
Namespace: "test-namespace",
219+
},
220+
Spec: ImageClusterInstallSpec{
221+
MachineNetworks: []MachineNetworkEntry{
222+
{CIDR: "192.0.2.0/24"},
223+
{CIDR: "invalid_cidr"},
224+
},
225+
},
226+
}
227+
228+
warns, err := newClusterInstall.ValidateCreate()
229+
Expect(warns).To(BeNil())
230+
Expect(err.Error()).To(ContainSubstring("invalid machine network"))
231+
Expect(err.Error()).To(ContainSubstring("machine network 1"))
232+
})
233+
234+
It("create success when both legacy and new machine network fields are valid", func() {
235+
newClusterInstall := &ImageClusterInstall{
236+
ObjectMeta: metav1.ObjectMeta{
237+
Name: "config",
238+
Namespace: "test-namespace",
239+
},
240+
Spec: ImageClusterInstallSpec{
241+
MachineNetwork: "192.0.2.0/24", // Legacy field
242+
MachineNetworks: []MachineNetworkEntry{ // New field takes precedence
243+
{CIDR: "192.0.2.0/24"},
244+
{CIDR: "2001:db8::/64"},
245+
},
246+
},
247+
}
248+
249+
warns, err := newClusterInstall.ValidateCreate()
250+
Expect(warns).To(BeNil())
251+
Expect(err).To(BeNil())
252+
})
253+
195254
It("create fail when proxy is invalid", func() {
196255
newClusterInstall := &ImageClusterInstall{
197256
ObjectMeta: metav1.ObjectMeta{

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/extensions.hive.openshift.io_imageclusterinstalls.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,26 @@ spec:
282282
MachineNetwork is the subnet provided by user for the ocp cluster.
283283
This will be used to create the node network and choose ip address for the node.
284284
Equivalent to install-config.yaml's machineNetwork.
285+
Deprecated: Use MachineNetworks instead.
285286
type: string
287+
machineNetworks:
288+
description: |-
289+
MachineNetworks is the list of IP address pools for machines.
290+
This enables dual-stack support by allowing multiple networks.
291+
If both MachineNetwork and MachineNetworks are specified, MachineNetworks takes precedence.
292+
Use instead of MachineNetwork.
293+
items:
294+
description: MachineNetworkEntry is a single IP address block for
295+
node IP blocks.
296+
properties:
297+
cidr:
298+
description: CIDR is the IP block address pool for machines
299+
within the cluster.
300+
type: string
301+
required:
302+
- cidr
303+
type: object
304+
type: array
286305
nodeIP:
287306
description: |-
288307
NodeIP is the desired IP for the host

config/samples/extensions_v1alpha1_imageclusterinstall.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ spec:
1313
bareMetalHostRef:
1414
name: host-0
1515
namespace: test-sno
16+
machineNetworks:
17+
- cidr: "192.0.2.0/24"
18+
- cidr: "2001:db8::/64"

controllers/imageclusterinstall_controller.go

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -448,27 +448,134 @@ func (r *ImageClusterInstallReconciler) validateBMH(
448448
}
449449

450450
// do not requeue in case of invalid BMH
451-
err := r.validateBMHMachineNetwork(ici.Spec.MachineNetwork, *bmh.Status.HardwareDetails)
451+
err := r.validateBMHMachineNetworks(ici.Spec.MachineNetwork, ici.Spec.MachineNetworks, *bmh.Status.HardwareDetails)
452452
if err != nil {
453453
cond.Message = err.Error()
454454
return ctrl.Result{}, err
455455
}
456456
return ctrl.Result{}, nil
457457
}
458458

459-
func (r *ImageClusterInstallReconciler) validateBMHMachineNetwork(
460-
machineNetwork string,
459+
func (r *ImageClusterInstallReconciler) validateBMHMachineNetworks(
460+
legacyMachineNetwork string,
461+
machineNetworks []v1alpha1.MachineNetworkEntry,
461462
hwDetails bmh_v1alpha1.HardwareDetails) error {
462-
if machineNetwork == "" {
463+
464+
effectiveNetworks, err := getEffectiveMachineNetworksCIDRs(legacyMachineNetwork, machineNetworks, r.Log)
465+
if err != nil {
466+
return err
467+
}
468+
if len(effectiveNetworks) == 0 {
463469
return nil
464470
}
465-
for _, nic := range hwDetails.NIC {
466-
inCIDR, _ := ipInCidr(nic.IP, machineNetwork)
467-
if inCIDR {
468-
return nil
471+
472+
if len(effectiveNetworks) == 1 {
473+
// Single-stack: check if any NIC has an IP in the machine network
474+
for _, nic := range hwDetails.NIC {
475+
if nic.IP == "" {
476+
continue
477+
}
478+
inCIDR, _ := ipInCidr(nic.IP, effectiveNetworks[0])
479+
if inCIDR {
480+
return nil
481+
}
482+
}
483+
return fmt.Errorf("bmh host doesn't have any nic with ip in provided machineNetwork %v", effectiveNetworks[0])
484+
}
485+
486+
// Dual-stack: need to find a NIC that has both IPv4 and IPv6 addresses
487+
// in the corresponding machine networks
488+
return r.validateDualStackMachineNetworks(effectiveNetworks, hwDetails.NIC)
489+
}
490+
491+
// validateDualStackMachineNetworks validates that there's a NIC with both
492+
// IPv4 and IPv6 addresses in their corresponding machine networks
493+
func (r *ImageClusterInstallReconciler) validateDualStackMachineNetworks(networks []string, nics []bmh_v1alpha1.NIC) error {
494+
var ipv4Networks, ipv6Networks []string
495+
for _, network := range networks {
496+
if isIPv4CIDR(network) {
497+
ipv4Networks = append(ipv4Networks, network)
498+
} else {
499+
ipv6Networks = append(ipv6Networks, network)
500+
}
501+
}
502+
503+
if len(ipv4Networks) == 0 || len(ipv6Networks) == 0 {
504+
return fmt.Errorf("dual-stack configuration requires both IPv4 and IPv6 machine networks, got IPv4: %v, IPv6: %v", ipv4Networks, ipv6Networks)
505+
}
506+
507+
nicsByMAC := make(map[string][]bmh_v1alpha1.NIC)
508+
for _, nic := range nics {
509+
if nic.MAC != "" && nic.IP != "" {
510+
nicsByMAC[nic.MAC] = append(nicsByMAC[nic.MAC], nic)
469511
}
470512
}
471-
return fmt.Errorf("bmh host doesn't have any nic with ip in provided machineNetwork %s", machineNetwork)
513+
514+
for _, macNics := range nicsByMAC {
515+
hasIPv4InNetwork := false
516+
hasIPv6InNetwork := false
517+
518+
for _, nic := range macNics {
519+
for _, ipv4Net := range ipv4Networks {
520+
if inCIDR, _ := ipInCidr(nic.IP, ipv4Net); inCIDR {
521+
hasIPv4InNetwork = true
522+
break
523+
}
524+
}
525+
526+
for _, ipv6Net := range ipv6Networks {
527+
if inCIDR, _ := ipInCidr(nic.IP, ipv6Net); inCIDR {
528+
hasIPv6InNetwork = true
529+
break
530+
}
531+
}
532+
533+
if hasIPv4InNetwork && hasIPv6InNetwork {
534+
return nil
535+
}
536+
}
537+
}
538+
539+
return fmt.Errorf("bmh host doesn't have a nic with both IPv4 and IPv6 addresses in the provided dual-stack machineNetworks. IPv4 networks: %v, IPv6 networks: %v", ipv4Networks, ipv6Networks)
540+
}
541+
542+
// isIPv4CIDR determines if a CIDR string represents an IPv4 network
543+
func isIPv4CIDR(cidr string) bool {
544+
_, ipNet, err := net.ParseCIDR(cidr)
545+
if err != nil {
546+
return false
547+
}
548+
return ipNet.IP.To4() != nil
549+
}
550+
551+
// getEffectiveMachineNetworksCIDRs returns the effective machine network CIDRs.
552+
// If both MachineNetwork and MachineNetworks are specified, MachineNetwork should match
553+
// the first element of MachineNetworks.
554+
func getEffectiveMachineNetworksCIDRs(
555+
legacyMachineNetwork string,
556+
machineNetworks []v1alpha1.MachineNetworkEntry,
557+
log logrus.FieldLogger,
558+
) ([]string, error) {
559+
if len(machineNetworks) > 0 {
560+
if legacyMachineNetwork != "" {
561+
first := machineNetworks[0].CIDR
562+
if legacyMachineNetwork != first {
563+
return nil, fmt.Errorf("MachineNetwork (%s) does not match the first MachineNetworks entry (%s)", legacyMachineNetwork, first)
564+
}
565+
}
566+
networks := make([]string, len(machineNetworks))
567+
for i, network := range machineNetworks {
568+
networks[i] = network.CIDR
569+
}
570+
return networks, nil
571+
}
572+
573+
if legacyMachineNetwork != "" {
574+
log.Infof("Using legacy MachineNetwork field '%s' - consider migrating to MachineNetworks field", legacyMachineNetwork)
575+
return []string{legacyMachineNetwork}, nil
576+
}
577+
578+
return nil, nil
472579
}
473580

474581
func (r *ImageClusterInstallReconciler) mapBMHToICI(ctx context.Context, obj client.Object) []reconcile.Request {

0 commit comments

Comments
 (0)