Skip to main content

vector/transforms/
aws_ec2_metadata.rs

1use std::{
2    collections::HashSet,
3    error, fmt,
4    future::ready,
5    pin::Pin,
6    sync::{Arc, LazyLock},
7};
8
9use arc_swap::ArcSwap;
10use bytes::Bytes;
11use futures::{Stream, StreamExt};
12use http::{Request, StatusCode, Uri, uri::PathAndQuery};
13use hyper::Body;
14use serde::Deserialize;
15use serde_with::serde_as;
16use snafu::ResultExt as _;
17use tokio::time::{Duration, Instant, sleep};
18use tracing::Instrument;
19use vector_lib::{
20    configurable::configurable_component,
21    lookup::{
22        OwnedTargetPath,
23        lookup_v2::{OptionalTargetPath, OwnedSegment},
24        owned_value_path,
25    },
26};
27use vrl::value::{Kind, kind::Collection};
28
29use crate::{
30    config::{
31        DataType, Input, OutputId, ProxyConfig, TransformConfig, TransformContext, TransformOutput,
32    },
33    cpu_time::spawn_timed,
34    event::Event,
35    http::HttpClient,
36    internal_events::{AwsEc2MetadataRefreshError, AwsEc2MetadataRefreshSuccessful},
37    schema,
38    transforms::{TaskTransform, Transform},
39};
40
41const ACCOUNT_ID_KEY: &str = "account-id";
42const AMI_ID_KEY: &str = "ami-id";
43const AVAILABILITY_ZONE_KEY: &str = "availability-zone";
44const INSTANCE_ID_KEY: &str = "instance-id";
45const INSTANCE_TYPE_KEY: &str = "instance-type";
46const LOCAL_HOSTNAME_KEY: &str = "local-hostname";
47const LOCAL_IPV4_KEY: &str = "local-ipv4";
48const PUBLIC_HOSTNAME_KEY: &str = "public-hostname";
49const PUBLIC_IPV4_KEY: &str = "public-ipv4";
50const REGION_KEY: &str = "region";
51const SUBNET_ID_KEY: &str = "subnet-id";
52const VPC_ID_KEY: &str = "vpc-id";
53const ROLE_NAME_KEY: &str = "role-name";
54const TAGS_KEY: &str = "tags";
55
56static AVAILABILITY_ZONE: LazyLock<PathAndQuery> =
57    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/placement/availability-zone"));
58static LOCAL_HOSTNAME: LazyLock<PathAndQuery> =
59    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/local-hostname"));
60static LOCAL_IPV4: LazyLock<PathAndQuery> =
61    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/local-ipv4"));
62static PUBLIC_HOSTNAME: LazyLock<PathAndQuery> =
63    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/public-hostname"));
64static PUBLIC_IPV4: LazyLock<PathAndQuery> =
65    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/public-ipv4"));
66static ROLE_NAME: LazyLock<PathAndQuery> =
67    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/iam/security-credentials/"));
68static MAC: LazyLock<PathAndQuery> =
69    LazyLock::new(|| PathAndQuery::from_static("/latest/meta-data/mac"));
70static DYNAMIC_DOCUMENT: LazyLock<PathAndQuery> =
71    LazyLock::new(|| PathAndQuery::from_static("/latest/dynamic/instance-identity/document"));
72static DEFAULT_FIELD_ALLOWLIST: &[&str] = &[
73    AMI_ID_KEY,
74    AVAILABILITY_ZONE_KEY,
75    INSTANCE_ID_KEY,
76    INSTANCE_TYPE_KEY,
77    LOCAL_HOSTNAME_KEY,
78    LOCAL_IPV4_KEY,
79    PUBLIC_HOSTNAME_KEY,
80    PUBLIC_IPV4_KEY,
81    REGION_KEY,
82    SUBNET_ID_KEY,
83    VPC_ID_KEY,
84    ROLE_NAME_KEY,
85];
86static API_TOKEN: LazyLock<PathAndQuery> =
87    LazyLock::new(|| PathAndQuery::from_static("/latest/api/token"));
88static TOKEN_HEADER: LazyLock<Bytes> = LazyLock::new(|| Bytes::from("X-aws-ec2-metadata-token"));
89
90/// Configuration for the `aws_ec2_metadata` transform.
91#[serde_as]
92#[configurable_component(transform(
93    "aws_ec2_metadata",
94    "Parse metadata emitted by AWS EC2 instances."
95))]
96#[derive(Clone, Debug, Derivative)]
97#[derivative(Default)]
98pub struct Ec2Metadata {
99    /// Overrides the default EC2 metadata endpoint.
100    #[serde(alias = "host", default = "default_endpoint")]
101    #[derivative(Default(value = "default_endpoint()"))]
102    endpoint: String,
103
104    /// Sets a prefix for all event fields added by the transform.
105    #[configurable(metadata(
106        docs::examples = "",
107        docs::examples = "ec2",
108        docs::examples = "aws.ec2",
109    ))]
110    namespace: Option<OptionalTargetPath>,
111
112    /// The interval between querying for updated metadata, in seconds.
113    #[serde(default = "default_refresh_interval_secs")]
114    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
115    #[derivative(Default(value = "default_refresh_interval_secs()"))]
116    refresh_interval_secs: Duration,
117
118    /// A list of metadata fields to include in each transformed event.
119    #[serde(default = "default_fields")]
120    #[derivative(Default(value = "default_fields()"))]
121    #[configurable(metadata(docs::examples = "instance-id", docs::examples = "local-hostname",))]
122    fields: Vec<String>,
123
124    /// A list of instance tags to include in each transformed event.
125    #[serde(default = "default_tags")]
126    #[derivative(Default(value = "default_tags()"))]
127    #[configurable(metadata(docs::examples = "Name", docs::examples = "Project",))]
128    tags: Vec<String>,
129
130    /// The timeout for querying the EC2 metadata endpoint, in seconds.
131    #[serde(default = "default_refresh_timeout_secs")]
132    #[serde_as(as = "serde_with::DurationSeconds<u64>")]
133    #[derivative(Default(value = "default_refresh_timeout_secs()"))]
134    refresh_timeout_secs: Duration,
135
136    #[configurable(derived)]
137    #[serde(default, skip_serializing_if = "crate::serde::is_default")]
138    proxy: ProxyConfig,
139
140    /// Requires the transform to be able to successfully query the EC2 metadata before starting to process the data.
141    #[serde(default = "default_required")]
142    #[derivative(Default(value = "default_required()"))]
143    required: bool,
144}
145
146fn default_endpoint() -> String {
147    String::from("http://169.254.169.254")
148}
149
150const fn default_refresh_interval_secs() -> Duration {
151    Duration::from_secs(10)
152}
153
154const fn default_refresh_timeout_secs() -> Duration {
155    Duration::from_secs(1)
156}
157
158fn default_fields() -> Vec<String> {
159    DEFAULT_FIELD_ALLOWLIST
160        .iter()
161        .map(|s| s.to_string())
162        .collect()
163}
164
165const fn default_tags() -> Vec<String> {
166    Vec::<String>::new()
167}
168
169const fn default_required() -> bool {
170    true
171}
172
173#[derive(Clone, Debug)]
174pub struct Ec2MetadataTransform {
175    state: Arc<ArcSwap<Vec<(MetadataKey, Bytes)>>>,
176}
177
178#[derive(Debug, Clone)]
179struct MetadataKey {
180    log_path: OwnedTargetPath,
181    metric_tag: String,
182}
183
184#[derive(Debug)]
185struct Keys {
186    account_id_key: MetadataKey,
187    ami_id_key: MetadataKey,
188    availability_zone_key: MetadataKey,
189    instance_id_key: MetadataKey,
190    instance_type_key: MetadataKey,
191    local_hostname_key: MetadataKey,
192    local_ipv4_key: MetadataKey,
193    public_hostname_key: MetadataKey,
194    public_ipv4_key: MetadataKey,
195    region_key: MetadataKey,
196    subnet_id_key: MetadataKey,
197    vpc_id_key: MetadataKey,
198    role_name_key: MetadataKey,
199    tags_key: MetadataKey,
200}
201
202impl_generate_config_from_default!(Ec2Metadata);
203
204#[async_trait::async_trait]
205#[typetag::serde(name = "aws_ec2_metadata")]
206impl TransformConfig for Ec2Metadata {
207    async fn build(&self, context: &TransformContext) -> crate::Result<Transform> {
208        let state = Arc::new(ArcSwap::new(Arc::new(vec![])));
209
210        let keys = Keys::new(self.namespace.clone());
211        let host = Uri::from_maybe_shared(self.endpoint.clone()).unwrap();
212        let refresh_interval = self.refresh_interval_secs;
213        let fields = self.fields.clone();
214        let tags = self.tags.clone();
215        let refresh_timeout = self.refresh_timeout_secs;
216        let required = self.required;
217
218        let proxy = ProxyConfig::merge_with_env(&context.globals.proxy, &self.proxy);
219        let http_client = HttpClient::new(None, &proxy)?;
220
221        let mut client = MetadataClient::new(
222            http_client,
223            host,
224            keys,
225            Arc::clone(&state),
226            refresh_interval,
227            refresh_timeout,
228            fields,
229            tags,
230        );
231
232        // If initial metadata is not required, log and proceed. Otherwise return error.
233        if let Err(error) = client.refresh_metadata().await {
234            if required {
235                return Err(error);
236            } else {
237                emit!(AwsEc2MetadataRefreshError { error });
238            }
239        }
240
241        // The metadata-refresh loop runs as its own tokio task, so the main
242        // transform task's CPU-time wrapper does not see it. Spawn the
243        // background task with the same component-tagged counter so its CPU
244        // is attributed to this transform.
245        spawn_timed(
246            async move {
247                client.run().await;
248            }
249            // TODO: Once #1338 is done we can fetch the current span
250            .instrument(info_span!("aws_ec2_metadata: worker").or_current()),
251            context.cpu_ns.clone(),
252        );
253
254        Ok(Transform::event_task(Ec2MetadataTransform { state }))
255    }
256
257    fn input(&self) -> Input {
258        Input::new(DataType::Metric | DataType::Log)
259    }
260
261    fn outputs(
262        &self,
263        _: &TransformContext,
264        input_definitions: &[(OutputId, schema::Definition)],
265    ) -> Vec<TransformOutput> {
266        let added_keys = Keys::new(self.namespace.clone());
267
268        let paths = [
269            &added_keys.account_id_key.log_path,
270            &added_keys.ami_id_key.log_path,
271            &added_keys.availability_zone_key.log_path,
272            &added_keys.instance_id_key.log_path,
273            &added_keys.instance_type_key.log_path,
274            &added_keys.local_hostname_key.log_path,
275            &added_keys.local_ipv4_key.log_path,
276            &added_keys.public_hostname_key.log_path,
277            &added_keys.public_ipv4_key.log_path,
278            &added_keys.region_key.log_path,
279            &added_keys.subnet_id_key.log_path,
280            &added_keys.vpc_id_key.log_path,
281            &added_keys.role_name_key.log_path,
282            &added_keys.tags_key.log_path,
283        ];
284
285        let schema_definition = input_definitions
286            .iter()
287            .map(|(output, definition)| {
288                let mut schema_definition = definition.clone();
289
290                // If the event is not an object, it will be converted to an object in this transform
291                if !schema_definition.event_kind().contains_object() {
292                    *schema_definition.event_kind_mut() = Kind::object(Collection::empty());
293                }
294
295                for path in paths {
296                    schema_definition =
297                        schema_definition.with_field(path, Kind::bytes().or_undefined(), None);
298                }
299
300                (output.clone(), schema_definition)
301            })
302            .collect();
303
304        vec![TransformOutput::new(
305            DataType::Metric | DataType::Log,
306            schema_definition,
307        )]
308    }
309}
310
311impl TaskTransform<Event> for Ec2MetadataTransform {
312    fn transform(
313        self: Box<Self>,
314        task: Pin<Box<dyn Stream<Item = Event> + Send>>,
315    ) -> Pin<Box<dyn Stream<Item = Event> + Send>>
316    where
317        Self: 'static,
318    {
319        let mut inner = self;
320        Box::pin(task.filter_map(move |event| ready(Some(inner.transform_one(event)))))
321    }
322}
323
324impl Ec2MetadataTransform {
325    fn transform_one(&mut self, mut event: Event) -> Event {
326        let state = self.state.load();
327        match event {
328            Event::Log(ref mut log) => {
329                state.iter().for_each(|(k, v)| {
330                    log.insert(&k.log_path, v.clone());
331                });
332            }
333            Event::Metric(ref mut metric) => {
334                state.iter().for_each(|(k, v)| {
335                    metric
336                        .replace_tag(k.metric_tag.clone(), String::from_utf8_lossy(v).to_string());
337                });
338            }
339            Event::Trace(_) => panic!("Traces are not supported."),
340        }
341        event
342    }
343}
344
345struct MetadataClient {
346    client: HttpClient<Body>,
347    host: Uri,
348    token: Option<(Bytes, Instant)>,
349    keys: Keys,
350    state: Arc<ArcSwap<Vec<(MetadataKey, Bytes)>>>,
351    refresh_interval: Duration,
352    refresh_timeout: Duration,
353    fields: HashSet<String>,
354    tags: HashSet<String>,
355}
356
357#[derive(Debug, Deserialize)]
358#[serde(rename_all = "camelCase")]
359#[allow(dead_code)] // deserialize all fields
360struct IdentityDocument {
361    account_id: String,
362    architecture: String,
363    image_id: String,
364    instance_id: String,
365    instance_type: String,
366    private_ip: String,
367    region: String,
368    version: String,
369}
370
371impl MetadataClient {
372    #[allow(clippy::too_many_arguments)]
373    pub fn new(
374        client: HttpClient<Body>,
375        host: Uri,
376        keys: Keys,
377        state: Arc<ArcSwap<Vec<(MetadataKey, Bytes)>>>,
378        refresh_interval: Duration,
379        refresh_timeout: Duration,
380        fields: Vec<String>,
381        tags: Vec<String>,
382    ) -> Self {
383        Self {
384            client,
385            host,
386            token: None,
387            keys,
388            state,
389            refresh_interval,
390            refresh_timeout,
391            fields: fields.into_iter().collect(),
392            tags: tags.into_iter().collect(),
393        }
394    }
395
396    async fn run(&mut self) {
397        loop {
398            match self.refresh_metadata().await {
399                Ok(_) => {
400                    emit!(AwsEc2MetadataRefreshSuccessful);
401                }
402                Err(error) => {
403                    emit!(AwsEc2MetadataRefreshError { error });
404                }
405            }
406
407            sleep(self.refresh_interval).await;
408        }
409    }
410
411    pub async fn get_token(&mut self) -> Result<Bytes, crate::Error> {
412        if let Some((token, next_refresh)) = self.token.clone() {
413            // If the next refresh is greater (in the future) than
414            // the current time we can return the token since its still valid
415            // otherwise lets refresh it.
416            if next_refresh > Instant::now() {
417                return Ok(token);
418            }
419        }
420
421        let mut parts = self.host.clone().into_parts();
422        parts.path_and_query = Some(API_TOKEN.clone());
423        let uri = Uri::from_parts(parts)?;
424
425        let req = Request::put(uri)
426            .header("X-aws-ec2-metadata-token-ttl-seconds", "21600")
427            .body(Body::empty())?;
428
429        let res = tokio::time::timeout(self.refresh_timeout, self.client.send(req))
430            .await?
431            .map_err(crate::Error::from)
432            .and_then(|res| match res.status() {
433                StatusCode::OK => Ok(res),
434                status_code => Err(UnexpectedHttpStatusError {
435                    status: status_code,
436                }
437                .into()),
438            })?;
439
440        let token = http_body::Body::collect(res.into_body()).await?.to_bytes();
441
442        let next_refresh = Instant::now() + Duration::from_secs(21600);
443        self.token = Some((token.clone(), next_refresh));
444
445        Ok(token)
446    }
447
448    pub async fn get_document(&mut self) -> Result<Option<IdentityDocument>, crate::Error> {
449        self.get_metadata(&DYNAMIC_DOCUMENT)
450            .await?
451            .map(|body| {
452                serde_json::from_slice(&body[..])
453                    .context(ParseIdentityDocumentSnafu {})
454                    .map_err(Into::into)
455            })
456            .transpose()
457    }
458
459    pub async fn refresh_metadata(&mut self) -> Result<(), crate::Error> {
460        let mut new_state = vec![];
461
462        // Fetch all resources, _then_ add them to the state map.
463        if let Some(document) = self.get_document().await? {
464            if self.fields.contains(ACCOUNT_ID_KEY) {
465                new_state.push((self.keys.account_id_key.clone(), document.account_id.into()));
466            }
467
468            if self.fields.contains(AMI_ID_KEY) {
469                new_state.push((self.keys.ami_id_key.clone(), document.image_id.into()));
470            }
471
472            if self.fields.contains(INSTANCE_ID_KEY) {
473                new_state.push((
474                    self.keys.instance_id_key.clone(),
475                    document.instance_id.into(),
476                ));
477            }
478
479            if self.fields.contains(INSTANCE_TYPE_KEY) {
480                new_state.push((
481                    self.keys.instance_type_key.clone(),
482                    document.instance_type.into(),
483                ));
484            }
485
486            if self.fields.contains(REGION_KEY) {
487                new_state.push((self.keys.region_key.clone(), document.region.into()));
488            }
489
490            if self.fields.contains(AVAILABILITY_ZONE_KEY)
491                && let Some(availability_zone) = self.get_metadata(&AVAILABILITY_ZONE).await?
492            {
493                new_state.push((self.keys.availability_zone_key.clone(), availability_zone));
494            }
495
496            if self.fields.contains(LOCAL_HOSTNAME_KEY)
497                && let Some(local_hostname) = self.get_metadata(&LOCAL_HOSTNAME).await?
498            {
499                new_state.push((self.keys.local_hostname_key.clone(), local_hostname));
500            }
501
502            if self.fields.contains(LOCAL_IPV4_KEY)
503                && let Some(local_ipv4) = self.get_metadata(&LOCAL_IPV4).await?
504            {
505                new_state.push((self.keys.local_ipv4_key.clone(), local_ipv4));
506            }
507
508            if self.fields.contains(PUBLIC_HOSTNAME_KEY)
509                && let Some(public_hostname) = self.get_metadata(&PUBLIC_HOSTNAME).await?
510            {
511                new_state.push((self.keys.public_hostname_key.clone(), public_hostname));
512            }
513
514            if self.fields.contains(PUBLIC_IPV4_KEY)
515                && let Some(public_ipv4) = self.get_metadata(&PUBLIC_IPV4).await?
516            {
517                new_state.push((self.keys.public_ipv4_key.clone(), public_ipv4));
518            }
519
520            if (self.fields.contains(SUBNET_ID_KEY) || self.fields.contains(VPC_ID_KEY))
521                && let Some(mac) = self.get_metadata(&MAC).await?
522            {
523                let mac = String::from_utf8_lossy(&mac[..]);
524
525                if self.fields.contains(SUBNET_ID_KEY) {
526                    let subnet_path =
527                        format!("/latest/meta-data/network/interfaces/macs/{mac}/subnet-id");
528
529                    let subnet_path = subnet_path.parse().context(ParsePathSnafu {
530                        value: subnet_path.clone(),
531                    })?;
532
533                    if let Some(subnet_id) = self.get_metadata(&subnet_path).await? {
534                        new_state.push((self.keys.subnet_id_key.clone(), subnet_id));
535                    }
536                }
537
538                if self.fields.contains(VPC_ID_KEY) {
539                    let vpc_path =
540                        format!("/latest/meta-data/network/interfaces/macs/{mac}/vpc-id");
541
542                    let vpc_path = vpc_path.parse().context(ParsePathSnafu {
543                        value: vpc_path.clone(),
544                    })?;
545
546                    if let Some(vpc_id) = self.get_metadata(&vpc_path).await? {
547                        new_state.push((self.keys.vpc_id_key.clone(), vpc_id));
548                    }
549                }
550            }
551
552            if self.fields.contains(ROLE_NAME_KEY)
553                && let Some(role_names) = self.get_metadata(&ROLE_NAME).await?
554            {
555                let role_names = String::from_utf8_lossy(&role_names[..]);
556
557                for (i, role_name) in role_names.lines().enumerate() {
558                    new_state.push((
559                        MetadataKey {
560                            log_path: self
561                                .keys
562                                .role_name_key
563                                .log_path
564                                .with_index_appended(i as isize),
565                            metric_tag: format!("{}[{}]", self.keys.role_name_key.metric_tag, i),
566                        },
567                        role_name.to_string().into(),
568                    ));
569                }
570            }
571
572            for tag in self.tags.clone() {
573                let tag_path = format!("/latest/meta-data/tags/instance/{tag}");
574
575                let tag_path = tag_path.parse().context(ParsePathSnafu {
576                    value: tag_path.clone(),
577                })?;
578
579                if let Some(tag_content) = self.get_metadata(&tag_path).await? {
580                    new_state.push((
581                        MetadataKey {
582                            log_path: self.keys.tags_key.log_path.with_field_appended(&tag),
583                            metric_tag: format!("{}[{}]", self.keys.tags_key.metric_tag, &tag),
584                        },
585                        tag_content,
586                    ));
587                }
588            }
589
590            self.state.store(Arc::new(new_state));
591        }
592
593        Ok(())
594    }
595
596    async fn get_metadata(&mut self, path: &PathAndQuery) -> Result<Option<Bytes>, crate::Error> {
597        let token = self
598            .get_token()
599            .await
600            .with_context(|_| FetchTokenSnafu {})?;
601
602        let mut parts = self.host.clone().into_parts();
603
604        parts.path_and_query = Some(path.clone());
605
606        let uri = Uri::from_parts(parts)?;
607
608        debug!(message = "Sending metadata request.", %uri);
609
610        let req = Request::get(uri)
611            .header(TOKEN_HEADER.as_ref(), token.as_ref())
612            .body(Body::empty())?;
613
614        match tokio::time::timeout(self.refresh_timeout, self.client.send(req))
615            .await?
616            .map_err(crate::Error::from)
617            .and_then(|res| match res.status() {
618                StatusCode::OK => Ok(Some(res)),
619                StatusCode::NOT_FOUND => Ok(None),
620                status_code => Err(UnexpectedHttpStatusError {
621                    status: status_code,
622                }
623                .into()),
624            })? {
625            Some(res) => {
626                let body = http_body::Body::collect(res.into_body()).await?.to_bytes();
627                Ok(Some(body))
628            }
629            None => Ok(None),
630        }
631    }
632}
633
634// This creates a simplified string from the namespace. Since the namespace is technically
635// a target path, it can contain syntax that is undesirable for a metric tag (such as prefix, quotes, etc)
636// This is mainly used for backwards compatibility.
637// see: https://github.com/vectordotdev/vector/issues/14931
638fn create_metric_namespace(namespace: &OwnedTargetPath) -> String {
639    let mut output = String::new();
640    for segment in &namespace.path.segments {
641        if !output.is_empty() {
642            output += ".";
643        }
644        match segment {
645            OwnedSegment::Field(field) => {
646                output += field;
647            }
648            OwnedSegment::Index(i) => {
649                output += &i.to_string();
650            }
651        }
652    }
653    output
654}
655
656fn create_key(namespace: &Option<OwnedTargetPath>, key: &str) -> MetadataKey {
657    if let Some(namespace) = namespace {
658        MetadataKey {
659            log_path: namespace.with_field_appended(key),
660            metric_tag: format!("{}.{}", create_metric_namespace(namespace), key),
661        }
662    } else {
663        MetadataKey {
664            log_path: OwnedTargetPath::event(owned_value_path!(key)),
665            metric_tag: key.to_owned(),
666        }
667    }
668}
669
670impl Keys {
671    pub fn new(namespace: Option<OptionalTargetPath>) -> Self {
672        let namespace = namespace.and_then(|namespace| namespace.path);
673
674        Keys {
675            account_id_key: create_key(&namespace, ACCOUNT_ID_KEY),
676            ami_id_key: create_key(&namespace, AMI_ID_KEY),
677            availability_zone_key: create_key(&namespace, AVAILABILITY_ZONE_KEY),
678            instance_id_key: create_key(&namespace, INSTANCE_ID_KEY),
679            instance_type_key: create_key(&namespace, INSTANCE_TYPE_KEY),
680            local_hostname_key: create_key(&namespace, LOCAL_HOSTNAME_KEY),
681            local_ipv4_key: create_key(&namespace, LOCAL_IPV4_KEY),
682            public_hostname_key: create_key(&namespace, PUBLIC_HOSTNAME_KEY),
683            public_ipv4_key: create_key(&namespace, PUBLIC_IPV4_KEY),
684            region_key: create_key(&namespace, REGION_KEY),
685            subnet_id_key: create_key(&namespace, SUBNET_ID_KEY),
686            vpc_id_key: create_key(&namespace, VPC_ID_KEY),
687            role_name_key: create_key(&namespace, ROLE_NAME_KEY),
688            tags_key: create_key(&namespace, TAGS_KEY),
689        }
690    }
691}
692
693#[derive(Debug)]
694struct UnexpectedHttpStatusError {
695    status: http::StatusCode,
696}
697
698impl fmt::Display for UnexpectedHttpStatusError {
699    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
700        write!(f, "got unexpected status code: {}", self.status)
701    }
702}
703
704impl error::Error for UnexpectedHttpStatusError {}
705
706#[derive(Debug, snafu::Snafu)]
707enum Ec2MetadataError {
708    #[snafu(display("Unable to fetch metadata authentication token: {}.", source))]
709    FetchToken { source: crate::Error },
710    #[snafu(display("Unable to parse identity document: {}.", source))]
711    ParseIdentityDocument { source: serde_json::Error },
712    #[snafu(display("Unable to parse metadata path {}, {}.", value, source))]
713    ParsePath {
714        value: String,
715        source: http::uri::InvalidUri,
716    },
717}
718
719#[cfg(test)]
720mod test {
721    use vector_lib::lookup::OwnedTargetPath;
722    use vrl::{owned_value_path, value::Kind};
723
724    use crate::{
725        config::{LogNamespace, OutputId, TransformConfig, schema::Definition},
726        transforms::aws_ec2_metadata::Ec2Metadata,
727    };
728
729    #[tokio::test]
730    async fn schema_def_with_string_input() {
731        let transform_config = Ec2Metadata {
732            namespace: Some(OwnedTargetPath::event(owned_value_path!("ec2", "metadata")).into()),
733            ..Default::default()
734        };
735
736        let input_definition =
737            Definition::new(Kind::bytes(), Kind::any_object(), [LogNamespace::Vector]);
738
739        let mut outputs = transform_config.outputs(
740            &Default::default(),
741            &[(OutputId::dummy(), input_definition)],
742        );
743        assert_eq!(outputs.len(), 1);
744        let output = outputs.pop().unwrap();
745        let actual_schema_def = output.schema_definitions(true)[&OutputId::dummy()].clone();
746        assert!(actual_schema_def.event_kind().is_object());
747    }
748}
749
750#[cfg(feature = "aws-ec2-metadata-integration-tests")]
751#[cfg(test)]
752mod integration_tests {
753    use tokio::sync::mpsc;
754    use tokio_stream::wrappers::ReceiverStream;
755    use vector_lib::{
756        assert_event_data_eq,
757        lookup::{
758            PathPrefix, event_path,
759            lookup_v2::{OwnedSegment, OwnedValuePath},
760        },
761    };
762    use vrl::{
763        path::parse_target_path,
764        value::{ObjectMap, Value},
765    };
766    use warp::Filter;
767
768    use super::*;
769    use crate::{
770        event::{LogEvent, Metric, metric},
771        test_util::{addr::next_addr, components::assert_transform_compliance},
772        transforms::test::create_topology,
773    };
774
775    fn ec2_metadata_address() -> String {
776        std::env::var("EC2_METADATA_ADDRESS").unwrap_or_else(|_| "http://localhost:1338".into())
777    }
778
779    fn expected_log_fields() -> Vec<(OwnedValuePath, &'static str)> {
780        vec![
781            (
782                vec![OwnedSegment::field(AVAILABILITY_ZONE_KEY)].into(),
783                "us-east-1a",
784            ),
785            (
786                vec![OwnedSegment::field(PUBLIC_IPV4_KEY)].into(),
787                "192.0.2.54",
788            ),
789            (
790                vec![OwnedSegment::field(PUBLIC_HOSTNAME_KEY)].into(),
791                "ec2-192-0-2-54.compute-1.amazonaws.com",
792            ),
793            (
794                vec![OwnedSegment::field(LOCAL_IPV4_KEY)].into(),
795                "172.16.34.43",
796            ),
797            (
798                vec![OwnedSegment::field(LOCAL_HOSTNAME_KEY)].into(),
799                "ip-172-16-34-43.ec2.internal",
800            ),
801            (
802                vec![OwnedSegment::field(INSTANCE_ID_KEY)].into(),
803                "i-1234567890abcdef0",
804            ),
805            (
806                vec![OwnedSegment::field(ACCOUNT_ID_KEY)].into(),
807                "0123456789",
808            ),
809            (
810                vec![OwnedSegment::field(AMI_ID_KEY)].into(),
811                "ami-0b69ea66ff7391e80",
812            ),
813            (
814                vec![OwnedSegment::field(INSTANCE_TYPE_KEY)].into(),
815                "m4.xlarge",
816            ),
817            (vec![OwnedSegment::field(REGION_KEY)].into(), "us-east-1"),
818            (vec![OwnedSegment::field(VPC_ID_KEY)].into(), "vpc-d295a6a7"),
819            (
820                vec![OwnedSegment::field(SUBNET_ID_KEY)].into(),
821                "subnet-0ac62554",
822            ),
823            (owned_value_path!("role-name", 0), "baskinc-role"),
824            (owned_value_path!("tags", "Name"), "test-instance"),
825            (owned_value_path!("tags", "Test"), "test-tag"),
826        ]
827    }
828
829    fn expected_metric_fields() -> Vec<(&'static str, &'static str)> {
830        vec![
831            (AVAILABILITY_ZONE_KEY, "us-east-1a"),
832            (PUBLIC_IPV4_KEY, "192.0.2.54"),
833            (
834                PUBLIC_HOSTNAME_KEY,
835                "ec2-192-0-2-54.compute-1.amazonaws.com",
836            ),
837            (LOCAL_IPV4_KEY, "172.16.34.43"),
838            (LOCAL_HOSTNAME_KEY, "ip-172-16-34-43.ec2.internal"),
839            (INSTANCE_ID_KEY, "i-1234567890abcdef0"),
840            (ACCOUNT_ID_KEY, "0123456789"),
841            (AMI_ID_KEY, "ami-0b69ea66ff7391e80"),
842            (INSTANCE_TYPE_KEY, "m4.xlarge"),
843            (REGION_KEY, "us-east-1"),
844            (VPC_ID_KEY, "vpc-d295a6a7"),
845            (SUBNET_ID_KEY, "subnet-0ac62554"),
846            ("role-name[0]", "baskinc-role"),
847            ("tags[Name]", "test-instance"),
848            ("tags[Test]", "test-tag"),
849        ]
850    }
851
852    fn make_metric() -> Metric {
853        Metric::new(
854            "event",
855            metric::MetricKind::Incremental,
856            metric::MetricValue::Counter { value: 1.0 },
857        )
858    }
859
860    #[test]
861    fn generate_config() {
862        crate::test_util::test_generate_config::<Ec2Metadata>();
863    }
864
865    #[tokio::test]
866    async fn enrich_log() {
867        assert_transform_compliance(async {
868            let mut fields = default_fields();
869            fields.extend(vec![String::from(ACCOUNT_ID_KEY)].into_iter());
870
871            let tags = vec![
872                String::from("Name"),
873                String::from("Test"),
874                String::from("MISSING_TAG"),
875            ];
876
877            let transform_config = Ec2Metadata {
878                endpoint: ec2_metadata_address(),
879                fields,
880                tags,
881                ..Default::default()
882            };
883
884            let (tx, rx) = mpsc::channel(1);
885            let (topology, mut out) =
886                create_topology(ReceiverStream::new(rx), transform_config).await;
887
888            // We need to sleep to let the background task fetch the data.
889            sleep(Duration::from_secs(1)).await;
890
891            let log = LogEvent::default();
892            let mut expected_log = log.clone();
893            for (k, v) in expected_log_fields().iter().cloned() {
894                expected_log.insert((PathPrefix::Event, &k), v);
895            }
896
897            tx.send(log.into()).await.unwrap();
898
899            let event = out.recv().await.unwrap();
900            assert_event_data_eq!(event.into_log(), expected_log);
901
902            drop(tx);
903            topology.stop().await;
904            assert_eq!(out.recv().await, None);
905        })
906        .await;
907    }
908
909    #[tokio::test(flavor = "multi_thread")]
910    async fn timeout() {
911        let (_guard, addr) = next_addr();
912
913        async fn sleepy() -> Result<impl warp::Reply, std::convert::Infallible> {
914            tokio::time::sleep(Duration::from_secs(3)).await;
915            Ok("I waited 3 seconds!")
916        }
917
918        let slow = warp::any().and_then(sleepy);
919        let server = warp::serve(slow).bind(addr);
920        let _server = tokio::spawn(server);
921
922        let config = Ec2Metadata {
923            endpoint: format!("http://{addr}"),
924            refresh_timeout_secs: Duration::from_secs(1),
925            ..Default::default()
926        };
927
928        match config.build(&TransformContext::default()).await {
929            Ok(_) => panic!("expected timeout failure"),
930            // cannot create tokio::time::error::Elapsed to compare with since constructor is
931            // private
932            Err(err) => assert_eq!(
933                err.to_string(),
934                "Unable to fetch metadata authentication token: deadline has elapsed."
935            ),
936        }
937    }
938
939    // validates the configuration setting 'required'=false allows vector to run
940    #[tokio::test(flavor = "multi_thread")]
941    async fn not_required() {
942        let (_guard, addr) = next_addr();
943
944        async fn sleepy() -> Result<impl warp::Reply, std::convert::Infallible> {
945            tokio::time::sleep(Duration::from_secs(3)).await;
946            Ok("I waited 3 seconds!")
947        }
948
949        let slow = warp::any().and_then(sleepy);
950        let server = warp::serve(slow).bind(addr);
951        let _server = tokio::spawn(server);
952
953        let config = Ec2Metadata {
954            endpoint: format!("http://{addr}"),
955            refresh_timeout_secs: Duration::from_secs(1),
956            required: false,
957            ..Default::default()
958        };
959
960        assert!(
961            config.build(&TransformContext::default()).await.is_ok(),
962            "expected no failure because 'required' config value set to false"
963        );
964    }
965
966    #[tokio::test]
967    async fn enrich_metric() {
968        assert_transform_compliance(async {
969            let mut fields = default_fields();
970            fields.extend(vec![String::from(ACCOUNT_ID_KEY)].into_iter());
971
972            let tags = vec![
973                String::from("Name"),
974                String::from("Test"),
975                String::from("MISSING_TAG"),
976            ];
977
978            let transform_config = Ec2Metadata {
979                endpoint: ec2_metadata_address(),
980                fields,
981                tags,
982                ..Default::default()
983            };
984
985            let (tx, rx) = mpsc::channel(1);
986            let (topology, mut out) =
987                create_topology(ReceiverStream::new(rx), transform_config).await;
988
989            // We need to sleep to let the background task fetch the data.
990            sleep(Duration::from_secs(1)).await;
991
992            let metric = make_metric();
993            let mut expected_metric = metric.clone();
994            for (k, v) in expected_metric_fields().iter() {
995                expected_metric.replace_tag(k.to_string(), v.to_string());
996            }
997
998            tx.send(metric.into()).await.unwrap();
999
1000            let event = out.recv().await.unwrap();
1001            assert_event_data_eq!(event.into_metric(), expected_metric);
1002
1003            drop(tx);
1004            topology.stop().await;
1005            assert_eq!(out.recv().await, None);
1006        })
1007        .await;
1008    }
1009
1010    #[tokio::test]
1011    async fn fields_log() {
1012        assert_transform_compliance(async {
1013            let transform_config = Ec2Metadata {
1014                endpoint: ec2_metadata_address(),
1015                fields: vec![PUBLIC_IPV4_KEY.into(), REGION_KEY.into()],
1016                tags: vec![
1017                    String::from("Name"),
1018                    String::from("Test"),
1019                    String::from("MISSING_TAG"),
1020                ],
1021                ..Default::default()
1022            };
1023
1024            let (tx, rx) = mpsc::channel(1);
1025            let (topology, mut out) =
1026                create_topology(ReceiverStream::new(rx), transform_config).await;
1027
1028            // We need to sleep to let the background task fetch the data.
1029            sleep(Duration::from_secs(1)).await;
1030
1031            let log = LogEvent::default();
1032            let mut expected_log = log.clone();
1033            expected_log.insert(
1034                &vrl::path::parse_target_path(&format!("\"{PUBLIC_IPV4_KEY}\"")).unwrap(),
1035                "192.0.2.54",
1036            );
1037            expected_log.insert(
1038                &vrl::path::parse_target_path(&format!("\"{REGION_KEY}\"")).unwrap(),
1039                "us-east-1",
1040            );
1041            expected_log.insert(
1042                &vrl::path::parse_target_path(&format!("\"{TAGS_KEY}\"")).unwrap(),
1043                ObjectMap::from([
1044                    ("Name".into(), Value::from("test-instance")),
1045                    ("Test".into(), Value::from("test-tag")),
1046                ]),
1047            );
1048
1049            tx.send(log.into()).await.unwrap();
1050
1051            let event = out.recv().await.unwrap();
1052            assert_event_data_eq!(event.into_log(), expected_log);
1053
1054            drop(tx);
1055            topology.stop().await;
1056            assert_eq!(out.recv().await, None);
1057        })
1058        .await;
1059    }
1060
1061    #[tokio::test]
1062    async fn fields_metric() {
1063        assert_transform_compliance(async {
1064            let transform_config = Ec2Metadata {
1065                endpoint: ec2_metadata_address(),
1066                fields: vec![PUBLIC_IPV4_KEY.into(), REGION_KEY.into()],
1067                tags: vec![
1068                    String::from("Name"),
1069                    String::from("Test"),
1070                    String::from("MISSING_TAG"),
1071                ],
1072                ..Default::default()
1073            };
1074
1075            let (tx, rx) = mpsc::channel(1);
1076            let (topology, mut out) =
1077                create_topology(ReceiverStream::new(rx), transform_config).await;
1078
1079            // We need to sleep to let the background task fetch the data.
1080            sleep(Duration::from_secs(1)).await;
1081
1082            let metric = make_metric();
1083            let mut expected_metric = metric.clone();
1084            expected_metric.replace_tag(PUBLIC_IPV4_KEY.to_string(), "192.0.2.54".to_string());
1085            expected_metric.replace_tag(REGION_KEY.to_string(), "us-east-1".to_string());
1086            expected_metric.replace_tag(
1087                format!("{}[{}]", TAGS_KEY, "Name"),
1088                "test-instance".to_string(),
1089            );
1090            expected_metric
1091                .replace_tag(format!("{}[{}]", TAGS_KEY, "Test"), "test-tag".to_string());
1092
1093            tx.send(metric.into()).await.unwrap();
1094
1095            let event = out.recv().await.unwrap();
1096            assert_event_data_eq!(event.into_metric(), expected_metric);
1097
1098            drop(tx);
1099            topology.stop().await;
1100            assert_eq!(out.recv().await, None);
1101        })
1102        .await;
1103    }
1104
1105    #[tokio::test]
1106    async fn namespace_log() {
1107        {
1108            assert_transform_compliance(async {
1109                let transform_config = Ec2Metadata {
1110                    endpoint: ec2_metadata_address(),
1111                    namespace: Some(
1112                        OwnedTargetPath::event(owned_value_path!("ec2", "metadata")).into(),
1113                    ),
1114                    ..Default::default()
1115                };
1116
1117                let (tx, rx) = mpsc::channel(1);
1118                let (topology, mut out) =
1119                    create_topology(ReceiverStream::new(rx), transform_config).await;
1120
1121                // We need to sleep to let the background task fetch the data.
1122                sleep(Duration::from_secs(1)).await;
1123
1124                let log = LogEvent::default();
1125
1126                tx.send(log.into()).await.unwrap();
1127
1128                let event = out.recv().await.unwrap();
1129
1130                assert_eq!(
1131                    event
1132                        .as_log()
1133                        .get(&parse_target_path("ec2.metadata.\"availability-zone\"").unwrap()),
1134                    Some(&"us-east-1a".into())
1135                );
1136
1137                drop(tx);
1138                topology.stop().await;
1139                assert_eq!(out.recv().await, None);
1140            })
1141            .await;
1142        }
1143
1144        {
1145            assert_transform_compliance(async {
1146                // Set an empty namespace to ensure we don't prepend one.
1147                let transform_config = Ec2Metadata {
1148                    endpoint: ec2_metadata_address(),
1149                    namespace: Some(OptionalTargetPath::none()),
1150                    ..Default::default()
1151                };
1152
1153                let (tx, rx) = mpsc::channel(1);
1154                let (topology, mut out) =
1155                    create_topology(ReceiverStream::new(rx), transform_config).await;
1156
1157                // We need to sleep to let the background task fetch the data.
1158                sleep(Duration::from_secs(1)).await;
1159
1160                let log = LogEvent::default();
1161
1162                tx.send(log.into()).await.unwrap();
1163
1164                let event = out.recv().await.unwrap();
1165                assert_eq!(
1166                    event.as_log().get(event_path!(AVAILABILITY_ZONE_KEY)),
1167                    Some(&"us-east-1a".into())
1168                );
1169
1170                drop(tx);
1171                topology.stop().await;
1172                assert_eq!(out.recv().await, None);
1173            })
1174            .await;
1175        }
1176    }
1177
1178    #[tokio::test]
1179    async fn namespace_metric() {
1180        {
1181            assert_transform_compliance(async {
1182                let transform_config = Ec2Metadata {
1183                    endpoint: ec2_metadata_address(),
1184                    namespace: Some(
1185                        OwnedTargetPath::event(owned_value_path!("ec2", "metadata")).into(),
1186                    ),
1187                    ..Default::default()
1188                };
1189
1190                let (tx, rx) = mpsc::channel(1);
1191                let (topology, mut out) =
1192                    create_topology(ReceiverStream::new(rx), transform_config).await;
1193
1194                // We need to sleep to let the background task fetch the data.
1195                sleep(Duration::from_secs(1)).await;
1196
1197                let metric = make_metric();
1198
1199                tx.send(metric.into()).await.unwrap();
1200
1201                let event = out.recv().await.unwrap();
1202                assert_eq!(
1203                    event
1204                        .as_metric()
1205                        .tag_value("ec2.metadata.availability-zone"),
1206                    Some("us-east-1a".to_string())
1207                );
1208
1209                drop(tx);
1210                topology.stop().await;
1211                assert_eq!(out.recv().await, None);
1212            })
1213            .await;
1214        }
1215
1216        {
1217            assert_transform_compliance(async {
1218                // Set an empty namespace to ensure we don't prepend one.
1219                let transform_config = Ec2Metadata {
1220                    endpoint: ec2_metadata_address(),
1221                    namespace: Some(OptionalTargetPath::none()),
1222                    ..Default::default()
1223                };
1224
1225                let (tx, rx) = mpsc::channel(1);
1226                let (topology, mut out) =
1227                    create_topology(ReceiverStream::new(rx), transform_config).await;
1228
1229                // We need to sleep to let the background task fetch the data.
1230                sleep(Duration::from_secs(1)).await;
1231
1232                let metric = make_metric();
1233
1234                tx.send(metric.into()).await.unwrap();
1235
1236                let event = out.recv().await.unwrap();
1237                assert_eq!(
1238                    event.as_metric().tag_value(AVAILABILITY_ZONE_KEY),
1239                    Some("us-east-1a".to_string())
1240                );
1241
1242                drop(tx);
1243                topology.stop().await;
1244                assert_eq!(out.recv().await, None);
1245            })
1246            .await;
1247        }
1248    }
1249}