// ----------------------------------------------------------------------------- // Azure RG Janitor - Spectre Edition // Run with: dotnet run cleanup-rg.cs // Targets: .NET 10 Preview 4+ // ----------------------------------------------------------------------------- #:package Azure.Identity@1.* #:package Azure.ResourceManager@1.* #:package Spectre.Console@0.50.0 using Azure.Identity; using Azure.ResourceManager; using Azure.ResourceManager.Resources; using Spectre.Console; // ─────────────── settings ───────────────────────────────────────────────────── const int MaxParallel = 10; // limit concurrent deletions // ────────────────────────────────────────────────────────────────────────────── // 1️⃣ Select subscription var arm = new ArmClient(new DefaultAzureCredential()); var subs = arm.GetSubscriptions().GetAllAsync(); var subList = new List(); await AnsiConsole.Status() .Spinner(Spinner.Known.Dots) .SpinnerStyle(Style.Parse("yellow")) .StartAsync("Fetching Azure subscriptions...", async ctx => { await foreach (var s in subs) subList.Add(s); }); if (subList.Count == 0) { AnsiConsole.MarkupLine("[red]❌ No subscriptions found.[/]"); return; } var subChoices = subList.Select(s => $"{s.Data.DisplayName} ({s.Data.SubscriptionId})").ToList(); subChoices.Add("Cancel"); var selectedSub = AnsiConsole.Prompt( new SelectionPrompt() .Title("Select an Azure subscription:") .AddChoices(subChoices) .UseConverter(x => x) // keep display as is .PageSize(10) .MoreChoicesText("[grey](Move up and down to reveal more subscriptions)[/]") .HighlightStyle("blue") .EnableSearch() ); if (selectedSub == "Cancel") { AnsiConsole.MarkupLine("[red]❌ Aborted by user.[/]"); return; } var subIdx = subChoices.IndexOf(selectedSub); var sub = subList[subIdx]; AnsiConsole.MarkupLine($"[bold blue]🔍 Scanning subscription [yellow]{sub.Data.DisplayName}[/]…[/]"); // 2️⃣ Find candidate resource groups var matches = new List(); await foreach (var rg in sub.GetResourceGroups().GetAllAsync()) { matches.Add(rg); } if (matches.Count == 0) { AnsiConsole.MarkupLine("[green]✔ No resource groups match.[/]"); return; } // 3️⃣ Pick list for resource groups var rgChoices = matches.Select(rg => $"{rg.Data.Name}").OrderBy(name => name, StringComparer.OrdinalIgnoreCase).ToList(); matches = matches.OrderBy(rg => rg.Data.Name, StringComparer.OrdinalIgnoreCase).ToList(); var prompt = new MultiSelectionPrompt() .Title($"Select resource groups to delete ([yellow]{matches.Count}[/] found):") .NotRequired() .PageSize(10) .InstructionsText("[grey](Press [blue][/] to toggle, [green][/] to accept, type to search)[/]") .AddChoices(rgChoices); // foreach (var choice in rgChoices) // prompt.Select(choice); // pre-select all var selectedRgs = AnsiConsole.Prompt(prompt) ?? rgChoices; AnsiConsole.MarkupLine($"[grey]Selected resource groups:[/]"); foreach (var name in selectedRgs) AnsiConsole.MarkupLine($" [yellow]{name}[/]"); if (selectedRgs.Count == 0) { AnsiConsole.MarkupLine("[red]❌ Aborted – nothing selected.[/]"); return; } matches = matches.Where(rg => selectedRgs.Contains(rg.Data.Name)).OrderBy(rg => rg.Data.Name, StringComparer.OrdinalIgnoreCase).ToList(); // 1️⃣ pretty-print the list var table = new Table() .Border(TableBorder.Rounded) .AddColumn("#") .AddColumn("Name"); int idx = 1; foreach (var rg in matches) table.AddRow(idx++.ToString(), rg.Data.Name); AnsiConsole.Write(table); // 2️⃣ confirmation bool proceed = AnsiConsole.Confirm( $"Delete [yellow]{matches.Count}[/] resource group(s) shown above?", defaultValue: false); if (!proceed) { AnsiConsole.MarkupLine("[red]❌ Aborted – nothing deleted.[/]"); return; } // 3️⃣ delete with a progress bar AnsiConsole.MarkupLine("[bold]🗑 Deleting…[/]"); using var gate = new SemaphoreSlim(MaxParallel); await AnsiConsole.Progress() .AutoClear(true) .Columns( [ new TaskDescriptionColumn(), new ProgressBarColumn(), new SpinnerColumn(), new ElapsedTimeColumn(), ]) .StartAsync(async ctx => { var overallTask = ctx.AddTask("Overall progress", maxValue: matches.Count); var rgTasks = matches.ToDictionary( rg => rg.Data.Name, rg => ctx.AddTask($"[grey]Deleting:[/] [yellow]{rg.Data.Name}[/]", maxValue: 1) ); var deleteTasks = matches.Select(async rg => { await gate.WaitAsync(); try { rgTasks[rg.Data.Name].Increment(0.5); // show started await rg.DeleteAsync(Azure.WaitUntil.Completed); rgTasks[rg.Data.Name].Value = 1; } finally { gate.Release(); overallTask.Increment(1); } }); await Task.WhenAll(deleteTasks); }); AnsiConsole.MarkupLine("[bold green]✅ Cleanup completed.[/]"); // Print what was deleted AnsiConsole.MarkupLine("[bold]Deleted resource groups:[/]"); foreach (var rg in matches) AnsiConsole.MarkupLine($" [yellow]{rg.Data.Name}[/]");