async_profiler_agent/metadata/
aws.rs1use reqwest::Method;
5use serde::Deserialize;
6use thiserror::Error;
7
8use super::AgentMetadata;
9
10#[derive(Error, Debug)]
11pub enum FargateMetadataToAgentMetadataError {
12 #[error("unable to parse task ARN as a valid ARN")]
13 TaskArnInvalid(#[from] aws_arn::Error),
14 #[error("AWS account id not found in Fargate metadata")]
15 AccountIdNotFound,
16 #[error("AWS region not found in Fargate metadata")]
17 AwsRegionNotFound,
18}
19
20#[derive(Error, Debug)]
21#[error("profiler metadata error: {0}")]
22pub enum AwsProfilerMetadataError {
23 #[error("failed to create profiler metadata file: {0}")]
24 FailedToCreateFile(#[from] std::io::Error),
25
26 #[error("failed fetching valid Fargate metadata: {0}")]
27 FargateMetadataToAgentMetadataError(#[from] FargateMetadataToAgentMetadataError),
28
29 #[error("retrieved invalid endpoint URI: {0}")]
30 InvalidUri(String),
31
32 #[error("failed to fetch metadata from endpoint over HTTP: {0}")]
33 FailedToFetchMetadataFromEndpoint(reqwest::Error),
34
35 #[error("failed to fetch metadata from IMDS endpoint over HTTP: {0}")]
36 FailedToFetchMetadataFromImds(#[from] aws_config::imds::client::error::ImdsError),
37
38 #[error("failed to parse metadata as valid UTF-8 from endpoint over HTTP: {0}")]
39 FailedToParseMetadataFromEndpoint(reqwest::Error),
40
41 #[error("failed to serialize metadata file: {0}")]
42 FailedToSerializeMetadataFile(#[from] serde_json::Error),
43}
44
45#[derive(Debug, Deserialize, PartialEq, Eq)]
46#[serde(rename_all = "camelCase")]
47struct ImdsEc2InstanceMetadata {
48 account_id: String,
49 region: String,
50 #[allow(dead_code)]
51 instance_type: String,
52 instance_id: String,
53}
54
55async fn read_ec2_metadata() -> Result<ImdsEc2InstanceMetadata, AwsProfilerMetadataError> {
56 let imds = aws_config::imds::Client::builder().build();
57 let imds_document = imds
58 .get("/latest/dynamic/instance-identity/document")
59 .await?;
60
61 Ok(serde_json::from_str(imds_document.as_ref())?)
62}
63
64#[derive(Deserialize, Debug, PartialEq, Eq)]
65struct FargateMetadata {
66 #[serde(rename = "Cluster")]
67 cluster: String,
68 #[serde(rename = "TaskARN")]
69 task_arn: String,
70}
71
72async fn read_fargate_metadata(
73 http_client: &reqwest::Client,
74) -> Result<FargateMetadata, AwsProfilerMetadataError> {
75 let Ok(md_uri) = std::env::var("ECS_CONTAINER_METADATA_URI_V4") else {
76 return Err(AwsProfilerMetadataError::InvalidUri(
77 "not running on fargate".into(),
78 ));
79 };
80 let uri = format!(
81 "{}/task",
82 md_uri
85 );
86
87 let req = http_client
88 .request(Method::GET, uri.clone())
89 .build()
90 .map_err(|_e| AwsProfilerMetadataError::InvalidUri(uri))?;
92 let res = http_client
93 .execute(req)
94 .await
95 .map_err(AwsProfilerMetadataError::FailedToFetchMetadataFromEndpoint)?;
96 let body_str = res
97 .text()
98 .await
99 .map_err(AwsProfilerMetadataError::FailedToParseMetadataFromEndpoint)?;
100
101 Ok(serde_json::from_str(&body_str)?)
102}
103
104impl super::AgentMetadata {
105 fn from_imds_ec2_instance_metadata(
106 imds_ec2_instance_metadata: ImdsEc2InstanceMetadata,
107 ) -> Self {
108 Self::Ec2AgentMetadata {
109 aws_account_id: imds_ec2_instance_metadata.account_id,
110 aws_region_id: imds_ec2_instance_metadata.region,
111 ec2_instance_id: imds_ec2_instance_metadata.instance_id,
112 }
113 }
114
115 fn try_from_fargate_metadata(
116 fargate_metadata: FargateMetadata,
117 ) -> Result<Self, FargateMetadataToAgentMetadataError> {
118 let ecs_task_arn: aws_arn::ResourceName = fargate_metadata.task_arn.parse()?;
119
120 Ok(Self::FargateAgentMetadata {
121 aws_account_id: ecs_task_arn
122 .account_id
123 .ok_or(FargateMetadataToAgentMetadataError::AccountIdNotFound)?
124 .to_string(),
125 aws_region_id: ecs_task_arn
126 .region
127 .ok_or(FargateMetadataToAgentMetadataError::AwsRegionNotFound)?
128 .to_string(),
129 ecs_task_arn: fargate_metadata.task_arn,
130 ecs_cluster_arn: fargate_metadata.cluster,
131 })
132 }
133}
134
135pub async fn load_agent_metadata() -> Result<AgentMetadata, AwsProfilerMetadataError> {
136 let agent_metadata: AgentMetadata = match read_ec2_metadata().await {
137 Ok(imds_ec2_instance_metadata) => {
138 AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata)
139 }
140 Err(_) => {
141 let http_client = reqwest::Client::new();
142 let fargate_metadata = read_fargate_metadata(&http_client).await?;
143
144 AgentMetadata::try_from_fargate_metadata(fargate_metadata)?
145 }
146 };
147 Ok(agent_metadata)
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
171 fn test_imds_ec2_metadata() {
172 let json_str = r#"
173{
174 "accountId" : "123456789012",
175 "architecture" : "x86_64",
176 "availabilityZone" : "eu-west-1b",
177 "billingProducts" : null,
178 "devpayProductCodes" : null,
179 "marketplaceProductCodes" : null,
180 "imageId" : "ami-052d4b310b0a459ff",
181 "instanceId" : "i-092eba08c089f6325",
182 "instanceType" : "c5.4xlarge",
183 "kernelId" : null,
184 "pendingTime" : "2025-03-20T16:41:24Z",
185 "privateIp" : "10.65.149.216",
186 "ramdiskId" : null,
187 "region" : "eu-west-1",
188 "version" : "2017-09-30"
189}"#;
190
191 let imds_ec2_instance_metadata: ImdsEc2InstanceMetadata =
192 serde_json::from_str(&json_str).unwrap();
193
194 assert_eq!(
195 imds_ec2_instance_metadata,
196 ImdsEc2InstanceMetadata {
197 account_id: "123456789012".to_owned(),
198 region: "eu-west-1".to_owned(),
199 instance_type: "c5.4xlarge".to_owned(),
200 instance_id: "i-092eba08c089f6325".to_owned(),
201 }
202 );
203
204 let agent_metadata =
205 AgentMetadata::from_imds_ec2_instance_metadata(imds_ec2_instance_metadata);
206
207 assert_eq!(
208 agent_metadata,
209 AgentMetadata::Ec2AgentMetadata {
210 aws_account_id: "123456789012".to_owned(),
211 aws_region_id: "eu-west-1".to_owned(),
212 ec2_instance_id: "i-092eba08c089f6325".to_owned(),
213 }
214 )
215 }
216
217 #[test]
218 fn test_fargate_metadata() {
219 let json_str = r#"
220{
221 "Cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
222 "TaskARN": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
223 "Family": "profiler-metadata",
224 "Revision": "1",
225 "DesiredStatus": "RUNNING",
226 "KnownStatus": "NONE",
227 "Limits": {
228 "CPU": 0.25,
229 "Memory": 2048
230 },
231 "PullStartedAt": "2025-03-20T16:41:24.713942268Z",
232 "PullStoppedAt": "2025-03-20T16:41:25.623883595Z",
233 "AvailabilityZone": "us-east-1f",
234 "LaunchType": "FARGATE",
235 "Containers": [
236 {
237 "DockerId": "5261e761e0e2a3d92da3f02c8e5bab1f-3356750833",
238 "Name": "profiler-metadata",
239 "DockerName": "profiler-metadata",
240 "Image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/profiler-metadata",
241 "ImageID": "sha256:ad9d89a36c31afef34c79e05263b06087ad354796cfd90c66ced30f40ea2dbf4",
242 "Labels": {
243 "com.amazonaws.ecs.cluster": "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster",
244 "com.amazonaws.ecs.container-name": "profiler-metadata",
245 "com.amazonaws.ecs.task-arn": "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f",
246 "com.amazonaws.ecs.task-definition-family": "profiler-metadata",
247 "com.amazonaws.ecs.task-definition-version": "1"
248 },
249 "DesiredStatus": "RUNNING",
250 "KnownStatus": "PULLED",
251 "Limits": {
252 "CPU": 0
253 },
254 "Type": "NORMAL",
255 "LogDriver": "awslogs",
256 "LogOptions": {
257 "awslogs-create-group": "true",
258 "awslogs-group": "/ecs/profiler-metadata",
259 "awslogs-region": "us-east-1",
260 "awslogs-stream": "ecs/profiler-metadata/5261e761e0e2a3d92da3f02c8e5bab1f",
261 "max-buffer-size": "25m",
262 "mode": "non-blocking"
263 },
264 "ContainerARN": "arn:aws:ecs:us-east-1:123456789012:container/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f/f4094744-1b40-4701-9f26-ad84ebb709d7",
265 "Networks": [
266 {
267 "NetworkMode": "awsvpc",
268 "IPv4Addresses": [
269 "172.31.233.169"
270 ],
271 "AttachmentIndex": 0,
272 "MACAddress": "16:ff:d6:e1:dc:99",
273 "IPv4SubnetCIDRBlock": "172.31.192.0/20",
274 "DomainNameServers": [
275 "172.31.0.2"
276 ],
277 "DomainNameSearchList": [
278 "ec2.internal"
279 ],
280 "PrivateDNSName": "ip-172-31-233-169.ec2.internal",
281 "SubnetGatewayIpv4Address": "172.31.192.1/20"
282 }
283 ],
284 "Snapshotter": "overlayfs"
285 }
286 ],
287 "ClockDrift": {
288 "ClockErrorBound": 0.3148955,
289 "ReferenceTimestamp": "2025-03-20T16:41:24Z",
290 "ClockSynchronizationStatus": "SYNCHRONIZED"
291 },
292 "EphemeralStorageMetrics": {
293 "Utilized": 208,
294 "Reserved": 20496
295 }
296}
297"#;
298
299 let fargate_metadata: FargateMetadata = serde_json::from_str(&json_str).unwrap();
300
301 assert_eq!(
302 fargate_metadata,
303 FargateMetadata {
304 cluster: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster"
305 .to_owned(),
306 task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
307 }
308 );
309
310 let agent_metadata = AgentMetadata::try_from_fargate_metadata(fargate_metadata).unwrap();
311
312 assert_eq!(
313 agent_metadata,
314 AgentMetadata::FargateAgentMetadata {
315 aws_account_id: "123456789012".to_owned(),
316 aws_region_id: "us-east-1".to_owned(),
317 ecs_task_arn: "arn:aws:ecs:us-east-1:123456789012:task/profiler-metadata-cluster/5261e761e0e2a3d92da3f02c8e5bab1f".to_owned(),
318 ecs_cluster_arn: "arn:aws:ecs:us-east-1:123456789012:cluster/profiler-metadata-cluster".to_owned(),
319 }
320 )
321 }
322}