diff --git a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_ClusterInfoStore.go b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_ClusterInfoStore.go index 1830657d..19cda09f 100644 --- a/_mocks/opencsg.com/csghub-server/builder/store/database/mock_ClusterInfoStore.go +++ b/_mocks/opencsg.com/csghub-server/builder/store/database/mock_ClusterInfoStore.go @@ -356,6 +356,53 @@ func (_c *MockClusterInfoStore_ByClusterID_Call) RunAndReturn(run func(context.C return _c } +// DeleteClusterNodeByID provides a mock function with given fields: ctx, id +func (_m *MockClusterInfoStore) DeleteClusterNodeByID(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteClusterNodeByID") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClusterInfoStore_DeleteClusterNodeByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClusterNodeByID' +type MockClusterInfoStore_DeleteClusterNodeByID_Call struct { + *mock.Call +} + +// DeleteClusterNodeByID is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockClusterInfoStore_Expecter) DeleteClusterNodeByID(ctx interface{}, id interface{}) *MockClusterInfoStore_DeleteClusterNodeByID_Call { + return &MockClusterInfoStore_DeleteClusterNodeByID_Call{Call: _e.mock.On("DeleteClusterNodeByID", ctx, id)} +} + +func (_c *MockClusterInfoStore_DeleteClusterNodeByID_Call) Run(run func(ctx context.Context, id int64)) *MockClusterInfoStore_DeleteClusterNodeByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockClusterInfoStore_DeleteClusterNodeByID_Call) Return(_a0 error) *MockClusterInfoStore_DeleteClusterNodeByID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClusterInfoStore_DeleteClusterNodeByID_Call) RunAndReturn(run func(context.Context, int64) error) *MockClusterInfoStore_DeleteClusterNodeByID_Call { + _c.Call.Return(run) + return _c +} + // DeleteNodeOwnership provides a mock function with given fields: ctx, clusterNodeID func (_m *MockClusterInfoStore) DeleteNodeOwnership(ctx context.Context, clusterNodeID int64) error { ret := _m.Called(ctx, clusterNodeID) diff --git a/_mocks/opencsg.com/csghub-server/component/mock_ClusterComponent.go b/_mocks/opencsg.com/csghub-server/component/mock_ClusterComponent.go index 66c6b025..d9cbccaf 100644 --- a/_mocks/opencsg.com/csghub-server/component/mock_ClusterComponent.go +++ b/_mocks/opencsg.com/csghub-server/component/mock_ClusterComponent.go @@ -83,6 +83,53 @@ func (_c *MockClusterComponent_CheckExclusiveResource_Call) RunAndReturn(run fun return _c } +// DeleteClusterNode provides a mock function with given fields: ctx, id +func (_m *MockClusterComponent) DeleteClusterNode(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteClusterNode") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockClusterComponent_DeleteClusterNode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteClusterNode' +type MockClusterComponent_DeleteClusterNode_Call struct { + *mock.Call +} + +// DeleteClusterNode is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *MockClusterComponent_Expecter) DeleteClusterNode(ctx interface{}, id interface{}) *MockClusterComponent_DeleteClusterNode_Call { + return &MockClusterComponent_DeleteClusterNode_Call{Call: _e.mock.On("DeleteClusterNode", ctx, id)} +} + +func (_c *MockClusterComponent_DeleteClusterNode_Call) Run(run func(ctx context.Context, id int64)) *MockClusterComponent_DeleteClusterNode_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *MockClusterComponent_DeleteClusterNode_Call) Return(_a0 error) *MockClusterComponent_DeleteClusterNode_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockClusterComponent_DeleteClusterNode_Call) RunAndReturn(run func(context.Context, int64) error) *MockClusterComponent_DeleteClusterNode_Call { + _c.Call.Return(run) + return _c +} + // GetClusterByID provides a mock function with given fields: ctx, clusterId func (_m *MockClusterComponent) GetClusterByID(ctx context.Context, clusterId string) (*database.ClusterInfo, error) { ret := _m.Called(ctx, clusterId) diff --git a/builder/store/database/cluster.go b/builder/store/database/cluster.go index eab6f5a8..bbf7ae9a 100644 --- a/builder/store/database/cluster.go +++ b/builder/store/database/cluster.go @@ -38,6 +38,7 @@ type ClusterInfoStore interface { UpdateClusterNodeByNode(ctx context.Context, node ClusterNode) error AddNodeOwnership(ctx context.Context, ownership ClusterNodeOwnership) error DeleteNodeOwnership(ctx context.Context, clusterNodeID int64) error + DeleteClusterNodeByID(ctx context.Context, id int64) error GetNodeOwnership(ctx context.Context, clusterNodeID int64) (*ClusterNodeOwnership, error) GetNodeOwnershipByNameSpace(ctx context.Context, nameSpace string) ([]ClusterNodeOwnership, error) UpdateNodeOwnership(ctx context.Context, ownership ClusterNodeOwnership) error @@ -507,6 +508,47 @@ func (s *clusterInfoStoreImpl) DeleteNodeOwnership(ctx context.Context, clusterN _, err := s.db.Operator.Core.NewDelete().Model(&ClusterNodeOwnership{}).Where("cluster_node_id = ?", clusterNodeID).Exec(ctx) return errorx.HandleDBError(err, nil) } +func (s *clusterInfoStoreImpl) DeleteClusterNodeByID(ctx context.Context, id int64) error { + err := s.db.Operator.Core.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error { + var node ClusterNode + err := tx.NewSelect().Model(&node).Where("id = ?", id).Scan(ctx) + if err != nil { + return err + } + clusterID := node.ClusterID + _, err = tx.NewDelete().Model(&ClusterNodeOwnership{}).Where("cluster_node_id = ?", id).Exec(ctx) + if err != nil { + return err + } + + _, err = tx.NewDelete().Model(&ClusterNode{}).Where("id = ?", id).Exec(ctx) + if err != nil { + return err + } + + count, err := tx.NewSelect().Model(&ClusterNode{}).Where("cluster_id = ?", clusterID).Count(ctx) + if err != nil { + return err + } + if count < 1 { + _, err = tx.NewDelete().Model(&ClusterInfo{}).Where("cluster_id = ?", clusterID).Exec(ctx) + if err != nil { + return err + } + _, err = tx.NewDelete().Model(&SpaceResource{}).Where("cluster_id = ?", clusterID).Exec(ctx) + if err != nil { + return err + } + } + + return nil + }) + + if err != nil { + return errorx.HandleDBError(err, nil) + } + return nil +} func (s *clusterInfoStoreImpl) GetNodeOwnership(ctx context.Context, clusterNodeID int64) (*ClusterNodeOwnership, error) { var ownership ClusterNodeOwnership diff --git a/builder/store/database/cluster_test.go b/builder/store/database/cluster_test.go index 5dfef0e2..269d1e3c 100644 --- a/builder/store/database/cluster_test.go +++ b/builder/store/database/cluster_test.go @@ -624,3 +624,138 @@ func TestClusterStore_ClusterNodeOperations(t *testing.T) { require.NoError(t, err) require.Nil(t, deletedOwnership) } + +func TestClusterStore_DeleteClusterNodeByID_NotLastNode(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewClusterInfoStoreWithDB(db) + + cluster, err := store.Add(ctx, "config-delete-not-last", "region-1", types.ConnectModeKubeConfig) + require.NoError(t, err) + + node1 := &database.ClusterNode{ + ClusterID: cluster.ClusterID, + Name: "node-1", + Status: "Ready", + } + _, err = db.Core.NewInsert().Model(node1).Exec(ctx) + require.NoError(t, err) + + node2 := &database.ClusterNode{ + ClusterID: cluster.ClusterID, + Name: "node-2", + Status: "Ready", + } + _, err = db.Core.NewInsert().Model(node2).Exec(ctx) + require.NoError(t, err) + + ownership := database.ClusterNodeOwnership{ + ClusterNodeID: node1.ID, + ClusterID: cluster.ClusterID, + Namespace: "ns-1", + } + err = store.AddNodeOwnership(ctx, ownership) + require.NoError(t, err) + + sr := &database.SpaceResource{ + Name: "resource-1", + Resources: "{}", + ClusterID: cluster.ClusterID, + } + _, err = db.Core.NewInsert().Model(sr).Exec(ctx) + require.NoError(t, err) + + err = store.DeleteClusterNodeByID(ctx, node1.ID) + require.NoError(t, err) + + _, err = store.GetClusterNodeByID(ctx, node1.ID) + require.Error(t, err) + + fetchedOwnership, err := store.GetNodeOwnership(ctx, node1.ID) + require.NoError(t, err) + require.Nil(t, fetchedOwnership) + + nodes, err := store.FindNodeByClusterID(ctx, cluster.ClusterID) + require.NoError(t, err) + require.Len(t, nodes, 1) + require.Equal(t, node2.ID, nodes[0].ID) + + existingCluster, err := store.ByClusterID(ctx, cluster.ClusterID) + require.NoError(t, err) + require.Equal(t, cluster.ClusterID, existingCluster.ClusterID) + + var remainingSR []database.SpaceResource + err = db.Core.NewSelect().Model(&remainingSR).Where("cluster_id = ?", cluster.ClusterID).Scan(ctx) + require.NoError(t, err) + require.Len(t, remainingSR, 1) +} + +func TestClusterStore_DeleteClusterNodeByID_LastNode(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewClusterInfoStoreWithDB(db) + + cluster, err := store.Add(ctx, "config-delete-last", "region-1", types.ConnectModeKubeConfig) + require.NoError(t, err) + + node := &database.ClusterNode{ + ClusterID: cluster.ClusterID, + Name: "node-only", + Status: "Ready", + } + _, err = db.Core.NewInsert().Model(node).Exec(ctx) + require.NoError(t, err) + + ownership := database.ClusterNodeOwnership{ + ClusterNodeID: node.ID, + ClusterID: cluster.ClusterID, + Namespace: "ns-1", + } + err = store.AddNodeOwnership(ctx, ownership) + require.NoError(t, err) + + sr := &database.SpaceResource{ + Name: "resource-1", + Resources: "{}", + ClusterID: cluster.ClusterID, + } + _, err = db.Core.NewInsert().Model(sr).Exec(ctx) + require.NoError(t, err) + + err = store.DeleteClusterNodeByID(ctx, node.ID) + require.NoError(t, err) + + _, err = store.GetClusterNodeByID(ctx, node.ID) + require.Error(t, err) + + fetchedOwnership, err := store.GetNodeOwnership(ctx, node.ID) + require.NoError(t, err) + require.Nil(t, fetchedOwnership) + + nodes, err := store.FindNodeByClusterID(ctx, cluster.ClusterID) + require.NoError(t, err) + require.Len(t, nodes, 0) + + _, err = store.ByClusterID(ctx, cluster.ClusterID) + require.Error(t, err) + + var remainingSR []database.SpaceResource + err = db.Core.NewSelect().Model(&remainingSR).Where("cluster_id = ?", cluster.ClusterID).Scan(ctx) + require.NoError(t, err) + require.Len(t, remainingSR, 0) +} + +func TestClusterStore_DeleteClusterNodeByID_NonExistentNode(t *testing.T) { + db := tests.InitTestDB() + defer db.Close() + ctx := context.TODO() + + store := database.NewClusterInfoStoreWithDB(db) + + err := store.DeleteClusterNodeByID(ctx, 99999) + require.Error(t, err) +} diff --git a/component/cluster.go b/component/cluster.go index acd1b1e3..837526a2 100644 --- a/component/cluster.go +++ b/component/cluster.go @@ -40,6 +40,7 @@ type ClusterComponent interface { GetWorkflowsByTimeRangeStream(ctx context.Context, req types.WorkflowTimeRangeReq) (<-chan []string, <-chan error) StopDeploy(ctx context.Context, stopReq types.DeployActReq) error StopWorkflow(ctx context.Context, stopReq types.ArgoWorkFlowDeleteReq) error + DeleteClusterNode(ctx context.Context, id int64) error } func NewClusterComponent(config *config.Config) (ClusterComponent, error) { diff --git a/component/cluster_ce.go b/component/cluster_ce.go index e9b02d46..9d8fe0af 100644 --- a/component/cluster_ce.go +++ b/component/cluster_ce.go @@ -76,3 +76,7 @@ func (c *clusterComponentImpl) StopDeploy(ctx context.Context, stopReq types.Dep func (c *clusterComponentImpl) StopWorkflow(ctx context.Context, stopReq types.ArgoWorkFlowDeleteReq) error { return nil } + +func (c *clusterComponentImpl) DeleteClusterNode(ctx context.Context, id int64) error { + return nil +}