use crate::interactive::pim_activate_entra;
use crate::menu::press_enter_to_continue;
use clap::Args;
use cloud_terrastodon_azure::AzureTenantArgument;
use cloud_terrastodon_azure::AzureTenantArgumentExt;
use cloud_terrastodon_azure::RolePermissionAction;
use cloud_terrastodon_azure::UnifiedRoleDefinitionsAndAssignmentsIterTools;
use cloud_terrastodon_azure::fetch_all_unified_role_definitions_and_assignments;
use cloud_terrastodon_azure::fetch_current_user;
use cloud_terrastodon_command::CommandBuilder;
use cloud_terrastodon_command::CommandKind;
use cloud_terrastodon_command::OutputBehaviour;
use cloud_terrastodon_hcl::TerraformChangeAction;
use cloud_terrastodon_hcl::TerraformPlan;
use eyre::Result;
use eyre::bail;
use itertools::Itertools;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ops::Not;
use std::path::PathBuf;
use tracing::debug;
#[derive(Args, Debug, Clone)]
pub struct TerraformApplyArgs {
#[arg(long, default_value_t)]
pub tenant: AzureTenantArgument<'static>,
#[arg(default_value = ".")]
pub source_dir: PathBuf,
}
impl TerraformApplyArgs {
pub async fn invoke(self) -> Result<()> {
let tenant_id = self.tenant.resolve().await?;
let plan_file = "apply.tfplan";
let mut cmd = CommandBuilder::new(CommandKind::Terraform);
cmd.use_run_dir(&self.source_dir);
cmd.args(["plan", "-out", plan_file]);
cmd.use_output_behaviour(OutputBehaviour::Display);
cmd.should_announce(true);
cmd.run_raw().await?;
let plan_file_path = self.source_dir.join(plan_file);
if !tokio::fs::try_exists(&plan_file_path)
.await
.unwrap_or_default()
{
bail!(
"Terraform plan file not found: {}",
plan_file_path.display()
);
}
let mut cmd = CommandBuilder::new(CommandKind::Terraform);
cmd.use_run_dir(&self.source_dir);
cmd.should_announce(true);
cmd.args(["show", "--json", plan_file]);
let plan_json = cmd.run::<TerraformPlan>().await?;
#[derive(Debug, Eq, PartialEq, Hash)]
pub enum RequiredPermission {
Entra(RolePermissionAction),
}
let mut required_roles = HashSet::new();
for resource_change in &plan_json.resource_changes {
let is_create_action = resource_change
.change
.actions
.contains(&TerraformChangeAction::Create);
match resource_change.r#type.as_ref() {
"azuread_application_registration" if is_create_action => {
required_roles.insert(RequiredPermission::Entra(RolePermissionAction::new(
"microsoft.directory/applications/create",
)));
}
"azuread_user" if is_create_action => {
required_roles.insert(RequiredPermission::Entra(RolePermissionAction::new(
"microsoft.directory/users/create",
)));
}
_ => {}
}
}
println!("This plan requires: {:#?}", required_roles);
let entra_rbac = fetch_all_unified_role_definitions_and_assignments(tenant_id).await?;
let current_user = fetch_current_user().await?;
let current_user_rbac = entra_rbac
.iter_role_assignments()
.filter_principal(¤t_user.id)
.collect_vec();
let mut requirement_satisfaction = required_roles
.iter()
.map(|req| (req, false))
.collect::<HashMap<_, _>>();
for (requirement, satisfied) in &mut requirement_satisfaction {
match requirement {
RequiredPermission::Entra(action) => {
for (_role_assignment, role_definition) in ¤t_user_rbac {
if role_definition.satisfies(std::slice::from_ref(action)) {
*satisfied = true;
debug!(
"Requirement {:?} satisfied by role {}",
requirement, role_definition.display_name
);
break;
}
}
}
}
}
let unsatisfied_requirements = requirement_satisfaction
.iter()
.filter_map(|(req, satisfied)| satisfied.not().then_some(req))
.collect_vec();
if !unsatisfied_requirements.is_empty() {
println!(
"The following requirements are not currently satisfied: {:#?}",
unsatisfied_requirements
);
press_enter_to_continue().await?;
pim_activate_entra(tenant_id).await?;
}
let mut cmd = CommandBuilder::new(CommandKind::Terraform);
cmd.use_run_dir(&self.source_dir);
cmd.args(["apply", plan_file]);
cmd.use_output_behaviour(OutputBehaviour::Display);
cmd.should_announce(true);
cmd.run_raw().await?;
Ok(())
}
}