Skip to content

Commit d3b88b7

Browse files
Merge pull request #190 from kuudori/HYPERFLEET-1093
HYPERFLEET-1093 - feat: add descriptor-driven delete policies to ResourceService
2 parents c6db3bf + 8e47f42 commit d3b88b7

4 files changed

Lines changed: 299 additions & 22 deletions

File tree

pkg/dao/mocks/resource.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,13 @@ func (d *resourceDaoMock) Delete(_ context.Context, kind, id string) error {
6363
return nil
6464
}
6565

66-
func (d *resourceDaoMock) CountByOwner(_ context.Context, kind, ownerID string) (int64, error) {
67-
var count int64
66+
func (d *resourceDaoMock) ExistsByOwner(_ context.Context, kind, ownerID string) (bool, error) {
6867
for _, r := range d.resources {
69-
if r.Kind == kind && r.OwnerID != nil && *r.OwnerID == ownerID {
70-
count++
68+
if r.Kind == kind && r.OwnerID != nil && *r.OwnerID == ownerID && r.DeletedTime == nil {
69+
return true, nil
7170
}
7271
}
73-
return count, nil
72+
return false, nil
7473
}
7574

7675
func (d *resourceDaoMock) FindByKind(_ context.Context, kind string) (api.ResourceList, error) {

pkg/dao/resource.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type ResourceDao interface {
1717
Create(ctx context.Context, resource *api.Resource) (*api.Resource, error)
1818
Save(ctx context.Context, resource *api.Resource) error
1919
Delete(ctx context.Context, kind, id string) error
20-
CountByOwner(ctx context.Context, kind, ownerID string) (int64, error)
20+
ExistsByOwner(ctx context.Context, kind, ownerID string) (bool, error)
2121
FindByKind(ctx context.Context, kind string) (api.ResourceList, error)
2222
FindByKindAndOwner(ctx context.Context, kind, ownerID string) (api.ResourceList, error)
2323
FindByIDs(ctx context.Context, kind string, ids []string) (api.ResourceList, error)
@@ -99,14 +99,15 @@ func (d *sqlResourceDao) Delete(ctx context.Context, kind, id string) error {
9999
return nil
100100
}
101101

102-
func (d *sqlResourceDao) CountByOwner(ctx context.Context, kind, ownerID string) (int64, error) {
102+
func (d *sqlResourceDao) ExistsByOwner(ctx context.Context, kind, ownerID string) (bool, error) {
103103
g2 := d.sessionFactory.New(ctx)
104-
var count int64
105-
if err := g2.Model(&api.Resource{}).Where(
106-
"kind = ? AND owner_id = ?", kind, ownerID).Count(&count).Error; err != nil {
107-
return 0, err
104+
var exists bool
105+
if err := g2.Raw(
106+
"SELECT EXISTS(SELECT 1 FROM resources WHERE kind = ? AND owner_id = ? AND deleted_time IS NULL)",
107+
kind, ownerID).Scan(&exists).Error; err != nil {
108+
return false, err
108109
}
109-
return count, nil
110+
return exists, nil
110111
}
111112

112113
func (d *sqlResourceDao) FindByKind(ctx context.Context, kind string) (api.ResourceList, error) {

pkg/services/resource.go

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
1010
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/auth"
1111
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/dao"
12+
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/db"
1213
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/errors"
1314
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/registry"
1415
)
@@ -128,7 +129,9 @@ func (s *sqlResourceService) Patch(
128129

129130
// Delete performs a soft-delete by setting DeletedTime and DeletedBy, then incrementing generation.
130131
// Idempotent — returns the resource unchanged if already marked for deletion.
131-
// Child cascade policies are not enforced here (deferred to HYPERFLEET-1093).
132+
// Before deleting, enforces child delete policies from the entity registry:
133+
// - restrict: blocks deletion (409) if active children of that type exist
134+
// - cascade: recursively soft-deletes children (DFS, innermost first)
132135
func (s *sqlResourceService) Delete(ctx context.Context, kind, id string) (*api.Resource, *errors.ServiceError) {
133136
if svcErr := validateKind(kind); svcErr != nil {
134137
return nil, svcErr
@@ -138,7 +141,6 @@ func (s *sqlResourceService) Delete(ctx context.Context, kind, id string) (*api.
138141
return nil, handleSoftDeleteError(kind, err)
139142
}
140143

141-
// Already marked for deletion — return as-is to keep the operation idempotent.
142144
if resource.DeletedTime != nil {
143145
return resource, nil
144146
}
@@ -151,13 +153,75 @@ func (s *sqlResourceService) Delete(ctx context.Context, kind, id string) (*api.
151153
resource.MarkDeleted(username, deletedTime)
152154
resource.IncrementGeneration()
153155

156+
if svcErr := s.enforceDeletePolicies(ctx, resource); svcErr != nil {
157+
db.MarkForRollback(ctx, svcErr)
158+
return nil, svcErr
159+
}
160+
154161
if saveErr := s.resourceDao.Save(ctx, resource); saveErr != nil {
155162
return nil, handleSoftDeleteError(kind, saveErr)
156163
}
157164

158165
return resource, nil
159166
}
160167

168+
func (s *sqlResourceService) enforceDeletePolicies(
169+
ctx context.Context, resource *api.Resource,
170+
) *errors.ServiceError {
171+
children := registry.ChildrenOf(resource.Kind)
172+
173+
for _, child := range children {
174+
if child.OnParentDelete == registry.OnParentDeleteRestrict {
175+
if svcErr := s.checkCanDelete(ctx, resource, child); svcErr != nil {
176+
return svcErr
177+
}
178+
}
179+
}
180+
181+
for _, child := range children {
182+
if child.OnParentDelete == registry.OnParentDeleteCascade {
183+
items, err := s.resourceDao.FindByKindAndOwner(ctx, child.Kind, resource.ID)
184+
if err != nil {
185+
return errors.GeneralError(
186+
"Unable to find %s children for cascade delete: %s", child.Kind, err,
187+
)
188+
}
189+
for _, item := range items {
190+
if item.DeletedTime != nil {
191+
continue
192+
}
193+
item.MarkDeleted(*resource.DeletedBy, *resource.DeletedTime)
194+
item.IncrementGeneration()
195+
196+
if svcErr := s.enforceDeletePolicies(ctx, item); svcErr != nil {
197+
return svcErr
198+
}
199+
if saveErr := s.resourceDao.Save(ctx, item); saveErr != nil {
200+
return handleSoftDeleteError(child.Kind, saveErr)
201+
}
202+
}
203+
}
204+
}
205+
206+
return nil
207+
}
208+
209+
func (s *sqlResourceService) checkCanDelete(
210+
ctx context.Context, resource *api.Resource, child registry.EntityDescriptor,
211+
) *errors.ServiceError {
212+
exists, err := s.resourceDao.ExistsByOwner(ctx, child.Kind, resource.ID)
213+
if err != nil {
214+
return errors.GeneralError("Unable to check %s children: %s", child.Kind, err)
215+
}
216+
if exists {
217+
return errors.ConflictState(
218+
"Cannot delete %s '%s': active %s(s) exist",
219+
resource.Kind, resource.ID, child.Kind,
220+
)
221+
}
222+
return nil
223+
}
224+
161225
// FindByIDs returns resources matching the given IDs, scoped to the specified kind.
162226
func (s *sqlResourceService) FindByIDs(
163227
ctx context.Context, kind string, ids []string,

0 commit comments

Comments
 (0)