Skip to content

Commit b3d73df

Browse files
authored
Merge pull request #2834 from bnallapeta/soft-delete
🐛 Handle SOFT_DELETED and DELETED states in server deletion
2 parents be5fef3 + 7121d6f commit b3d73df

File tree

3 files changed

+112
-3
lines changed

3 files changed

+112
-3
lines changed

api/v1beta1/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,11 @@ var (
792792
// InstanceStateDeleted is the string representing an instance in a deleted state.
793793
InstanceStateDeleted = InstanceState("DELETED")
794794

795+
// InstanceStateSoftDeleted is the string representing an instance in a soft-deleted state.
796+
// This state occurs when OpenStack is configured with a reclaim_instance_interval > 0,
797+
// allowing recovery of deleted instances within the reclaim period.
798+
InstanceStateSoftDeleted = InstanceState("SOFT_DELETED")
799+
795800
// InstanceStateUndefined is the string representing an undefined instance state.
796801
InstanceStateUndefined = InstanceState("")
797802
)

pkg/cloud/services/compute/instance.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,15 @@ func (s *Service) DeleteInstance(eventObject runtime.Object, instanceStatus *Ins
490490
if err != nil {
491491
return false, err
492492
}
493-
if i != nil {
494-
return false, nil
493+
// Server not found means it has been permanently deleted
494+
if i == nil {
495+
return true, nil
496+
}
497+
// Server in SOFT_DELETED or DELETED state means deletion succeeded. This respects OpenStack's soft delete policy.
498+
if i.State() == infrav1.InstanceStateSoftDeleted || i.State() == infrav1.InstanceStateDeleted {
499+
return true, nil
495500
}
496-
return true, nil
501+
return false, nil
497502
})
498503
if err != nil {
499504
record.Warnf(eventObject, "FailedDeleteServer", "Failed to delete server %s with id %s: %v", instance.Name, instance.ID, err)

pkg/cloud/services/compute/instance_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/go-logr/logr/testr"
2828
"github.com/google/go-cmp/cmp"
29+
"github.com/gophercloud/gophercloud/v2"
2930
"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
3031
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/keypairs"
3132
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
@@ -1021,3 +1022,101 @@ func TestService_ReconcileInstance(t *testing.T) {
10211022
})
10221023
}
10231024
}
1025+
1026+
func TestService_DeleteInstance(t *testing.T) {
1027+
const (
1028+
serverID = "ce96e584-7ebc-46d6-9e55-987d72e3806c"
1029+
serverName = "test-server"
1030+
)
1031+
1032+
tests := []struct {
1033+
name string
1034+
expect func(m *mock.MockComputeClientMockRecorder)
1035+
wantErr bool
1036+
}{
1037+
{
1038+
name: "Server not found after delete",
1039+
expect: func(m *mock.MockComputeClientMockRecorder) {
1040+
m.DeleteServer(serverID).Return(nil)
1041+
m.GetServer(serverID).Return(nil, &gophercloud.ErrResourceNotFound{})
1042+
},
1043+
wantErr: false,
1044+
},
1045+
{
1046+
name: "Server in SOFT_DELETED state",
1047+
expect: func(m *mock.MockComputeClientMockRecorder) {
1048+
m.DeleteServer(serverID).Return(nil)
1049+
m.GetServer(serverID).Return(&servers.Server{
1050+
ID: serverID,
1051+
Name: serverName,
1052+
Status: "SOFT_DELETED",
1053+
}, nil)
1054+
},
1055+
wantErr: false,
1056+
},
1057+
{
1058+
name: "Server in DELETED state",
1059+
expect: func(m *mock.MockComputeClientMockRecorder) {
1060+
m.DeleteServer(serverID).Return(nil)
1061+
m.GetServer(serverID).Return(&servers.Server{
1062+
ID: serverID,
1063+
Name: serverName,
1064+
Status: "DELETED",
1065+
}, nil)
1066+
},
1067+
wantErr: false,
1068+
},
1069+
{
1070+
name: "Delete API returns not found",
1071+
expect: func(m *mock.MockComputeClientMockRecorder) {
1072+
m.DeleteServer(serverID).Return(&gophercloud.ErrResourceNotFound{})
1073+
},
1074+
wantErr: false,
1075+
},
1076+
{
1077+
name: "Delete API returns error",
1078+
expect: func(m *mock.MockComputeClientMockRecorder) {
1079+
m.DeleteServer(serverID).Return(errors.New("API error"))
1080+
},
1081+
wantErr: true,
1082+
},
1083+
{
1084+
name: "GetServer returns error",
1085+
expect: func(m *mock.MockComputeClientMockRecorder) {
1086+
m.DeleteServer(serverID).Return(nil)
1087+
m.GetServer(serverID).Return(nil, errors.New("API error"))
1088+
},
1089+
wantErr: true,
1090+
},
1091+
}
1092+
1093+
for _, tt := range tests {
1094+
t.Run(tt.name, func(t *testing.T) {
1095+
mockCtrl := gomock.NewController(t)
1096+
log := testr.New(t)
1097+
mockScopeFactory := scope.NewMockScopeFactory(mockCtrl, "")
1098+
1099+
tt.expect(mockScopeFactory.ComputeClient.EXPECT())
1100+
1101+
s, err := NewService(scope.NewWithLogger(mockScopeFactory, log))
1102+
if err != nil {
1103+
t.Fatalf("Failed to create service: %v", err)
1104+
}
1105+
1106+
instanceStatus := &InstanceStatus{
1107+
server: &servers.Server{
1108+
ID: serverID,
1109+
Name: serverName,
1110+
},
1111+
logger: log,
1112+
}
1113+
1114+
eventObject := &infrav1.OpenStackMachine{}
1115+
err = s.DeleteInstance(eventObject, instanceStatus)
1116+
if (err != nil) != tt.wantErr {
1117+
t.Errorf("Service.DeleteInstance() error = %v, wantErr %v", err, tt.wantErr)
1118+
return
1119+
}
1120+
})
1121+
}
1122+
}

0 commit comments

Comments
 (0)