@@ -2,9 +2,12 @@ package main
2
2
3
3
import (
4
4
"context"
5
+ "encoding/csv"
6
+ "errors"
5
7
"fmt"
6
8
"log"
7
9
"os"
10
+ "strings"
8
11
"text/tabwriter"
9
12
"time"
10
13
@@ -90,6 +93,27 @@ func main() {
90
93
},
91
94
},
92
95
},
96
+ {
97
+ Name : "db:cleanup" ,
98
+ Usage : "remove all tenants and members that do not appear in the organization list" ,
99
+ Category : "client" ,
100
+ Before : connectDB ,
101
+ After : closeDB ,
102
+ Action : cleanup ,
103
+ Flags : []cli.Flag {
104
+ & cli.StringFlag {
105
+ Name : "orgs" ,
106
+ Aliases : []string {"f" },
107
+ Usage : "path to a CSV file containing a list of organization IDs to keep" ,
108
+ Required : true ,
109
+ },
110
+ & cli.BoolFlag {
111
+ Name : "dry-run" ,
112
+ Aliases : []string {"d" },
113
+ Usage : "show the effect of cleanup without execution" ,
114
+ },
115
+ },
116
+ },
93
117
}
94
118
95
119
if err := app .Run (os .Args ); err != nil {
@@ -328,6 +352,135 @@ func reindex(c *cli.Context) (err error) {
328
352
return nil
329
353
}
330
354
355
+ func cleanup (c * cli.Context ) (err error ) {
356
+ ctx , cancel := context .WithTimeout (context .Background (), timeout )
357
+ defer cancel ()
358
+
359
+ dry := c .Bool ("dry-run" )
360
+
361
+ // Load the organizations from the CSV file
362
+ var f * os.File
363
+ if f , err = os .Open (c .String ("orgs" )); err != nil {
364
+ return cli .Exit (err , 1 )
365
+ }
366
+
367
+ // Ensure there is a header row
368
+ var header []string
369
+ reader := csv .NewReader (f )
370
+ if header , err = reader .Read (); err != nil {
371
+ return cli .Exit (err , 1 )
372
+ }
373
+
374
+ // Find the ID column or assume the first column is the ID
375
+ var idCol int
376
+ for i , col := range header {
377
+ if strings .ToLower (col ) == "id" {
378
+ idCol = i
379
+ break
380
+ }
381
+ }
382
+
383
+ orgs := make (map [ulid.ULID ]struct {})
384
+ for {
385
+ var record []string
386
+ if record , err = reader .Read (); err != nil {
387
+ break
388
+ }
389
+
390
+ var id ulid.ULID
391
+ if id , err = ulid .Parse (record [idCol ]); err != nil {
392
+ return cli .Exit (fmt .Errorf ("could not parse org ID: %s" , record [idCol ]), 1 )
393
+ }
394
+
395
+ orgs [id ] = struct {}{}
396
+ }
397
+
398
+ if len (orgs ) == 0 {
399
+ return cli .Exit (fmt .Errorf ("no organizations found in CSV file" ), 1 )
400
+ }
401
+
402
+ // Fetch tenants not in the organization list
403
+ strandedTenants := make (map [ulid.ULID ]* db.Tenant )
404
+ for {
405
+ var (
406
+ tenants []* db.Tenant
407
+ next * pagination.Cursor
408
+ )
409
+ if tenants , next , err = db .ListTenants (ctx , ulid.ULID {}, next ); err != nil {
410
+ return cli .Exit (err , 1 )
411
+ }
412
+
413
+ for _ , tenant := range tenants {
414
+ if _ , ok := orgs [tenant .OrgID ]; ! ok {
415
+ strandedTenants [tenant .ID ] = tenant
416
+ }
417
+ }
418
+
419
+ if next == nil {
420
+ break
421
+ }
422
+ }
423
+
424
+ // Fetch members not in the organization list
425
+ strandedMembers := make (map [ulid.ULID ]* db.Member )
426
+ for {
427
+ var (
428
+ members []* db.Member
429
+ next * pagination.Cursor
430
+ )
431
+ if members , next , err = db .ListMembers (ctx , ulid.ULID {}, next ); err != nil {
432
+ return cli .Exit (err , 1 )
433
+ }
434
+
435
+ for _ , member := range members {
436
+ if _ , ok := orgs [member .OrgID ]; ! ok {
437
+ strandedMembers [member .ID ] = member
438
+ }
439
+ }
440
+
441
+ if next == nil {
442
+ break
443
+ }
444
+ }
445
+
446
+ if dry {
447
+ fmt .Println ("The following tenants would be removed:" )
448
+ for _ , tenant := range strandedTenants {
449
+ fmt .Println (tenant .ID .String (), tenant .Name , tenant .Modified )
450
+ }
451
+
452
+ fmt .Println ("The following members would be removed:" )
453
+ for _ , member := range strandedMembers {
454
+ fmt .Println (member .ID .String (), member .Email , member .Organization , member .Modified )
455
+ }
456
+
457
+ return nil
458
+ } else {
459
+ var errs error
460
+ fmt .Println ("Removing" , len (strandedTenants ), "tenants and" , len (strandedMembers ), "members" )
461
+ for _ , tenant := range strandedTenants {
462
+ fmt .Println ("Removing tenant" , tenant .ID .String (), tenant .Name , tenant .Modified )
463
+ if err = db .DeleteTenant (ctx , tenant .OrgID , tenant .ID ); err != nil {
464
+ errs = errors .Join (errs , err )
465
+ }
466
+ }
467
+
468
+ for _ , member := range strandedMembers {
469
+ fmt .Println ("Removing member" , member .ID .String (), member .Email , member .Organization , member .Modified )
470
+ if err = db .DeleteMember (ctx , member .OrgID , member .ID ); err != nil {
471
+ errs = errors .Join (errs , err )
472
+ }
473
+ }
474
+
475
+ if errs != nil {
476
+ return cli .Exit (errs , 1 )
477
+ }
478
+ fmt .Println ("Successfully removed" , len (strandedTenants ), "tenants and" , len (strandedMembers ), "members" )
479
+ }
480
+
481
+ return nil
482
+ }
483
+
331
484
//===========================================================================
332
485
// Helpers
333
486
//===========================================================================
@@ -349,7 +502,7 @@ func connectDB(c *cli.Context) (err error) {
349
502
}
350
503
conf .ConsoleLog = false
351
504
352
- // Connect tot he trtl server
505
+ // Connect to the trtl server
353
506
if err = db .Connect (conf .Database ); err != nil {
354
507
return cli .Exit (err , 1 )
355
508
}
0 commit comments