Skip to main content

libdd_common/
azure_app_services.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::regex_engine::Regex;
5#[cfg(target_os = "linux")]
6use std::collections::HashMap;
7use std::sync::LazyLock;
8
9const WEBSITE_OWNER_NAME: &str = "WEBSITE_OWNER_NAME";
10const WEBSITE_SITE_NAME: &str = "WEBSITE_SITE_NAME";
11const WEBSITE_RESOURCE_GROUP: &str = "WEBSITE_RESOURCE_GROUP";
12const SITE_EXTENSION_VERSION: &str = "DD_AAS_DOTNET_EXTENSION_VERSION";
13const WEBSITE_OS: &str = "WEBSITE_OS";
14const INSTANCE_NAME: &str = "COMPUTERNAME";
15const INSTANCE_ID: &str = "WEBSITE_INSTANCE_ID";
16const SERVICE_CONTEXT: &str = "DD_AZURE_APP_SERVICES";
17const FUNCTIONS_WORKER_RUNTIME: &str = "FUNCTIONS_WORKER_RUNTIME";
18const FUNCTIONS_WORKER_RUNTIME_VERSION: &str = "FUNCTIONS_WORKER_RUNTIME_VERSION";
19const FUNCTIONS_EXTENSION_VERSION: &str = "FUNCTIONS_EXTENSION_VERSION";
20const DD_AZURE_RESOURCE_GROUP: &str = "DD_AZURE_RESOURCE_GROUP";
21const WEBSITE_SKU: &str = "WEBSITE_SKU";
22
23const UNKNOWN_VALUE: &str = "unknown";
24
25enum AzureContext {
26    AzureFunctions,
27    AzureAppService,
28}
29
30/// Snapshot of the AAS-relevant subset of the process environment, captured at
31/// exec time.
32///
33/// On Linux we parse `/proc/self/environ` (a kernel-managed snapshot of the
34/// initial `envp` block) instead of calling `getenv`. This avoids the
35/// well-known race between libc's `getenv(3)` and `setenv(3)` from another
36/// thread, which has been observed to crash `ddog_prof_Exporter_new` when an
37/// embedder (e.g. dd-trace-py via `os.environ[...] = ...`) mutates the
38/// environment concurrently with our AAS detection.
39///
40/// `/proc/self/environ` only contains variables present at `execve()` time;
41/// later `setenv` calls are not reflected. That is exactly what AAS detection
42/// needs — every Azure App Services / Functions variable we inspect is set by
43/// the platform before the process starts.
44///
45/// **Only the AAS variable names listed in [`AAS_VAR_NAMES`] are retained**;
46/// all other entries (which on real AAS deployments commonly include unrelated
47/// app secrets, API keys, connection strings, etc.) are dropped at parse time
48/// so we don't keep a long-lived in-memory copy of them.
49///
50/// On non-Linux platforms we fall back to `std::env::var`. AAS deployments are
51/// almost exclusively Linux containers, and Windows' `GetEnvironmentVariableW`
52/// (which `std::env::var` uses) is itself thread-safe.
53#[cfg(target_os = "linux")]
54static PROC_ENVIRON: LazyLock<HashMap<String, String>> = LazyLock::new(read_proc_self_environ);
55
56/// AAS-related variable names this module reads. Only entries with one of
57/// these names are kept in [`PROC_ENVIRON`]; everything else from
58/// `/proc/self/environ` is dropped at parse time.
59#[cfg(target_os = "linux")]
60const AAS_VAR_NAMES: &[&str] = &[
61    WEBSITE_OWNER_NAME,
62    WEBSITE_SITE_NAME,
63    WEBSITE_RESOURCE_GROUP,
64    SITE_EXTENSION_VERSION,
65    WEBSITE_OS,
66    INSTANCE_NAME,
67    INSTANCE_ID,
68    SERVICE_CONTEXT,
69    FUNCTIONS_WORKER_RUNTIME,
70    FUNCTIONS_WORKER_RUNTIME_VERSION,
71    FUNCTIONS_EXTENSION_VERSION,
72    DD_AZURE_RESOURCE_GROUP,
73    WEBSITE_SKU,
74];
75
76#[cfg(target_os = "linux")]
77fn read_proc_self_environ() -> HashMap<String, String> {
78    let bytes = match std::fs::read("/proc/self/environ") {
79        Ok(b) => b,
80        Err(_) => return HashMap::new(),
81    };
82    let mut map = HashMap::new();
83    for entry in bytes.split(|&b| b == 0) {
84        if entry.is_empty() {
85            continue;
86        }
87        let s = match std::str::from_utf8(entry) {
88            Ok(s) => s,
89            Err(_) => continue,
90        };
91        if let Some(eq) = s.find('=') {
92            let key = &s[..eq];
93            if AAS_VAR_NAMES.contains(&key) {
94                map.insert(key.to_string(), s[eq + 1..].to_string());
95            }
96        }
97    }
98    map
99}
100
101fn read_aas_var(name: &str) -> Option<String> {
102    #[cfg(target_os = "linux")]
103    {
104        PROC_ENVIRON.get(name).cloned()
105    }
106    #[cfg(not(target_os = "linux"))]
107    {
108        std::env::var(name).ok()
109    }
110}
111
112macro_rules! get_trimmed_env_var {
113    ($name:expr) => {
114        crate::azure_app_services::read_aas_var($name)
115            .map(|v| v.trim().to_string())
116            .filter(|s| !s.is_empty())
117    };
118}
119
120macro_rules! get_value_or_unknown {
121    ($name:expr) => {
122        $name.as_ref().map(|s| s.as_str()).unwrap_or(UNKNOWN_VALUE)
123    };
124}
125
126trait ToBoolean {
127    fn to_bool(&self) -> bool;
128}
129
130impl ToBoolean for String {
131    fn to_bool(&self) -> bool {
132        matches!(
133            self.to_lowercase().as_str(),
134            "true" | "t" | "y" | "1" | "yes"
135        )
136    }
137}
138
139pub trait QueryEnv {
140    fn get_var(&self, var: &str) -> Option<String>;
141}
142
143struct RealEnv;
144
145impl QueryEnv for RealEnv {
146    fn get_var(&self, var: &str) -> Option<String> {
147        get_trimmed_env_var!(var)
148    }
149}
150
151#[derive(Default)]
152pub struct AzureMetadata {
153    resource_id: Option<String>,
154    subscription_id: Option<String>,
155    site_name: Option<String>,
156    resource_group: Option<String>,
157    extension_version: Option<String>,
158    operating_system: String,
159    instance_name: Option<String>,
160    instance_id: Option<String>,
161    site_kind: String,
162    site_type: String,
163    runtime: Option<String>,
164    runtime_version: Option<String>,
165    function_runtime_version: Option<String>,
166}
167
168impl AzureMetadata {
169    fn get_azure_context<T: QueryEnv>(query: &T) -> AzureContext {
170        match (
171            query.get_var(FUNCTIONS_WORKER_RUNTIME),
172            query.get_var(FUNCTIONS_EXTENSION_VERSION),
173        ) {
174            (Some(_), Some(_)) => AzureContext::AzureFunctions,
175            (Some(_), None) => AzureContext::AzureFunctions,
176            (None, Some(_)) => AzureContext::AzureFunctions,
177            (None, None) => AzureContext::AzureAppService,
178        }
179    }
180
181    fn extract_subscription_id(s: Option<String>) -> Option<String> {
182        s?.split('+')
183            .next()
184            .filter(|s| !s.trim().is_empty())
185            .map(|v| v.to_string())
186    }
187
188    fn extract_resource_group(s: Option<String>) -> Option<String> {
189        #[allow(clippy::unwrap_used)]
190        let re: Regex = Regex::new(r".+\+(.+)-.+webspace(-Linux)?").unwrap();
191
192        s.as_ref().and_then(|text| {
193            re.captures(text)
194                .and_then(|caps| caps.get(1).map(|m| m.as_str().to_string()))
195        })
196    }
197
198    /*
199     * Computation of the resource id follow the same way the .NET tracer is doing:
200     * https://github.com/DataDog/dd-trace-dotnet/blob/834a4b05b4ed91a819eb78761bf1ddb805969f65/tracer/src/Datadog.Trace/PlatformHelpers/AzureAppServices.cs#L215
201     */
202    fn build_resource_id(
203        subscription_id: Option<&String>,
204        site_name: Option<&String>,
205        resource_group: Option<&String>,
206    ) -> Option<String> {
207        match (subscription_id, site_name, resource_group) {
208            (Some(id_sub), Some(sitename), Some(res_grp)) => Some(
209                format!("/subscriptions/{id_sub}/resourcegroups/{res_grp}/providers/microsoft.web/sites/{sitename}")
210                .to_lowercase(),
211            ),
212            _ => None,
213        }
214    }
215
216    fn build_metadata<T: QueryEnv>(query: T) -> Option<Self> {
217        let subscription_id =
218            AzureMetadata::extract_subscription_id(query.get_var(WEBSITE_OWNER_NAME));
219        let site_name = query.get_var(WEBSITE_SITE_NAME);
220
221        let (site_kind, site_type) = match AzureMetadata::get_azure_context(&query) {
222            AzureContext::AzureFunctions => ("functionapp".to_owned(), "function".to_owned()),
223            _ => ("app".to_owned(), "app".to_owned()),
224        };
225
226        let resource_group = query
227            .get_var(DD_AZURE_RESOURCE_GROUP)
228            .or_else(|| query.get_var(WEBSITE_RESOURCE_GROUP))
229            .or_else(|| {
230                // Check if we're in flex consumption plan first
231                match query.get_var(WEBSITE_SKU).as_deref() {
232                    Some("FlexConsumption") => None,
233                    /* Flex Consumption plans need the `DD_AZURE_RESOURCE_GROUP` env var. If this
234                     * logic ever changes, update the logic in
235                     * `serverless-components/src/datadog-trace-agent` and the serverless compat
236                     * layers accordingly. */
237                    _ => AzureMetadata::extract_resource_group(query.get_var(WEBSITE_OWNER_NAME)),
238                }
239            });
240
241        let resource_id = AzureMetadata::build_resource_id(
242            subscription_id.as_ref(),
243            site_name.as_ref(),
244            resource_group.as_ref(),
245        );
246        let extension_version = query.get_var(SITE_EXTENSION_VERSION);
247        let operating_system = query
248            .get_var(WEBSITE_OS)
249            .unwrap_or(std::env::consts::OS.to_string());
250        let instance_name = query.get_var(INSTANCE_NAME);
251        let instance_id = query.get_var(INSTANCE_ID);
252
253        let runtime = query.get_var(FUNCTIONS_WORKER_RUNTIME);
254        let runtime_version = query.get_var(FUNCTIONS_WORKER_RUNTIME_VERSION);
255        let function_runtime_version = query.get_var(FUNCTIONS_EXTENSION_VERSION);
256
257        Some(AzureMetadata {
258            resource_id,
259            subscription_id,
260            site_name,
261            resource_group,
262            extension_version,
263            operating_system,
264            instance_name,
265            instance_id,
266            site_kind,
267            site_type,
268            runtime,
269            runtime_version,
270            function_runtime_version,
271        })
272    }
273
274    pub fn new<T: QueryEnv>(query: T) -> Option<Self> {
275        let is_relevant = query
276            .get_var(SERVICE_CONTEXT)
277            .map(|s| s.to_bool())
278            .unwrap_or(false);
279
280        if !is_relevant {
281            return None;
282        }
283
284        AzureMetadata::build_metadata(query)
285    }
286
287    pub fn new_function<T: QueryEnv>(query: T) -> Option<Self> {
288        match matches!(
289            AzureMetadata::get_azure_context(&query),
290            AzureContext::AzureFunctions
291        ) {
292            true => AzureMetadata::build_metadata(query),
293            false => None,
294        }
295    }
296
297    pub fn get_resource_id(&self) -> &str {
298        get_value_or_unknown!(self.resource_id)
299    }
300
301    pub fn get_subscription_id(&self) -> &str {
302        get_value_or_unknown!(self.subscription_id)
303    }
304
305    pub fn get_site_name(&self) -> &str {
306        get_value_or_unknown!(self.site_name)
307    }
308
309    pub fn get_resource_group(&self) -> &str {
310        get_value_or_unknown!(self.resource_group)
311    }
312
313    pub fn get_extension_version(&self) -> &str {
314        get_value_or_unknown!(self.extension_version)
315    }
316
317    pub fn get_operating_system(&self) -> &str {
318        self.operating_system.as_str()
319    }
320
321    pub fn get_instance_name(&self) -> &str {
322        get_value_or_unknown!(self.instance_name)
323    }
324
325    pub fn get_instance_id(&self) -> &str {
326        get_value_or_unknown!(self.instance_id)
327    }
328
329    pub fn get_site_type(&self) -> &str {
330        self.site_type.as_str()
331    }
332
333    pub fn get_site_kind(&self) -> &str {
334        self.site_kind.as_str()
335    }
336
337    pub fn get_runtime(&self) -> &str {
338        get_value_or_unknown!(self.runtime)
339    }
340
341    pub fn get_runtime_version(&self) -> &str {
342        get_value_or_unknown!(self.runtime_version)
343    }
344
345    pub fn get_function_runtime_version(&self) -> &str {
346        get_value_or_unknown!(self.function_runtime_version)
347    }
348
349    /// Returns Azure App Services tags as an iterator of (tag_name, tag_value) tuples.
350    /// These tags are specific to Azure App Services.
351    pub fn get_app_service_tags(&self) -> impl ExactSizeIterator<Item = (&'static str, &str)> {
352        [
353            (
354                "aas.environment.extension_version",
355                self.get_extension_version(),
356            ),
357            ("aas.environment.instance_id", self.get_instance_id()),
358            ("aas.environment.instance_name", self.get_instance_name()),
359            ("aas.environment.os", self.get_operating_system()),
360            ("aas.resource.group", self.get_resource_group()),
361            ("aas.resource.id", self.get_resource_id()),
362            ("aas.site.kind", self.get_site_kind()),
363            ("aas.site.name", self.get_site_name()),
364            ("aas.site.type", self.get_site_type()),
365            ("aas.subscription.id", self.get_subscription_id()),
366        ]
367        .into_iter()
368    }
369
370    /// Returns Azure Functions tags as an iterator of (tag_name, tag_value) tuples.
371    /// These tags are specific to Azure Functions.
372    pub fn get_function_tags(&self) -> impl ExactSizeIterator<Item = (&'static str, &str)> {
373        [
374            ("aas.environment.instance_id", self.get_instance_id()),
375            ("aas.environment.instance_name", self.get_instance_name()),
376            ("aas.environment.os", self.get_operating_system()),
377            ("aas.environment.runtime", self.get_runtime()),
378            (
379                "aas.environment.runtime_version",
380                self.get_runtime_version(),
381            ),
382            (
383                "aas.environment.function_runtime",
384                self.get_function_runtime_version(),
385            ),
386            ("aas.resource.group", self.get_resource_group()),
387            ("aas.resource.id", self.get_resource_id()),
388            ("aas.site.kind", self.get_site_kind()),
389            ("aas.site.name", self.get_site_name()),
390            ("aas.site.type", self.get_site_type()),
391            ("aas.subscription.id", self.get_subscription_id()),
392        ]
393        .into_iter()
394    }
395}
396
397pub static AAS_METADATA: LazyLock<Option<AzureMetadata>> =
398    LazyLock::new(|| AzureMetadata::new(RealEnv {}));
399
400pub static AAS_METADATA_FUNCTION: LazyLock<Option<AzureMetadata>> =
401    LazyLock::new(|| AzureMetadata::new_function(RealEnv {}));
402
403#[cfg(test)]
404mod tests {
405
406    use indexmap::IndexMap;
407
408    use crate::azure_app_services::{QueryEnv, WEBSITE_OWNER_NAME};
409
410    use super::*;
411
412    struct MockEnv {
413        pub env_vars: IndexMap<String, String>,
414    }
415
416    impl MockEnv {
417        pub fn new(vars: &[(&str, &str)]) -> Self {
418            let mut env_vars: IndexMap<String, String> = IndexMap::new();
419            vars.iter().for_each(|(name, value)| {
420                env_vars.insert(name.to_string(), value.to_string());
421            });
422
423            MockEnv { env_vars }
424        }
425    }
426
427    impl QueryEnv for MockEnv {
428        fn get_var(&self, var: &str) -> Option<String> {
429            self.env_vars.get(var).cloned()
430        }
431    }
432
433    #[test]
434    fn test_metadata_is_not_relevant_by_default() {
435        let mocked_env = MockEnv::new(&[]);
436
437        let metadata = AzureMetadata::new(mocked_env);
438        assert!(metadata.is_none());
439    }
440
441    #[test]
442    fn test_metadata_is_relevant_first() {
443        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "true")]);
444
445        let metadata = AzureMetadata::new(mocked_env);
446        assert!(metadata.is_some());
447    }
448
449    #[test]
450    fn test_metadata_is_relevant_second() {
451        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "t")]);
452
453        let metadata = AzureMetadata::new(mocked_env);
454        assert!(metadata.is_some());
455    }
456
457    #[test]
458    fn test_metadata_is_relevant_third() {
459        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "TrUe")]);
460
461        let metadata = AzureMetadata::new(mocked_env);
462        assert!(metadata.is_some());
463    }
464
465    #[test]
466    fn test_metadata_is_relevant_fourth() {
467        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "1")]);
468
469        let metadata = AzureMetadata::new(mocked_env);
470        assert!(metadata.is_some());
471    }
472
473    #[test]
474    fn test_metadata_is_relevant_fifth() {
475        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "yEs")]);
476
477        let metadata = AzureMetadata::new(mocked_env);
478        assert!(metadata.is_some());
479    }
480
481    #[test]
482    fn test_metadata_is_relevant_sixth() {
483        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "Y")]);
484
485        let metadata = AzureMetadata::new(mocked_env);
486        assert!(metadata.is_some());
487    }
488
489    #[test]
490    fn test_metadata_is_not_relevant_if_explicit() {
491        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "0")]);
492
493        let metadata = AzureMetadata::new(mocked_env);
494        assert!(metadata.is_none());
495    }
496
497    #[test]
498    fn test_extract_subscription_without_plus_sign() {
499        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "foo"), (SERVICE_CONTEXT, "1")]);
500
501        let metadata = AzureMetadata::new(mocked_env).unwrap();
502
503        let expected_id = "foo";
504
505        assert_eq!(metadata.get_subscription_id(), expected_id);
506    }
507
508    #[test]
509    fn test_extract_subscription_with_plus_sign() {
510        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "foo+bar"), (SERVICE_CONTEXT, "1")]);
511
512        let metadata = AzureMetadata::new(mocked_env).unwrap();
513
514        let expected_id = "foo";
515        assert_eq!(metadata.get_subscription_id(), expected_id);
516    }
517
518    #[test]
519    fn test_extract_subscription_with_empty_string() {
520        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, ""), (SERVICE_CONTEXT, "1")]);
521
522        let metadata = AzureMetadata::new(mocked_env).unwrap();
523
524        assert_eq!(metadata.get_subscription_id(), UNKNOWN_VALUE);
525    }
526
527    #[test]
528    fn test_extract_subscription_with_only_whitespaces() {
529        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "    "), (SERVICE_CONTEXT, "1")]);
530
531        let metadata = AzureMetadata::new(mocked_env).unwrap();
532
533        assert_eq!(metadata.get_subscription_id(), UNKNOWN_VALUE);
534    }
535
536    #[test]
537    fn test_extract_subscription_with_only_plus_sign() {
538        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "+"), (SERVICE_CONTEXT, "1")]);
539
540        let metadata = AzureMetadata::new(mocked_env).unwrap();
541
542        assert_eq!(metadata.get_subscription_id(), UNKNOWN_VALUE);
543    }
544
545    #[test]
546    fn test_extract_subscription_with_whitespaces_separated_by_plus() {
547        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "   + "), (SERVICE_CONTEXT, "1")]);
548
549        let metadata = AzureMetadata::new(mocked_env).unwrap();
550
551        assert_eq!(metadata.get_subscription_id(), UNKNOWN_VALUE);
552    }
553
554    #[test]
555    fn test_extract_subscription_plus_sign_and_other_string() {
556        let mocked_env = MockEnv::new(&[(WEBSITE_OWNER_NAME, "+other"), (SERVICE_CONTEXT, "1")]);
557
558        let metadata = AzureMetadata::new(mocked_env).unwrap();
559
560        assert_eq!(metadata.get_subscription_id(), UNKNOWN_VALUE);
561    }
562
563    #[test]
564    fn test_extract_resource_group_pattern_match_linux() {
565        let mocked_env = MockEnv::new(&[
566            (
567                WEBSITE_OWNER_NAME,
568                "00000000-0000-0000-0000-000000000000+test-rg-EastUSwebspace-Linux",
569            ),
570            ("FUNCTIONS_WORKER_RUNTIME", "node"),
571            ("FUNCTIONS_EXTENSION_VERSION", "~4"),
572        ]);
573
574        let metadata = AzureMetadata::new_function(mocked_env).unwrap();
575
576        let expected_resource_group = "test-rg";
577
578        assert_eq!(metadata.get_resource_group(), expected_resource_group);
579    }
580
581    #[test]
582    fn test_extract_resource_group_pattern_match_windows() {
583        let mocked_env = MockEnv::new(&[
584            (
585                WEBSITE_OWNER_NAME,
586                "00000000-0000-0000-0000-000000000000+test-rg-EastUSwebspace",
587            ),
588            ("FUNCTIONS_WORKER_RUNTIME", "node"),
589            ("FUNCTIONS_EXTENSION_VERSION", "~4"),
590        ]);
591
592        let metadata = AzureMetadata::new_function(mocked_env).unwrap();
593
594        let expected_resource_group = "test-rg";
595
596        assert_eq!(metadata.get_resource_group(), expected_resource_group);
597    }
598
599    #[test]
600    fn test_extract_resource_group_no_pattern_match() {
601        let mocked_env = MockEnv::new(&[
602            (WEBSITE_OWNER_NAME, "foo"),
603            (FUNCTIONS_WORKER_RUNTIME, "node"),
604            (FUNCTIONS_EXTENSION_VERSION, "~4"),
605        ]);
606
607        let metadata = AzureMetadata::new_function(mocked_env).unwrap();
608
609        assert_eq!(metadata.get_resource_group(), UNKNOWN_VALUE);
610    }
611
612    #[test]
613    fn test_use_resource_group_from_env_var_if_available() {
614        let mocked_env = MockEnv::new(&[
615            (WEBSITE_RESOURCE_GROUP, "test-rg-env-var"),
616            (
617                WEBSITE_OWNER_NAME,
618                "00000000-0000-0000-0000-000000000000+test-rg-EastUSwebspace-Linux",
619            ),
620            (SERVICE_CONTEXT, "1"),
621            (WEBSITE_SKU, "ElasticPremium"),
622        ]);
623
624        let metadata = AzureMetadata::new(mocked_env).unwrap();
625
626        let expected_resource_group = "test-rg-env-var";
627
628        assert_eq!(metadata.get_resource_group(), expected_resource_group);
629    }
630
631    #[test]
632    fn test_flex_consumption_resource_group_is_none_without_dd_azure_resource_group() {
633        let mocked_env = MockEnv::new(&[
634            (
635                WEBSITE_OWNER_NAME,
636                "00000000-0000-0000-0000-000000000000+flex-EastUSwebspace-Linux",
637            ),
638            (WEBSITE_SKU, "FlexConsumption"),
639            (SERVICE_CONTEXT, "1"),
640        ]);
641
642        let metadata = AzureMetadata::new(mocked_env).unwrap();
643
644        assert_eq!(metadata.get_resource_group(), UNKNOWN_VALUE);
645    }
646
647    #[test]
648    fn test_flex_consumption_uses_dd_azure_resource_group() {
649        let mocked_env = MockEnv::new(&[
650            (
651                WEBSITE_OWNER_NAME,
652                "00000000-0000-0000-0000-000000000000+flex-EastUSwebspace-Linux",
653            ),
654            (DD_AZURE_RESOURCE_GROUP, "test-flex-rg"),
655            (WEBSITE_SKU, "FlexConsumption"),
656            (SERVICE_CONTEXT, "1"),
657        ]);
658
659        let metadata = AzureMetadata::new(mocked_env).unwrap();
660
661        // Should use the DD_AZURE_RESOURCE_GROUP value instead of extracting from
662        // WEBSITE_OWNER_NAME
663        assert_eq!(metadata.get_resource_group(), "test-flex-rg");
664    }
665
666    #[test]
667    fn test_dd_azure_resource_group_has_highest_priority() {
668        let mocked_env = MockEnv::new(&[
669            (WEBSITE_RESOURCE_GROUP, "test-rg-env-var"),
670            (
671                WEBSITE_OWNER_NAME,
672                "00000000-0000-0000-0000-000000000000+test-rg-EastUSwebspace-Linux",
673            ),
674            (DD_AZURE_RESOURCE_GROUP, "dd-azure-rg-override"),
675            (SERVICE_CONTEXT, "1"),
676        ]);
677
678        let metadata = AzureMetadata::new(mocked_env).unwrap();
679
680        // DD_AZURE_RESOURCE_GROUP should have highest priority over WEBSITE_RESOURCE_GROUP and
681        // WEBSITE_OWNER_NAME
682        let expected_resource_group = "dd-azure-rg-override";
683
684        assert_eq!(metadata.get_resource_group(), expected_resource_group);
685    }
686
687    #[test]
688    fn test_build_resource_id() {
689        let mocked_env = MockEnv::new(&[
690            (WEBSITE_OWNER_NAME, "foo"),
691            (WEBSITE_SITE_NAME, "my_website"),
692            (WEBSITE_RESOURCE_GROUP, "resource_group"),
693            (SERVICE_CONTEXT, "1"),
694        ]);
695
696        let metadata = AzureMetadata::new(mocked_env).unwrap();
697
698        assert_eq!(
699            metadata.get_resource_id(),
700            "/subscriptions/foo/resourcegroups/resource_group/providers/microsoft.web/sites/my_website"
701        )
702    }
703
704    #[test]
705    fn test_build_resource_id_with_missing_subscription_id() {
706        let mocked_env = MockEnv::new(&[
707            (WEBSITE_SITE_NAME, "my_website"),
708            (WEBSITE_RESOURCE_GROUP, "resource_group"),
709            (SERVICE_CONTEXT, "1"),
710        ]);
711
712        let metadata = AzureMetadata::new(mocked_env).unwrap();
713
714        assert_eq!(metadata.get_resource_id(), UNKNOWN_VALUE)
715    }
716
717    #[test]
718    fn test_build_resource_id_with_missing_site_name() {
719        let mocked_env = MockEnv::new(&[
720            (WEBSITE_OWNER_NAME, "foo"),
721            (WEBSITE_RESOURCE_GROUP, "resource_group"),
722            (SERVICE_CONTEXT, "1"),
723        ]);
724
725        let metadata = AzureMetadata::new(mocked_env).unwrap();
726
727        assert_eq!(metadata.get_resource_id(), UNKNOWN_VALUE)
728    }
729
730    #[test]
731    fn test_build_resource_id_with_missing_resource_group() {
732        let mocked_env = MockEnv::new(&[
733            (WEBSITE_OWNER_NAME, "foo"),
734            (WEBSITE_SITE_NAME, "my_website"),
735            (SERVICE_CONTEXT, "1"),
736        ]);
737
738        let metadata = AzureMetadata::new(mocked_env).unwrap();
739
740        assert_eq!(metadata.get_resource_id(), UNKNOWN_VALUE)
741    }
742
743    #[test]
744    fn test_build_resource_id_with_missing_info() {
745        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "1")]);
746        let metadata = AzureMetadata::new(mocked_env).unwrap();
747
748        assert_eq!(metadata.get_resource_id(), UNKNOWN_VALUE)
749    }
750
751    #[test]
752    fn test_site_type_and_kind_default() {
753        let mocked_env = MockEnv::new(&[(SERVICE_CONTEXT, "1")]);
754        let metadata = AzureMetadata::new(mocked_env).unwrap();
755
756        assert_eq!(metadata.get_site_type(), "app");
757        assert_eq!(metadata.get_site_kind(), "app")
758    }
759
760    #[test]
761    fn test_site_type_and_kind_if_worker_runtime_not_specified() {
762        let mocked_env = MockEnv::new(&[
763            (FUNCTIONS_WORKER_RUNTIME, "my_runtime"),
764            (SERVICE_CONTEXT, "1"),
765        ]);
766        let metadata = AzureMetadata::new(mocked_env).unwrap();
767
768        assert_eq!(metadata.get_site_kind(), "functionapp");
769        assert_eq!(metadata.get_site_type(), "function")
770    }
771
772    #[test]
773    fn test_site_type_and_kind_if_extension_version_not_specified() {
774        let mocked_env = MockEnv::new(&[
775            (FUNCTIONS_EXTENSION_VERSION, "next_version"),
776            (SERVICE_CONTEXT, "1"),
777        ]);
778        let metadata = AzureMetadata::new(mocked_env).unwrap();
779
780        assert_eq!(metadata.get_site_kind(), "functionapp");
781        assert_eq!(metadata.get_site_type(), "function")
782    }
783
784    #[test]
785    fn test_site_type_and_kind_if_both_specified() {
786        let mocked_env = MockEnv::new(&[
787            (FUNCTIONS_WORKER_RUNTIME, "my_runtime"),
788            (FUNCTIONS_EXTENSION_VERSION, "next_version"),
789            (SERVICE_CONTEXT, "1"),
790        ]);
791        let metadata = AzureMetadata::new(mocked_env).unwrap();
792
793        assert_eq!(metadata.get_site_kind(), "functionapp");
794        assert_eq!(metadata.get_site_type(), "function")
795    }
796
797    #[test]
798    fn test_check_other_simple_env_retrieval() {
799        let expected_site_name = "my_site_name".to_owned();
800        let expected_resource_group = "my_resource_group".to_owned();
801        let expected_site_version = "v42".to_owned();
802        let expected_operating_system = "FreeBSD".to_owned();
803        let expected_instance_name = "my_instance_name".to_owned();
804        let expected_instance_id = "my_instance_id".to_owned();
805        let expected_function_extension_version = "~4".to_owned();
806        let expected_runtime = "node".to_owned();
807        let expected_runtime_version = "18".to_owned();
808
809        let mocked_env = MockEnv::new(&[
810            (WEBSITE_SITE_NAME, expected_site_name.as_str()),
811            (WEBSITE_RESOURCE_GROUP, expected_resource_group.as_str()),
812            (SITE_EXTENSION_VERSION, expected_site_version.as_str()),
813            (WEBSITE_OS, expected_operating_system.as_str()),
814            (INSTANCE_NAME, expected_instance_name.as_str()),
815            (INSTANCE_ID, expected_instance_id.as_str()),
816            (SERVICE_CONTEXT, "1"),
817            (
818                FUNCTIONS_EXTENSION_VERSION,
819                expected_function_extension_version.as_str(),
820            ),
821            (FUNCTIONS_WORKER_RUNTIME, expected_runtime.as_str()),
822            (
823                FUNCTIONS_WORKER_RUNTIME_VERSION,
824                expected_runtime_version.as_str(),
825            ),
826        ]);
827
828        let metadata = AzureMetadata::new(mocked_env).unwrap();
829
830        assert_eq!(expected_site_name, metadata.get_site_name());
831        assert_eq!(expected_resource_group, metadata.get_resource_group());
832        assert_eq!(expected_site_version, metadata.get_extension_version());
833        assert_eq!(expected_operating_system, metadata.get_operating_system());
834        assert_eq!(expected_instance_name, metadata.get_instance_name());
835        assert_eq!(expected_instance_id, metadata.get_instance_id());
836        assert_eq!(
837            expected_function_extension_version,
838            metadata.get_function_runtime_version()
839        );
840        assert_eq!(expected_runtime, metadata.get_runtime());
841        assert_eq!(expected_runtime_version, metadata.get_runtime_version());
842    }
843
844    #[test]
845    fn test_get_app_service_tags() {
846        let expected_site_name = "my_site_name";
847        let expected_resource_group = "my_resource_group";
848        let expected_site_version = "v42";
849        let expected_operating_system = "FreeBSD";
850        let expected_instance_name = "my_instance_name";
851        let expected_instance_id = "my_instance_id";
852        let expected_subscription_id = "sub-123";
853        let expected_resource_id = "/subscriptions/sub-123/resourcegroups/my_resource_group/providers/microsoft.web/sites/my_site_name";
854
855        let mocked_env = MockEnv::new(&[
856            (WEBSITE_SITE_NAME, expected_site_name),
857            (WEBSITE_RESOURCE_GROUP, expected_resource_group),
858            (SITE_EXTENSION_VERSION, expected_site_version),
859            (WEBSITE_OS, expected_operating_system),
860            (INSTANCE_NAME, expected_instance_name),
861            (INSTANCE_ID, expected_instance_id),
862            (SERVICE_CONTEXT, "1"),
863            (
864                WEBSITE_OWNER_NAME,
865                &format!("{}+rg-webspace", expected_subscription_id),
866            ),
867        ]);
868
869        let metadata = AzureMetadata::new(mocked_env).unwrap();
870
871        // Collect tags into a HashMap for easy lookup
872        let tags: std::collections::HashMap<&str, &str> = metadata.get_app_service_tags().collect();
873
874        // Verify all 10 App Service tags are present
875        assert_eq!(tags.len(), 10);
876        assert_eq!(tags.get("aas.resource.id"), Some(&expected_resource_id));
877        assert_eq!(
878            tags.get("aas.environment.extension_version"),
879            Some(&expected_site_version)
880        );
881        assert_eq!(
882            tags.get("aas.environment.instance_id"),
883            Some(&expected_instance_id)
884        );
885        assert_eq!(
886            tags.get("aas.environment.instance_name"),
887            Some(&expected_instance_name)
888        );
889        assert_eq!(
890            tags.get("aas.environment.os"),
891            Some(&expected_operating_system)
892        );
893        assert_eq!(
894            tags.get("aas.resource.group"),
895            Some(&expected_resource_group)
896        );
897        assert_eq!(tags.get("aas.site.name"), Some(&expected_site_name));
898        assert_eq!(tags.get("aas.site.kind"), Some(&"app"));
899        assert_eq!(tags.get("aas.site.type"), Some(&"app"));
900        assert_eq!(
901            tags.get("aas.subscription.id"),
902            Some(&expected_subscription_id)
903        );
904
905        // Verify runtime tags are NOT present
906        assert_eq!(tags.get("aas.environment.runtime"), None);
907        assert_eq!(tags.get("aas.environment.runtime_version"), None);
908        assert_eq!(tags.get("aas.environment.function_runtime"), None);
909
910        // Verify it's an ExactSizeIterator
911        let iter = metadata.get_app_service_tags();
912        assert_eq!(iter.len(), 10);
913    }
914
915    #[test]
916    fn test_get_function_tags() {
917        let expected_site_name = "my_site_name";
918        let expected_resource_group = "my_resource_group";
919        let expected_operating_system = "FreeBSD";
920        let expected_instance_name = "my_instance_name";
921        let expected_instance_id = "my_instance_id";
922        let expected_function_extension_version = "~4";
923        let expected_runtime = "node";
924        let expected_runtime_version = "18";
925        let expected_subscription_id = "sub-123";
926        let expected_resource_id = "/subscriptions/sub-123/resourcegroups/my_resource_group/providers/microsoft.web/sites/my_site_name";
927
928        let mocked_env = MockEnv::new(&[
929            (WEBSITE_SITE_NAME, expected_site_name),
930            (WEBSITE_RESOURCE_GROUP, expected_resource_group),
931            (WEBSITE_OS, expected_operating_system),
932            (INSTANCE_NAME, expected_instance_name),
933            (INSTANCE_ID, expected_instance_id),
934            (SERVICE_CONTEXT, "1"),
935            (
936                FUNCTIONS_EXTENSION_VERSION,
937                expected_function_extension_version,
938            ),
939            (FUNCTIONS_WORKER_RUNTIME, expected_runtime),
940            (FUNCTIONS_WORKER_RUNTIME_VERSION, expected_runtime_version),
941            (
942                WEBSITE_OWNER_NAME,
943                &format!("{}+rg-webspace", expected_subscription_id),
944            ),
945        ]);
946
947        let metadata = AzureMetadata::new(mocked_env).unwrap();
948
949        // Collect tags into a HashMap for easy lookup
950        let tags: std::collections::HashMap<&str, &str> = metadata.get_function_tags().collect();
951
952        // Verify all 12 Function tags are present
953        assert_eq!(tags.len(), 12);
954        assert_eq!(tags.get("aas.resource.id"), Some(&expected_resource_id));
955        assert_eq!(
956            tags.get("aas.environment.instance_id"),
957            Some(&expected_instance_id)
958        );
959        assert_eq!(
960            tags.get("aas.environment.instance_name"),
961            Some(&expected_instance_name)
962        );
963        assert_eq!(
964            tags.get("aas.environment.os"),
965            Some(&expected_operating_system)
966        );
967        assert_eq!(tags.get("aas.environment.runtime"), Some(&expected_runtime));
968        assert_eq!(
969            tags.get("aas.environment.runtime_version"),
970            Some(&expected_runtime_version)
971        );
972        assert_eq!(
973            tags.get("aas.environment.function_runtime"),
974            Some(&expected_function_extension_version)
975        );
976        assert_eq!(
977            tags.get("aas.resource.group"),
978            Some(&expected_resource_group)
979        );
980        assert_eq!(tags.get("aas.site.name"), Some(&expected_site_name));
981        assert_eq!(tags.get("aas.site.kind"), Some(&"functionapp"));
982        assert_eq!(tags.get("aas.site.type"), Some(&"function"));
983        assert_eq!(
984            tags.get("aas.subscription.id"),
985            Some(&expected_subscription_id)
986        );
987
988        // Verify extension_version tag is NOT present
989        assert_eq!(tags.get("aas.environment.extension_version"), None);
990
991        // Verify it's an ExactSizeIterator
992        let iter = metadata.get_function_tags();
993        assert_eq!(iter.len(), 12);
994    }
995
996    #[test]
997    fn test_get_trimmed_env_var_missing() {
998        // The proc-environ snapshot is frozen at exec time, so we can't inject
999        // an empty value here. Just verify the missing-var path returns None.
1000        assert_eq!(
1001            get_trimmed_env_var!("__LIBDD_AAS_DEFINITELY_NOT_SET__"),
1002            None
1003        );
1004    }
1005}