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