use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Display;
use std::path::PathBuf;
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq)]
pub enum UpdatePhase {
#[default]
#[serde(rename = "no-update")]
NoUpdate,
#[serde(rename = "prepare")]
Prepare,
#[serde(rename = "replace")]
Replace,
#[serde(rename = "cleanup")]
Cleanup,
}
impl Display for UpdatePhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UpdatePhase::NoUpdate => write!(f, "no-update"),
UpdatePhase::Prepare => write!(f, "prepare"),
UpdatePhase::Replace => write!(f, "replace"),
UpdatePhase::Cleanup => write!(f, "cleanup"),
}
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
pub struct UpdateState {
#[serde(rename = "app", default, skip_serializing_if = "Option::is_none")]
pub target_application: Option<PathBuf>,
#[serde(rename = "update", default, skip_serializing_if = "Option::is_none")]
pub temporary_application: Option<PathBuf>,
#[serde(rename = "trace", default, skip_serializing_if = "Option::is_none")]
pub trace_context: Option<HashMap<String, String>>,
pub phase: UpdatePhase,
}
impl UpdateState {
pub fn for_phase(&self, phase: UpdatePhase) -> Self {
UpdateState {
target_application: self.target_application.clone(),
temporary_application: self.temporary_application.clone(),
trace_context: self.trace_context.clone(),
phase,
}
}
#[cfg(feature = "opentelemetry")]
pub(crate) fn capture_trace_context(&mut self) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let context = tracing::Span::current().context();
let mut carrier = HashMap::new();
opentelemetry::global::get_text_map_propagator(|propagator| {
propagator.inject_context(&context, &mut carrier);
});
if !carrier.is_empty() {
self.trace_context = Some(carrier);
}
}
#[cfg(not(feature = "opentelemetry"))]
pub(crate) fn capture_trace_context(&mut self) {}
#[cfg(feature = "tracing")]
pub(crate) fn adopt_trace_context(&self, span: &tracing::Span) {
#[cfg(feature = "opentelemetry")]
{
use tracing_opentelemetry::OpenTelemetrySpanExt;
if let Some(carrier) = &self.trace_context {
let parent = opentelemetry::global::get_text_map_propagator(|propagator| {
propagator.extract(carrier)
});
let _ = span.set_parent(parent);
}
}
#[cfg(not(feature = "opentelemetry"))]
let _ = span;
}
}
impl Display for UpdateState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.phase)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize() {
assert_eq!(
serde_json::to_string(&UpdateState {
target_application: Some(PathBuf::from("/bin/app")),
temporary_application: Some(PathBuf::from("/tmp/app-update")),
trace_context: None,
phase: UpdatePhase::Replace
})
.unwrap(),
r#"{"app":"/bin/app","update":"/tmp/app-update","phase":"replace"}"#
);
assert_eq!(
serde_json::to_string(&UpdateState {
target_application: None,
temporary_application: Some(PathBuf::from("/tmp/app-update")),
trace_context: None,
phase: UpdatePhase::Cleanup
})
.unwrap(),
r#"{"update":"/tmp/app-update","phase":"cleanup"}"#
);
}
#[test]
fn test_deserialize() {
let update = UpdateState {
target_application: None,
temporary_application: Some(PathBuf::from("/tmp/app-update")),
trace_context: None,
phase: UpdatePhase::Cleanup,
};
let deserialized: UpdateState =
serde_json::from_str(r#"{"update":"/tmp/app-update","phase":"cleanup"}"#).unwrap();
assert_eq!(deserialized, update);
}
#[test]
fn test_trace_context_round_trips_through_json() {
let mut carrier = HashMap::new();
carrier.insert(
"traceparent".to_string(),
"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01".to_string(),
);
let state = UpdateState {
target_application: Some(PathBuf::from("/bin/app")),
temporary_application: Some(PathBuf::from("/tmp/app-update")),
trace_context: Some(carrier),
phase: UpdatePhase::Replace,
};
let json = serde_json::to_string(&state).unwrap();
assert!(
json.contains("\"trace\":{"),
"the carrier should serialize under the `trace` key: {json}"
);
let restored: UpdateState = serde_json::from_str(&json).unwrap();
assert_eq!(
restored, state,
"the trace context should survive a serialize/deserialize round-trip"
);
}
#[test]
fn test_to_string() {
assert_eq!(UpdatePhase::Prepare.to_string(), "prepare");
assert_eq!(UpdatePhase::Replace.to_string(), "replace");
assert_eq!(UpdatePhase::Cleanup.to_string(), "cleanup");
assert_eq!(UpdatePhase::NoUpdate.to_string(), "no-update");
}
#[test]
fn test_for_phase() {
let mut carrier = HashMap::new();
carrier.insert("traceparent".to_string(), "abc".to_string());
let update = UpdateState {
target_application: Some(PathBuf::from("/bin/app")),
temporary_application: Some(PathBuf::from("/tmp/app-update")),
trace_context: Some(carrier),
phase: UpdatePhase::Replace,
};
let new_update = update.for_phase(UpdatePhase::Cleanup);
assert_eq!(new_update.target_application, update.target_application);
assert_eq!(
new_update.temporary_application,
update.temporary_application
);
assert_eq!(
new_update.trace_context, update.trace_context,
"the trace context should be carried forward to the next phase"
);
assert_eq!(
update.phase,
UpdatePhase::Replace,
"the old update entry should not be modified"
);
assert_eq!(
new_update.phase,
UpdatePhase::Cleanup,
"the new update entry should have the correct phase"
);
}
}
#[cfg(all(test, feature = "opentelemetry"))]
mod opentelemetry_tests {
use super::*;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_sdk::propagation::TraceContextPropagator;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::prelude::*;
fn trace_id_of(carrier: &HashMap<String, String>) -> String {
carrier
.get("traceparent")
.expect("the W3C propagator should emit a traceparent header")
.split('-')
.nth(1)
.expect("a traceparent has a trace-id segment")
.to_string()
}
fn subscriber() -> impl tracing::Subscriber {
let tracer = SdkTracerProvider::builder()
.build()
.tracer("update-rs-test");
tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer))
}
#[test]
fn trace_context_continues_across_a_relaunch() {
opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new());
let captured = tracing::subscriber::with_default(subscriber(), || {
let span = tracing::info_span!("phase-n");
let _enter = span.enter();
let mut state = UpdateState {
phase: UpdatePhase::Replace,
..Default::default()
};
state.capture_trace_context();
state
});
let carrier = captured
.trace_context
.clone()
.expect("an active span should produce a trace context");
let expected_trace_id = trace_id_of(&carrier);
let json = serde_json::to_string(&captured).unwrap();
let resumed: UpdateState = serde_json::from_str(&json).unwrap();
let continued_trace_id = tracing::subscriber::with_default(subscriber(), || {
let span = tracing::info_span!("phase-n-plus-1");
resumed.adopt_trace_context(&span);
span.in_scope(|| {
let mut next = UpdateState {
phase: UpdatePhase::Cleanup,
..Default::default()
};
next.capture_trace_context();
trace_id_of(
&next
.trace_context
.expect("the adopted span should produce a trace context"),
)
})
});
assert_eq!(
continued_trace_id, expected_trace_id,
"the resumed phase should continue the trace captured before the relaunch"
);
}
}