use chrono::Utc;
use fakecloud_autoscaling::state::{AsgInstance, AutoScalingGroup, LaunchConfiguration};
use serde_json::Value;
use uuid::Uuid;
use super::{ProvisionResult, ResourceDefinition, ResourceProvisioner};
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 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> = (0..desired.max(0))
.map(|k| {
let hex = Uuid::new_v4().simple().to_string();
AsgInstance {
instance_id: format!("i-{}", &hex[..17]),
availability_zone: azs[k as usize % azs.len()].clone(),
lifecycle_state: "InService".to_string(),
health_status: "Healthy".to_string(),
launch_configuration_name: lcn.clone(),
protected_from_scale_in: false,
}
})
.collect();
let arn = self.asg_arn("autoScalingGroup", &name);
let group = AutoScalingGroup {
arn: arn.clone(),
name: name.clone(),
launch_configuration_name: lcn,
launch_template: None,
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);
Ok(ProvisionResult::new(name).with("Arn", arn))
}
pub(super) fn delete_autoscaling(&self, resource_type: &str, name: &str) {
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);
}
"AWS::AutoScaling::AutoScalingGroup" => {
acct.groups.remove(name);
}
_ => {}
}
}
}