1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
use super::{EvalContext, NativeRuleRegistry};
use diagnostics::Diagnostic;
use rules::Category;
use template_model::consts::{
EDGE_KIND_GET_ATT, EDGE_KIND_REF, EDGE_KIND_SUB, FIELD_CONDITION_CONTEXT, FIELD_KIND, FIELD_OUTGOING_REFS,
FIELD_RESOURCES, FIELD_SOURCE_PATH, FIELD_TARGET, KEY_DEPENDS_ON, OUTPUT_PSEUDO_RESOURCE_PREFIX,
};
use template_model::resolver::RefKind;
use validation_engine::make_resource_diagnostic;
pub fn register(reg: &mut NativeRuleRegistry) {
reg.add(Category::Reference, eval_references);
}
/// Whether a property path points at a value selected by an `Fn::If` branch
/// (a path segment `Fn::If` followed by branch index `1` or `2`). Such a
/// reference is guarded by the surrounding `Fn::If`, so the conditional-target
/// reference check does not apply.
fn path_inside_fn_if_branch(path: &str) -> bool {
let segments: Vec<&str> = path.split('.').collect();
segments.windows(2).any(|w| w[0] == "Fn::If" && (w[1] == "1" || w[1] == "2"))
}
fn eval_references(ctx: &EvalContext) -> Vec<Diagnostic> {
let mut out = Vec::new();
let m = ctx.model;
let input = ctx.input;
for (name, res) in &m.resources {
for dep in &res.depends_on {
if !m.resources.contains_key(dep.as_str()) && !m.sam_implicit_resources.contains(dep.as_str()) {
// A dynamic reference cannot name a resource: DependsOn takes
// literal logical IDs only, so say that rather than implying a
// resource of that name could be added.
let message = if dep.contains("{{resolve:") {
format!("DependsOn must be a resource logical ID, not a dynamic reference: '{}'", dep)
} else {
format!("DependsOn target '{}' does not exist as a resource", dep)
};
out.push(make_resource_diagnostic("E3005", &message, m, name, "", None));
}
}
}
for (name, res) in &m.resources {
for dep in &res.depends_on {
if let Some(dep_res) = m.resources.get(dep.as_str())
&& let Some(ref dep_cond) = dep_res.condition
{
let source_cond = res.condition.as_deref();
if !m.conditions.condition_implies(source_cond.unwrap_or(""), dep_cond) && source_cond.is_some()
|| (source_cond.is_none() && dep_res.condition.is_some())
{
let implies = match source_cond {
Some(sc) => m.conditions.condition_implies(sc, dep_cond),
None => false, // unconditional resource depends on conditional
};
if !implies {
out.push(make_resource_diagnostic(
"E3005",
&format!("'{}' will not exist when condition '{}' is False", dep, dep_cond),
m,
name,
KEY_DEPENDS_ON,
Some(&format!("Add a Condition to '{}' that implies '{}'", name, dep_cond)),
));
}
}
}
}
}
for (name, res) in &m.resources {
for dep in &res.depends_on {
for edge in m.graph.outgoing(name) {
if edge.target == *dep {
let kind_str = match &edge.kind {
RefKind::Ref => EDGE_KIND_REF,
RefKind::GetAtt { attr: _ } => EDGE_KIND_GET_ATT,
RefKind::Sub { var: _ } => EDGE_KIND_SUB,
_ => continue,
};
out.push(make_resource_diagnostic(
"W3005",
&format!("'{}' dependency already enforced by a '{}' at '{}'", dep, kind_str, edge.source_path),
m,
name,
KEY_DEPENDS_ON,
Some("Remove the DependsOn entry"),
));
}
}
}
}
if let Some(resources) = input.get(FIELD_RESOURCES).and_then(|r| r.as_object()) {
for (source, res_json) in resources {
if let Some(edges) = res_json.get(FIELD_OUTGOING_REFS).and_then(|r| r.as_array()) {
for edge in edges {
let kind = edge.get(FIELD_KIND).and_then(|k| k.as_str()).unwrap_or("");
if kind != EDGE_KIND_REF && kind != EDGE_KIND_GET_ATT {
continue;
}
let target = edge.get(FIELD_TARGET).and_then(|t| t.as_str()).unwrap_or("");
let source_path = edge.get(FIELD_SOURCE_PATH).and_then(|p| p.as_str()).unwrap_or("");
// A reference that is itself a value inside an Fn::If branch is
// already guarded by that Fn::If; the explicit branch choice
// makes it safe, so skip these.
if path_inside_fn_if_branch(source_path) {
continue;
}
if let Some(target_res) = m.resources.get(target)
&& let Some(ref target_cond) = target_res.condition
{
let source_cond = m.resources.get(source.as_str()).and_then(|r| r.condition.as_deref());
let implies = match source_cond {
Some(sc) => m.conditions.condition_implies(sc, target_cond),
None => false,
};
if !implies {
// Check if the reference is inside an Fn::If guarded by a
// condition that (in conjunction with source_cond) implies
// the target's condition. Uses SAT: if
// `source_cond=T, part=T, target_cond=F` is unsatisfiable,
// the reference is safe.
let guarded = edge
.get(FIELD_CONDITION_CONTEXT)
.and_then(|c| c.as_str())
.map(|cc| {
cc.split(',').filter(|p| !p.is_empty()).any(|part| {
let mut assumptions: Vec<(String, bool)> =
vec![(target_cond.clone(), false), (part.to_string(), true)];
if let Some(sc) = source_cond {
assumptions.push((sc.to_string(), true));
}
!m.conditions.is_satisfiable(&assumptions)
})
})
.unwrap_or(false);
if !guarded {
out.push(make_resource_diagnostic("W1001",
&format!("Reference to '{}' which is conditional on '{}' - target may not exist", target, target_cond),
m,
source,
source_path,
Some("Add a Condition to the referencing resource that implies the target's condition"),
));
}
}
}
}
}
}
}
// Check output references to conditional resources
for (out_name, output) in &m.outputs {
let output_pseudo_id = format!("{}{}", OUTPUT_PSEUDO_RESOURCE_PREFIX, out_name);
for edge in m.graph.outgoing(&output_pseudo_id) {
let kind_str = match &edge.kind {
RefKind::Ref => EDGE_KIND_REF,
RefKind::GetAtt { .. } => EDGE_KIND_GET_ATT,
_ => continue,
};
let _ = kind_str;
if let Some(target_res) = m.resources.get(&edge.target)
&& let Some(ref target_cond) = target_res.condition
{
let source_cond = output.condition.as_deref();
let implies = match source_cond {
Some(sc) => m.conditions.condition_implies(sc, target_cond),
None => false,
};
if !implies {
// An output is not a resource - the edge's section-absolute
// source path identifies it.
out.push(make_resource_diagnostic(
"W1001",
&format!(
"Reference to '{}' which is conditional on '{}' - target may not exist",
edge.target, target_cond
),
m,
"",
&edge.source_path,
Some("Add a Condition to the output that implies the target's condition"),
));
}
}
}
}
out
}