From e56b052a5267d9e0896899f674cec70c92148cf3 Mon Sep 17 00:00:00 2001 From: dev-arya23 Date: Sat, 27 Jun 2026 22:09:14 +0530 Subject: [PATCH] controller/orgunit: reconciler for hold-period expiry and hard-delete (#182) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a reconciler controller using core/reconciler to handle hold-period expiry and hard-delete of soft-deleted org-units. Changes: - pkg/controller/orgunit/reconciler.go: OrgUnitReconciler that computes holdExpiry = deleted + hold_deleted_ou, requeues if not yet expired, hard-deletes via DeleteKey when hold period expires - pkg/table/org-unit.go: ReconcilerGetAllKeys override returns only keys where deleted > 0 (soft-deleted entries pending hard-delete), adds Deleted field to OrgUnitEntry - main.go: conditional startup — reconciler only registers when experimental.allow_ou_delete is true, threads ExperimentalConfig --- main.go | 13 +++- pkg/controller/orgunit/reconciler.go | 92 ++++++++++++++++++++++++++++ pkg/table/org-unit.go | 31 ++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 pkg/controller/orgunit/reconciler.go diff --git a/main.go b/main.go index 61b1efe..72a93dc 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ import ( "github.com/go-core-stack/auth-gateway/pkg/apidocs" "github.com/go-core-stack/auth-gateway/pkg/auth" "github.com/go-core-stack/auth-gateway/pkg/config" + "github.com/go-core-stack/auth-gateway/pkg/controller/orgunit" "github.com/go-core-stack/auth-gateway/pkg/controller/request" "github.com/go-core-stack/auth-gateway/pkg/controller/roledef" "github.com/go-core-stack/auth-gateway/pkg/controller/tenant" @@ -544,6 +545,16 @@ func main() { log.Panicf("failed to create email verification cleanup controller: %s", err) } + // Start org-unit cleanup reconciler only when soft-delete is enabled. + // The reconciler handles hold-period expiry and hard-delete of + // soft-deleted org-units. + if conf.GetExperimental().AllowOUDelete { + _, err = orgunit.NewOrgUnitCleanupController(conf.GetExperimental()) + if err != nil { + log.Panicf("failed to create org-unit cleanup controller: %s", err) + } + } + // role definition manager resourceMgr := roledef.NewResourceManager() @@ -575,7 +586,7 @@ func main() { _ = server.NewRegistrationServer(serverCtx, APIEndpoint) // setup Org Unit server - _ = server.NewOrgUnitServer(serverCtx, APIEndpoint) + _ = server.NewOrgUnitServer(serverCtx, conf.GetExperimental(), APIEndpoint) // setup org unit role server _ = server.NewOrgUnitRoleServer(serverCtx, APIEndpoint) diff --git a/pkg/controller/orgunit/reconciler.go b/pkg/controller/orgunit/reconciler.go new file mode 100644 index 0000000..41d829f --- /dev/null +++ b/pkg/controller/orgunit/reconciler.go @@ -0,0 +1,92 @@ +// Copyright © 2025-2026 Prabhjot Singh Sethi, All Rights reserved +// Author: Prabhjot Singh Sethi + +package orgunit + +import ( + "context" + "log" + "time" + + "github.com/go-core-stack/auth-gateway/pkg/config" + "github.com/go-core-stack/auth-gateway/pkg/table" + "github.com/go-core-stack/core/errors" + "github.com/go-core-stack/core/reconciler" +) + +// OrgUnitCleanupController manages the lifecycle of soft-deleted org-units, +// hard-deleting them after the configured hold period expires. +type OrgUnitCleanupController struct { + tbl *table.OrgUnitTable + holdDuration int +} + +type orgUnitReconciler struct { + reconciler.Controller + ctrl *OrgUnitCleanupController +} + +func (r *orgUnitReconciler) Reconcile(k any) (*reconciler.Result, error) { + ctx := context.Background() + key := k.(*table.OrgUnitKey) + + entry, err := r.ctrl.tbl.Find(ctx, key) + if err != nil { + if !errors.IsNotFound(err) { + // transient error — requeue with backoff + return &reconciler.Result{RequeueAfter: 5 * time.Second}, nil + } + // entry already gone — nothing to do + return &reconciler.Result{}, nil + } + + // only process soft-deleted entries + if entry.Deleted == 0 { + return &reconciler.Result{}, nil + } + + holdExpiry := entry.Deleted + int64(r.ctrl.holdDuration) + now := time.Now().Unix() + + if holdExpiry > now { + // hold period has not expired — requeue after remaining time (+1s buffer) + remaining := holdExpiry - now + return &reconciler.Result{RequeueAfter: time.Duration(remaining+1) * time.Second}, nil + } + + // hold period expired — hard-delete the entry + err = r.ctrl.tbl.DeleteKey(ctx, key) + if err != nil && !errors.IsNotFound(err) { + // transient error — requeue with backoff + log.Printf("orgunit reconciler: failed to hard-delete org-unit %s: %s", key.ID, err) + return &reconciler.Result{RequeueAfter: 5 * time.Second}, nil + } + + log.Printf("orgunit reconciler: hard-deleted org-unit %s after hold period", key.ID) + return &reconciler.Result{}, nil +} + +// NewOrgUnitCleanupController creates and registers the org-unit reconciler. +// It should only be called when experimental.allow_ou_delete is enabled. +func NewOrgUnitCleanupController(experimental config.ExperimentalConfig) (*OrgUnitCleanupController, error) { + tbl, err := table.GetOrgUnitTable() + if err != nil { + return nil, err + } + + ctrl := &OrgUnitCleanupController{ + tbl: tbl, + holdDuration: experimental.HoldDeletedOU, + } + + r := &orgUnitReconciler{ + ctrl: ctrl, + } + + err = tbl.Register("OrgUnitCleanupController", r) + if err != nil { + return nil, err + } + + return ctrl, nil +} diff --git a/pkg/table/org-unit.go b/pkg/table/org-unit.go index ef1ac17..c528cf2 100644 --- a/pkg/table/org-unit.go +++ b/pkg/table/org-unit.go @@ -5,6 +5,9 @@ package table import ( "context" + "log" + + "go.mongodb.org/mongo-driver/v2/bson" "github.com/go-core-stack/core/db" "github.com/go-core-stack/core/errors" @@ -36,6 +39,11 @@ type OrgUnitEntry struct { // Tenant this OU belongs to Tenant string `bson:"tenant,omitempty"` + + // Deleted is the unix timestamp when the OU was soft-deleted. + // Zero value means the OU is active; omitted from MongoDB documents + // when zero. + Deleted int64 `bson:"deleted,omitempty"` } type OrgUnitTable struct { @@ -60,6 +68,29 @@ func (t *OrgUnitTable) FindByTenant(ctx context.Context, tenant, ouId string) ([ return t.FindMany(ctx, filter, 0, 0) } +// ReconcilerGetAllKeys returns keys for all soft-deleted org-unit entries +// (deleted > 0). This overrides the generic Table.ReconcilerGetAllKeys to +// ensure the reconciler only bootstraps entries that are pending hard-delete. +func (t *OrgUnitTable) ReconcilerGetAllKeys() []any { + type keyOnly struct { + Key OrgUnitKey `bson:"_id,omitempty"` + } + + filter := bson.M{"deleted": bson.M{"$gt": 0}} + + list := []keyOnly{} + err := t.col.FindMany(context.Background(), filter, &list) + if err != nil { + log.Panicf("orgunit: failed to fetch deleted keys: %s", err) + } + + keys := make([]any, 0, len(list)) + for _, k := range list { + keys = append(keys, &k.Key) + } + return keys +} + func (t *OrgUnitTable) StartEventLogger() error { logger := db.NewEventLogger[OrgUnitKey, OrgUnitEntry](t.col, nil) return logger.Start(context.Background())