alien_core/
gate_resolution.rs1use crate::{
5 ownership_policy_for_resource_type, GateAnswers, ResourceEntry, Stack, StackInputDefaultValue,
6 StackInputDefinition,
7};
8use std::collections::{HashMap, HashSet};
9
10pub fn gate_value_as_bool(value: &serde_json::Value) -> Option<bool> {
15 match value {
16 serde_json::Value::Bool(answer) => Some(*answer),
17 serde_json::Value::String(text) => match text.as_str() {
18 "true" => Some(true),
19 "false" => Some(false),
20 _ => None,
21 },
22 _ => None,
23 }
24}
25
26pub fn frozen_gate_of(entry: &ResourceEntry) -> Option<&str> {
28 gate_of(entry, true)
29}
30
31pub fn live_gate_of(entry: &ResourceEntry) -> Option<&str> {
35 gate_of(entry, false)
36}
37
38fn gate_of(entry: &ResourceEntry, setup_created: bool) -> Option<&str> {
39 let input_id = entry.enabled_when.as_deref()?;
40 let emitted_in_setup =
41 ownership_policy_for_resource_type(entry.config.resource_type().as_ref())
42 .should_emit_in_setup(entry.lifecycle);
43 (emitted_in_setup == setup_created).then_some(input_id)
44}
45
46pub fn frozen_gated(stack: &Stack) -> impl Iterator<Item = (&String, &str)> {
48 stack
49 .resources()
50 .filter_map(|(resource_id, entry)| Some((resource_id, frozen_gate_of(entry)?)))
51}
52
53pub fn live_gated(stack: &Stack) -> impl Iterator<Item = (&String, &str)> {
55 stack
56 .resources()
57 .filter_map(|(resource_id, entry)| Some((resource_id, live_gate_of(entry)?)))
58}
59
60pub fn gate_resolves_true(
63 inputs: &[StackInputDefinition],
64 input_id: &str,
65 input_values: &HashMap<String, serde_json::Value>,
66 resource_id: &str,
67) -> Result<bool, String> {
68 if let Some(value) = input_values.get(input_id) {
69 return gate_value_as_bool(value).ok_or_else(|| {
70 format!(
71 "Input '{input_id}' enables resource '{resource_id}' but its value is not \
72 a boolean: {value}"
73 )
74 });
75 }
76
77 match inputs
78 .iter()
79 .find(|input| input.id == input_id)
80 .and_then(|input| input.default.as_ref())
81 {
82 Some(StackInputDefaultValue::Boolean(answer)) => Ok(*answer),
83 _ => Err(format!(
84 "Input '{input_id}' enables resource '{resource_id}' but no value was provided \
85 and the input declares no boolean default"
86 )),
87 }
88}
89
90pub fn live_gate_resolves_true(
95 inputs: &[StackInputDefinition],
96 input_id: &str,
97 input_values: &HashMap<String, serde_json::Value>,
98 persisted_gate_answers: &GateAnswers,
99 still_frozen_gating: &HashSet<String>,
100 resource_id: &str,
101) -> Result<(bool, &'static str), String> {
102 if input_values.contains_key(input_id) {
110 return Ok((
111 gate_resolves_true(inputs, input_id, input_values, resource_id)?,
112 "provided",
113 ));
114 }
115 if still_frozen_gating.contains(input_id) {
116 if let Some(answer) = persisted_gate_answers.get(input_id) {
117 return Ok((*answer, "persisted"));
118 }
119 }
120 Ok((
121 gate_resolves_true(inputs, input_id, input_values, resource_id)?,
122 "default",
123 ))
124}
125
126pub fn declined_live_resources(
128 stack: &Stack,
129 input_values: &HashMap<String, serde_json::Value>,
130 persisted_gate_answers: &GateAnswers,
131 still_frozen_gating: &HashSet<String>,
132) -> Result<Vec<String>, String> {
133 let mut declined = Vec::new();
134 for (resource_id, input_id) in live_gated(stack) {
135 let (accepted, _source) = live_gate_resolves_true(
136 &stack.inputs,
137 input_id,
138 input_values,
139 persisted_gate_answers,
140 still_frozen_gating,
141 resource_id,
142 )?;
143 if !accepted {
144 declined.push(resource_id.clone());
145 }
146 }
147 Ok(declined)
148}
149
150pub fn surviving_frozen_gate_answers(stack: &Stack) -> (GateAnswers, HashSet<String>) {
154 let still_frozen_gating: HashSet<String> = frozen_gated(stack)
155 .map(|(_, input_id)| input_id.to_string())
156 .collect();
157 let answers = still_frozen_gating
158 .iter()
159 .map(|input_id| (input_id.clone(), true))
160 .collect();
161 (answers, still_frozen_gating)
162}
163
164pub fn remove_declined_resources(stack: &mut Stack, declined: &[String]) {
168 if declined.is_empty() {
169 return;
170 }
171
172 for resource_id in declined {
173 tracing::info!(
174 resource_id = %resource_id,
175 "The deployer declined this gated resource; it leaves the desired stack"
176 );
177 stack.resources.shift_remove(resource_id);
178 }
179
180 for (resource_id, entry) in stack.resources.iter_mut() {
184 let dropped = match crate::resource_links_mut(&mut entry.config) {
185 Some(owner) => {
186 let before = owner.links().len();
187 owner
188 .links_mut()
189 .retain(|link| !declined.contains(&link.id));
190 before - owner.links().len()
191 }
192 None => 0,
193 };
194 if dropped > 0 {
195 tracing::info!(
196 resource_id = %resource_id,
197 dropped,
198 declined = ?declined,
199 "Dropped links to declined resources; this resource keeps its own lifecycle"
200 );
201 }
202
203 let ordering_before = entry.dependencies.len();
204 entry
205 .dependencies
206 .retain(|dependency| !declined.contains(&dependency.id));
207 if ordering_before > entry.dependencies.len() {
210 tracing::info!(
211 resource_id = %resource_id,
212 dropped = ordering_before - entry.dependencies.len(),
213 "Dropped ordering edges to declined resources"
214 );
215 }
216 }
217
218 scrub_declined_grants(stack, declined);
219}
220
221fn scrub_declined_grants(stack: &mut Stack, declined: &[String]) {
226 let scrub = |profile: &mut crate::permissions::PermissionProfile| {
227 for resource_id in declined {
228 if profile.0.shift_remove(resource_id).is_some() {
229 tracing::info!(
230 resource_id = %resource_id,
231 "Dropped the grant for a declined resource"
232 );
233 }
234 }
235 };
236
237 for profile in stack.permissions.profiles.values_mut() {
238 scrub(profile);
239 }
240 match &mut stack.permissions.management {
241 crate::permissions::ManagementPermissions::Extend(profile)
242 | crate::permissions::ManagementPermissions::Override(profile) => scrub(profile),
243 crate::permissions::ManagementPermissions::Auto => {}
244 }
245}