diff --git a/crates/protocol/src/error.rs b/crates/protocol/src/error.rs new file mode 100644 index 00000000..1b0a3da6 --- /dev/null +++ b/crates/protocol/src/error.rs @@ -0,0 +1,11 @@ +use thiserror::Error; +use tokio_util::codec::LinesCodecError; + +#[derive(Debug, Error)] +pub enum ProtocolError { + #[error("daemon protocol JSON error: {0}")] + Json(#[from] serde_json::Error), + + #[error("daemon protocol frame error: {0}")] + Frame(#[from] LinesCodecError), +} diff --git a/crates/protocol/src/event.rs b/crates/protocol/src/event.rs new file mode 100644 index 00000000..1edb55b0 --- /dev/null +++ b/crates/protocol/src/event.rs @@ -0,0 +1,27 @@ +use serde::Serialize; + +#[derive(Debug, Serialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum DaemonEvent<'message> { + JobStarted { + job_id: &'message str, + kind: &'message str, + scope: &'message str, + }, + Progress { + job_id: &'message str, + message: &'message str, + }, + Log { + job_id: &'message str, + message: &'message str, + }, + JobCompleted { + job_id: &'message str, + summary: &'message str, + }, + JobFailed { + job_id: &'message str, + error: &'message str, + }, +} diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index b261a4ac..6318ea74 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1,414 +1,20 @@ -use futures_util::SinkExt; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -use tokio::io::{AsyncRead, AsyncWrite}; -use tokio_util::codec::{Framed, LinesCodec, LinesCodecError}; - -pub const PROTOCOL_VERSION: u16 = 2; - -const MAX_PROTOCOL_LINE_BYTES: usize = 64 * 1024; -const RESPONSE_LINE_TYPE: &str = "response"; - -pub type DaemonTransport = Framed; - -#[derive(Debug, Error)] -pub enum ProtocolError { - #[error("daemon protocol JSON error: {0}")] - Json(#[from] serde_json::Error), - - #[error("daemon protocol frame error: {0}")] - Frame(#[from] LinesCodecError), -} - -#[derive(Debug, Deserialize, Serialize)] -pub struct DaemonRequest { - pub protocol_version: u16, - - #[serde(flatten)] - pub command: DaemonCommand, -} - -#[derive(Debug, Deserialize, Serialize)] -#[serde(tag = "command", rename_all = "snake_case")] -pub enum DaemonCommand { - Health, - RunJob { kind: String, scope: String }, - ManagedResourceUpdateCheck, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct DaemonResponse { - #[serde(rename = "type")] - line_type: String, - protocol_version: u16, - status: ResponseStatus, - message: String, - - #[serde(skip_serializing_if = "Option::is_none")] - job_id: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - update_check: Option, -} - -impl DaemonResponse { - pub fn ok(message: impl Into) -> Self { - Self::new(ResponseStatus::Ok, message, None, None) - } - - pub fn accepted(message: impl Into, job_id: impl Into) -> Self { - Self::new(ResponseStatus::Accepted, message, Some(job_id.into()), None) - } - - pub fn error(message: impl Into) -> Self { - Self::new(ResponseStatus::Error, message, None, None) - } - - pub fn ok_update_check( - message: impl Into, - update_check: ManagedResourceUpdateCheck, - ) -> Self { - Self::new(ResponseStatus::Ok, message, None, Some(update_check)) - } - - pub fn line_type(&self) -> &str { - &self.line_type - } - - pub fn protocol_version(&self) -> u16 { - self.protocol_version - } - - pub fn status(&self) -> ResponseStatus { - self.status - } - - pub fn message(&self) -> &str { - &self.message - } - - pub fn job_id(&self) -> Option<&str> { - self.job_id.as_deref() - } - - pub fn update_check(&self) -> Option<&ManagedResourceUpdateCheck> { - self.update_check.as_ref() - } - - fn new( - status: ResponseStatus, - message: impl Into, - job_id: Option, - update_check: Option, - ) -> Self { - Self { - line_type: RESPONSE_LINE_TYPE.to_string(), - protocol_version: PROTOCOL_VERSION, - status, - message: message.into(), - job_id, - update_check, - } - } -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ManagedResourceUpdateCheck { - pub managed_resources: Vec, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ManagedResourceUpdateCheckTrack { - pub status: ManagedResourceUpdateStatus, - pub resource: String, - pub track: String, - pub current_artifact_version: String, - pub current_artifact_path: String, - pub latest_artifact_version: Option, - pub current_revocation: Option, - pub latest_revocation: Option, - pub blocked_by: Option, - pub reason: Option, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ManagedResourceUpdateRevocation { - pub artifact_version: String, - pub reason: String, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -pub struct ManagedResourceUpdateBlocker { - pub minimum_pv_version: String, - pub current_pv_version: String, -} - -impl std::fmt::Display for ManagedResourceUpdateBlocker { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "requires PV {}, current PV {}", - self.minimum_pv_version, self.current_pv_version - ) - } -} - -#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ManagedResourceUpdateStatus { - Current, - UpdateAvailable, - Blocked, - Revoked, - Unavailable, -} - -impl From for ManagedResourceUpdateStatus { - fn from(status: resources::ManagedResourceUpdateStatus) -> Self { - match status { - resources::ManagedResourceUpdateStatus::Current => Self::Current, - resources::ManagedResourceUpdateStatus::UpdateAvailable => Self::UpdateAvailable, - resources::ManagedResourceUpdateStatus::Blocked => Self::Blocked, - resources::ManagedResourceUpdateStatus::Revoked => Self::Revoked, - resources::ManagedResourceUpdateStatus::Unavailable => Self::Unavailable, - } - } -} - -impl From for ManagedResourceUpdateCheckTrack { - fn from(track: resources::ManagedResourceUpdateCheckTrack) -> Self { - Self { - status: track.status().into(), - resource: track.resource_name().as_str().to_string(), - track: track.track().as_str().to_string(), - current_artifact_version: track.current_artifact_version().as_str().to_string(), - current_artifact_path: track.current_artifact_path().to_string(), - latest_artifact_version: track - .latest_artifact_version() - .map(|version| version.as_str().to_string()), - current_revocation: track.current_revocation().map(Into::into), - latest_revocation: track.latest_revocation().map(Into::into), - blocked_by: track.blocked_by().map(Into::into), - reason: track.reason().map(ToString::to_string), - } - } -} - -impl From<&resources::ManagedResourceUpdateRevocation> for ManagedResourceUpdateRevocation { - fn from(revocation: &resources::ManagedResourceUpdateRevocation) -> Self { - Self { - artifact_version: revocation.artifact_version().as_str().to_string(), - reason: revocation.reason().to_string(), - } - } -} - -impl From<&resources::ManagedResourceUpdateBlocker> for ManagedResourceUpdateBlocker { - fn from(blocker: &resources::ManagedResourceUpdateBlocker) -> Self { - Self { - minimum_pv_version: blocker.minimum_pv_version().to_string(), - current_pv_version: blocker.current_pv_version().to_string(), - } - } -} - -#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum ResponseStatus { - Ok, - Accepted, - Error, -} - -#[derive(Debug, Serialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum DaemonEvent<'message> { - JobStarted { - job_id: &'message str, - kind: &'message str, - scope: &'message str, - }, - Progress { - job_id: &'message str, - message: &'message str, - }, - Log { - job_id: &'message str, - message: &'message str, - }, - JobCompleted { - job_id: &'message str, - summary: &'message str, - }, - JobFailed { - job_id: &'message str, - error: &'message str, - }, -} - -pub fn transport(stream: Stream) -> DaemonTransport -where - Stream: AsyncRead + AsyncWrite, -{ - Framed::new( - stream, - LinesCodec::new_with_max_length(MAX_PROTOCOL_LINE_BYTES), - ) -} - -pub async fn write_line( - transport: &mut DaemonTransport, - line: &impl Serialize, -) -> Result<(), ProtocolError> -where - Stream: AsyncWrite + Unpin, -{ - let encoded = serde_json::to_string(line)?; - - transport.send(encoded).await?; - - Ok(()) -} - +mod error; +mod event; +mod request; +mod response; #[cfg(test)] -mod tests { - use futures_util::StreamExt; - use serde_json::json; - use tokio::io::duplex; - - use super::{ - DaemonCommand, DaemonRequest, DaemonResponse, ManagedResourceUpdateCheck, - ManagedResourceUpdateCheckTrack, ManagedResourceUpdateStatus, PROTOCOL_VERSION, - ResponseStatus, transport, write_line, - }; - - #[test] - fn managed_resource_update_check_command_bumps_protocol_version() -> anyhow::Result<()> { - let request = DaemonRequest { - protocol_version: PROTOCOL_VERSION, - command: DaemonCommand::ManagedResourceUpdateCheck, - }; - - assert_eq!(PROTOCOL_VERSION, 2); - assert_eq!( - serde_json::to_value(&request)?, - json!({ - "protocol_version": 2, - "command": "managed_resource_update_check", - }) - ); - - Ok(()) - } - - #[test] - fn response_envelope_round_trips_through_protocol_type() -> anyhow::Result<()> { - let response = DaemonResponse::accepted("job accepted", "job-1"); - let encoded = serde_json::to_value(&response)?; +mod tests; +mod transport; +mod update_check; + +pub use error::ProtocolError; +pub use event::DaemonEvent; +pub use request::{DaemonCommand, DaemonRequest}; +pub use response::{DaemonResponse, ResponseStatus}; +pub use transport::{DaemonTransport, transport, write_line}; +pub use update_check::{ + ManagedResourceUpdateBlocker, ManagedResourceUpdateCheck, ManagedResourceUpdateCheckTrack, + ManagedResourceUpdateRevocation, ManagedResourceUpdateStatus, +}; - assert_eq!( - encoded, - json!({ - "type": "response", - "protocol_version": PROTOCOL_VERSION, - "status": "accepted", - "message": "job accepted", - "job_id": "job-1", - }) - ); - - let decoded = serde_json::from_value::(encoded)?; - - assert_eq!(decoded.status(), ResponseStatus::Accepted); - assert_eq!(decoded.message(), "job accepted"); - assert_eq!(decoded.job_id(), Some("job-1")); - - Ok(()) - } - - #[test] - fn update_check_response_round_trips_with_managed_resources() -> anyhow::Result<()> { - let response = DaemonResponse::ok_update_check( - "Managed Resource update check completed", - ManagedResourceUpdateCheck { - managed_resources: vec![ManagedResourceUpdateCheckTrack { - status: ManagedResourceUpdateStatus::UpdateAvailable, - resource: "redis".to_string(), - track: "8.8".to_string(), - current_artifact_version: "8.8.0-pv1".to_string(), - current_artifact_path: "/Users/me/.pv/resources/redis/8.8/releases/8.8.0-pv1" - .to_string(), - latest_artifact_version: Some("8.8.1-pv1".to_string()), - current_revocation: None, - latest_revocation: None, - blocked_by: None, - reason: None, - }], - }, - ); - let encoded = serde_json::to_value(&response)?; - - assert_eq!( - encoded, - json!({ - "type": "response", - "protocol_version": PROTOCOL_VERSION, - "status": "ok", - "message": "Managed Resource update check completed", - "update_check": { - "managed_resources": [ - { - "status": "update_available", - "resource": "redis", - "track": "8.8", - "current_artifact_version": "8.8.0-pv1", - "current_artifact_path": "/Users/me/.pv/resources/redis/8.8/releases/8.8.0-pv1", - "latest_artifact_version": "8.8.1-pv1", - "current_revocation": null, - "latest_revocation": null, - "blocked_by": null, - "reason": null - } - ] - } - }) - ); - - let decoded = serde_json::from_value::(encoded)?; - - assert_eq!(decoded.status(), ResponseStatus::Ok); - assert_eq!( - decoded - .update_check() - .map(|check| check.managed_resources.len()), - Some(1) - ); - - Ok(()) - } - - #[tokio::test] - async fn transport_frames_generic_async_streams() -> anyhow::Result<()> { - let (client, server) = duplex(1024); - let mut writer = transport(client); - let mut reader = transport(server); - - write_line(&mut writer, &DaemonResponse::ok("daemon healthy")).await?; - - let Some(line) = reader.next().await else { - anyhow::bail!("reader closed before receiving a protocol line"); - }; - - assert_eq!( - serde_json::from_str::(&line?)?, - json!({ - "type": "response", - "protocol_version": PROTOCOL_VERSION, - "status": "ok", - "message": "daemon healthy", - }) - ); - - Ok(()) - } -} +pub const PROTOCOL_VERSION: u16 = 2; diff --git a/crates/protocol/src/request.rs b/crates/protocol/src/request.rs new file mode 100644 index 00000000..545fd3a7 --- /dev/null +++ b/crates/protocol/src/request.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct DaemonRequest { + pub protocol_version: u16, + + #[serde(flatten)] + pub command: DaemonCommand, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "command", rename_all = "snake_case")] +pub enum DaemonCommand { + Health, + RunJob { kind: String, scope: String }, + ManagedResourceUpdateCheck, +} diff --git a/crates/protocol/src/response.rs b/crates/protocol/src/response.rs new file mode 100644 index 00000000..44cc400d --- /dev/null +++ b/crates/protocol/src/response.rs @@ -0,0 +1,90 @@ +use serde::{Deserialize, Serialize}; + +use crate::PROTOCOL_VERSION; +use crate::update_check::ManagedResourceUpdateCheck; + +const RESPONSE_LINE_TYPE: &str = "response"; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct DaemonResponse { + #[serde(rename = "type")] + line_type: String, + protocol_version: u16, + status: ResponseStatus, + message: String, + + #[serde(skip_serializing_if = "Option::is_none")] + job_id: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + update_check: Option, +} + +impl DaemonResponse { + pub fn ok(message: impl Into) -> Self { + Self::new(ResponseStatus::Ok, message, None, None) + } + + pub fn accepted(message: impl Into, job_id: impl Into) -> Self { + Self::new(ResponseStatus::Accepted, message, Some(job_id.into()), None) + } + + pub fn error(message: impl Into) -> Self { + Self::new(ResponseStatus::Error, message, None, None) + } + + pub fn ok_update_check( + message: impl Into, + update_check: ManagedResourceUpdateCheck, + ) -> Self { + Self::new(ResponseStatus::Ok, message, None, Some(update_check)) + } + + pub fn line_type(&self) -> &str { + &self.line_type + } + + pub fn protocol_version(&self) -> u16 { + self.protocol_version + } + + pub fn status(&self) -> ResponseStatus { + self.status + } + + pub fn message(&self) -> &str { + &self.message + } + + pub fn job_id(&self) -> Option<&str> { + self.job_id.as_deref() + } + + pub fn update_check(&self) -> Option<&ManagedResourceUpdateCheck> { + self.update_check.as_ref() + } + + fn new( + status: ResponseStatus, + message: impl Into, + job_id: Option, + update_check: Option, + ) -> Self { + Self { + line_type: RESPONSE_LINE_TYPE.to_string(), + protocol_version: PROTOCOL_VERSION, + status, + message: message.into(), + job_id, + update_check, + } + } +} + +#[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ResponseStatus { + Ok, + Accepted, + Error, +} diff --git a/crates/protocol/src/tests.rs b/crates/protocol/src/tests.rs new file mode 100644 index 00000000..388ea42f --- /dev/null +++ b/crates/protocol/src/tests.rs @@ -0,0 +1,139 @@ +use futures_util::StreamExt; +use serde_json::json; +use tokio::io::duplex; + +use crate::{ + DaemonCommand, DaemonRequest, DaemonResponse, ManagedResourceUpdateCheck, + ManagedResourceUpdateCheckTrack, ManagedResourceUpdateStatus, PROTOCOL_VERSION, ResponseStatus, + transport, write_line, +}; + +#[test] +fn managed_resource_update_check_command_bumps_protocol_version() -> anyhow::Result<()> { + let request = DaemonRequest { + protocol_version: PROTOCOL_VERSION, + command: DaemonCommand::ManagedResourceUpdateCheck, + }; + + assert_eq!(PROTOCOL_VERSION, 2); + assert_eq!( + serde_json::to_value(&request)?, + json!({ + "protocol_version": 2, + "command": "managed_resource_update_check", + }) + ); + + Ok(()) +} + +#[test] +fn response_envelope_round_trips_through_protocol_type() -> anyhow::Result<()> { + let response = DaemonResponse::accepted("job accepted", "job-1"); + let encoded = serde_json::to_value(&response)?; + + assert_eq!( + encoded, + json!({ + "type": "response", + "protocol_version": PROTOCOL_VERSION, + "status": "accepted", + "message": "job accepted", + "job_id": "job-1", + }) + ); + + let decoded = serde_json::from_value::(encoded)?; + + assert_eq!(decoded.status(), ResponseStatus::Accepted); + assert_eq!(decoded.message(), "job accepted"); + assert_eq!(decoded.job_id(), Some("job-1")); + + Ok(()) +} + +#[test] +fn update_check_response_round_trips_with_managed_resources() -> anyhow::Result<()> { + let response = DaemonResponse::ok_update_check( + "Managed Resource update check completed", + ManagedResourceUpdateCheck { + managed_resources: vec![ManagedResourceUpdateCheckTrack { + status: ManagedResourceUpdateStatus::UpdateAvailable, + resource: "redis".to_string(), + track: "8.8".to_string(), + current_artifact_version: "8.8.0-pv1".to_string(), + current_artifact_path: "/Users/me/.pv/resources/redis/8.8/releases/8.8.0-pv1" + .to_string(), + latest_artifact_version: Some("8.8.1-pv1".to_string()), + current_revocation: None, + latest_revocation: None, + blocked_by: None, + reason: None, + }], + }, + ); + let encoded = serde_json::to_value(&response)?; + + assert_eq!( + encoded, + json!({ + "type": "response", + "protocol_version": PROTOCOL_VERSION, + "status": "ok", + "message": "Managed Resource update check completed", + "update_check": { + "managed_resources": [ + { + "status": "update_available", + "resource": "redis", + "track": "8.8", + "current_artifact_version": "8.8.0-pv1", + "current_artifact_path": "/Users/me/.pv/resources/redis/8.8/releases/8.8.0-pv1", + "latest_artifact_version": "8.8.1-pv1", + "current_revocation": null, + "latest_revocation": null, + "blocked_by": null, + "reason": null + } + ] + } + }) + ); + + let decoded = serde_json::from_value::(encoded)?; + + assert_eq!(decoded.status(), ResponseStatus::Ok); + assert_eq!( + decoded + .update_check() + .map(|check| check.managed_resources.len()), + Some(1) + ); + + Ok(()) +} + +#[tokio::test] +async fn transport_frames_generic_async_streams() -> anyhow::Result<()> { + let (client, server) = duplex(1024); + let mut writer = transport(client); + let mut reader = transport(server); + + write_line(&mut writer, &DaemonResponse::ok("daemon healthy")).await?; + + let Some(line) = reader.next().await else { + anyhow::bail!("reader closed before receiving a protocol line"); + }; + + assert_eq!( + serde_json::from_str::(&line?)?, + json!({ + "type": "response", + "protocol_version": PROTOCOL_VERSION, + "status": "ok", + "message": "daemon healthy", + }) + ); + + Ok(()) +} diff --git a/crates/protocol/src/transport.rs b/crates/protocol/src/transport.rs new file mode 100644 index 00000000..2ab3e468 --- /dev/null +++ b/crates/protocol/src/transport.rs @@ -0,0 +1,34 @@ +use futures_util::SinkExt; +use serde::Serialize; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio_util::codec::{Framed, LinesCodec}; + +use crate::error::ProtocolError; + +const MAX_PROTOCOL_LINE_BYTES: usize = 64 * 1024; + +pub type DaemonTransport = Framed; + +pub fn transport(stream: Stream) -> DaemonTransport +where + Stream: AsyncRead + AsyncWrite, +{ + Framed::new( + stream, + LinesCodec::new_with_max_length(MAX_PROTOCOL_LINE_BYTES), + ) +} + +pub async fn write_line( + transport: &mut DaemonTransport, + line: &impl Serialize, +) -> Result<(), ProtocolError> +where + Stream: AsyncWrite + Unpin, +{ + let encoded = serde_json::to_string(line)?; + + transport.send(encoded).await?; + + Ok(()) +} diff --git a/crates/protocol/src/update_check.rs b/crates/protocol/src/update_check.rs new file mode 100644 index 00000000..c15e1343 --- /dev/null +++ b/crates/protocol/src/update_check.rs @@ -0,0 +1,101 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ManagedResourceUpdateCheck { + pub managed_resources: Vec, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ManagedResourceUpdateCheckTrack { + pub status: ManagedResourceUpdateStatus, + pub resource: String, + pub track: String, + pub current_artifact_version: String, + pub current_artifact_path: String, + pub latest_artifact_version: Option, + pub current_revocation: Option, + pub latest_revocation: Option, + pub blocked_by: Option, + pub reason: Option, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ManagedResourceUpdateRevocation { + pub artifact_version: String, + pub reason: String, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ManagedResourceUpdateBlocker { + pub minimum_pv_version: String, + pub current_pv_version: String, +} + +impl std::fmt::Display for ManagedResourceUpdateBlocker { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + formatter, + "requires PV {}, current PV {}", + self.minimum_pv_version, self.current_pv_version + ) + } +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum ManagedResourceUpdateStatus { + Current, + UpdateAvailable, + Blocked, + Revoked, + Unavailable, +} + +impl From for ManagedResourceUpdateStatus { + fn from(status: resources::ManagedResourceUpdateStatus) -> Self { + match status { + resources::ManagedResourceUpdateStatus::Current => Self::Current, + resources::ManagedResourceUpdateStatus::UpdateAvailable => Self::UpdateAvailable, + resources::ManagedResourceUpdateStatus::Blocked => Self::Blocked, + resources::ManagedResourceUpdateStatus::Revoked => Self::Revoked, + resources::ManagedResourceUpdateStatus::Unavailable => Self::Unavailable, + } + } +} + +impl From for ManagedResourceUpdateCheckTrack { + fn from(track: resources::ManagedResourceUpdateCheckTrack) -> Self { + Self { + status: track.status().into(), + resource: track.resource_name().as_str().to_string(), + track: track.track().as_str().to_string(), + current_artifact_version: track.current_artifact_version().as_str().to_string(), + current_artifact_path: track.current_artifact_path().to_string(), + latest_artifact_version: track + .latest_artifact_version() + .map(|version| version.as_str().to_string()), + current_revocation: track.current_revocation().map(Into::into), + latest_revocation: track.latest_revocation().map(Into::into), + blocked_by: track.blocked_by().map(Into::into), + reason: track.reason().map(ToString::to_string), + } + } +} + +impl From<&resources::ManagedResourceUpdateRevocation> for ManagedResourceUpdateRevocation { + fn from(revocation: &resources::ManagedResourceUpdateRevocation) -> Self { + Self { + artifact_version: revocation.artifact_version().as_str().to_string(), + reason: revocation.reason().to_string(), + } + } +} + +impl From<&resources::ManagedResourceUpdateBlocker> for ManagedResourceUpdateBlocker { + fn from(blocker: &resources::ManagedResourceUpdateBlocker) -> Self { + Self { + minimum_pv_version: blocker.minimum_pv_version().to_string(), + current_pv_version: blocker.current_pv_version().to_string(), + } + } +}