use chrono::Utc;
use fakecloud_autoscaling::state::{
AsgInstance, AutoScalingGroup, LaunchConfiguration, LaunchTemplateSpec,
};
use serde_json::Value;
use uuid::Uuid;
use super::{ProvisionResult, ResourceDefinition, ResourceProvisioner};
fn parse_cfn_launch_template(v: Option<&Value>) -> Option<LaunchTemplateSpec> {
let obj = v?;
let id = obj.get("LaunchTemplateId").and_then(|x| x.as_str());
let name = obj.get("LaunchTemplateName").and_then(|x| x.as_str());
if id.is_none() && name.is_none() {
return None;
}
Some(LaunchTemplateSpec {
launch_template_id: id.map(String::from),
launch_template_name: name.map(String::from),
version: obj
.get("Version")
.and_then(|x| x.as_str())
.map(String::from),
})
}
fn prop_str<'a>(p: &'a Value, k: &str) -> Option<&'a str> {
p.get(k).and_then(|v| v.as_str())
}
fn prop_i64(p: &Value, k: &str) -> Option<i64> {
p.get(k).and_then(|v| {
v.as_i64()
.or_else(|| v.as_str().and_then(|s| s.parse().ok()))
})
}
fn str_list(p: &Value, k: &str) -> Vec<String> {
p.get(k)
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
impl ResourceProvisioner {
fn asg_arn(&self, kind: &str, name: &str) -> String {
format!(
"arn:aws:autoscaling:{}:{}:{kind}:{}:{kind}Name/{name}",
self.region,
self.account_id,
Uuid::new_v4()
)
}
pub(super) fn create_autoscaling_launch_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = prop_str(props, "LaunchConfigurationName")
.map(String::from)
.unwrap_or_else(|| resource.logical_id.clone());
let image_id = prop_str(props, "ImageId")
.ok_or("AWS::AutoScaling::LaunchConfiguration requires ImageId")?
.to_string();
let instance_type = prop_str(props, "InstanceType")
.ok_or("AWS::AutoScaling::LaunchConfiguration requires InstanceType")?
.to_string();
let lc = LaunchConfiguration {
arn: self.asg_arn("launchConfiguration", &name),
name: name.clone(),
image_id,
instance_type,
key_name: prop_str(props, "KeyName").map(String::from),
security_groups: str_list(props, "SecurityGroups"),
user_data: prop_str(props, "UserData").map(String::from),
iam_instance_profile: prop_str(props, "IamInstanceProfile").map(String::from),
associate_public_ip_address: props
.get("AssociatePublicIpAddress")
.and_then(|v| v.as_bool()),
instance_monitoring: props
.get("InstanceMonitoring")
.and_then(|v| v.as_bool())
.unwrap_or(true),
ebs_optimized: props
.get("EbsOptimized")
.and_then(|v| v.as_bool())
.unwrap_or(false),
spot_price: prop_str(props, "SpotPrice").map(String::from),
placement_tenancy: prop_str(props, "PlacementTenancy").map(String::from),
created_time: Utc::now(),
};
self.autoscaling_state
.write()
.get_or_create(&self.account_id)
.launch_configurations
.insert(name.clone(), lc);
Ok(ProvisionResult::new(name))
}
pub(super) fn create_autoscaling_group(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let name = prop_str(props, "AutoScalingGroupName")
.map(String::from)
.unwrap_or_else(|| resource.logical_id.clone());
let min_size = prop_i64(props, "MinSize").unwrap_or(0);
let max_size = prop_i64(props, "MaxSize").unwrap_or(min_size);
let desired = prop_i64(props, "DesiredCapacity").unwrap_or(min_size);
let mut azs = str_list(props, "AvailabilityZones");
if azs.is_empty() {
azs.push(format!("{}a", self.region));
}
let lcn = prop_str(props, "LaunchConfigurationName").map(String::from);
let launch_template =
parse_cfn_launch_template(props.get("LaunchTemplate")).or_else(|| {
props
.get("MixedInstancesPolicy")
.and_then(|m| m.get("LaunchTemplate"))
.and_then(|lt| lt.get("LaunchTemplateSpecification"))
.and_then(|spec| parse_cfn_launch_template(Some(spec)))
});
let vpc_zone_identifier = props
.get("VPCZoneIdentifier")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_str())
.collect::<Vec<_>>()
.join(",")
})
.or_else(|| prop_str(props, "VPCZoneIdentifier").map(String::from));
let instances: Vec<AsgInstance> = Vec::new();
let arn = self.asg_arn("autoScalingGroup", &name);
let group = AutoScalingGroup {
arn: arn.clone(),
name: name.clone(),
launch_configuration_name: lcn,
launch_template,
min_size,
max_size,
desired_capacity: desired,
default_cooldown: prop_i64(props, "Cooldown").unwrap_or(300),
availability_zones: azs,
vpc_zone_identifier,
health_check_type: prop_str(props, "HealthCheckType")
.unwrap_or("EC2")
.to_string(),
health_check_grace_period: prop_i64(props, "HealthCheckGracePeriod").unwrap_or(0),
target_group_arns: str_list(props, "TargetGroupARNs"),
load_balancer_names: str_list(props, "LoadBalancerNames"),
new_instances_protected_from_scale_in: props
.get("NewInstancesProtectedFromScaleIn")
.and_then(|v| v.as_bool())
.unwrap_or(false),
created_time: Utc::now(),
instances,
tags: Vec::new(),
status: None,
service_linked_role_arn: format!(
"arn:aws:iam::{}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
self.account_id
),
};
self.autoscaling_state
.write()
.get_or_create(&self.account_id)
.groups
.insert(name.clone(), group);
self.pending_container_spawns
.lock()
.push(super::ContainerSpawnIntent::AsgInstances {
group_name: name.clone(),
});
Ok(ProvisionResult::new(name).with("Arn", arn))
}
pub(super) fn delete_autoscaling(&self, resource_type: &str, name: &str) {
let removed_instance_ids = {
let mut st = self.autoscaling_state.write();
let acct = st.get_or_create(&self.account_id);
match resource_type {
"AWS::AutoScaling::LaunchConfiguration" => {
acct.launch_configurations.remove(name);
Vec::new()
}
"AWS::AutoScaling::AutoScalingGroup" => acct
.groups
.remove(name)
.map(|g| {
g.instances
.iter()
.map(|i| i.instance_id.clone())
.collect::<Vec<_>>()
})
.unwrap_or_default(),
_ => Vec::new(),
}
};
if !removed_instance_ids.is_empty() {
self.pending_container_teardowns.lock().push(
super::ContainerTeardownIntent::AsgInstances {
instance_ids: removed_instance_ids,
},
);
}
}
}