Skip to content

Commit dcae832

Browse files
Add machine reconcile delete on termination (#336)
* Add support for machine deletion after instance termination
1 parent 241cf91 commit dcae832

File tree

3 files changed

+136
-15
lines changed

3 files changed

+136
-15
lines changed

api/v1beta2/ocimachine_types.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ import (
2828
const (
2929
// MachineFinalizer allows ReconcileMachine to clean up OCI resources associated with OCIMachine before
3030
// removing it from the apiserver.
31-
MachineFinalizer = "ocimachine.infrastructure.cluster.x-k8s.io"
31+
MachineFinalizer = "ocimachine.infrastructure.cluster.x-k8s.io"
32+
DeleteMachineOnInstanceTermination = "ociclusters.x-k8s.io/delete-machine-on-instance-termination"
3233
)
3334

3435
// OCIMachineSpec defines the desired state of OCIMachine

controllers/ocimachine_controller.go

+22-1
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,13 @@ func (r *OCIMachineReconciler) OCIManagedClusterToOCIMachines() handler.MapFunc
293293
func (r *OCIMachineReconciler) reconcileNormal(ctx context.Context, logger logr.Logger, machineScope *scope.MachineScope) (ctrl.Result, error) {
294294
controllerutil.AddFinalizer(machineScope.OCIMachine, infrastructurev1beta2.MachineFinalizer)
295295
machine := machineScope.OCIMachine
296+
infraMachine := machineScope.Machine
297+
298+
annotations := infraMachine.GetAnnotations()
299+
deleteMachineOnTermination := false
300+
if annotations != nil {
301+
_, deleteMachineOnTermination = annotations[infrastructurev1beta2.DeleteMachineOnInstanceTermination]
302+
}
296303
// Make sure bootstrap data is available and populated.
297304
if machineScope.Machine.Spec.Bootstrap.DataSecretName == nil {
298305
r.Recorder.Event(machine, corev1.EventTypeNormal, infrastructurev1beta2.WaitingForBootstrapDataReason, "Bootstrap data secret reference is not yet available")
@@ -364,7 +371,21 @@ func (r *OCIMachineReconciler) reconcileNormal(ctx context.Context, logger logr.
364371
"Instance is in ready state")
365372
conditions.MarkTrue(machineScope.OCIMachine, infrastructurev1beta2.InstanceReadyCondition)
366373
machineScope.SetReady()
367-
return reconcile.Result{}, nil
374+
if deleteMachineOnTermination {
375+
// typically, if the VM is terminated, we should get machine events, so ideally, the 300 seconds
376+
// requeue time is not required, but in case, the event is missed, adding the requeue time
377+
return reconcile.Result{RequeueAfter: 300 * time.Second}, nil
378+
} else {
379+
return reconcile.Result{}, nil
380+
}
381+
case core.InstanceLifecycleStateTerminated:
382+
if deleteMachineOnTermination && infraMachine.DeletionTimestamp == nil {
383+
logger.Info("Deleting underlying machine as instance is terminated")
384+
if err := machineScope.Client.Delete(ctx, infraMachine); err != nil {
385+
return reconcile.Result{}, errors.Wrapf(err, "failed to delete machine %s/%s", infraMachine.Namespace, infraMachine.Name)
386+
}
387+
}
388+
fallthrough
368389
default:
369390
conditions.MarkFalse(machineScope.OCIMachine, infrastructurev1beta2.InstanceReadyCondition, infrastructurev1beta2.InstanceProvisionFailedReason, clusterv1.ConditionSeverityError, "")
370391
machineScope.SetFailureReason(capierrors.CreateMachineError)

controllers/ocimachine_controller_test.go

+112-13
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import (
4040
"k8s.io/client-go/tools/record"
4141
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
4242
"sigs.k8s.io/cluster-api/util/conditions"
43+
ctrl "sigs.k8s.io/controller-runtime"
4344
"sigs.k8s.io/controller-runtime/pkg/client"
4445
"sigs.k8s.io/controller-runtime/pkg/client/fake"
46+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
4547
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4648
"sigs.k8s.io/controller-runtime/pkg/log"
4749
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -196,19 +198,22 @@ func TestNormalReconciliationFunction(t *testing.T) {
196198
teardown := func(t *testing.T, g *WithT) {
197199
mockCtrl.Finish()
198200
}
199-
tests := []struct {
201+
type test struct {
200202
name string
201203
errorExpected bool
202204
expectedEvent string
203205
eventNotExpected string
204206
conditionAssertion []conditionAssertion
205-
testSpecificSetup func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient)
206-
}{
207+
deleteMachines []clusterv1.Machine
208+
testSpecificSetup func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient)
209+
validate func(g *WithT, t *test, result ctrl.Result)
210+
}
211+
tests := []test{
207212
{
208213
name: "instance in provisioning state",
209214
errorExpected: false,
210215
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityInfo, infrastructurev1beta2.InstanceNotReadyReason}},
211-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
216+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
212217
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
213218
InstanceId: common.String("test"),
214219
})).
@@ -224,7 +229,59 @@ func TestNormalReconciliationFunction(t *testing.T) {
224229
name: "instance in running state",
225230
errorExpected: false,
226231
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionTrue, "", ""}},
227-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
232+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
233+
machineScope.OCIMachine.Status.Addresses = []clusterv1.MachineAddress{
234+
{
235+
Type: clusterv1.MachineInternalIP,
236+
Address: "1.1.1.1",
237+
},
238+
}
239+
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
240+
InstanceId: common.String("test"),
241+
})).
242+
Return(core.GetInstanceResponse{
243+
Instance: core.Instance{
244+
Id: common.String("test"),
245+
LifecycleState: core.InstanceLifecycleStateRunning,
246+
},
247+
}, nil)
248+
},
249+
},
250+
{
251+
name: "instance in running state, reconcile every 5 minutes",
252+
errorExpected: false,
253+
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionTrue, "", ""}},
254+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
255+
if machineScope.Machine.ObjectMeta.Annotations == nil {
256+
machineScope.Machine.ObjectMeta.Annotations = make(map[string]string)
257+
}
258+
machineScope.Machine.ObjectMeta.Annotations[infrastructurev1beta2.DeleteMachineOnInstanceTermination] = ""
259+
machineScope.OCIMachine.Status.Addresses = []clusterv1.MachineAddress{
260+
{
261+
Type: clusterv1.MachineInternalIP,
262+
Address: "1.1.1.1",
263+
},
264+
}
265+
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
266+
InstanceId: common.String("test"),
267+
})).
268+
Return(core.GetInstanceResponse{
269+
Instance: core.Instance{
270+
Id: common.String("test"),
271+
LifecycleState: core.InstanceLifecycleStateRunning,
272+
},
273+
}, nil)
274+
},
275+
},
276+
{
277+
name: "instance in running state, reconcile every 5 minutes",
278+
errorExpected: false,
279+
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionTrue, "", ""}},
280+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
281+
if machineScope.Machine.ObjectMeta.Annotations == nil {
282+
machineScope.Machine.ObjectMeta.Annotations = make(map[string]string)
283+
}
284+
machineScope.Machine.ObjectMeta.Annotations[infrastructurev1beta2.DeleteMachineOnInstanceTermination] = ""
228285
machineScope.OCIMachine.Status.Addresses = []clusterv1.MachineAddress{
229286
{
230287
Type: clusterv1.MachineInternalIP,
@@ -241,13 +298,49 @@ func TestNormalReconciliationFunction(t *testing.T) {
241298
},
242299
}, nil)
243300
},
301+
validate: func(g *WithT, t *test, result ctrl.Result) {
302+
g.Expect(result.RequeueAfter).To(Equal(300 * time.Second))
303+
},
244304
},
245305
{
246306
name: "instance in terminated state",
247307
errorExpected: true,
248308
expectedEvent: "invalid lifecycle state TERMINATED",
249309
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityError, infrastructurev1beta2.InstanceProvisionFailedReason}},
250-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
310+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
311+
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
312+
InstanceId: common.String("test"),
313+
})).
314+
Return(core.GetInstanceResponse{
315+
Instance: core.Instance{
316+
Id: common.String("test"),
317+
LifecycleState: core.InstanceLifecycleStateTerminated,
318+
},
319+
}, nil)
320+
},
321+
validate: func(g *WithT, t *test, result ctrl.Result) {
322+
g.Expect(result.RequeueAfter).To(Equal(0 * time.Second))
323+
},
324+
},
325+
{
326+
name: "instance in terminated state, machine has to be deleted",
327+
errorExpected: true,
328+
expectedEvent: "invalid lifecycle state TERMINATED",
329+
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityError, infrastructurev1beta2.InstanceProvisionFailedReason}},
330+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
331+
fakeClient := fake.NewClientBuilder().WithObjects(getSecret()).Build()
332+
if machineScope.Machine.ObjectMeta.Annotations == nil {
333+
machineScope.Machine.ObjectMeta.Annotations = make(map[string]string)
334+
}
335+
machineScope.Machine.ObjectMeta.Annotations[infrastructurev1beta2.DeleteMachineOnInstanceTermination] = ""
336+
t.deleteMachines = make([]clusterv1.Machine, 0)
337+
machineScope.Client = interceptor.NewClient(fakeClient, interceptor.Funcs{
338+
Delete: func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteOption) error {
339+
m := obj.(*clusterv1.Machine)
340+
t.deleteMachines = append(t.deleteMachines, *m)
341+
return nil
342+
},
343+
})
251344
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
252345
InstanceId: common.String("test"),
253346
})).
@@ -258,13 +351,16 @@ func TestNormalReconciliationFunction(t *testing.T) {
258351
},
259352
}, nil)
260353
},
354+
validate: func(g *WithT, t *test, result ctrl.Result) {
355+
g.Expect(len(t.deleteMachines)).To(Equal(1))
356+
},
261357
},
262358
{
263359
name: "control plane backend vnic attachments call error",
264360
errorExpected: true,
265361
expectedEvent: "server error",
266362
conditionAssertion: []conditionAssertion{{infrastructurev1beta2.InstanceReadyCondition, corev1.ConditionFalse, clusterv1.ConditionSeverityError, infrastructurev1beta2.InstanceIPAddressNotFound}},
267-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
363+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbclient *mock_nlb.MockNetworkLoadBalancerClient) {
268364
machineScope.Machine.ObjectMeta.Labels = make(map[string]string)
269365
machineScope.Machine.ObjectMeta.Labels[clusterv1.MachineControlPlaneLabel] = "true"
270366
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
@@ -287,7 +383,7 @@ func TestNormalReconciliationFunction(t *testing.T) {
287383
{
288384
name: "control plane backend backend exists",
289385
errorExpected: false,
290-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
386+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
291387
machineScope.Machine.ObjectMeta.Labels = make(map[string]string)
292388
machineScope.Machine.ObjectMeta.Labels[clusterv1.MachineControlPlaneLabel] = "true"
293389
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
@@ -344,7 +440,7 @@ func TestNormalReconciliationFunction(t *testing.T) {
344440
{
345441
name: "backend creation",
346442
errorExpected: false,
347-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
443+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
348444
machineScope.Machine.ObjectMeta.Labels = make(map[string]string)
349445
machineScope.Machine.ObjectMeta.Labels[clusterv1.MachineControlPlaneLabel] = "true"
350446
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
@@ -419,7 +515,7 @@ func TestNormalReconciliationFunction(t *testing.T) {
419515
{
420516
name: "ip address exists",
421517
errorExpected: false,
422-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
518+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
423519
machineScope.Machine.ObjectMeta.Labels = make(map[string]string)
424520
machineScope.OCIMachine.Status.Addresses = []clusterv1.MachineAddress{
425521
{
@@ -478,7 +574,7 @@ func TestNormalReconciliationFunction(t *testing.T) {
478574
{
479575
name: "backend creation fails",
480576
errorExpected: true,
481-
testSpecificSetup: func(machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
577+
testSpecificSetup: func(t *test, machineScope *scope.MachineScope, computeClient *mock_compute.MockComputeClient, vcnClient *mock_vcn.MockClient, nlbClient *mock_nlb.MockNetworkLoadBalancerClient) {
482578
machineScope.Machine.ObjectMeta.Labels = make(map[string]string)
483579
machineScope.Machine.ObjectMeta.Labels[clusterv1.MachineControlPlaneLabel] = "true"
484580
computeClient.EXPECT().GetInstance(gomock.Any(), gomock.Eq(core.GetInstanceRequest{
@@ -557,9 +653,9 @@ func TestNormalReconciliationFunction(t *testing.T) {
557653
g := NewWithT(t)
558654
defer teardown(t, g)
559655
setup(t, g)
560-
tc.testSpecificSetup(ms, computeClient, vcnClient, nlbClient)
656+
tc.testSpecificSetup(&tc, ms, computeClient, vcnClient, nlbClient)
561657
ctx := context.Background()
562-
_, err := r.reconcileNormal(ctx, log.FromContext(ctx), ms)
658+
result, err := r.reconcileNormal(ctx, log.FromContext(ctx), ms)
563659
if len(tc.conditionAssertion) > 0 {
564660
expectConditions(g, ociMachine, tc.conditionAssertion)
565661
}
@@ -571,6 +667,9 @@ func TestNormalReconciliationFunction(t *testing.T) {
571667
if tc.expectedEvent != "" {
572668
g.Eventually(recorder.Events).Should(Receive(ContainSubstring(tc.expectedEvent)))
573669
}
670+
if tc.validate != nil {
671+
tc.validate(g, &tc, result)
672+
}
574673
})
575674
}
576675
}

0 commit comments

Comments
 (0)