Skip to content

Commit 266b030

Browse files
authored
aws: Add pagination, progress bars, and human time (#6861)
describe_parameters did not have proper pagination so added some with rate limit handling that should gracefully return what we've collected so far. Also adds some progress bars to make it easier to observe progress over time. As well this also adds the ability to specify time with human readable types of input like: `--older-than '1h'` This was tested already on the PyTorch AWS account and has already deleted > 20k parameters with this. Output looks like: ![image](https://github.com/user-attachments/assets/60840598-1611-4ac2-b139-cf0c4e548a4b) --------- Signed-off-by: Eli Uriegas <[email protected]>
1 parent bdc5b86 commit 266b030

File tree

6 files changed

+111
-22
lines changed

6 files changed

+111
-22
lines changed

aws/tools/cleanup-ssm/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ clap = { version = "4.5.40", features = ["derive"] }
1010
tokio = { version = "1.45.1", features = ["full"] }
1111
chrono = "0.4"
1212
aws-smithy-types = "1.3"
13+
humantime = "2.1"
14+
indicatif = "0.17"
1315

1416
[dev-dependencies]
1517
mockall = "0.12"

aws/tools/cleanup-ssm/src/cleanup.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::SsmClient;
2+
use indicatif::{ProgressBar, ProgressStyle};
23

34
const BATCH_SIZE: usize = 10;
45

@@ -9,18 +10,40 @@ pub async fn delete_parameters_in_batches<C: SsmClient>(
910
let mut total_deleted = 0;
1011
let mut total_failed = 0;
1112

12-
for chunk in parameters_to_delete.chunks(BATCH_SIZE) {
13+
let total_params = parameters_to_delete.len();
14+
let total_batches = (total_params + BATCH_SIZE - 1).div_ceil(BATCH_SIZE);
15+
16+
// Create progress bar for deletion
17+
let pb = ProgressBar::new(total_batches as u64);
18+
pb.set_style(
19+
ProgressStyle::default_bar()
20+
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} batches ({msg})")
21+
.unwrap()
22+
.progress_chars("#>-")
23+
);
24+
pb.set_message("Deleting parameters...");
25+
26+
for (batch_idx, chunk) in parameters_to_delete.chunks(BATCH_SIZE).enumerate() {
1327
let (deleted, failed) = client.delete_parameters(chunk.to_vec()).await?;
1428

1529
total_deleted += deleted.len();
1630
total_failed += failed.len();
1731

18-
println!("Deleted {} parameters", deleted.len());
19-
if !failed.is_empty() {
20-
println!("Failed to delete {} parameters", failed.len());
21-
}
32+
pb.set_message(format!(
33+
"Batch {}/{} - Deleted: {}, Failed: {}",
34+
batch_idx + 1,
35+
total_batches,
36+
total_deleted,
37+
total_failed
38+
));
39+
pb.inc(1);
2240
}
2341

42+
pb.finish_with_message(format!(
43+
"✓ Completed! Total deleted: {}, Total failed: {}",
44+
total_deleted, total_failed
45+
));
46+
2447
Ok((total_deleted, total_failed))
2548
}
2649

aws/tools/cleanup-ssm/src/client.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use crate::SsmClient;
22
use aws_config::BehaviorVersion;
33
use aws_config::Region;
4+
use aws_sdk_ssm::error::ProvideErrorMetadata;
45
use aws_sdk_ssm::types::ParameterMetadata;
56
use aws_sdk_ssm::{Client, Error};
7+
use indicatif::{ProgressBar, ProgressStyle};
8+
use tokio::time::{sleep, Duration};
69

710
pub struct AwsSsmClient {
811
client: Client,
@@ -22,8 +25,58 @@ impl AwsSsmClient {
2225

2326
impl SsmClient for AwsSsmClient {
2427
async fn describe_parameters(&self) -> Result<Vec<ParameterMetadata>, Error> {
25-
let resp = self.client.describe_parameters().send().await?;
26-
Ok(resp.parameters().to_vec())
28+
let mut all_parameters = Vec::new();
29+
let mut next_token: Option<String> = None;
30+
31+
// Create a progress bar for fetching parameters
32+
let pb = ProgressBar::new_spinner();
33+
pb.set_style(
34+
ProgressStyle::default_spinner()
35+
.template("{spinner:.green} Fetching SSM parameters... [{elapsed_precise}] {msg}")
36+
.unwrap(),
37+
);
38+
pb.set_message("Starting...");
39+
40+
loop {
41+
let mut request = self.client.describe_parameters().max_results(50);
42+
43+
if let Some(token) = next_token {
44+
request = request.next_token(token);
45+
}
46+
47+
match request.send().await {
48+
Ok(resp) => {
49+
let new_params = resp.parameters().to_vec();
50+
all_parameters.extend(new_params);
51+
pb.set_message(format!("Found {} parameters", all_parameters.len()));
52+
next_token = resp.next_token().map(|s| s.to_string());
53+
54+
if next_token.is_none() {
55+
break;
56+
}
57+
58+
// Add delay to avoid rate limiting, 250ms seems to be the sweet spot
59+
sleep(Duration::from_millis(250)).await;
60+
}
61+
Err(e) => {
62+
// Check if it's a throttling error
63+
let metadata = e.meta();
64+
if metadata.code() == Some("ThrottlingException") {
65+
pb.finish_with_message(format!(
66+
"Rate limit reached. Collected {} parameters",
67+
all_parameters.len()
68+
));
69+
return Ok(all_parameters);
70+
}
71+
// For other errors, return them
72+
pb.abandon_with_message("Error fetching parameters");
73+
return Err(e.into());
74+
}
75+
}
76+
}
77+
78+
pb.finish_with_message(format!("✓ Collected {} parameters", all_parameters.len()));
79+
Ok(all_parameters)
2780
}
2881

2982
async fn delete_parameters(

aws/tools/cleanup-ssm/src/filter.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ use chrono::{DateTime, Duration};
55
pub fn filter_old_parameters<T: TimeProvider>(
66
parameters: &[ParameterMetadata],
77
time_provider: &T,
8-
older_than_days: u16,
8+
older_than_seconds: f64,
99
) -> Vec<String> {
10-
let threshold = time_provider.now() - Duration::days(older_than_days.into());
10+
let threshold = time_provider.now() - Duration::seconds(older_than_seconds as i64);
1111
let mut parameters_to_delete = Vec::new();
1212

1313
for parameter in parameters {
@@ -54,7 +54,7 @@ mod tests {
5454
let parameters = vec![];
5555
let time_provider = MockTimeProvider::new(Utc::now());
5656

57-
let result = filter_old_parameters(&parameters, &time_provider, 1);
57+
let result = filter_old_parameters(&parameters, &time_provider, 86400.0); // 1 day in seconds
5858

5959
assert_eq!(result.len(), 0);
6060
}
@@ -72,7 +72,7 @@ mod tests {
7272
let time_provider = MockTimeProvider::new(now);
7373
let parameters = vec![parameter];
7474

75-
let result = filter_old_parameters(&parameters, &time_provider, 1);
75+
let result = filter_old_parameters(&parameters, &time_provider, 86400.0); // 1 day in seconds
7676

7777
assert_eq!(result.len(), 0);
7878
}
@@ -90,7 +90,7 @@ mod tests {
9090
let time_provider = MockTimeProvider::new(now);
9191
let parameters = vec![parameter];
9292

93-
let result = filter_old_parameters(&parameters, &time_provider, 1);
93+
let result = filter_old_parameters(&parameters, &time_provider, 86400.0); // 1 day in seconds
9494

9595
assert_eq!(result.len(), 1);
9696
assert_eq!(result[0], "old-param");
@@ -115,7 +115,7 @@ mod tests {
115115
let time_provider = MockTimeProvider::new(now);
116116
let parameters = vec![old_parameter, recent_parameter];
117117

118-
let result = filter_old_parameters(&parameters, &time_provider, 2);
118+
let result = filter_old_parameters(&parameters, &time_provider, 172800.0); // 2 days in seconds
119119

120120
assert_eq!(result.len(), 1);
121121
assert_eq!(result[0], "old-param");
@@ -130,7 +130,7 @@ mod tests {
130130
let time_provider = MockTimeProvider::new(Utc::now());
131131
let parameters = vec![parameter];
132132

133-
let result = filter_old_parameters(&parameters, &time_provider, 1);
133+
let result = filter_old_parameters(&parameters, &time_provider, 86400.0); // 1 day in seconds
134134

135135
assert_eq!(result.len(), 0);
136136
}
@@ -147,7 +147,7 @@ mod tests {
147147
let time_provider = MockTimeProvider::new(now);
148148
let parameters = vec![parameter];
149149

150-
let result = filter_old_parameters(&parameters, &time_provider, 1);
150+
let result = filter_old_parameters(&parameters, &time_provider, 86400.0); // 1 day in seconds
151151

152152
assert_eq!(result.len(), 0);
153153
}

aws/tools/cleanup-ssm/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub mod filter;
1212
pub struct CleanupConfig {
1313
pub region: String,
1414
pub dry_run: bool,
15-
pub older_than_days: u16,
15+
pub older_than_seconds: f64,
1616
}
1717

1818
#[derive(Debug)]
@@ -53,7 +53,7 @@ pub async fn cleanup_ssm_parameters<C: SsmClient, T: TimeProvider>(
5353
let parameters = client.describe_parameters().await?;
5454

5555
let parameters_to_delete =
56-
filter::filter_old_parameters(&parameters, time_provider, config.older_than_days);
56+
filter::filter_old_parameters(&parameters, time_provider, config.older_than_seconds);
5757

5858
println!("Found {} parameters to delete", parameters_to_delete.len());
5959
let parameters_found = parameters_to_delete.len();
@@ -125,7 +125,7 @@ mod tests {
125125
let config = CleanupConfig {
126126
region: "us-east-1".to_string(),
127127
dry_run: true,
128-
older_than_days: 1,
128+
older_than_seconds: 86400.0, // 1 day in seconds
129129
};
130130

131131
let result = cleanup_ssm_parameters(&mock_client, &time_provider, &config)

aws/tools/cleanup-ssm/src/main.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,32 @@ struct Args {
1010
// if true, will not delete any parameters
1111
#[clap(long, default_value_t = true, action = clap::ArgAction::Set)]
1212
dry_run: bool,
13-
// number of days older than the parameter to delete
14-
#[clap(long, default_value = "1")]
15-
older_than: u16,
13+
// time duration older than which to delete parameters (e.g., "1d", "2h", "30m")
14+
#[clap(long, default_value = "1d")]
15+
older_than: String,
1616
}
1717

1818
#[tokio::main]
1919
async fn main() -> Result<(), Box<aws_sdk_ssm::Error>> {
2020
let args = Args::parse();
2121

22+
// Parse the human-readable time string into a Duration
23+
let duration = humantime::parse_duration(&args.older_than).unwrap_or_else(|e| {
24+
eprintln!("Error: Invalid time format '{}': {}", args.older_than, e);
25+
eprintln!("Supported formats: 30m, 2h, 1d, 2w (minutes, hours, days, weeks)");
26+
eprintln!("Note: Decimal values like '1.5d' are not supported. Use '36h' instead.");
27+
std::process::exit(1);
28+
});
29+
30+
// Get duration in seconds
31+
let older_than_seconds = duration.as_secs_f64();
32+
2233
let client = AwsSsmClient::new(&args.region).await?;
2334
let time_provider = SystemTimeProvider;
2435
let config = CleanupConfig {
2536
region: args.region,
2637
dry_run: args.dry_run,
27-
older_than_days: args.older_than,
38+
older_than_seconds,
2839
};
2940

3041
let result = cleanup_ssm_parameters(&client, &time_provider, &config).await?;

0 commit comments

Comments
 (0)