Skip to content

Commit 636b635

Browse files
Merge pull request #440 from rabi/fix_ip_fr3
[18.0-fr3] Fix ip reassignment for ipset after scale-in
2 parents f18bd78 + 9d7d179 commit 636b635

File tree

5 files changed

+157
-43
lines changed

5 files changed

+157
-43
lines changed

apis/network/v1beta1/ipset_webhook.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (r *IPSet) ValidateCreate() (admission.Warnings, error) {
8282
basePath := field.NewPath("spec")
8383

8484
// validate requested networks exist in netcfg
85-
allErrs = append(allErrs, valiateIPSetNetwork(r.Spec.Networks, basePath, &netcfg.Spec)...)
85+
allErrs = append(allErrs, validateIPSetNetwork(r.Spec.Networks, basePath, &netcfg.Spec)...)
8686

8787
if len(allErrs) == 0 {
8888
return nil, nil
@@ -132,10 +132,10 @@ func (r *IPSet) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
132132
basePath := field.NewPath("spec")
133133

134134
// validate requested networks exist in
135-
allErrs = append(allErrs, valiateIPSetNetwork(r.Spec.Networks, basePath, &netcfg.Spec)...)
135+
allErrs = append(allErrs, validateIPSetNetwork(r.Spec.Networks, basePath, &netcfg.Spec)...)
136136

137137
// validate against the previous object only
138-
allErrs = append(allErrs, valiateIPSetChanged(r.Spec.Networks, oldIPSet.Spec.Networks, basePath)...)
138+
allErrs = append(allErrs, validateIPSetChanged(r.Spec.Networks, oldIPSet.Spec.Networks, basePath)...)
139139
}
140140

141141
if len(allErrs) == 0 {
@@ -153,14 +153,14 @@ func (r *IPSet) ValidateDelete() (admission.Warnings, error) {
153153
return nil, nil
154154
}
155155

156-
// valiateIPSetNetwork
156+
// validateIPSetNetwork
157157
// - networks are uniq in the list
158158
// - networks and subnets exist in netcfg
159159
// - FixedIP is a valid IP address
160160
// - FixedIP has correct IP version of subnet
161161
// - FixedIP is in the subnet cidr
162162
// - Route is only specified on a single network per IPFamily
163-
func valiateIPSetNetwork(
163+
func validateIPSetNetwork(
164164
networks []IPSetNetwork,
165165
path *field.Path,
166166
netCfgSpec *NetConfigSpec,
@@ -246,12 +246,12 @@ func valiateIPSetNetwork(
246246
return allErrs
247247
}
248248

249-
// valiateIPSetChanged
249+
// validateIPSetChanged
250250
// - if a previous requested network is still in the list
251251
// - if subnet changed within a network
252252
// - if fixedIP changed
253253
// - if defaultRoute changed
254-
func valiateIPSetChanged(
254+
func validateIPSetChanged(
255255
networks []IPSetNetwork,
256256
oldNetworks []IPSetNetwork,
257257
path *field.Path,

apis/network/v1beta1/ipset_webhook_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func TestIPSetValiateIPSetNetwork(t *testing.T) {
289289
basePath := field.NewPath("spec")
290290

291291
var err error
292-
allErrs := valiateIPSetNetwork(tt.c.Spec.Networks, basePath, &tt.n)
292+
allErrs := validateIPSetNetwork(tt.c.Spec.Networks, basePath, &tt.n)
293293
if len(allErrs) > 0 {
294294
err = apierrors.NewInvalid(GroupVersion.WithKind("NetConfig").GroupKind(), tt.c.Name, allErrs)
295295
}
@@ -477,12 +477,12 @@ func TestIPSetUpdateValidation(t *testing.T) {
477477

478478
var err error
479479

480-
allErrs := valiateIPSetNetwork(tt.newSpec.Networks, basePath, &tt.n)
480+
allErrs := validateIPSetNetwork(tt.newSpec.Networks, basePath, &tt.n)
481481
if len(allErrs) > 0 {
482482
err = apierrors.NewInvalid(GroupVersion.WithKind("IPSet").GroupKind(), newCfg.Name, allErrs)
483483
}
484484

485-
allErrs = valiateIPSetChanged(tt.newSpec.Networks, tt.oldSpec.Networks, basePath)
485+
allErrs = validateIPSetChanged(tt.newSpec.Networks, tt.oldSpec.Networks, basePath)
486486
if len(allErrs) > 0 {
487487
err = apierrors.NewInvalid(GroupVersion.WithKind("IPSet").GroupKind(), newCfg.Name, allErrs)
488488
}

controllers/network/ipset_controller.go

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"sort"
2525
"strings"
2626

27-
corev1 "k8s.io/api/core/v1"
2827
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
2928
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3029
"k8s.io/apimachinery/pkg/runtime"
@@ -383,13 +382,15 @@ func (r *IPSetReconciler) ensureReservation(
383382
Namespace: ipset.Namespace,
384383
Name: ipset.Name,
385384
}
386-
reservationSpec := networkv1.ReservationSpec{
387-
IPSetRef: corev1.ObjectReference{
388-
Namespace: ipset.Namespace,
389-
Name: ipset.Name,
390-
UID: ipset.GetUID(),
391-
},
392-
Reservation: map[string]networkv1.IPAddress{},
385+
// Get existing reservation to preserve IP assignments
386+
reservation, err := r.getReservation(ctx, ipset)
387+
var reservationSpec = reservation.Spec
388+
389+
if err != nil {
390+
if !k8s_errors.IsNotFound(err) {
391+
return nil, err
392+
}
393+
reservationSpec.Reservation = map[string]networkv1.IPAddress{}
393394
}
394395
reservationLabels := map[string]string{}
395396

@@ -425,30 +426,39 @@ func (r *IPSetReconciler) ensureReservation(
425426
fmt.Sprintf("%s/%s", ipam.IPAMLabelKey, string(netDef.Name)): string(subnetDef.Name),
426427
})
427428

428-
ipDetails := ipam.AssignIPDetails{
429-
IPSet: ipset.Name,
430-
NetName: string(netDef.Name),
431-
SubNet: subnetDef,
432-
Reservelist: reservations,
433-
}
429+
var ip *networkv1.IPAddress
430+
431+
// Check if we already have an IP for this network
432+
if existingIP, exists := reservationSpec.Reservation[string(netDef.Name)]; exists {
433+
// Use existing IP assignment
434+
ip = &existingIP
435+
} else {
436+
// Need to assign a new IP
437+
ipDetails := ipam.AssignIPDetails{
438+
IPSet: ipset.Name,
439+
NetName: string(netDef.Name),
440+
SubNet: subnetDef,
441+
Reservelist: reservations,
442+
}
434443

435-
if ipsetNet.FixedIP != nil {
436-
ipDetails.FixedIP, err = netip.ParseAddr(string(*ipsetNet.FixedIP))
437-
if err != nil {
438-
return nil, fmt.Errorf("failed parse FixedIP %s", string(*ipsetNet.FixedIP))
444+
if ipsetNet.FixedIP != nil {
445+
ipDetails.FixedIP, err = netip.ParseAddr(string(*ipsetNet.FixedIP))
446+
if err != nil {
447+
return nil, fmt.Errorf("failed parse FixedIP %s", string(*ipsetNet.FixedIP))
448+
}
449+
if !ipDetails.FixedIP.IsValid() {
450+
return nil, fmt.Errorf("failed parse FixedIP %s", string(*ipsetNet.FixedIP))
451+
}
439452
}
440-
if !ipDetails.FixedIP.IsValid() {
441-
return nil, fmt.Errorf("failed parse FixedIP %s", string(*ipsetNet.FixedIP))
453+
454+
ip, err = ipDetails.AssignIP()
455+
if err != nil {
456+
return nil, fmt.Errorf("failed to do ip reservation: %w", err)
442457
}
443-
}
444458

445-
ip, err := ipDetails.AssignIP()
446-
if err != nil {
447-
return nil, fmt.Errorf("failed to do ip reservation: %w", err)
459+
// Add the new IP to the reservation
460+
reservationSpec.Reservation[string(netDef.Name)] = *ip
448461
}
449-
450-
// add IP to the reservation and IPSet status reservations
451-
reservationSpec.Reservation[string(netDef.Name)] = *ip
452462
ipsetRes := networkv1.IPSetReservation{
453463
Network: netDef.Name,
454464
Subnet: subnetDef.Name,

pkg/ipam/funcs.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type AssignIPDetails struct {
1616
FixedIP netip.Addr
1717
// internal use only
1818
excluded map[netip.Addr]bool
19-
reserved map[netip.Addr]string
19+
reserved map[netip.Addr]bool
2020
}
2121

2222
func (a *AssignIPDetails) buildExcluded() error {
@@ -38,11 +38,11 @@ func (a *AssignIPDetails) buildReserved() error {
3838
if a.reserved != nil {
3939
return nil
4040
}
41-
a.reserved = make(map[netip.Addr]string, len(a.Reservelist.Items))
41+
a.reserved = make(map[netip.Addr]bool, len(a.Reservelist.Items))
4242
for _, r := range a.Reservelist.Items {
4343
if res, ok := r.Spec.Reservation[a.NetName]; ok {
4444
if ip, err := netip.ParseAddr(res.Address); err == nil {
45-
a.reserved[ip] = r.Spec.IPSetRef.Name
45+
a.reserved[ip] = true
4646
} else {
4747
return fmt.Errorf("failed to parse reservation ip %s: %w", res.Address, err)
4848
}
@@ -87,8 +87,8 @@ func (a *AssignIPDetails) fixedIPExists() (*networkv1.IPAddress, error) {
8787
return nil, fmt.Errorf("FixedIP %s is in ExcludeAddresses", a.FixedIP.String())
8888
}
8989

90-
if reservedIPSet, ok := a.reserved[a.FixedIP]; ok && reservedIPSet != a.IPSet {
91-
return nil, fmt.Errorf("%s already reserved for %s", a.FixedIP.String(), reservedIPSet)
90+
if _, ok := a.reserved[a.FixedIP]; ok {
91+
return nil, fmt.Errorf("%s already reserved", a.FixedIP.String())
9292
}
9393

9494
return &networkv1.IPAddress{
@@ -132,7 +132,7 @@ func (a *AssignIPDetails) iterateForAssignment() (*networkv1.IPAddress, error) {
132132
if _, ok := a.excluded[ip]; ok {
133133
continue
134134
}
135-
if reservedIPSet, ok := a.reserved[ip]; ok && reservedIPSet != a.IPSet {
135+
if _, ok := a.reserved[ip]; ok {
136136
continue
137137
}
138138

tests/functional/ipset_controller_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,110 @@ var _ = Describe("IPSet controller", func() {
392392
})
393393
})
394394

395+
When("IP assignments are preserved during reconciliation", func() {
396+
var netCfgName types.NamespacedName
397+
var ipSetAName types.NamespacedName
398+
var ipSetBName types.NamespacedName
399+
400+
BeforeEach(func() {
401+
// Create NetConfig with allocation range that allows multiple IPs
402+
netCfg := CreateNetConfig(namespace, GetDefaultNetConfigSpec())
403+
netCfgName.Name = netCfg.GetName()
404+
netCfgName.Namespace = netCfg.GetNamespace()
405+
406+
Eventually(func(g Gomega) {
407+
res := GetNetConfig(netCfgName)
408+
g.Expect(res).ToNot(BeNil())
409+
}, timeout, interval).Should(Succeed())
410+
411+
// Create IPSet A - will get first available IP (172.17.0.100)
412+
ipsetA := CreateIPSet(namespace, GetDefaultIPSetSpec())
413+
ipSetAName = types.NamespacedName{
414+
Name: ipsetA.GetName(),
415+
Namespace: namespace,
416+
}
417+
418+
// Create IPSet B - will get second available IP (172.17.0.101)
419+
ipsetB := CreateIPSet(namespace, GetDefaultIPSetSpec())
420+
ipSetBName = types.NamespacedName{
421+
Name: ipsetB.GetName(),
422+
Namespace: namespace,
423+
}
424+
425+
// Wait for both IPSets to be ready
426+
th.ExpectCondition(
427+
ipSetAName,
428+
ConditionGetterFunc(IPSetConditionGetter),
429+
condition.ReadyCondition,
430+
corev1.ConditionTrue,
431+
)
432+
433+
th.ExpectCondition(
434+
ipSetBName,
435+
ConditionGetterFunc(IPSetConditionGetter),
436+
condition.ReadyCondition,
437+
corev1.ConditionTrue,
438+
)
439+
440+
DeferCleanup(func(_ SpecContext) {
441+
th.DeleteInstance(ipsetA)
442+
th.DeleteInstance(ipsetB)
443+
th.DeleteInstance(netCfg)
444+
}, NodeTimeout(timeout))
445+
})
446+
447+
It("should preserve existing IP assignments when other IPSets are deleted", func() {
448+
// Get the initial IP assignments
449+
resA := GetReservationFromNet(ipSetAName, "net-1")
450+
resB := GetReservationFromNet(ipSetBName, "net-1")
451+
452+
// Verify they got different IPs
453+
Expect(resA.Address).ToNot(Equal(resB.Address))
454+
Expect(resA.Address).ToNot(BeEmpty())
455+
Expect(resB.Address).ToNot(BeEmpty())
456+
457+
originalIPSetA := resA.Address
458+
459+
// Delete IPSet B - this should free up its IP address
460+
ipsetB := GetIPSet(ipSetBName)
461+
th.DeleteInstance(ipsetB)
462+
463+
// Wait for IPSet B to be deleted
464+
Eventually(func() error {
465+
instance := &networkv1.IPSet{}
466+
return k8sClient.Get(ctx, ipSetBName, instance)
467+
}, timeout, interval).Should(MatchError(ContainSubstring("not found")))
468+
469+
// Force reconciliation of IPSet A by updating a field
470+
Eventually(func(g Gomega) {
471+
instance := GetIPSet(ipSetAName)
472+
// Add an annotation to trigger reconciliation
473+
if instance.Annotations == nil {
474+
instance.Annotations = make(map[string]string)
475+
}
476+
instance.Annotations["test.force.reconcile"] = "true"
477+
g.Expect(k8sClient.Update(ctx, instance)).Should(Succeed())
478+
}, timeout, interval).Should(Succeed())
479+
480+
// Give the controller time to reconcile
481+
Eventually(func(g Gomega) {
482+
instance := GetIPSet(ipSetAName)
483+
g.Expect(instance.Annotations["test.force.reconcile"]).To(Equal("true"))
484+
th.ExpectCondition(
485+
ipSetAName,
486+
ConditionGetterFunc(IPSetConditionGetter),
487+
condition.ReadyCondition,
488+
corev1.ConditionTrue,
489+
)
490+
}, timeout, interval).Should(Succeed())
491+
492+
// Verify IPSet A still has its original IP (not the freed IP from B)
493+
resAAfterReconcile := GetReservationFromNet(ipSetAName, "net-1")
494+
Expect(resAAfterReconcile.Address).To(Equal(originalIPSetA))
495+
Expect(resAAfterReconcile.Address).ToNot(Equal(resB.Address))
496+
})
497+
})
498+
395499
When("an IPSet with Immutable flag gets created", func() {
396500
BeforeEach(func() {
397501
net1Spec := GetNetSpec(net1, GetSubnet1(subnet1))

0 commit comments

Comments
 (0)