@@ -960,3 +960,124 @@ func TestNodePoolHardDelete(t *testing.T) {
960960 Expect (adapterStatuses ).To (HaveLen (2 ))
961961 })
962962}
963+
964+ func TestNodePoolForceDelete (t * testing.T ) {
965+ t .Run ("given a soft-deleted nodepool, when force-deleted, then nodepool and adapter statuses are removed" , func (t * testing.T ) { //nolint:lll
966+ RegisterTestingT (t )
967+ h , client := test .RegisterIntegration (t )
968+ account := h .NewRandAccount ()
969+ ctx := h .NewAuthenticatedContext (account )
970+
971+ cluster , err := h .Factories .NewClusters (h .NewID ())
972+ Expect (err ).NotTo (HaveOccurred ())
973+
974+ nodePool , err := h .Factories .NewNodePools (h .NewID ())
975+ Expect (err ).NotTo (HaveOccurred ())
976+ dbSession := h .DBFactory .New (ctx )
977+ err = dbSession .Model (nodePool ).Update ("owner_id" , cluster .ID ).Error
978+ Expect (err ).NotTo (HaveOccurred ())
979+
980+ // Soft-delete the nodepool to put it in Finalizing state
981+ delResp , err := client .DeleteNodePoolByIdWithResponse (ctx , cluster .ID , nodePool .ID , test .WithAuthToken (ctx ))
982+ Expect (err ).NotTo (HaveOccurred ())
983+ Expect (delResp .StatusCode ()).To (Equal (http .StatusAccepted ))
984+
985+ // Report adapter statuses so there are records to cascade-delete
986+ newGeneration := delResp .JSON202 .Generation
987+ statusInput := newAdapterStatusRequest (
988+ "validation" ,
989+ newGeneration ,
990+ []openapi.ConditionRequest {
991+ {
992+ Type : api .AdapterConditionTypeApplied ,
993+ Status : openapi .AdapterConditionStatusFalse ,
994+ Reason : util .PtrString ("Stuck" ),
995+ },
996+ {
997+ Type : api .AdapterConditionTypeAvailable ,
998+ Status : openapi .AdapterConditionStatusFalse ,
999+ Reason : util .PtrString ("Stuck" ),
1000+ },
1001+ {
1002+ Type : api .AdapterConditionTypeHealth ,
1003+ Status : openapi .AdapterConditionStatusTrue ,
1004+ Reason : util .PtrString ("Healthy" ),
1005+ },
1006+ },
1007+ nil ,
1008+ )
1009+ statusResp , err := client .PutNodePoolStatusesWithResponse (
1010+ ctx , cluster .ID , nodePool .ID ,
1011+ openapi .PutNodePoolStatusesJSONRequestBody (statusInput ), test .WithAuthToken (ctx ),
1012+ )
1013+ Expect (err ).NotTo (HaveOccurred ())
1014+ Expect (statusResp .StatusCode ()).To (Equal (http .StatusCreated ))
1015+
1016+ // Force-delete the nodepool
1017+ forceDeleteResp , err := client .ForceDeleteNodePoolWithResponse (
1018+ ctx , cluster .ID , nodePool .ID ,
1019+ openapi.ForceDeleteNodePoolJSONRequestBody {Reason : "integration test - adapter stuck" },
1020+ test .WithAuthToken (ctx ),
1021+ )
1022+ Expect (err ).NotTo (HaveOccurred ())
1023+ Expect (forceDeleteResp .StatusCode ()).To (Equal (http .StatusNoContent ))
1024+
1025+ // Verify nodepool is gone
1026+ var nodePoolCheck api.NodePool
1027+ dbErr := dbSession .First (& nodePoolCheck , "id = ?" , nodePool .ID ).Error
1028+ Expect (dbErr ).To (HaveOccurred (), "Nodepool should be hard-deleted from DB after force-delete" )
1029+ Expect (dbErr .Error ()).To (ContainSubstring ("record not found" ))
1030+
1031+ // Verify adapter statuses are gone
1032+ var adapterStatuses []api.AdapterStatus
1033+ err = dbSession .Where ("resource_type = ? AND resource_id = ?" , api .ResourceTypeNodePool , nodePool .ID ).
1034+ Find (& adapterStatuses ).Error
1035+ Expect (err ).NotTo (HaveOccurred ())
1036+ Expect (adapterStatuses ).To (BeEmpty ())
1037+
1038+ // Verify parent cluster still exists
1039+ var clusterCheck api.Cluster
1040+ err = dbSession .First (& clusterCheck , "id = ?" , cluster .ID ).Error
1041+ Expect (err ).NotTo (HaveOccurred ())
1042+ })
1043+
1044+ errorCases := []struct {
1045+ name string
1046+ reason string
1047+ expectedStatus int
1048+ createNodePool bool
1049+ }{
1050+ {"given empty reason, when force-deleted, then returns 400" , "" , http .StatusBadRequest , true },
1051+ {"given non-existent nodepool, when force-deleted, then returns 404" , "should fail" , http .StatusNotFound , false },
1052+ {"given nodepool not in Finalizing, when force-deleted, then returns 409" , "should fail" , http .StatusConflict , true },
1053+ }
1054+
1055+ for _ , tc := range errorCases {
1056+ t .Run (tc .name , func (t * testing.T ) {
1057+ RegisterTestingT (t )
1058+ h , client := test .RegisterIntegration (t )
1059+ account := h .NewRandAccount ()
1060+ ctx := h .NewAuthenticatedContext (account )
1061+
1062+ cluster , err := h .Factories .NewClusters (h .NewID ())
1063+ Expect (err ).NotTo (HaveOccurred ())
1064+
1065+ nodePoolID := h .NewID ()
1066+ if tc .createNodePool {
1067+ nodePool , npErr := h .Factories .NewNodePools (h .NewID ())
1068+ Expect (npErr ).NotTo (HaveOccurred ())
1069+ dbSession := h .DBFactory .New (ctx )
1070+ Expect (dbSession .Model (nodePool ).Update ("owner_id" , cluster .ID ).Error ).NotTo (HaveOccurred ())
1071+ nodePoolID = nodePool .ID
1072+ }
1073+
1074+ forceDeleteResp , err := client .ForceDeleteNodePoolWithResponse (
1075+ ctx , cluster .ID , nodePoolID ,
1076+ openapi.ForceDeleteNodePoolJSONRequestBody {Reason : tc .reason },
1077+ test .WithAuthToken (ctx ),
1078+ )
1079+ Expect (err ).NotTo (HaveOccurred ())
1080+ Expect (forceDeleteResp .StatusCode ()).To (Equal (tc .expectedStatus ))
1081+ })
1082+ }
1083+ }
0 commit comments