Skip to content

Commit 993e306

Browse files
Introduce 'vhosts delete_multiple'
1 parent e04aa58 commit 993e306

File tree

10 files changed

+556
-19
lines changed

10 files changed

+556
-19
lines changed

.config/nextest.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ read_only = { max-threads = 16 }
1515
isolated_vhosts = { max-threads = 8 }
1616
# Tests that modify users/permissions (global state)
1717
user_management = { max-threads = 1 }
18+
# Tests that modify virtual hosts (global state)
19+
vhost_management = { max-threads = 1 }
1820
# Tests that modify runtime parameters
1921
runtime_params = { max-threads = 1 }
2022
# Tests requiring complete isolation
@@ -103,7 +105,13 @@ test-group = 'runtime_params'
103105
[[profile.default.overrides]]
104106
filter = 'binary(vhosts_tests)'
105107
priority = 62
106-
test-group = 'user_management'
108+
test-group = 'vhost_management'
109+
110+
# Virtual host delete_multiple tests (global vhost operations, can be flaky due to race conditions)
111+
[[profile.default.overrides]]
112+
filter = 'binary(vhosts_delete_multiple_tests)'
113+
priority = 61
114+
test-group = 'vhost_management'
107115

108116
# Combined integration tests (creates global users)
109117
[[profile.default.overrides]]

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66

77
* Several commands now have minimalistic progress indicators: `federation disable_tls_peer_verification_for_all_upstreams`, `federation enable_tls_peer_verification_for_all_upstreams`, `shovels disable_tls_peer_verification_for_all_source_uris`, `shovels disable_tls_peer_verification_for_all_destination_uris`, `shovels enable_tls_peer_verification_for_all_source_uris`, and `shovels enable_tls_peer_verification_for_all_destination_uris`
88

9+
* `vhosts delete_multiple` is a new command that deletes multiple virtual hosts matching a regular expression pattern:
10+
11+
```shell
12+
# Delete all virtual hosts matching a pattern (requires explicit approval)
13+
rabbitmqadmin vhosts delete_multiple --name-pattern "test-.*" --approve
14+
15+
# Dry-run to see what would be deleted without actually deleting
16+
rabbitmqadmin vhosts delete_multiple --name-pattern "staging-.*" --dry-run
17+
18+
# Non-interactive mode (no --approve flag needed)
19+
rabbitmqadmin --non-interactive vhosts delete_multiple --name-pattern "temp-.*"
20+
```
21+
22+
One virtual host — named `/`, that is, the default one — is always skipped to preserve
23+
at least one functional virtual host at all times.
24+
25+
**Important**: this command is **very destructive** and should be used with caution. Always test with `--dry-run` first.
26+
927
## v2.13.0 (Sep 26, 2025)
1028

1129
### Enhancements

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ toml = "0.9"
2828
color-print = "0.3"
2929
thiserror = "2"
3030
shellexpand = "3.1"
31+
regex = "1"
3132

3233
log = "0.4"
3334
rustls = { version = "0.23", features = ["aws_lc_rs"] }

src/cli.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2711,7 +2711,7 @@ pub fn nodes_subcommands(pre_flight_settings: PreFlightSettings) -> [Command; 3]
27112711
.map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options))
27122712
}
27132713

2714-
pub fn vhosts_subcommands(pre_flight_settings: PreFlightSettings) -> [Command; 3] {
2714+
pub fn vhosts_subcommands(pre_flight_settings: PreFlightSettings) -> [Command; 4] {
27152715
let list_cmd = Command::new("list")
27162716
.long_about("Lists virtual hosts")
27172717
.after_help(color_print::cformat!(
@@ -2764,7 +2764,32 @@ pub fn vhosts_subcommands(pre_flight_settings: PreFlightSettings) -> [Command; 3
27642764
)
27652765
.arg(idempotently_arg.clone());
27662766

2767-
[list_cmd, declare_cmd, delete_cmd]
2767+
let bulk_delete_cmd = Command::new("delete_multiple")
2768+
.about(color_print::cstr!("<bold><red>DANGER ZONE.</red></bold> Deletes multiple virtual hosts at once using a name matching pattern"))
2769+
.after_help(color_print::cformat!("<bold>Doc guide:</bold>: {}", VIRTUAL_HOST_GUIDE_URL))
2770+
.arg(
2771+
Arg::new("name_pattern")
2772+
.long("name-pattern")
2773+
.help("a regular expression that will be used to match virtual host names")
2774+
.required(true),
2775+
)
2776+
.arg(
2777+
Arg::new("approve")
2778+
.long("approve")
2779+
.action(ArgAction::SetTrue)
2780+
.help("this operation is very destructive and requires an explicit approval")
2781+
.required(false),
2782+
)
2783+
.arg(
2784+
Arg::new("dry_run")
2785+
.long("dry-run")
2786+
.action(ArgAction::SetTrue)
2787+
.help("show what would be deleted without performing the actual deletion")
2788+
.required(false),
2789+
)
2790+
.arg(idempotently_arg.clone());
2791+
2792+
[list_cmd, declare_cmd, delete_cmd, bulk_delete_cmd]
27682793
.map(|cmd| cmd.infer_long_args(pre_flight_settings.infer_long_options))
27692794
}
27702795

src/commands.rs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
// limitations under the License.
1414
#![allow(clippy::result_large_err)]
1515

16-
use crate::constants::DEFAULT_BLANKET_POLICY_PRIORITY;
16+
use crate::constants::{DEFAULT_BLANKET_POLICY_PRIORITY, DEFAULT_VHOST};
1717
use crate::errors::CommandRunError;
1818
use crate::output::ProgressReporter;
19+
use crate::pre_flight;
1920
use clap::ArgMatches;
2021
use rabbitmq_http_client::blocking_api::Client;
2122
use rabbitmq_http_client::blocking_api::Result as ClientResult;
@@ -39,6 +40,7 @@ use rabbitmq_http_client::requests::{
3940

4041
use rabbitmq_http_client::transformers::{TransformationChain, VirtualHostTransformationChain};
4142
use rabbitmq_http_client::{password_hashing, requests, responses};
43+
use regex::Regex;
4244
use serde::de::DeserializeOwned;
4345
use serde_json::Value;
4446
use std::fs;
@@ -1265,6 +1267,88 @@ pub fn delete_vhost(client: APIClient, command_args: &ArgMatches) -> ClientResul
12651267
client.delete_vhost(name, idempotently)
12661268
}
12671269

1270+
pub fn delete_multiple_vhosts(
1271+
client: APIClient,
1272+
command_args: &ArgMatches,
1273+
prog_rep: &mut dyn ProgressReporter,
1274+
) -> Result<Option<Vec<responses::VirtualHost>>, CommandRunError> {
1275+
let name_pattern = command_args.get_one::<String>("name_pattern").unwrap();
1276+
let approve = command_args
1277+
.get_one::<bool>("approve")
1278+
.cloned()
1279+
.unwrap_or(false);
1280+
let dry_run = command_args
1281+
.get_one::<bool>("dry_run")
1282+
.cloned()
1283+
.unwrap_or(false);
1284+
let idempotently = command_args
1285+
.get_one::<bool>("idempotently")
1286+
.cloned()
1287+
.unwrap_or(false);
1288+
let non_interactive_cli = command_args
1289+
.get_one::<bool>("non_interactive")
1290+
.cloned()
1291+
.unwrap_or_else(|| pre_flight::InteractivityMode::from_env().is_non_interactive());
1292+
1293+
let regex =
1294+
Regex::new(name_pattern).map_err(|_| CommandRunError::UnsupportedArgumentValue {
1295+
property: "name_pattern".to_string(),
1296+
})?;
1297+
1298+
let vhosts = client.list_vhosts()?;
1299+
1300+
let matching_vhosts: Vec<responses::VirtualHost> = vhosts
1301+
.into_iter()
1302+
.filter(|vhost| regex.is_match(&vhost.name))
1303+
.filter(|vhost| vhost.name != DEFAULT_VHOST)
1304+
.collect();
1305+
1306+
if dry_run {
1307+
return Ok(Some(matching_vhosts));
1308+
}
1309+
1310+
if !approve && !pre_flight::is_non_interactive() && !non_interactive_cli {
1311+
return Err(CommandRunError::FailureDuringExecution {
1312+
message: "This operation is destructive and requires the --approve flag".to_string(),
1313+
});
1314+
}
1315+
1316+
let total = matching_vhosts.len();
1317+
1318+
if total == 0 {
1319+
return Ok(None);
1320+
}
1321+
1322+
prog_rep.start_operation(total, "Deleting virtual hosts");
1323+
1324+
let mut successes = 0;
1325+
let mut failures = 0;
1326+
1327+
for (index, vhost) in matching_vhosts.iter().enumerate() {
1328+
let vhost_name = &vhost.name;
1329+
match client.delete_vhost(vhost_name, idempotently) {
1330+
Ok(_) => {
1331+
prog_rep.report_progress(index + 1, total, vhost_name);
1332+
successes += 1;
1333+
}
1334+
Err(error) => {
1335+
prog_rep.report_failure(vhost_name, &error.to_string());
1336+
failures += 1;
1337+
}
1338+
}
1339+
}
1340+
1341+
prog_rep.finish_operation(total);
1342+
1343+
if failures > 0 && successes == 0 {
1344+
return Err(CommandRunError::FailureDuringExecution {
1345+
message: format!("Failed to delete all {} virtual hosts", failures),
1346+
});
1347+
}
1348+
1349+
Ok(None)
1350+
}
1351+
12681352
pub fn delete_user(client: APIClient, command_args: &ArgMatches) -> ClientResult<()> {
12691353
// the flag is required
12701354
let name = command_args.get_one::<String>("name").unwrap();

src/main.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,22 @@ fn dispatch_common_subcommand(
11611161
let result = commands::delete_vhost(client, second_level_args);
11621162
res_handler.delete_operation_result(result);
11631163
}
1164+
("vhosts", "delete_multiple") => {
1165+
let mut prog_rep = res_handler.instantiate_progress_reporter();
1166+
let result =
1167+
commands::delete_multiple_vhosts(client, second_level_args, &mut *prog_rep);
1168+
match result {
1169+
Ok(Some(vhosts)) => {
1170+
res_handler.tabular_result(Ok(vhosts));
1171+
}
1172+
Ok(None) => {
1173+
res_handler.no_output_on_success(Ok(()));
1174+
}
1175+
Err(e) => {
1176+
res_handler.no_output_on_success::<()>(Err(e));
1177+
}
1178+
}
1179+
}
11641180
("vhosts", "list") => {
11651181
let result = commands::list_vhosts(client);
11661182
res_handler.tabular_result(result)

src/output.rs

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -461,42 +461,53 @@ pub trait ProgressReporter {
461461
fn report_progress(&mut self, current: usize, total: usize, item_name: &str);
462462
fn report_success(&mut self, item_name: &str);
463463
fn report_skip(&mut self, item_name: &str, reason: &str);
464+
fn report_failure(&mut self, item_name: &str, error: &str);
464465
fn finish_operation(&mut self, total: usize);
465466
}
466467

467468
#[allow(dead_code)]
468469
pub struct InteractiveProgressReporter {
469470
operation_name: String,
471+
failures: usize,
472+
current_position: usize,
473+
total: usize,
474+
results: Vec<char>,
470475
}
471476

472477
#[allow(dead_code)]
473478
impl InteractiveProgressReporter {
474479
pub fn new() -> Self {
475480
Self {
476481
operation_name: String::new(),
482+
failures: 0,
483+
current_position: 0,
484+
total: 0,
485+
results: Vec::new(),
477486
}
478487
}
479488
}
480489

481490
impl ProgressReporter for InteractiveProgressReporter {
482-
fn start_operation(&mut self, _total: usize, operation_name: &str) {
491+
fn start_operation(&mut self, total: usize, operation_name: &str) {
483492
self.operation_name = operation_name.to_string();
493+
self.failures = 0;
494+
self.current_position = 0;
495+
self.total = total;
496+
self.results = vec!['.'; total];
484497
println!("{}...", operation_name);
485498
}
486499

487-
fn report_progress(&mut self, current: usize, total: usize, _item_name: &str) {
488-
let percentage = if total > 0 {
489-
(current * 100) / total
490-
} else {
491-
0
492-
};
493-
let bar_width = 50;
494-
let filled = if total > 0 {
495-
(current * bar_width) / total
500+
fn report_progress(&mut self, _current: usize, _total: usize, _item_name: &str) {
501+
if self.current_position < self.results.len() {
502+
self.results[self.current_position] = '#';
503+
}
504+
self.current_position += 1;
505+
let percentage = if self.total > 0 {
506+
(self.current_position * 100) / self.total
496507
} else {
497508
0
498509
};
499-
let bar = format!("{}{}", "#".repeat(filled), ".".repeat(bar_width - filled));
510+
let bar: String = self.results.iter().collect();
500511
print!("\rProgress: [{:3}%] [{}]", percentage, bar);
501512
io::stdout().flush().unwrap();
502513
}
@@ -509,8 +520,34 @@ impl ProgressReporter for InteractiveProgressReporter {
509520
// No-op: progress bar already shows the advancement
510521
}
511522

523+
fn report_failure(&mut self, _item_name: &str, _error: &str) {
524+
self.failures += 1;
525+
if self.current_position < self.results.len() {
526+
self.results[self.current_position] = 'X';
527+
}
528+
self.current_position += 1;
529+
let percentage = if self.total > 0 {
530+
(self.current_position * 100) / self.total
531+
} else {
532+
0
533+
};
534+
let bar: String = self.results.iter().collect();
535+
print!("\rProgress: [{:3}%] [{}]", percentage, bar);
536+
io::stdout().flush().unwrap();
537+
}
538+
512539
fn finish_operation(&mut self, total: usize) {
513-
println!("\n✅ Completed: {} items processed", total);
540+
let successes = total - self.failures;
541+
if self.failures == 0 {
542+
println!("\n✅ Completed: {} items processed successfully", total);
543+
} else if successes == 0 {
544+
println!("\n❌ Failed: All {} items failed to process", total);
545+
} else {
546+
println!(
547+
"\n⚠️ Completed with failures: {} succeeded, {} failed out of {} total",
548+
successes, self.failures, total
549+
);
550+
}
514551
}
515552
}
516553

@@ -536,16 +573,21 @@ impl ProgressReporter for NonInteractiveProgressReporter {
536573
}
537574

538575
fn report_progress(&mut self, _current: usize, _total: usize, _item_name: &str) {
539-
print!(".");
576+
print!("#");
540577
io::stdout().flush().unwrap();
541578
}
542579

543580
fn report_success(&mut self, _item_name: &str) {
544-
// Dot already printed in report_progress
581+
// Hash already printed in report_progress
545582
}
546583

547584
fn report_skip(&mut self, _item_name: &str, _reason: &str) {
548-
// Dot already printed in report_progress
585+
// Hash already printed in report_progress
586+
}
587+
588+
fn report_failure(&mut self, _item_name: &str, _error: &str) {
589+
print!("X");
590+
io::stdout().flush().unwrap();
549591
}
550592

551593
fn finish_operation(&mut self, total: usize) {
@@ -580,6 +622,10 @@ impl ProgressReporter for QuietProgressReporter {
580622
// Silent
581623
}
582624

625+
fn report_failure(&mut self, _item_name: &str, _error: &str) {
626+
// Silent
627+
}
628+
583629
fn finish_operation(&mut self, total: usize) {
584630
println!("Completed: {} items processed", total);
585631
}

0 commit comments

Comments
 (0)