Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ jobs:
command: test
args: --all-features --no-fail-fast
env:
RUSTFLAGS: "-Zinstrument-coverage"
RUSTFLAGS: "-C instrument-coverage"
LLVM_PROFILE_FILE: "hawkbitrs-%p-%m.profraw"
- name: Install grcov
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
run: cargo install grcov
- name: Run grcov
run: grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore "hawkbit/examples/*" --ignore "*target*" -o coverage.lcov
- name: Upload coverage to Codecov
Expand Down
19 changes: 9 additions & 10 deletions hawkbit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,18 @@ repository = "https://github.com/collabora/hawkbit-rs"
documentation = "https://docs.rs/hawkbit_mock/"

[dependencies]
reqwest = { version = "0.11", features = ["json", "stream"] }
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1.1", features = ["time", "fs"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
thiserror = "2.0"
url = "2.2"
strum = { version = "0.21", features = ["derive"] }
strum = { version = "0.27", features = ["derive"] }
cfg-if = "1.0"
digest = { version = "0.9", optional = true }
md-5 = { version = "0.9", optional = true }
sha-1 = { version = "0.9", optional = true }
sha2 = { version = "0.9", optional = true }
generic-array = {version = "0.14", optional = true }
digest = { version = "0.10", optional = true }
md-5 = { version = "0.10", optional = true }
sha-1 = { version = "0.10", optional = true }
sha2 = { version = "0.10", optional = true }
futures = "0.3"
bytes = "1.0"

Expand All @@ -32,12 +31,12 @@ hawkbit_mock = { path = "../hawkbit_mock/" }
structopt = "0.3"
anyhow = "1.0"
log = "0.4"
env_logger = "0.8"
env_logger = "0.11"
tempdir = "0.3"
assert_matches = "1.4"

[features]
hash-digest= ["digest", "generic-array"]
hash-digest= ["digest"]
hash-md5 = ["md-5", "hash-digest"]
hash-sha1 = ["sha-1", "hash-digest"]
hash-sha256 = ["sha2", "hash-digest"]
2 changes: 2 additions & 0 deletions hawkbit/src/ddi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod cancel_action;
mod client;
mod common;
mod config_data;
mod confirmation_base;
mod deployment_base;
mod feedback;
mod poll;
Expand All @@ -25,6 +26,7 @@ pub use cancel_action::CancelAction;
pub use client::{Client, Error};
pub use common::{Execution, Finished};
pub use config_data::{ConfigRequest, Mode};
pub use confirmation_base::{ConfirmationInfo, ConfirmationRequest, ConfirmationResponse};
#[cfg(feature = "hash-digest")]
pub use deployment_base::ChecksumType;
pub use deployment_base::{
Expand Down
1 change: 1 addition & 0 deletions hawkbit/src/ddi/cancel_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl CancelAction {
}
}

#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct CancelReply {
id: String,
Expand Down
6 changes: 5 additions & 1 deletion hawkbit/src/ddi/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ pub enum Execution {
Rejected,
/// This can be used by the target to inform that it continued to work on the action.
Resumed,
/// This can be used to inform the server that the target finished downloading the artifact.
Downloaded,
/// This can be used to inform the server that the target is downloading the artifact.
Download,
}

#[derive(Debug, Serialize)]
Expand Down Expand Up @@ -73,7 +77,7 @@ pub(crate) async fn send_feedback_internal<T: Serialize>(
let details = details.iter().map(|m| m.to_string()).collect();
let feedback = Feedback::new(id, execution, finished, progress, details);

let reply = client.post(&url.to_string()).json(&feedback).send().await?;
let reply = client.post(url.to_string()).json(&feedback).send().await?;
reply.error_for_status()?;

Ok(())
Expand Down
182 changes: 182 additions & 0 deletions hawkbit/src/ddi/confirmation_base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2025, Liebherr Digital Development Center GmbH.
// SPDX-License-Identifier: MIT OR Apache-2.0

use reqwest::{Client, Url};
use serde::{Deserialize, Serialize};

use crate::ddi::client::Error;
use crate::ddi::deployment_base::{Chunk, Deployment};

#[derive(Debug)]
#[allow(dead_code)]
/// A pending confirmation whose details have not been retrieved yet.
///
/// Call [`ConfirmationRequest::fetch()`] to retrieve the details from server.
pub struct ConfirmationRequest {
client: Client,
url: String,
details: Vec<String>,
}

impl ConfirmationRequest {
pub(crate) fn new(client: Client, url: String) -> Self {
Self {
client,
url,
details: vec![],
}
}

/// confirm the confirmation request. The server should then proceed with the update and make a deploymentBase available.
pub async fn confirm(self) -> Result<(), Error> {
let confirmation = Confirmation::new(ConfirmationResponse::Confirmed, 1);

// get feedback url
let mut url: Url = self.url.parse()?;
{
let mut paths = url
.path_segments_mut()
.map_err(|_| url::ParseError::SetHostOnCannotBeABaseUrl)?;
paths.push("feedback");
}
url.set_query(None);

let reply = self
.client
.post(url.to_string())
.json(&confirmation)
.send()
.await?;
reply.error_for_status_ref()?;
Ok(())
}

/// decline the confirmation request. This will not change the status on the server and the same confirmation request will be received on the next poll.
pub async fn decline(self) -> Result<(), Error> {
let confirmation = Confirmation::new(ConfirmationResponse::Denied, -1);

// get feedback url
let mut url: Url = self.url.parse()?;
{
let mut paths = url
.path_segments_mut()
.map_err(|_| url::ParseError::SetHostOnCannotBeABaseUrl)?;
paths.push("feedback");
}
url.set_query(None);

let reply = self
.client
.post(url.to_string())
.json(&confirmation)
.send()
.await?;
reply.error_for_status_ref()?;
Ok(())
}

/// Fetch the details of the update to be confirmed
pub async fn update_info(&self) -> Result<ConfirmationInfo, Error> {
let reply = self.client.get(&self.url).send().await?;
reply.error_for_status_ref()?;

let reply: Reply = reply.json().await?;
Ok(ConfirmationInfo {
reply,
client: self.client.clone(),
})
}

/// The metadata of all chunks of the update.
pub async fn metadata(&self) -> Result<Vec<(String, String)>, Error> {
let client = self.client.clone();

// get update information from the server
let update = self.update_info().await?.reply;

// get all chunks of the update
let chunks: Vec<Chunk> = update
.confirmation
.chunks
.iter()
.map(move |c| Chunk::new(c, client.clone()))
.collect();

// collect all metadata of each chunk
let metadata = chunks
.iter()
.flat_map(|c| c.metadata().collect::<Vec<(&str, &str)>>())
.map(|(k, v): (&str, &str)| (k.to_string(), v.to_string()))
.collect();

Ok(metadata)
}
}

/// The downloaded details of a confirmation request.
#[derive(Debug)]
pub struct ConfirmationInfo {
client: Client,
reply: Reply,
}

impl ConfirmationInfo {
/// Get all metadata of all chunks of the update.
pub fn metadata(&self) -> Vec<(String, String)> {
// get all chunks of the update
let chunks: Vec<Chunk> = self
.reply
.confirmation
.chunks
.iter()
.map(move |c| Chunk::new(c, self.client.clone()))
.collect();

// collect all metadata of each chunk
let metadata = chunks
.iter()
.flat_map(|c| c.metadata().collect::<Vec<(&str, &str)>>())
.map(|(k, v): (&str, &str)| (k.to_string(), v.to_string()))
.collect();

metadata
}

/// Get the action ID of the update to be confirmed.
pub fn action_id(&self) -> &str {
&self.reply.id
}
}

#[derive(Debug, Deserialize)]
pub(crate) struct Reply {
id: String,
confirmation: Deployment,
}

/// The response to a confirmation request.
#[derive(Debug, Deserialize, Serialize, Copy, Clone, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum ConfirmationResponse {
/// Proceed with the download
Confirmed,
/// Decline the confirmation and do not update
Denied,
}

#[derive(Debug, Serialize)]
pub struct Confirmation {
confirmation: ConfirmationResponse,
code: i32,
details: Vec<String>,
}

impl Confirmation {
pub fn new(confirmation: ConfirmationResponse, code: i32) -> Self {
Self {
confirmation,
code,
details: vec![],
}
}
}
Loading