use super::resources_extra::{fargate_condition_scenarios, merge_reachable_scenario_conditions};
use super::{EvalContext, NativeRuleRegistry};
use diagnostics::{Diagnostic, RelatedResource, ResourceRef};
use rules::Category;
use std::collections::BTreeSet;
use template_model::consts::{
FIELD_CREATION_POLICY, FIELD_RESOURCE_TYPE, FIELD_RESOURCES, FIELD_UPDATE_POLICY, KEY_CREATION_POLICY,
KEY_UPDATE_POLICY,
};
use template_model::fargate::task_size_is_offered;
use template_model::resolver::ResolvedValue;
use template_model::{SemanticModel, SourceSpan, render_value};
use validation_engine::make_resource_diagnostic;
pub fn register(reg: &mut NativeRuleRegistry) {
reg.add(Category::Resource, eval_resources);
reg.add(Category::Resource, crate::rules::resources_extra::eval_extra_resources);
}
fn resolve_concrete(m: &SemanticModel, rid: &str, path: &str) -> Option<serde_json::Value> {
match m.resolve_deep(rid, path).or_else(|| m.resolve(rid, path).cloned())? {
ResolvedValue::Concrete { value: v } => Some(v.into_inner()),
_ => None,
}
}
fn eval_resources(ctx: &EvalContext) -> Vec<Diagnostic> {
let mut out = Vec::new();
let m = ctx.model;
for name in m.resources_of_type("AWS::ECS::TaskDefinition") {
let fargate_scenarios = fargate_condition_scenarios(m, name);
if fargate_scenarios.is_empty() {
continue;
}
let cpu_scenarios = m.resolve_scenarios_json(name, "Properties.Cpu");
let memory_scenarios = m.resolve_scenarios_json(name, "Properties.Memory");
let mut invalid_sizes = BTreeSet::new();
for fargate_conditions in &fargate_scenarios {
for (cpu, cpu_conditions) in &cpu_scenarios {
let Some(fargate_cpu_conditions) =
merge_reachable_scenario_conditions(m, name, fargate_conditions, cpu_conditions)
else {
continue;
};
for (memory, memory_conditions) in &memory_scenarios {
if merge_reachable_scenario_conditions(m, name, &fargate_cpu_conditions, memory_conditions)
.is_some()
&& matches!(task_size_is_offered(cpu, memory), Some(false))
{
invalid_sizes.insert((render_value(cpu), render_value(memory)));
}
}
}
}
for (cpu, memory) in invalid_sizes {
out.push(make_resource_diagnostic(
"E3047",
&format!("Cpu {cpu} is not compatible with Memory {memory} for Fargate"),
m,
name,
"Properties.Cpu",
Some("Use a valid Fargate CPU/memory combination (e.g., Cpu: 256 with Memory: 512, 1024, or 2048)"),
));
}
}
let subnets = m.resources_of_type("AWS::EC2::Subnet");
for (i, b_name) in subnets.iter().enumerate() {
if i == 0 {
continue;
}
if m.is_from_parameter(b_name, "Properties.CidrBlock") {
continue;
}
let Some(serde_json::Value::String(b_cidr)) = resolve_concrete(m, b_name, "Properties.CidrBlock") else {
continue;
};
let Ok(net_b) = b_cidr.parse::<ipnetwork::IpNetwork>() else {
continue;
};
let b_cond = m.resources.get(b_name.as_str()).and_then(|r| r.condition.as_deref());
let b_vpc = resolve_concrete(m, b_name, "Properties.VpcId");
for a_name in &subnets[..i] {
let a_cond = m.resources.get(a_name.as_str()).and_then(|r| r.condition.as_deref());
if !m.conditions.resources_compatible(a_cond, b_cond) {
continue;
}
if m.is_from_parameter(a_name, "Properties.CidrBlock") {
continue;
}
let a_vpc = resolve_concrete(m, a_name, "Properties.VpcId");
if a_vpc != b_vpc {
continue;
}
let Some(serde_json::Value::String(a_cidr)) = resolve_concrete(m, a_name, "Properties.CidrBlock") else {
continue;
};
let Ok(net_a) = a_cidr.parse::<ipnetwork::IpNetwork>() else {
continue;
};
if !(net_a.contains(net_b.network()) || net_b.contains(net_a.network())) {
continue;
}
let mut diag = make_resource_diagnostic(
"E3060",
&format!("'{}' overlaps with '{}'", b_cidr, a_cidr),
m,
b_name,
"Properties.CidrBlock",
None,
);
let span = m.resource_span(a_name, "");
diag.related_resources.get_or_insert_with(Vec::new).push(RelatedResource {
resource: Some(ResourceRef {
id: Some(a_name.clone()),
resource_type: m.resources.get(a_name.as_str()).map(|r| r.resource_type.clone()),
}),
location: Some(SourceSpan {
start_line: span.start_line,
start_column: span.start_column,
end_line: span.end_line,
end_column: span.end_column,
}),
message: format!("Overlapping subnet CIDR {}", a_cidr),
});
out.push(diag);
}
}
for (name, res) in &m.resources {
if res.resource_type.ends_with("::MODULE") && res.properties.contains_key("Tags") {
out.push(make_resource_diagnostic(
"E5001",
&format!("Tags is not permitted within Module resource '{}'", name),
m,
name,
"Properties.Tags",
None,
));
}
}
if let Some(resources) = ctx.input.get(FIELD_RESOURCES).and_then(|r| r.as_object()) {
for (name, res) in resources {
let rtype = res.get(FIELD_RESOURCE_TYPE).and_then(|t| t.as_str()).unwrap_or("");
if rtype.ends_with("::MODULE") {
if res.get(FIELD_CREATION_POLICY).is_some_and(|v| !v.is_null()) {
out.push(make_resource_diagnostic(
"E5001",
&format!("CreationPolicy is not permitted within Module resource '{}'", name),
m,
name,
KEY_CREATION_POLICY,
None,
));
}
if res.get(FIELD_UPDATE_POLICY).is_some_and(|v| !v.is_null()) {
out.push(make_resource_diagnostic(
"E5001",
&format!("UpdatePolicy is not permitted within Module resource '{}'", name),
m,
name,
KEY_UPDATE_POLICY,
None,
));
}
}
}
}
for name in m.resources_of_type("AWS::Lambda::Function") {
if is_zip_deployment(m, name) {
let missing: Vec<&str> = ctx
.cached_data
.rule_tables
.lambda_zip_required_properties
.iter()
.map(|s| s.as_str())
.filter(|prop| {
!m.resources.get(name.as_str()).map(|r| r.properties.contains_key(*prop)).unwrap_or(false)
})
.collect();
if !missing.is_empty() {
let formatted = missing.iter().map(|p| format!("'{}'", p)).collect::<Vec<_>>().join(", ");
out.push(make_resource_diagnostic(
"W2533",
&format!(
"Properties [{}] missing for zip file deployment at Resources/{}/Properties",
formatted, name
),
m,
name,
"Properties.Code",
Some("Add the missing properties for zip file deployment"),
));
}
}
}
for name in m.resources_of_type("AWS::SQS::Queue") {
if let Some(serde_json::Value::Bool(true)) = resolve_concrete(m, name, "Properties.FifoQueue")
&& let Some(serde_json::Value::String(qname)) = resolve_concrete(m, name, "Properties.QueueName")
&& !qname.ends_with(".fifo")
{
out.push(make_resource_diagnostic(
"E2504",
&format!("FIFO queue name '{}' must end with '.fifo'", qname),
m,
name,
"Properties.QueueName",
None,
));
}
}
out
}
fn is_zip_deployment(m: &SemanticModel, name: &str) -> bool {
if let Some(serde_json::Value::String(pt)) = resolve_concrete(m, name, "Properties.PackageType") {
return pt == "Zip";
}
if !m.resources.get(name).map(|r| r.properties.contains_key("PackageType")).unwrap_or(false)
&& let Some(serde_json::Value::Object(code)) = resolve_concrete(m, name, "Properties.Code")
{
return code.contains_key("ZipFile") || code.contains_key("S3Key");
}
false
}