use indexmap::IndexMap;
use lava_outcome_chain::{
ChangeSummary, ContentHash, OutcomeChain, OutcomePayload, OutcomeSink, ResourceAddress,
Signature, SigningProvider,
};
use crate::{Phase, ReconcileOutcome};
#[must_use]
pub fn build_payload(
address: ResourceAddress,
spec_source: &str,
outcome: &ReconcileOutcome,
phase: Phase,
) -> OutcomePayload {
let spec_hash = ContentHash::of(spec_source.as_bytes());
let terraform_json_hash = outcome
.terraform_json
.as_ref()
.map(|v| ContentHash::of(serde_json::to_string(v).unwrap_or_default().as_bytes()))
.unwrap_or_else(ContentHash::genesis);
let diagnostics: Vec<String> = outcome
.conditions
.iter()
.map(|c| {
format!(
"{kind}={status}{}",
c.message
.as_ref()
.map(|m| format!(" ({m})"))
.unwrap_or_default(),
kind = c.kind,
status = c.status,
)
})
.collect();
OutcomePayload {
resource: address,
spec_hash,
terraform_json_hash,
plan_id: None, phase: phase.as_str().to_string(),
change_summary: ChangeSummary::default(),
diagnostics,
}
}
pub fn record_phase<S, G>(
chain: &mut OutcomeChain<OutcomePayload, S, G>,
address: ResourceAddress,
spec_source: &str,
outcome: &ReconcileOutcome,
phase: Phase,
) -> Result<lava_outcome_chain::Receipt<OutcomePayload>, lava_outcome_chain::AppendError>
where
S: OutcomeSink<OutcomePayload>,
G: SigningProvider,
{
let payload = build_payload(address, spec_source, outcome, phase);
chain.append(payload)
}
pub const NO_SIGNATURE: Signature = Signature::None;
pub type SpecInputs<'a> = (&'a str, &'a IndexMap<String, String>);
#[cfg(test)]
mod tests {
use super::*;
use crate::{Condition, Phase};
use lava_outcome_chain::{verify_chain, InMemorySink, NoOpVerifier, NoSigning};
use serde_json::json;
fn sample_outcome() -> ReconcileOutcome {
ReconcileOutcome {
phase: Phase::Synthesized,
conditions: vec![Condition::ok("Synthesized", "RenderOk")],
terraform_json: Some(json!({ "resource": { "aws_vpc": { "main": {} } } })),
}
}
#[test]
fn build_payload_populates_spec_hash_from_source() {
let p1 = build_payload(
ResourceAddress::new("c", "n", "x"),
"source-v1",
&sample_outcome(),
Phase::Synthesized,
);
let p2 = build_payload(
ResourceAddress::new("c", "n", "x"),
"source-v2",
&sample_outcome(),
Phase::Synthesized,
);
assert_ne!(p1.spec_hash, p2.spec_hash);
}
#[test]
fn build_payload_hashes_terraform_json_when_present() {
let p = build_payload(
ResourceAddress::new("c", "n", "x"),
"src",
&sample_outcome(),
Phase::Synthesized,
);
assert!(!p.terraform_json_hash.is_genesis());
}
#[test]
fn build_payload_uses_genesis_when_terraform_json_absent() {
let mut o = sample_outcome();
o.terraform_json = None;
let p = build_payload(
ResourceAddress::new("c", "n", "x"),
"src",
&o,
Phase::Failed,
);
assert!(p.terraform_json_hash.is_genesis());
}
#[test]
fn record_phase_appends_and_chain_verifies() {
let mut chain = OutcomeChain::new(InMemorySink::<OutcomePayload>::default(), NoSigning);
for phase in [
Phase::Pending,
Phase::Synthesized,
Phase::Planned,
Phase::Applied,
] {
record_phase(
&mut chain,
ResourceAddress::new("rio", "infra", "prod"),
"src",
&sample_outcome(),
phase,
)
.unwrap();
}
let receipts = chain.read_all().unwrap();
assert_eq!(receipts.len(), 4);
verify_chain(&receipts, &NoOpVerifier).unwrap();
}
}