From 01aa49433e021bd5e1ee4aa593ce82a2bbe6a43b Mon Sep 17 00:00:00 2001 From: Pascal Fischer <32096965+pascal-fischer@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:33:24 +0200 Subject: [PATCH] [management] delete targets when deleting exposed service (#6442) --- .../reverseproxy/service/manager/manager.go | 12 ++++ .../service/manager/manager_test.go | 70 +++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/management/internals/modules/reverseproxy/service/manager/manager.go b/management/internals/modules/reverseproxy/service/manager/manager.go index e6b006759..365fbab40 100644 --- a/management/internals/modules/reverseproxy/service/manager/manager.go +++ b/management/internals/modules/reverseproxy/service/manager/manager.go @@ -918,6 +918,10 @@ func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID strin } for _, svc := range services { + if err = transaction.DeleteServiceTargets(ctx, accountID, svc.ID); err != nil { + return fmt.Errorf("failed to delete service targets: %w", err) + } + if err = transaction.DeleteService(ctx, accountID, svc.ID); err != nil { return fmt.Errorf("failed to delete service: %w", err) } @@ -1270,6 +1274,10 @@ func (m *Manager) deletePeerService(ctx context.Context, accountID, peerID, serv return status.Errorf(status.PermissionDenied, "cannot delete service exposed by another peer") } + if err = transaction.DeleteServiceTargets(ctx, accountID, serviceID); err != nil { + return fmt.Errorf("delete service targets: %w", err) + } + if err = transaction.DeleteService(ctx, accountID, serviceID); err != nil { return fmt.Errorf("delete service: %w", err) } @@ -1319,6 +1327,10 @@ func (m *Manager) deleteExpiredPeerService(ctx context.Context, accountID, peerI return nil } + if err = transaction.DeleteServiceTargets(ctx, accountID, serviceID); err != nil { + return fmt.Errorf("delete service targets: %w", err) + } + if err = transaction.DeleteService(ctx, accountID, serviceID); err != nil { return fmt.Errorf("delete service: %w", err) } diff --git a/management/internals/modules/reverseproxy/service/manager/manager_test.go b/management/internals/modules/reverseproxy/service/manager/manager_test.go index 0497415b7..ace105b31 100644 --- a/management/internals/modules/reverseproxy/service/manager/manager_test.go +++ b/management/internals/modules/reverseproxy/service/manager/manager_test.go @@ -458,6 +458,9 @@ func TestDeletePeerService_SourcePeerValidation(t *testing.T) { txMock.EXPECT(). GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteServiceTargets(ctx, accountID, serviceID). + Return(nil) txMock.EXPECT(). DeleteService(ctx, accountID, serviceID). Return(nil) @@ -560,6 +563,9 @@ func TestDeletePeerService_SourcePeerValidation(t *testing.T) { txMock.EXPECT(). GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteServiceTargets(ctx, accountID, serviceID). + Return(nil) txMock.EXPECT(). DeleteService(ctx, accountID, serviceID). Return(nil) @@ -604,6 +610,9 @@ func TestDeletePeerService_SourcePeerValidation(t *testing.T) { txMock.EXPECT(). GetServiceByID(ctx, store.LockingStrengthUpdate, accountID, serviceID). Return(newEphemeralService(), nil) + txMock.EXPECT(). + DeleteServiceTargets(ctx, accountID, serviceID). + Return(nil) txMock.EXPECT(). DeleteService(ctx, accountID, serviceID). Return(nil) @@ -1192,6 +1201,67 @@ func TestDeleteService_DeletesTargets(t *testing.T) { assert.Len(t, targets, 0, "All targets should be deleted when service is deleted") } +func TestDeleteExpiredPeerService_DeletesTargets(t *testing.T) { + ctx := context.Background() + mgr, testStore := setupIntegrationTest(t) + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Mode: "http", + }) + require.NoError(t, err) + + svcID := resolveServiceIDByDomain(t, testStore, resp.Domain) + + targets, err := testStore.GetTargetsByServiceID(ctx, store.LockingStrengthNone, testAccountID, svcID) + require.NoError(t, err) + require.Len(t, targets, 1, "ephemeral peer-exposed service should have exactly one persisted target before reaping") + + expireEphemeralService(t, testStore, testAccountID, resp.Domain) + err = mgr.deleteExpiredPeerService(ctx, testAccountID, testPeerID, svcID) + require.NoError(t, err) + + _, err = testStore.GetServiceByDomain(ctx, resp.Domain) + require.Error(t, err, "expired peer-exposed service should be deleted") + s, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, status.NotFound, s.Type()) + + targets, err = testStore.GetTargetsByServiceID(ctx, store.LockingStrengthNone, testAccountID, svcID) + require.NoError(t, err) + assert.Len(t, targets, 0, "orphaned target rows must be deleted when an expired peer-exposed service is reaped") +} + +func TestDeleteServiceFromPeer_DeletesTargets(t *testing.T) { + ctx := context.Background() + mgr, testStore := setupIntegrationTest(t) + + resp, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{ + Port: 8080, + Mode: "http", + }) + require.NoError(t, err) + + svcID := resolveServiceIDByDomain(t, testStore, resp.Domain) + + targets, err := testStore.GetTargetsByServiceID(ctx, store.LockingStrengthNone, testAccountID, svcID) + require.NoError(t, err) + require.Len(t, targets, 1, "ephemeral peer-exposed service should have exactly one persisted target before stopping") + + err = mgr.StopServiceFromPeer(ctx, testAccountID, testPeerID, svcID) + require.NoError(t, err) + + _, err = testStore.GetServiceByDomain(ctx, resp.Domain) + require.Error(t, err, "stopped peer-exposed service should be deleted") + s, ok := status.FromError(err) + require.True(t, ok) + assert.Equal(t, status.NotFound, s.Type()) + + targets, err = testStore.GetTargetsByServiceID(ctx, store.LockingStrengthNone, testAccountID, svcID) + require.NoError(t, err) + assert.Len(t, targets, 0, "orphaned target rows must be deleted when a peer stops its exposed service") +} + func TestValidateProtocolChange(t *testing.T) { tests := []struct { name string