use crate::management_groups::fetch_root_management_group;
use crate::resource_groups::fetch_all_resource_groups;
use cloud_terrastodon_azure_types::prelude::AsScope;
use cloud_terrastodon_azure_types::prelude::EligibleChildResource;
use cloud_terrastodon_azure_types::prelude::EligibleChildResourceKind;
use cloud_terrastodon_azure_types::prelude::Scope;
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;
#[derive(Eq, PartialEq, Debug, Default)]
pub enum FetchChildrenBehaviour {
#[default]
Unspecified,
GetAllChildren,
}
pub async fn fetch_eligible_child_resources(
scope: &impl Scope,
behaviour: FetchChildrenBehaviour,
) -> Result<Vec<EligibleChildResource>> {
let scope = scope.expanded_form();
let scope = scope.strip_prefix('/').unwrap_or(&scope);
let mut url = format!(
"https://management.azure.com/{scope}/providers/Microsoft.Authorization/eligibleChildResources?api-version=2020-10-01"
);
if behaviour == FetchChildrenBehaviour::GetAllChildren {
url.push_str("&getAllChildren=true");
}
let mut cmd = CommandBuilder::new(CommandKind::AzureCLI);
cmd.args(["rest", "--method", "GET", "--url", &url]);
let mut cache_chunks = PathBuf::from_iter(["az", "rest", "GET", "eligibleChildResources"]);
scope
.split("/")
.filter(|x| !x.is_empty())
.for_each(|x| cache_chunks.push(x));
cmd.cache(CacheKey::new(cache_chunks));
#[derive(Deserialize)]
struct Response {
value: Vec<EligibleChildResource>,
}
let resp: Response = cmd.run().await?;
Ok(resp.value)
}
#[must_use = "This is a future request, you must .await it"]
pub struct EligibleChildResourceListRequest;
pub fn fetch_all_eligible_resource_containers() -> EligibleChildResourceListRequest {
EligibleChildResourceListRequest
}
#[async_trait]
impl CacheableCommand for EligibleChildResourceListRequest {
type Output = Vec<EligibleChildResource>;
fn cache_key(&self) -> CacheKey {
CacheKey::new(PathBuf::from_iter([
"az",
"management",
"eligible_child_resources",
]))
}
async fn run(self) -> Result<Self::Output> {
let root_mg = fetch_root_management_group().await?;
let scope = root_mg.as_scope();
let mut resource_containers =
fetch_eligible_child_resources(scope, FetchChildrenBehaviour::GetAllChildren).await?;
let rgs = fetch_all_resource_groups()
.await?
.into_iter()
.map(|x| EligibleChildResource {
name: x.name.to_string(),
kind: EligibleChildResourceKind::ResourceGroup,
id: x.as_scope().as_scope_impl(),
});
resource_containers.extend(rgs);
Ok(resource_containers)
}
}
cloud_terrastodon_command::impl_cacheable_into_future!(EligibleChildResourceListRequest);
#[cfg(test)]
mod tests {
use super::*;
use crate::management_groups::fetch_root_management_group;
use crate::prelude::test_helpers::expect_aad_premium_p2_license;
use crate::subscriptions::fetch_all_subscriptions;
use cloud_terrastodon_azure_types::prelude::AsScope;
use cloud_terrastodon_azure_types::prelude::Scope;
use cloud_terrastodon_user_input::Choice;
use cloud_terrastodon_user_input::PickerTui;
#[test_log::test(tokio::test)]
async fn it_works() -> Result<()> {
let mg = fetch_root_management_group().await?;
let scope = mg.as_scope();
let Some(found) = expect_aad_premium_p2_license(
fetch_eligible_child_resources(scope, FetchChildrenBehaviour::GetAllChildren).await,
)
.await?
else {
return Ok(());
};
assert!(!found.is_empty());
for x in found {
println!("- {x:?}");
}
Ok(())
}
#[test_log::test(tokio::test)]
async fn it_works2() -> Result<()> {
let Some(found) =
expect_aad_premium_p2_license(fetch_all_eligible_resource_containers().await).await?
else {
return Ok(());
};
assert!(!found.is_empty());
for x in found {
println!("- {x:?}");
}
Ok(())
}
#[test_log::test(tokio::test)]
async fn it_works3() -> Result<()> {
let rg = fetch_all_resource_groups()
.await?
.into_iter()
.next()
.unwrap();
let result = expect_aad_premium_p2_license(
fetch_eligible_child_resources(rg.as_scope(), FetchChildrenBehaviour::GetAllChildren)
.await,
)
.await;
match result {
Ok(_) => eyre::bail!(
"Expected error, but got success; GetAllChildren is only supported for management groups?"
),
Err(_) => return Ok(()),
}
}
#[test_log::test(tokio::test)]
async fn it_works4() -> Result<()> {
let subs = fetch_all_subscriptions().await?;
let sub = subs.first().unwrap();
let result = expect_aad_premium_p2_license(
fetch_eligible_child_resources(sub.as_scope(), FetchChildrenBehaviour::GetAllChildren)
.await,
)
.await;
match result {
Ok(_) => eyre::bail!(
"Expected error, but got success; GetAllChildren is only supported for management groups?"
),
Err(_) => return Ok(()),
}
}
#[test_log::test(tokio::test)]
#[ignore]
async fn it_works_interactive() -> Result<()> {
let mg = fetch_root_management_group().await?;
let mut scope = mg.as_scope().as_scope_impl().to_owned();
loop {
println!("{}", scope);
let Some(resources) = expect_aad_premium_p2_license(
fetch_eligible_child_resources(&scope, FetchChildrenBehaviour::default()).await,
)
.await?
else {
return Ok(());
};
let next_scope: EligibleChildResource =
PickerTui::new().set_header("Choose a scope").pick_one(
resources.into_iter().map(|x| Choice {
key: x.name.to_owned(),
value: x,
}),
)?;
scope = next_scope.id;
}
}
#[test_log::test(tokio::test)]
#[ignore]
async fn it_works_interactive2() -> Result<()> {
let Some(resources) =
expect_aad_premium_p2_license(fetch_all_eligible_resource_containers().await).await?
else {
return Ok(());
};
let chosen: Vec<EligibleChildResource> =
PickerTui::new().pick_many(resources.into_iter().map(|x| Choice {
key: x.to_string(),
value: x,
}))?;
assert!(!chosen.is_empty());
Ok(())
}
}