Skip to content

Commit f111b97

Browse files
committed
neonvm-controller: fail releasing IP (#1278)
We must not leak IP addresses. The release will be retried. Also, we have to make it idempotent. Signed-off-by: Oleg Vasilev <[email protected]>
1 parent d49cb94 commit f111b97

File tree

4 files changed

+65
-12
lines changed

4 files changed

+65
-12
lines changed

pkg/neonvm/controllers/vm_controller.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,10 @@ func (r *VMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re
151151
if controllerutil.ContainsFinalizer(&vm, virtualmachineFinalizer) {
152152
// our finalizer is present, so lets handle any external dependency
153153
log.Info("Performing Finalizer Operations for VirtualMachine before delete it")
154-
r.doFinalizerOperationsForVirtualMachine(ctx, &vm)
154+
if err := r.doFinalizerOperationsForVirtualMachine(ctx, &vm); err != nil {
155+
log.Error(err, "Failed to perform finalizer operations for VirtualMachine")
156+
return ctrl.Result{}, err
157+
}
155158

156159
// remove our finalizer from the list and update it.
157160
log.Info("Removing Finalizer for VirtualMachine after successfully perform the operations")
@@ -226,7 +229,7 @@ func (r *VMReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Re
226229
}
227230

228231
// doFinalizerOperationsForVirtualMachine will perform the required operations before delete the CR.
229-
func (r *VMReconciler) doFinalizerOperationsForVirtualMachine(ctx context.Context, vm *vmv1.VirtualMachine) {
232+
func (r *VMReconciler) doFinalizerOperationsForVirtualMachine(ctx context.Context, vm *vmv1.VirtualMachine) error {
230233
// Note: It is not recommended to use finalizers with the purpose of delete resources which are
231234
// created and managed in the reconciliation. These ones, such as the Pod created on this reconcile,
232235
// are defined as depended of the custom resource. See that we use the method ctrl.SetControllerReference.
@@ -245,14 +248,13 @@ func (r *VMReconciler) doFinalizerOperationsForVirtualMachine(ctx context.Contex
245248
if vm.Spec.ExtraNetwork != nil {
246249
ip, err := r.IPAM.ReleaseIP(ctx, types.NamespacedName{Name: vm.Name, Namespace: vm.Namespace})
247250
if err != nil {
248-
// ignore error
249-
log.Error(err, "fail to release IP, error ignored")
250-
return
251+
return fmt.Errorf("fail to release IP: %w", err)
251252
}
252253
message := fmt.Sprintf("Released IP %s", ip.String())
253254
log.Info(message)
254255
r.Recorder.Event(vm, "Normal", "OverlayNet", message)
255256
}
257+
return nil
256258
}
257259

258260
func getRunnerVersion(pod *corev1.Pod) (api.RunnerProtoVersion, error) {

pkg/neonvm/ipam/allocate.go

+16-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package ipam
22

33
import (
4+
"context"
45
"net"
56

67
whereaboutsallocate "github.com/k8snetworkplumbingwg/whereabouts/pkg/allocate"
78
whereaboutslogging "github.com/k8snetworkplumbingwg/whereabouts/pkg/logging"
89
whereaboutstypes "github.com/k8snetworkplumbingwg/whereabouts/pkg/types"
10+
"sigs.k8s.io/controller-runtime/pkg/log"
911

1012
"k8s.io/apimachinery/pkg/types"
1113
)
@@ -16,20 +18,21 @@ type ipamAction = func(
1618
) (net.IPNet, []whereaboutstypes.IPReservation, error)
1719

1820
// makeAcquireAction creates a callback which changes IPPool state to include a new IP reservation.
19-
func makeAcquireAction(vmName types.NamespacedName) ipamAction {
21+
func makeAcquireAction(ctx context.Context, vmName types.NamespacedName) ipamAction {
2022
return func(ipRange RangeConfiguration, reservation []whereaboutstypes.IPReservation) (net.IPNet, []whereaboutstypes.IPReservation, error) {
21-
return doAcquire(ipRange, reservation, vmName)
23+
return doAcquire(ctx, ipRange, reservation, vmName)
2224
}
2325
}
2426

2527
// makeReleaseAction creates a callback which changes IPPool state to deallocate an IP reservation.
26-
func makeReleaseAction(vmName types.NamespacedName) ipamAction {
28+
func makeReleaseAction(ctx context.Context, vmName types.NamespacedName) ipamAction {
2729
return func(ipRange RangeConfiguration, reservation []whereaboutstypes.IPReservation) (net.IPNet, []whereaboutstypes.IPReservation, error) {
28-
return doRelease(ipRange, reservation, vmName)
30+
return doRelease(ctx, ipRange, reservation, vmName)
2931
}
3032
}
3133

3234
func doAcquire(
35+
_ context.Context,
3336
ipRange RangeConfiguration,
3437
reservation []whereaboutstypes.IPReservation,
3538
vmName types.NamespacedName,
@@ -57,19 +60,27 @@ func doAcquire(
5760
}
5861

5962
func doRelease(
63+
ctx context.Context,
6064
ipRange RangeConfiguration,
6165
reservation []whereaboutstypes.IPReservation,
6266
vmName types.NamespacedName,
6367
) (net.IPNet, []whereaboutstypes.IPReservation, error) {
6468
// reduce whereabouts logging
6569
whereaboutslogging.SetLogLevel("error")
6670

71+
log := log.FromContext(ctx)
72+
6773
_, ipnet, _ := net.ParseCIDR(ipRange.Range)
6874

6975
// try to release IP for given VM
7076
newReservation, ip, err := whereaboutsallocate.IterateForDeallocation(reservation, vmName.String(), getMatchingIPReservationIndex)
7177
if err != nil {
72-
return net.IPNet{}, nil, err
78+
// The only reason to get an error here is if we are trying
79+
// to deallocate the same IP twice.
80+
log.Info("Failed to deallocate IP", "error", err)
81+
82+
// Ignore the error.
83+
return net.IPNet{IP: ip, Mask: ipnet.Mask}, newReservation, nil
7384
}
7485

7586
return net.IPNet{IP: ip, Mask: ipnet.Mask}, newReservation, nil

pkg/neonvm/ipam/ipam.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ type IPAM struct {
5656
}
5757

5858
func (i *IPAM) AcquireIP(ctx context.Context, vmName types.NamespacedName) (net.IPNet, error) {
59-
ip, err := i.runIPAM(ctx, makeAcquireAction(vmName))
59+
ip, err := i.runIPAM(ctx, makeAcquireAction(ctx, vmName))
6060
if err != nil {
6161
return net.IPNet{}, fmt.Errorf("failed to acquire IP: %w", err)
6262
}
6363
return ip, nil
6464
}
6565

6666
func (i *IPAM) ReleaseIP(ctx context.Context, vmName types.NamespacedName) (net.IPNet, error) {
67-
ip, err := i.runIPAM(ctx, makeReleaseAction(vmName))
67+
ip, err := i.runIPAM(ctx, makeReleaseAction(ctx, vmName))
6868
if err != nil {
6969
return net.IPNet{}, fmt.Errorf("failed to release IP: %w", err)
7070
}

pkg/neonvm/ipam/ipam_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ipam_test
33
import (
44
"context"
55
"fmt"
6+
"net"
67
"testing"
78

89
nadv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
@@ -95,3 +96,42 @@ func TestIPAM(t *testing.T) {
9596
require.NotNil(t, ip3)
9697
assert.Equal(t, ip2, ip3)
9798
}
99+
100+
func TestIPAMReleaseTwice(t *testing.T) {
101+
ipam := makeIPAM(t,
102+
`{
103+
"ipRanges": [
104+
{
105+
"range":"10.100.123.0/24",
106+
"range_start":"10.100.123.1",
107+
"range_end":"10.100.123.254"
108+
}
109+
]
110+
}`,
111+
)
112+
113+
defer ipam.Close()
114+
115+
name := types.NamespacedName{
116+
Namespace: "default",
117+
Name: "vm",
118+
}
119+
120+
ip, err := ipam.AcquireIP(context.Background(), name)
121+
require.NoError(t, err)
122+
require.NotNil(t, ip)
123+
124+
// Release the IP
125+
ipResult, err := ipam.ReleaseIP(context.Background(), name)
126+
require.NoError(t, err)
127+
require.Equal(t, ip, ipResult)
128+
129+
// Release the IP again
130+
ipResult, err = ipam.ReleaseIP(context.Background(), name)
131+
require.NoError(t, err)
132+
// This time we get nil IP
133+
require.Equal(t, net.IPNet{
134+
IP: nil,
135+
Mask: ip.Mask,
136+
}, ipResult)
137+
}

0 commit comments

Comments
 (0)