cloud_terrastodon_azure 0.35.1

Helpers for interacting with Azure for the Cloud Terrastodon project
use crate::ResourceGraphHelper;
use crate::list_tracked_tenants;
use cloud_terrastodon_azure_types::AzureTenantId;
use cloud_terrastodon_azure_types::Subscription;
use cloud_terrastodon_azure_types::SubscriptionId;
use cloud_terrastodon_command::CacheKey;
use cloud_terrastodon_command::CacheableCommand;
use cloud_terrastodon_command::CommandBuilder;
use cloud_terrastodon_command::CommandKind;
use cloud_terrastodon_command::async_trait;
use eyre::Result;
use eyre::bail;
use indoc::indoc;
use std::path::PathBuf;
use tracing::debug;

#[must_use = "This is a future request, you must .await it"]
pub struct SubscriptionListRequest {
    pub tenant_id: AzureTenantId,
}

pub fn fetch_all_subscriptions(tenant_id: AzureTenantId) -> SubscriptionListRequest {
    SubscriptionListRequest { tenant_id }
}

#[expect(async_fn_in_trait)]
pub trait SubscriptionIdExt {
    async fn resolve_tenant_id(&self) -> Result<AzureTenantId>;
}

impl SubscriptionIdExt for SubscriptionId {
    async fn resolve_tenant_id(&self) -> Result<AzureTenantId> {
        let tracked_tenants = list_tracked_tenants().await?;
        for tenant_id in tracked_tenants.iter().copied() {
            let Some(subscription) = fetch_all_subscriptions(tenant_id)
                .await?
                .into_iter()
                .find(|subscription| subscription.id == *self)
            else {
                continue;
            };
            return Ok(subscription.tenant_id);
        }

        bail!(
            "Failed to resolve tracked tenant for subscription '{}' across {} tracked tenants.",
            self,
            tracked_tenants.len()
        )
    }
}

#[async_trait]
impl CacheableCommand for SubscriptionListRequest {
    type Output = Vec<Subscription>;

    fn cache_key(&self) -> CacheKey {
        CacheKey::new(PathBuf::from_iter([
            "az",
            "resource_graph",
            "subscriptions",
            self.tenant_id.to_string().as_str(),
        ]))
    }

    async fn run(self) -> Result<Self::Output> {
        debug!("Fetching subscriptions");
        let query = indoc! {r#"
        resourcecontainers
        | where type =~ "Microsoft.Resources/subscriptions"
        | project 
            name,
            id,
            tenant_id=tenantId,
            management_group_ancestors_chain=properties.managementGroupAncestorsChain,
            tags=tags
    "#};

        let subscriptions = ResourceGraphHelper::new(self.tenant_id, query, Some(self.cache_key()))
            .collect_all::<Subscription>()
            .await?;
        debug!("Found {} subscriptions", subscriptions.len());
        Ok(subscriptions)
    }
}

pub async fn get_active_subscription_id() -> Result<SubscriptionId> {
    let mut cmd = CommandBuilder::new(CommandKind::AzureCLI);
    cmd.args([
        "account",
        "list",
        "--query",
        "[?isDefault].id",
        "--output",
        "json",
    ]);
    let rtn = cmd.run::<[SubscriptionId; 1]>().await?[0];
    Ok(rtn)
}

cloud_terrastodon_command::impl_cacheable_into_future!(SubscriptionListRequest);

#[cfg(test)]
mod tests {
    use super::*;
    use crate::get_test_tenant_id;

    #[tokio::test]
    async fn it_works() -> Result<()> {
        let tenant_id = get_test_tenant_id().await?;
        let result = fetch_all_subscriptions(tenant_id).await?;
        assert!(!result.is_empty());
        Ok(())
    }

    #[tokio::test]
    async fn resolves_tenant_for_subscription_id() -> Result<()> {
        let tenant_id = get_test_tenant_id().await?;
        let subscription_id = fetch_all_subscriptions(tenant_id)
            .await?
            .first()
            .unwrap()
            .id;
        let resolved = subscription_id.resolve_tenant_id().await?;
        assert_eq!(resolved, tenant_id);
        Ok(())
    }

    #[tokio::test]
    pub async fn get_active() -> eyre::Result<()> {
        let active = get_active_subscription_id().await?;
        let active_text = active.to_string();
        assert!(!active_text.is_empty());
        Ok(())
    }
}