@@ -18,6 +18,7 @@ import (
1818 "github.com/newrelic/newrelic-client-go/v2/newrelic"
1919
2020 "github.com/asomensari-es/nr-tf-util/internal/audit"
21+ "github.com/asomensari-es/nr-tf-util/internal/cleanup"
2122 "github.com/asomensari-es/nr-tf-util/internal/config"
2223 "github.com/asomensari-es/nr-tf-util/internal/generator"
2324 "github.com/asomensari-es/nr-tf-util/internal/ratelimit"
@@ -55,8 +56,10 @@ func main() {
5556 )
5657 var accountIDs multiString
5758 var skipList multiString
59+ var assetTypes multiString
5860 flag .Var (& accountIDs , "account-id" , "Account ID to scan (repeat for multiple; default: auto-discover)" )
5961 flag .Var (& skipList , "skip" , "Fetcher/asset type to skip (repeat for multiple)" )
62+ flag .Var (& assetTypes , "asset-type" , "Asset type to clean up (repeat for multiple; cleanup mode only)" )
6063
6164 flag .Usage = func () {
6265 fmt .Fprintf (os .Stderr , `nr-tf-util %s — New Relic Terraform utility
@@ -65,11 +68,14 @@ USAGE
6568 nr-tf-util [options]
6669
6770MODES
71+ --mode audit Scan accounts for hardcoded entity IDs and GUIDs that will
72+ break during an account migration, and flag alert configuration
73+ health issues (disabled conditions, empty policies, etc.).
74+ Optionally export findings to CSV with --csv.
75+ --mode cleanup Read an audit CSV (--csv) and permanently delete the matched
76+ assets. Requires --asset-type. Supports --dry-run.
6877 --mode import (default) Scan accounts and generate Terraform import blocks
6978 ready for: terraform plan -generate-config-out=resources.tf
70- --mode audit Scan accounts for hardcoded entity IDs and GUIDs that will
71- break during an account migration, and print a report with
72- migration suggestions.
7379
7480AUTHENTICATION
7581 --api-key <key> New Relic User API key (NRAK-...)
@@ -78,6 +84,7 @@ AUTHENTICATION
7884ACCOUNT SELECTION
7985 --account-id <id> Account ID to scan (repeat for multiple accounts)
8086 Omit to auto-discover all accessible accounts
87+ (not used in cleanup mode — accounts come from CSV)
8188
8289REGION
8390 --region <name> US (default) | EU | FedRAMP | JP
@@ -97,21 +104,52 @@ FILTERING
97104 workloads service_levels
98105 workflows
99106
107+ AUDIT MODE FLAGS
108+ --csv <file> Write findings to a CSV file (e.g. --csv findings.csv)
109+
110+ CLEANUP MODE FLAGS
111+ --csv <file> Input CSV file produced by audit mode (required)
112+ --asset-type <type> Asset type to delete (repeat for multiple; required)
113+ Supported values: nrql_condition alert_policy
114+ --dry-run Print what would be deleted without deleting anything
115+
100116IMPORT MODE FLAGS
101117 --out <dir> Root output directory (default: ./output)
102118 One subdirectory per account: <out>/<accountId>-<slug>/
103119 --dry-run Show what would be written without writing any files
104120
105- AUDIT MODE FLAGS
106- (no additional flags — uses the same filtering, rate-limit, and timeout options)
107-
108121BEHAVIOUR
109122 --verbose Show additional detail in progress output
110123 --quiet Suppress all non-error output
111124 --rate-limit <n> Max API requests per second (default: 25)
112125 --max-retries <n> Max retries on API failure (default: 3)
113126 --timeout <duration> Per-account timeout, e.g. 2m30s (default: 5m)
114127
128+ EXAMPLES — audit mode
129+ export NEW_RELIC_API_KEY=NRAK-...
130+
131+ # Audit all accessible accounts
132+ nr-tf-util --mode audit
133+
134+ # Audit a single account and export findings to CSV
135+ nr-tf-util --mode audit --account-id 2883194 --csv findings.csv
136+
137+ # Skip legacy-condition and uncovered-policy checks
138+ nr-tf-util --mode audit --skip legacy_conditions --skip uncovered_alert_policies
139+
140+ EXAMPLES — cleanup mode
141+ # Dry-run: see what would be deleted from the CSV
142+ nr-tf-util --mode cleanup --csv findings.csv \
143+ --asset-type nrql_condition --dry-run
144+
145+ # Delete disabled NRQL conditions
146+ nr-tf-util --mode cleanup --csv findings.csv \
147+ --asset-type nrql_condition
148+
149+ # Delete both empty policies and disabled conditions
150+ nr-tf-util --mode cleanup --csv findings.csv \
151+ --asset-type alert_policy --asset-type nrql_condition
152+
115153EXAMPLES — import mode
116154 # Generate import blocks for all accessible accounts
117155 nr-tf-util --api-key NRAK-...
@@ -123,21 +161,6 @@ EXAMPLES — import mode
123161 # Skip dashboards and workloads
124162 nr-tf-util --api-key NRAK-... --skip dashboards --skip workloads
125163
126- EXAMPLES — audit mode
127- # Audit all accessible accounts for hardcoded IDs
128- nr-tf-util --mode audit --api-key NRAK-...
129-
130- # Audit a specific account
131- nr-tf-util --mode audit --api-key NRAK-... --account-id 2883194
132-
133- # Audit only dashboards and NRQL conditions
134- nr-tf-util --mode audit --api-key NRAK-... \
135- --skip workloads --skip service_levels --skip workflows
136-
137- # Use an environment variable for the key
138- export NEW_RELIC_API_KEY=NRAK-...
139- nr-tf-util --mode audit --account-id 2883194
140-
141164NEXT STEPS (after import mode)
142165 cd output/<account-dir>/
143166 terraform init
@@ -157,9 +180,9 @@ NEXT STEPS (after import mode)
157180
158181 // ── Validate mode ──────────────────────────────────────────────────────────
159182 switch * mode {
160- case "import" , "audit" :
183+ case "import" , "audit" , "cleanup" :
161184 default :
162- fmt .Fprintf (os .Stderr , "error: unknown --mode %q (valid: import, audit )\n \n " , * mode )
185+ fmt .Fprintf (os .Stderr , "error: unknown --mode %q (valid: audit, cleanup, import )\n \n " , * mode )
163186 flag .Usage ()
164187 os .Exit (1 )
165188 }
@@ -198,11 +221,20 @@ NEXT STEPS (after import mode)
198221 printf ("\n nr-tf-util %s [mode: %s]\n \n " , Version , * mode )
199222 printf (" Region ..... %s\n " , reg .Name )
200223 printf (" Endpoint ..... %s\n " , reg .NerdGraphURL )
201- if * mode == "import" {
224+ switch * mode {
225+ case "import" :
202226 printf (" Output ..... %s\n " , * outDir )
203227 if * dryRun {
204228 printf (" [DRY-RUN — no files will be written]\n " )
205229 }
230+ case "cleanup" :
231+ printf (" CSV input .... %s\n " , * csvOut )
232+ if * dryRun {
233+ printf (" [DRY-RUN — no assets will be deleted]\n " )
234+ }
235+ if len (assetTypes ) > 0 {
236+ printf (" Asset types .. %s\n " , strings .Join (assetTypes , ", " ))
237+ }
206238 }
207239 printf (" Rate limit ... %d req/s\n " , * rateLimit )
208240 if len (skipList ) > 0 {
@@ -228,10 +260,17 @@ NEXT STEPS (after import mode)
228260
229261 ng := & nrClient .NerdGraph
230262
231- // ── Resolve accounts ───────────────────────────────────────────────────── ──
263+ // ── Resolve accounts (not needed for cleanup — accounts come from the CSV) ──
232264 ctx , cancel := context .WithTimeout (context .Background (), * timeout )
233265 defer cancel ()
234266
267+ limiter := ratelimit .New (* rateLimit )
268+
269+ if * mode == "cleanup" {
270+ runCleanup (ctx , ng , * csvOut , assetTypes , * dryRun )
271+ return
272+ }
273+
235274 var accounts []resources.Account
236275
237276 if len (accountIDs ) > 0 {
@@ -276,7 +315,6 @@ NEXT STEPS (after import mode)
276315 for _ , s := range skipList {
277316 skipSet [s ] = true
278317 }
279- limiter := ratelimit .New (* rateLimit )
280318
281319 // ── Mode branch ────────────────────────────────────────────────────────────
282320 switch * mode {
@@ -459,6 +497,48 @@ func runAudit(
459497 printf ("Duration: %s\n \n " , elapsed )
460498}
461499
500+ // ── Cleanup mode ─────────────────────────────────────────────────────────────
501+
502+ func runCleanup (ctx context.Context , ng resources.NerdGrapher , csvPath string , assetTypes []string , dryRun bool ) {
503+ if csvPath == "" {
504+ fmt .Fprintln (os .Stderr , "error: --csv <file> is required for cleanup mode" )
505+ os .Exit (1 )
506+ }
507+ if len (assetTypes ) == 0 {
508+ fmt .Fprintln (os .Stderr , "error: --asset-type is required for cleanup mode (supported: nrql_condition, alert_policy)" )
509+ os .Exit (1 )
510+ }
511+ supported := make ([]string , 0 , len (cleanup .SupportedAssetTypes ))
512+ for k := range cleanup .SupportedAssetTypes {
513+ supported = append (supported , k )
514+ }
515+ sort .Strings (supported )
516+ for _ , at := range assetTypes {
517+ if ! cleanup .SupportedAssetTypes [at ] {
518+ fmt .Fprintf (os .Stderr , "error: unknown --asset-type %q (supported: %s)\n " , at , strings .Join (supported , ", " ))
519+ os .Exit (1 )
520+ }
521+ }
522+
523+ f , err := os .Open (csvPath )
524+ if err != nil {
525+ fmt .Fprintf (os .Stderr , "error: cannot open CSV file %q: %v\n " , csvPath , err )
526+ os .Exit (1 )
527+ }
528+ defer f .Close ()
529+
530+ targets , err := cleanup .ParseCSV (f , assetTypes )
531+ if err != nil {
532+ fmt .Fprintf (os .Stderr , "error: parsing CSV: %v\n " , err )
533+ os .Exit (1 )
534+ }
535+
536+ if err := cleanup .Run (ctx , ng , targets , dryRun , os .Stdout , os .Stdin ); err != nil {
537+ fmt .Fprintf (os .Stderr , "error: %v\n " , err )
538+ os .Exit (1 )
539+ }
540+ }
541+
462542// ── Helpers ───────────────────────────────────────────────────────────────────
463543
464544var nonAlnum = regexp .MustCompile (`[^a-z0-9]+` )
0 commit comments