1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! EC2: instance termination, and the VPC context (subnets, security
//! groups) the environment forms need to offer real choices.
use super::*;
/// One subnet in a VPC. Used by `:subnets` to populate the picker.
#[derive(Clone, Debug)]
pub struct SubnetInfo {
pub id: String,
pub availability_zone: String,
pub cidr_block: String,
/// Friendly name from the `Name` tag, if any.
pub name_tag: Option<String>,
}
/// One security group in a VPC. Used by `:security-groups`.
#[derive(Clone, Debug)]
pub struct SecurityGroupInfo {
pub id: String,
pub group_name: String,
pub description: String,
}
impl AwsClient {
/// List subnets in a VPC, ordered by AZ then CIDR for stable picker
/// rows. Returns the wide rows the `:subnets` picker needs (id + AZ
/// + CIDR + Name tag) so callers don't need a second round-trip.
pub async fn list_subnets_in_vpc(&self, vpc_id: &str) -> Result<Vec<SubnetInfo>> {
use aws_sdk_ec2::types::Filter;
// Paginate: DescribeSubnets caps a page well below the size of a
// shared VPC's subnet list, and a picker that silently shows a
// subset is worse than a slow one — the operator concludes the
// subnet doesn't exist.
let (this, vpc) = (self, vpc_id);
let raw = super::paginate("DescribeSubnets", move |token| async move {
let mut req = this.ec2.describe_subnets().max_results(1000).filters(
Filter::builder()
.name("vpc-id")
.values(vpc.to_string())
.build(),
);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("DescribeSubnets failed")?;
Ok((resp.subnets.unwrap_or_default(), resp.next_token))
})
.await?
// `.complete()`, not `.items()`: this feeds a picker, and a
// picker showing a prefix of the real list doesn't read as
// "short" — the operator scrolls, doesn't find what they want,
// and concludes it isn't there.
.complete("DescribeSubnets")?;
let mut out: Vec<SubnetInfo> = raw
.into_iter()
.map(|s| {
let name_tag = s.tags.as_ref().and_then(|tags| {
tags.iter()
.find(|t| t.key.as_deref() == Some("Name"))
.and_then(|t| t.value.clone())
});
SubnetInfo {
id: s.subnet_id.unwrap_or_default(),
availability_zone: s.availability_zone.unwrap_or_default(),
cidr_block: s.cidr_block.unwrap_or_default(),
name_tag,
}
})
.collect();
out.sort_by(|a, b| {
a.availability_zone
.cmp(&b.availability_zone)
.then(a.cidr_block.cmp(&b.cidr_block))
});
Ok(out)
}
/// List security groups in a VPC, ordered by name for stable picker
/// rows.
pub async fn list_security_groups_in_vpc(
&self,
vpc_id: &str,
) -> Result<Vec<SecurityGroupInfo>> {
use aws_sdk_ec2::types::Filter;
// Paginate: DescribeSecurityGroups defaults to 1000 per page and
// a shared VPC can exceed that. Same reasoning as the subnet
// listing above — a truncated picker reads as "not there".
let (this, vpc) = (self, vpc_id);
let raw = super::paginate("DescribeSecurityGroups", move |token| async move {
let mut req = this
.ec2
.describe_security_groups()
.max_results(1000)
.filters(
Filter::builder()
.name("vpc-id")
.values(vpc.to_string())
.build(),
);
if let Some(t) = token {
req = req.next_token(t);
}
let resp = req.send().await.wrap_err("DescribeSecurityGroups failed")?;
Ok((resp.security_groups.unwrap_or_default(), resp.next_token))
})
.await?
.complete("DescribeSecurityGroups")?;
let mut out: Vec<SecurityGroupInfo> = raw
.into_iter()
.map(|g| SecurityGroupInfo {
id: g.group_id.unwrap_or_default(),
group_name: g.group_name.unwrap_or_default(),
description: g.description.unwrap_or_default(),
})
.collect();
out.sort_by(|a, b| a.group_name.cmp(&b.group_name));
Ok(out)
}
/// Terminate a single EC2 instance by ID. ASG (created by EB) re-launches
/// a replacement automatically. The API returns immediately; the
/// instance enters `shutting-down` and EB's events panel will surface
/// the replacement within ~30 s.
pub async fn terminate_instance(&self, instance_id: &str) -> Result<()> {
self.ec2
.terminate_instances()
.instance_ids(instance_id)
.send()
.await
.wrap_err("ec2:TerminateInstances failed")?;
Ok(())
}
}