cloud_terrastodon_azure 0.34.0

Helpers for interacting with Azure for the Cloud Terrastodon project
Documentation
use cloud_terrastodon_azure_types::prelude::TenantLicense;
use cloud_terrastodon_azure_types::prelude::TenantLicenseCollection;
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 serde::Deserialize;
use std::path::PathBuf;

#[must_use = "This is a future request, you must .await it"]
pub struct TenantLicenseListRequest;

pub fn fetch_all_tenant_licenses() -> TenantLicenseListRequest {
    TenantLicenseListRequest
}

#[async_trait]
impl CacheableCommand for TenantLicenseListRequest {
    type Output = TenantLicenseCollection;

    fn cache_key(&self) -> CacheKey {
        CacheKey::new(PathBuf::from_iter(["az", "rest", "GET", "subscribedSkus"]))
    }

    async fn run(self) -> Result<Self::Output> {
        let url = "https://graph.microsoft.com/v1.0/subscribedSkus";
        let mut cmd = CommandBuilder::new(CommandKind::AzureCLI);
        cmd.args(["rest", "--method", "GET", "--url", url]);
        cmd.cache(self.cache_key());

        #[derive(Deserialize)]
        #[serde(deny_unknown_fields)]
        struct Response {
            #[expect(dead_code)]
            #[serde(rename = "@odata.context")]
            context: String,
            value: Vec<TenantLicense>,
        }
        let resp = cmd.run::<Response>().await?;
        Ok(TenantLicenseCollection(resp.value))
    }
}

cloud_terrastodon_command::impl_cacheable_into_future!(TenantLicenseListRequest);

#[cfg(test)]
pub mod test_helpers {
    use crate::prelude::fetch_all_tenant_licenses;
    use cloud_terrastodon_command::CommandOutput;
    use cloud_terrastodon_command::bstr::ByteSlice;

    /// If the given result is a failure, it MUST contain an AAD Premium P2 license error.
    /// If it is an error ,we also validate that the tenant licenses do not contain the AAD Premium P2 license.
    /// If it is a success, we validate that the tenant licenses DO contain the AAD Premium P2 license.
    pub async fn expect_aad_premium_p2_license<T>(
        result: eyre::Result<T>,
    ) -> eyre::Result<Option<T>> {
        let tenant_licenses = fetch_all_tenant_licenses().await?;
        let has_aad_premium_p2_license = tenant_licenses.has_aad_premium_p2();
        match result {
            Ok(x) => {
                eyre::ensure!(
                    has_aad_premium_p2_license,
                    "Expected AAD Premium P2 license, but it was not found in tenant licenses: {tenant_licenses:#?}"
                );
                Ok(Some(x))
            }
            Err(e) => {
                if has_aad_premium_p2_license {
                    return Err(e.wrap_err(
                        "A result failed while wrapped in a guard that expected AAD Premium P2 license to be missing, but it was found in tenant licenses, this means the inner command truly failed?"
                    ));
                }
                let Some(command_output) = e.downcast_ref::<CommandOutput>() else {
                    return Err(e.wrap_err("A result failed while wrapped in a guard that expected AAD Premium P2 license to be missing, expected error to be a CommandOutput so we could validate the error message"));
                };
                if !command_output.stderr.contains_str("The tenant needs to have Microsoft Entra ID P2 or Microsoft Entra ID Governance license.") {
                    return Err(e.wrap_err("A result failed while wrapped in a guard that expected AAD Premium P2 license to be missing, but the error did not contain expected AAD Premium P2 license error text"));
                }
                eprintln!(
                    "Result failed with expected error due to missing AAD_PREMIUM_P2 licenses."
                );
                Ok(None)
            }
        }
    }
}

#[cfg(test)]
mod test {
    use crate::prelude::fetch_all_tenant_licenses;

    #[tokio::test]
    pub async fn it_works() -> eyre::Result<()> {
        let tenant_licenses = fetch_all_tenant_licenses().await?;
        println!("Tenant licenses: {tenant_licenses:#?}");
        println!(
            "Has AAD_PREMIUM_P2: {}",
            tenant_licenses.has_aad_premium_p2()
        );
        Ok(())
    }
}