Skip to main content

trellis_core/
audit.rs

1use crate::{
2    AuditEntry, AuditEvent, Graph, GraphError, GraphResult, NodeChangeExplanation, NodeHandle,
3    NodeId, OutputFrameExplanation, OutputFrameKindTrace, OutputKey, ResourceCommandCause,
4    ResourceCommandExplanation, ResourceCommandKind, ResourceKey, ScopeId, ScopeResourceInventory,
5    TransactionResult,
6};
7
8impl<C, O> Graph<C, O> {
9    /// Returns the committed audit log.
10    pub fn audit_log(&self) -> &[AuditEntry] {
11        &self.audit.log
12    }
13
14    /// Explains why a typed node last changed.
15    pub fn why_changed<H: NodeHandle>(&self, node: H) -> Option<&NodeChangeExplanation> {
16        self.why_changed_by_id(node.id())
17    }
18
19    /// Explains why a node id last changed.
20    pub fn why_changed_by_id(&self, node: NodeId) -> Option<&NodeChangeExplanation> {
21        self.audit.node_changes.get(&node)
22    }
23
24    /// Explains the latest resource command for a resource key.
25    pub fn why_resource_command(&self, key: &ResourceKey) -> Option<&ResourceCommandExplanation> {
26        self.audit.resource_commands.get(key)
27    }
28
29    /// Explains the latest output frame for an output key.
30    pub fn why_output_frame(&self, key: OutputKey) -> Option<&OutputFrameExplanation> {
31        self.audit.output_frames.get(&key)
32    }
33
34    /// Returns a dependency path from an upstream node to a downstream node.
35    pub fn dependency_path(&self, from: NodeId, to: NodeId) -> Option<Vec<NodeId>> {
36        if !self.nodes.contains_key(&from) || !self.nodes.contains_key(&to) {
37            return None;
38        }
39        let mut path = vec![from];
40        let mut visited = std::collections::BTreeSet::new();
41        self.dependency_path_inner(from, to, &mut visited, &mut path)
42            .then_some(path)
43    }
44
45    /// Returns resources currently owned by a scope.
46    pub fn scope_resource_inventory(&self, scope: ScopeId) -> GraphResult<ScopeResourceInventory> {
47        self.scope_meta(scope)
48            .ok_or(GraphError::UnknownScope(scope))?;
49        let resources = self
50            .resource_owners
51            .iter()
52            .filter(|(_, owners)| owners.contains(&scope))
53            .map(|(key, _)| key.clone())
54            .collect();
55        Ok(ScopeResourceInventory { scope, resources })
56    }
57
58    pub(crate) fn record_transaction_audit(&mut self, result: &TransactionResult<C, O>) {
59        self.audit.log.extend(result.audit_log.iter().cloned());
60        let changed_inputs = result.changed_inputs.clone();
61        let changed_nodes = changed_nodes(result);
62        for entry in &result.audit_log {
63            if let Some(node) = event_node(entry.event) {
64                let dependency_paths = self.paths_from_inputs_to_targets(&changed_inputs, &[node]);
65                let input_causes = input_causes_from_paths(&dependency_paths);
66                self.audit.node_changes.insert(
67                    node,
68                    NodeChangeExplanation {
69                        node,
70                        transaction_id: entry.transaction_id,
71                        revision: entry.revision,
72                        event: entry.event,
73                        input_causes,
74                        dependency_paths,
75                    },
76                );
77            }
78        }
79
80        let causes = std::mem::take(&mut self.audit.pending_resource_causes);
81        debug_assert_eq!(causes.len(), result.resource_plan.commands().len());
82        for (index, command) in result.resource_plan.commands().iter().enumerate() {
83            let cause = causes
84                .get(index)
85                .copied()
86                .expect("resource command cause recorded during reconciliation");
87            let collection_diffs = cause.collection().into_iter().collect::<Vec<_>>();
88            let dependency_paths =
89                self.paths_from_inputs_to_targets(&changed_inputs, &collection_diffs);
90            let input_causes = input_causes_from_paths(&dependency_paths);
91            self.audit.resource_commands.insert(
92                command.key().clone(),
93                ResourceCommandExplanation {
94                    key: command.key().clone(),
95                    scope: command.scope(),
96                    transaction_id: result.transaction_id,
97                    revision: result.revision,
98                    kind: ResourceCommandKind::from_command(command),
99                    cause,
100                    collection_diffs,
101                    changed_nodes: changed_nodes.clone(),
102                    input_causes,
103                    dependency_paths,
104                },
105            );
106        }
107
108        for frame in &result.output_frames {
109            let dependencies = self
110                .output_meta(frame.output_key)
111                .map(|meta| meta.dependencies().as_slice().to_vec())
112                .unwrap_or_default();
113            let changed_dependencies = dependencies
114                .iter()
115                .copied()
116                .filter(|node| changed_nodes.contains(node))
117                .collect::<Vec<_>>();
118            let dependency_paths =
119                self.paths_from_inputs_to_targets(&changed_inputs, &changed_dependencies);
120            let input_causes = input_causes_from_paths(&dependency_paths);
121            self.audit.output_frames.insert(
122                frame.output_key,
123                OutputFrameExplanation {
124                    output_key: frame.output_key,
125                    scope: frame.scope,
126                    transaction_id: frame.transaction_id,
127                    revision: frame.revision,
128                    kind: OutputFrameKindTrace::from_kind(&frame.kind),
129                    dependencies,
130                    changed_dependencies,
131                    input_causes,
132                    dependency_paths,
133                },
134            );
135        }
136    }
137
138    fn paths_from_inputs_to_targets(
139        &self,
140        inputs: &[NodeId],
141        targets: &[NodeId],
142    ) -> Vec<Vec<NodeId>> {
143        inputs
144            .iter()
145            .flat_map(|input| {
146                targets
147                    .iter()
148                    .filter_map(|target| self.dependency_path(*input, *target))
149            })
150            .collect()
151    }
152
153    fn dependency_path_inner(
154        &self,
155        current: NodeId,
156        target: NodeId,
157        visited: &mut std::collections::BTreeSet<NodeId>,
158        path: &mut Vec<NodeId>,
159    ) -> bool {
160        if current == target {
161            return true;
162        }
163        if !visited.insert(current) {
164            return false;
165        }
166        for next in self.downstream_nodes(current) {
167            path.push(next);
168            if self.dependency_path_inner(next, target, visited, path) {
169                return true;
170            }
171            path.pop();
172        }
173        false
174    }
175
176    fn downstream_nodes(&self, node: NodeId) -> Vec<NodeId> {
177        self.nodes
178            .values()
179            .filter_map(|meta| {
180                meta.dependencies()
181                    .as_slice()
182                    .contains(&node)
183                    .then_some(meta.id())
184            })
185            .collect()
186    }
187}
188
189impl ResourceCommandCause {
190    fn collection(self) -> Option<NodeId> {
191        match self {
192            Self::Planner { collection } => Some(collection),
193            Self::ScopeClosed { .. } => None,
194        }
195    }
196}
197
198fn input_causes_from_paths(paths: &[Vec<NodeId>]) -> Vec<NodeId> {
199    let mut causes = Vec::new();
200    for path in paths {
201        if let Some(input) = path.first()
202            && !causes.contains(input)
203        {
204            causes.push(*input);
205        }
206    }
207    causes
208}
209
210fn changed_nodes<C, O>(result: &TransactionResult<C, O>) -> Vec<NodeId> {
211    let mut nodes = result.changed_inputs.clone();
212    nodes.extend(result.changed_derived_nodes.iter().copied());
213    nodes.extend(result.changed_collection_nodes.iter().copied());
214    nodes
215}
216
217fn event_node(event: AuditEvent) -> Option<NodeId> {
218    match event {
219        AuditEvent::InputChanged(node)
220        | AuditEvent::DerivedChanged(node)
221        | AuditEvent::CollectionChanged(node)
222        | AuditEvent::NodeCreated(node) => Some(node),
223        AuditEvent::NodeAttached { node, .. } => Some(node),
224        AuditEvent::InputUnchanged(_)
225        | AuditEvent::ScopeCreated(_)
226        | AuditEvent::ScopeClosed(_) => None,
227    }
228}