use crate::reflow::HclReflower;
use cloud_terrastodon_azure::uuid::Uuid;
use cloud_terrastodon_hcl_types::AzureAdResourceBlockKind;
use cloud_terrastodon_hcl_types::AzureDevOpsResourceBlockKind;
use cloud_terrastodon_hcl_types::AzureRmResourceBlockKind;
use cloud_terrastodon_hcl_types::ResourceBlockResourceKind;
use hcl::edit::Decorate;
use hcl::edit::Decorated;
use hcl::edit::expr::Array;
use hcl::edit::expr::Expression;
use hcl::edit::structure::Attribute;
use hcl::edit::structure::Block;
use hcl::edit::structure::Body;
use hcl::edit::visit_mut::VisitMut;
use hcl::edit::visit_mut::visit_block_mut;
use std::collections::HashMap;
use std::path::PathBuf;
use tracing::warn;
pub struct ReflowRemoveDefaultAttributes;
#[async_trait::async_trait]
impl HclReflower for ReflowRemoveDefaultAttributes {
async fn reflow(
&mut self,
hcl: HashMap<PathBuf, Body>,
) -> eyre::Result<HashMap<PathBuf, Body>> {
let mut reflowed = HashMap::new();
for (path, mut body) in hcl {
self.visit_body_mut(&mut body);
reflowed.insert(path, body);
}
Ok(reflowed)
}
}
impl VisitMut for ReflowRemoveDefaultAttributes {
fn visit_block_mut(&mut self, node: &mut Block) {
if node.ident.as_str() != "resource" {
return;
}
visit_block_mut(self, node);
let [resource_kind, _name] = node.labels.as_slice() else {
return;
};
let Ok(resource_kind) = resource_kind.parse() else {
warn!("Failed to identify resource kind for {resource_kind:?}");
return;
};
let body = &mut node.body;
match resource_kind {
ResourceBlockResourceKind::AzureRM(AzureRmResourceBlockKind::RoleAssignment) => {
remove_second_if_both_present(body, "role_definition_name", "role_definition_id");
remove_if_null_or_empty(body, "condition");
remove_if_null_or_empty(body, "condition_version");
remove_if_null_or_empty(body, "delegated_managed_identity_resource_id");
remove_if_null_or_empty(body, "description");
remove_if_null_or_empty(body, "skip_service_principal_aad_check");
let _ = body.remove_attribute("name");
}
ResourceBlockResourceKind::AzureAD(AzureAdResourceBlockKind::Group) => {
remove_second_if_both_present(body, "security_enabled", "mail_enabled");
if body.has_attribute("members") && body.has_blocks("dynamic_membership") {
let mut members = body.get_attribute_mut("members").unwrap();
let mut array = Array::new();
array.set_trailing("");
members.decor_mut().set_prefix("#");
*members.value_mut() = Expression::Array(array);
}
remove_if_null_or_empty(body, "description");
remove_if_null_or_empty(body, "theme");
remove_if_null_or_empty(body, "visibility");
remove_if_null_or_empty(body, "onpremises_group_type");
remove_if_false(body, "assignable_to_role");
remove_if_false(body, "auto_subscribe_new_members");
remove_if_false(body, "external_senders_allowed");
remove_if_false(body, "hide_from_address_lists");
remove_if_false(body, "hide_from_outlook_clients");
remove_if_false(body, "prevent_duplicate_names");
remove_if_false(body, "writeback_enabled");
remove_if_empty_array(body, "administrative_unit_ids");
remove_if_empty_array(body, "behaviors");
remove_if_empty_array(body, "provisioning_options");
remove_if_empty_array(body, "types");
fn is_default_nick(s: &str) -> bool {
s.parse::<Uuid>().is_ok()
}
if let Some(attrib) = body.get_attribute("mail_nickname")
&& let Some(nick) = attrib.value.as_str()
&& is_default_nick(nick)
{
body.remove_attribute("mail_nickname").unwrap();
}
}
ResourceBlockResourceKind::AzureRM(AzureRmResourceBlockKind::ResourceGroup) => {
remove_if_null_or_empty(body, "managed_by");
}
ResourceBlockResourceKind::AzureDevOps(AzureDevOpsResourceBlockKind::Project) => {
let features = body.get_attribute("features");
if let Some(features) = features
&& features
.value
.as_object()
.map(|x| x.is_empty())
.unwrap_or(false)
{
body.remove_attribute("features");
}
}
ResourceBlockResourceKind::AzureRM(AzureRmResourceBlockKind::PolicyDefinition)
| ResourceBlockResourceKind::AzureRM(AzureRmResourceBlockKind::PolicySetDefinition) => {
replace_if_null_or_empty(
body,
"display_name",
body.get_attribute("name")
.unwrap()
.value
.as_str()
.unwrap()
.to_owned(),
);
}
_ => {}
}
}
}
fn is_null_or_empty(attrib: &Attribute) -> bool {
attrib.value.is_null() || attrib.value.as_str().filter(|x| x.is_empty()).is_some()
}
fn remove_if_null_or_empty(body: &mut Body, key: &str) {
if let Some(attrib) = body.get_attribute(key)
&& is_null_or_empty(attrib)
{
body.remove_attribute(key).unwrap();
}
}
fn replace_if_null_or_empty(body: &mut Body, key: &str, default: impl Into<String>) {
if let Some(mut attrib) = body.get_attribute_mut(key)
&& is_null_or_empty(&attrib)
{
*attrib.value_mut() = Expression::String(Decorated::new(default.into()));
}
}
fn remove_if_false(body: &mut Body, key: &str) {
if let Some(attrib) = body.get_attribute(key)
&& let Some(false) = attrib.value.as_bool()
{
body.remove_attribute(key).unwrap();
}
}
fn remove_if_empty_array(body: &mut Body, key: &str) {
if let Some(attrib) = body.get_attribute(key)
&& let Some(x) = attrib.value.as_array()
&& x.is_empty()
{
body.remove_attribute(key).unwrap();
}
}
fn remove_second_if_both_present(body: &mut Body, keep: &str, remove: &str) {
if body.has_attribute(keep) && body.has_attribute(remove) {
body.remove_attribute(remove).unwrap();
}
}