From 88ef4b2fd0076811687a0b23ec21edad1ca2e0af Mon Sep 17 00:00:00 2001 From: Rina Fujino <18257209+rina23q@users.noreply.github.com> Date: Wed, 18 May 2022 01:22:31 +0200 Subject: Add user, group, mode support Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com> --- plugins/c8y_configuration_plugin/src/config.rs | 145 ++++++++++++++----- plugins/c8y_configuration_plugin/src/download.rs | 170 +++++++++++------------ plugins/c8y_configuration_plugin/src/error.rs | 16 +-- plugins/c8y_configuration_plugin/src/main.rs | 11 +- plugins/c8y_configuration_plugin/src/upload.rs | 13 +- 5 files changed, 216 insertions(+), 139 deletions(-) (limited to 'plugins') diff --git a/plugins/c8y_configuration_plugin/src/config.rs b/plugins/c8y_configuration_plugin/src/config.rs index 839eab6d..0c42a804 100644 --- a/plugins/c8y_configuration_plugin/src/config.rs +++ b/plugins/c8y_configuration_plugin/src/config.rs @@ -7,6 +7,7 @@ use std::collections::HashSet; use std::fs; use std::hash::{Hash, Hasher}; use std::path::Path; +use tedge_utils::file::FilePermissions; use tracing::{info, warn}; #[derive(Deserialize, Debug, Default)] @@ -21,6 +22,9 @@ pub struct RawFileEntry { pub path: String, #[serde(rename = "type")] config_type: Option, + user: Option, + group: Option, + mode: Option, } #[derive(Debug, Eq, PartialEq, Default, Clone)] @@ -32,6 +36,7 @@ pub struct PluginConfig { pub struct FileEntry { pub path: String, config_type: String, + pub file_permissions: FilePermissions, } impl Hash for FileEntry { @@ -53,8 +58,18 @@ impl Borrow for FileEntry { } impl FileEntry { - pub fn new(path: String, config_type: String) -> Self { - Self { path, config_type } + pub fn new( + path: String, + config_type: String, + user: Option, + group: Option, + mode: Option, + ) -> Self { + Self { + path, + config_type, + file_permissions: FilePermissions { user, group, mode }, + } } } @@ -65,16 +80,13 @@ impl RawPluginConfig { match fs::read_to_string(config_file_path) { Ok(contents) => match toml::from_str(contents.as_str()) { Ok(config) => config, - _ => { - warn!("The config file {} is malformed.", path_str); + Err(err) => { + warn!("The config file {path_str} is malformed. {err}"); Self::default() } }, - Err(_) => { - warn!( - "The config file {} does not exist or is not readable.", - path_str - ); + Err(err) => { + warn!("The config file {path_str} does not exist or is not readable. {err}"); Self::default() } } @@ -92,6 +104,9 @@ impl PluginConfig { let c8y_configuration_plugin = FileEntry::new( config_file_path.display().to_string(), DEFAULT_PLUGIN_CONFIG_TYPE.into(), + None, + None, + None, ); Self { files: HashSet::from([c8y_configuration_plugin]), @@ -104,7 +119,22 @@ impl PluginConfig { let config_type = raw_entry .config_type .unwrap_or_else(|| raw_entry.path.clone()); - let entry = FileEntry::new(raw_entry.path, config_type.clone()); + + if config_type.contains(&['+', '#']) { + warn!( + "The config type '{}' contains the forbidden characters, '+' or '#'.", + config_type + ); + return original_plugin_config; + } + + let entry = FileEntry::new( + raw_entry.path, + config_type.clone(), + raw_entry.user, + raw_entry.group, + raw_entry.mode, + ); if !self.files.insert(entry) { warn!("The config file has the duplicated type '{}'.", config_type); return original_plugin_config; @@ -125,7 +155,10 @@ impl PluginConfig { .collect::>() } - pub fn get_path_from_type(&self, config_type: &str) -> Result { + pub fn get_file_entry_from_type( + &self, + config_type: &str, + ) -> Result { let file_entry = self .files .get(&config_type.to_string()) @@ -133,7 +166,7 @@ impl PluginConfig { config_type: config_type.to_owned(), })? .to_owned(); - Ok(file_entry.path) + Ok(file_entry) } // 119,typeA,typeB,... @@ -181,23 +214,23 @@ mod tests { assert_eq!( config.files, vec![ - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), None ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), Some("\"double quotation\"".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/mosquitto/mosquitto.conf".to_string(), Some("'single quotation'".to_string()) ) @@ -223,23 +256,23 @@ mod tests { assert_eq!( config.files, vec![ - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), None ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), Some("\"double quotation\"".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/mosquitto/mosquitto.conf".to_string(), Some("'single quotation'".to_string()) ) @@ -257,8 +290,8 @@ mod tests { "#, PluginConfig { files: HashSet::from([ - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), - FileEntry::new("/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string()), ]) }; "standard case" )] @@ -273,8 +306,8 @@ mod tests { "#, PluginConfig { files: HashSet::from([ - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge2".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge2".to_string()), ]) }; "file path duplication" )] @@ -288,14 +321,36 @@ mod tests { type = "tedge" "#, PluginConfig { - files: HashSet::new() - }; "file type duplication" + files: HashSet::new() + }; "file type duplication" )] #[test_case( - r#"files = []"#, + r#" + [[files]] + path = "/etc/tedge/tedge.toml" + type = "tedge#" + "#, PluginConfig { files: HashSet::new() } + ;"type contains sharp" + )] + #[test_case( + r#" + [[files]] + path = "/etc/tedge/tedge.toml" + type = "tedge+" + "#, + PluginConfig { + files: HashSet::new() + } + ;"type contains plus" + )] + #[test_case( + r#"files = []"#, + PluginConfig { + files: HashSet::new() + } ;"empty case" )] #[test_case( @@ -338,8 +393,24 @@ mod tests { } impl RawFileEntry { - pub fn new(path: String, config_type: Option) -> Self { - Self { path, config_type } + pub fn new_with_path_and_type(path: String, config_type: Option) -> Self { + Self { + path, + config_type, + user: None, + group: None, + mode: None, + } + } + } + + impl FileEntry { + pub fn new_with_path_and_type(path: String, config_type: String) -> Self { + Self { + path, + config_type, + file_permissions: FilePermissions::default(), + } } } @@ -347,7 +418,7 @@ mod tests { impl PluginConfig { fn add_file_entry(&self, path: String, config_type: String) -> Self { let mut files = self.files.clone(); - let _ = files.insert(FileEntry::new(path, config_type)); + let _ = files.insert(FileEntry::new(path, config_type, None, None, None)); Self { files } } } @@ -357,7 +428,7 @@ mod tests { let temp_dir = TempDir::new()?; let config_root = temp_dir.path().to_path_buf(); let config_file_path = config_root.join(PLUGIN_CONFIG_FILE); - let mut file = std::fs::File::create(config_file_path.as_path())?; + let mut file = fs::File::create(config_file_path.as_path())?; file.write_all(content.as_bytes())?; Ok((temp_dir, config_root)) } @@ -365,7 +436,7 @@ mod tests { #[test] fn get_smartrest_single_type() { let plugin_config = PluginConfig { - files: HashSet::from([FileEntry::new( + files: HashSet::from([FileEntry::new_with_path_and_type( "/path/to/file".to_string(), "typeA".to_string(), )]), @@ -378,9 +449,9 @@ mod tests { fn get_smartrest_multiple_types() { let plugin_config = PluginConfig { files: HashSet::from([ - FileEntry::new("path1".to_string(), "typeA".to_string()), - FileEntry::new("path2".to_string(), "typeB".to_string()), - FileEntry::new("path3".to_string(), "typeC".to_string()), + FileEntry::new_with_path_and_type("path1".to_string(), "typeA".to_string()), + FileEntry::new_with_path_and_type("path2".to_string(), "typeB".to_string()), + FileEntry::new_with_path_and_type("path3".to_string(), "typeC".to_string()), ]), }; let output = plugin_config.to_smartrest_payload(); diff --git a/plugins/c8y_configuration_plugin/src/download.rs b/plugins/c8y_configuration_plugin/src/download.rs index e58ef1f6..9a345385 100644 --- a/plugins/c8y_configuration_plugin/src/download.rs +++ b/plugins/c8y_configuration_plugin/src/download.rs @@ -10,11 +10,11 @@ use c8y_smartrest::smartrest_serializer::{ }; use download::{Auth, DownloadInfo, Downloader}; use mqtt_channel::{Connection, Message, SinkExt, Topic}; -use serde::{Deserialize, Serialize}; use serde_json::json; use std::fs; use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use tedge_utils::file::{get_filename, get_metadata, FilePermissions}; use tracing::{info, warn}; pub async fn handle_config_download_request( @@ -27,21 +27,31 @@ pub async fn handle_config_download_request( let executing_message = DownloadConfigFileStatusMessage::executing()?; let () = mqtt_client.published.send(executing_message).await?; - let changed_config_type = smartrest_request.config_type.clone(); - - match download_config_file(plugin_config, smartrest_request, tmp_dir, http_client).await { + let target_config_type = smartrest_request.config_type.clone(); + let target_file_entry = plugin_config.get_file_entry_from_type(&target_config_type)?; + + match download_config_file( + smartrest_request.url.as_str(), + PathBuf::from(&target_file_entry.path), + tmp_dir, + target_file_entry.file_permissions, + http_client, + ) + .await + { Ok(_) => { - info!("The configuration download for '{changed_config_type}' is successful."); + info!("The configuration download for '{target_config_type}' is successful."); let successful_message = DownloadConfigFileStatusMessage::successful(None)?; let () = mqtt_client.published.send(successful_message).await?; - let notification_message = get_file_change_notification_message(changed_config_type); + let notification_message = + get_file_change_notification_message(&target_file_entry.path, &target_config_type); let () = mqtt_client.published.send(notification_message).await?; Ok(()) } Err(err) => { - error!("The configuration download for '{changed_config_type}' is failed.",); + error!("The configuration download for '{target_config_type}' is failed.",); let failed_message = DownloadConfigFileStatusMessage::failed(err.to_string())?; let () = mqtt_client.published.send(failed_message).await?; @@ -51,14 +61,15 @@ pub async fn handle_config_download_request( } async fn download_config_file( - plugin_config: &PluginConfig, - smartrest_request: SmartRestConfigDownloadRequest, + download_url: &str, + file_path: PathBuf, tmp_dir: PathBuf, + file_permissions: FilePermissions, http_client: &mut impl C8YHttpProxy, ) -> Result<(), anyhow::Error> { // Convert smartrest request to config download request struct let mut config_download_request = - ConfigDownloadRequest::try_new(smartrest_request, plugin_config, tmp_dir)?; + ConfigDownloadRequest::try_new(download_url, file_path, tmp_dir, file_permissions)?; // Confirm that the file has write access before any http request attempt let () = config_download_request.has_write_access()?; @@ -81,55 +92,59 @@ async fn download_config_file( Ok(()) } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct ConfigDownloadRequest { pub download_info: DownloadInfo, - pub destination_path: PathBuf, + pub file_path: PathBuf, pub tmp_dir: PathBuf, + pub file_permissions: FilePermissions, pub file_name: String, } impl ConfigDownloadRequest { fn try_new( - request: SmartRestConfigDownloadRequest, - plugin_config: &PluginConfig, + download_url: &str, + file_path: PathBuf, tmp_dir: PathBuf, + file_permissions: FilePermissions, ) -> Result { - let destination_path_string = plugin_config.get_path_from_type(&request.config_type)?; - let destination_path = PathBuf::from(destination_path_string); - let file_name = Self::get_filename(destination_path.clone())?; + let file_name = get_filename(file_path.clone()).ok_or_else(|| { + ConfigManagementError::FileNameNotFound { + path: file_path.clone(), + } + })?; Ok(Self { download_info: DownloadInfo { - url: request.url, + url: download_url.into(), auth: None, }, - destination_path, + file_path, tmp_dir, + file_permissions, file_name, }) } - fn get_filename(path: PathBuf) -> Result { - let filename = path - .file_name() - .ok_or_else(|| ConfigManagementError::FileNameNotFound { path: path.clone() })? - .to_str() - .ok_or_else(|| ConfigManagementError::InvalidFileName { path: path.clone() })? - .to_string(); - Ok(filename) - } - fn has_write_access(&self) -> Result<(), ConfigManagementError> { // The file does not exist before downloading a file - if !&self.destination_path.is_file() { - return Ok(()); - } - // Need a permission check when the file exists already - let metadata = Self::get_metadata(&self.destination_path)?; + let metadata = + if self.file_path.is_file() { + get_metadata(&self.file_path)? + } else { + // If the file does not exist before downloading file, check the directory perms + let parent_dir = &self.file_path.parent().ok_or_else(|| { + ConfigManagementError::NoWriteAccess { + path: self.file_path.clone(), + } + })?; + get_metadata(parent_dir)? + }; + + // Write permission check if metadata.permissions().readonly() { - Err(error::ConfigManagementError::ReadOnlyFile { - path: self.destination_path.clone(), + Err(ConfigManagementError::NoWriteAccess { + path: self.file_path.clone(), }) } else { Ok(()) @@ -140,19 +155,13 @@ impl ConfigDownloadRequest { Downloader::new(&self.file_name, &None, &self.tmp_dir) } - fn get_metadata(path: &Path) -> Result { - fs::metadata(&path).map_err(|_| ConfigManagementError::FileNotAccessible { - path: path.to_path_buf(), - }) - } - fn move_file(&self) -> Result<(), ConfigManagementError> { let src = &self.tmp_dir.join(&self.file_name); - let dest = &self.destination_path; + let dest = &self.file_path; - let original_permission_mode = match self.destination_path.is_file() { + let original_permission_mode = match self.file_path.is_file() { true => { - let metadata = Self::get_metadata(&self.destination_path)?; + let metadata = get_metadata(&self.file_path)?; let mode = metadata.permissions().mode(); Some(mode) } @@ -164,19 +173,22 @@ impl ConfigDownloadRequest { dest: dest.to_path_buf(), })?; - // Change the file permission back to the original one - if let Some(mode) = original_permission_mode { - let mut permissions = Self::get_metadata(&self.destination_path)?.permissions(); - let _ = permissions.set_mode(mode); - let _ = std::fs::set_permissions(&self.destination_path, permissions); - } + let file_permissions = if let Some(mode) = original_permission_mode { + // Use the same file permission as the original one + FilePermissions::new(None, None, Some(mode)) + } else { + // Set the user, group, and mode as given for a new file + self.file_permissions.clone() + }; + + let () = file_permissions.change_permissions(&self.file_path.display().to_string())?; Ok(()) } } -pub fn get_file_change_notification_message(config_type: String) -> Message { - let notification = json!({ "changedFile": config_type }).to_string(); +pub fn get_file_change_notification_message(file_path: &str, config_type: &str) -> Message { + let notification = json!({ "path": file_path }).to_string(); let topic = Topic::new(format!("{CONFIG_CHANGE_TOPIC}/{config_type}").as_str()) .unwrap_or_else(|_err| { warn!("The type cannot be used as a part of the topic name. Using {CONFIG_CHANGE_TOPIC} instead."); @@ -212,29 +224,17 @@ impl TryIntoOperationStatusMessage for DownloadConfigFileStatusMessage { #[cfg(test)] mod tests { use super::*; - use crate::config::FileEntry; use assert_matches::*; - use c8y_smartrest::smartrest_deserializer::SmartRestRequestGeneric; - use std::collections::HashSet; #[test] fn create_config_download_request() -> Result<(), anyhow::Error> { - let payload = "524,rina0005,https://test.cumulocity.com/inventory/binaries/70208,tedge"; - let smartrest_request = SmartRestConfigDownloadRequest::from_smartrest(payload)?; - let plugin_config = PluginConfig { - files: HashSet::from([ - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), - FileEntry::new( - "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), - "c8y-bridge".to_string(), - ), - ]), - }; let config_download_request = ConfigDownloadRequest::try_new( - smartrest_request, - &plugin_config, + "https://test.cumulocity.com/inventory/binaries/70208", + PathBuf::from("/etc/tedge/tedge.toml"), PathBuf::from("/tmp"), + FilePermissions::default(), )?; + assert_eq!( config_download_request, ConfigDownloadRequest { @@ -242,8 +242,9 @@ mod tests { url: "https://test.cumulocity.com/inventory/binaries/70208".to_string(), auth: None }, - destination_path: PathBuf::from("/etc/tedge/tedge.toml"), + file_path: PathBuf::from("/etc/tedge/tedge.toml"), tmp_dir: PathBuf::from("/tmp"), + file_permissions: FilePermissions::new(None, None, None), file_name: "tedge.toml".to_string() } ); @@ -251,25 +252,16 @@ mod tests { } #[test] - fn requested_config_does_not_match_config_plugin() -> Result<(), anyhow::Error> { - let payload = - "524,rina0005,https://test.cumulocity.com/inventory/binaries/70208,not_in_config.toml"; - let smartrest_request = SmartRestConfigDownloadRequest::from_smartrest(payload)?; - let plugin_config = PluginConfig { - files: HashSet::from([FileEntry::new( - "/etc/tedge/tedge.toml".to_string(), - "tedge".to_string(), - )]), - }; - let config_download_request = ConfigDownloadRequest::try_new( - smartrest_request, - &plugin_config, + fn create_config_download_request_without_file_name() -> Result<(), anyhow::Error> { + let error = ConfigDownloadRequest::try_new( + "https://test.cumulocity.com/inventory/binaries/70208", + PathBuf::from("/"), PathBuf::from("/tmp"), - ); - assert_matches!( - config_download_request, - Err(ConfigManagementError::InvalidRequestedConfigType { .. }) - ); + FilePermissions::default(), + ) + .unwrap_err(); + + assert_matches!(error, ConfigManagementError::FileNameNotFound { .. }); Ok(()) } diff --git a/plugins/c8y_configuration_plugin/src/error.rs b/plugins/c8y_configuration_plugin/src/error.rs index efcb0913..a8841788 100644 --- a/plugins/c8y_configuration_plugin/src/error.rs +++ b/plugins/c8y_configuration_plugin/src/error.rs @@ -1,19 +1,14 @@ use std::path::PathBuf; +use tedge_utils::file::FileError; #[derive(thiserror::Error, Debug)] pub enum ConfigManagementError { - #[error("The file is read-only {path:?}")] - ReadOnlyFile { path: PathBuf }, + #[error("No write access to {path:?}")] + NoWriteAccess { path: PathBuf }, - #[error("The file name is not found from {path:?}")] + #[error("The file name is not found or invalid: {path:?}")] FileNameNotFound { path: PathBuf }, - #[error("The file name is invalid. {path:?}")] - InvalidFileName { path: PathBuf }, - - #[error("The file is not accessible. {path:?}")] - FileNotAccessible { path: PathBuf }, - #[error("Failed to copy a file from {src:?} to {dest:?}")] FileCopyFailed { src: PathBuf, dest: PathBuf }, @@ -27,4 +22,7 @@ pub enum ConfigManagementError { #[error(transparent)] FromConfigSetting(#[from] tedge_config::ConfigSettingError), + + #[error(transparent)] + FromFile(#[from] FileError), } diff --git a/plugins/c8y_configuration_plugin/src/main.rs b/plugins/c8y_configuration_plugin/src/main.rs index 0616a836..71b1d9ea 100644 --- a/plugins/c8y_configuration_plugin/src/main.rs +++ b/plugins/c8y_configuration_plugin/src/main.rs @@ -13,7 +13,7 @@ use c8y_smartrest::smartrest_deserializer::{ }; use c8y_smartrest::topic::C8yTopic; use clap::Parser; -use mqtt_channel::{SinkExt, StreamExt}; +use mqtt_channel::{Message, SinkExt, StreamExt, Topic}; use std::path::{Path, PathBuf}; use tedge_config::{ ConfigRepository, ConfigSettingAccessor, MqttPortSetting, TEdgeConfig, TmpPathSetting, @@ -128,6 +128,13 @@ async fn run( debug!("Plugin init message: {:?}", msg); let () = mqtt_client.published.send(msg).await?; + // Get pending operations + let msg = Message::new( + &Topic::new_unchecked(C8yTopic::SmartRestResponse.as_str()), + "500", + ); + let () = mqtt_client.published.send(msg).await?; + // Mqtt message loop while let Some(message) = mqtt_client.received.next().await { debug!("Received {:?}", message); @@ -236,7 +243,7 @@ mod tests { let test_config_type = "config_type"; let plugin_config = PluginConfig { - files: HashSet::from([FileEntry::new( + files: HashSet::from([FileEntry::new_with_path_and_type( test_config_path.to_string(), test_config_type.to_string(), )]), diff --git a/plugins/c8y_configuration_plugin/src/upload.rs b/plugins/c8y_configuration_plugin/src/upload.rs index af676ffb..585c54de 100644 --- a/plugins/c8y_configuration_plugin/src/upload.rs +++ b/plugins/c8y_configuration_plugin/src/upload.rs @@ -12,6 +12,7 @@ use c8y_smartrest::{ }; use mqtt_channel::{Connection, SinkExt}; use std::path::Path; +use tracing::{error, info}; struct UploadConfigFileStatusMessage {} @@ -52,7 +53,9 @@ pub async fn handle_config_upload_request( let msg = UploadConfigFileStatusMessage::executing()?; let () = mqtt_client.published.send(msg).await?; - let config_file_path = plugin_config.get_path_from_type(&config_upload_request.config_type)?; + let config_file_path = plugin_config + .get_file_entry_from_type(&config_upload_request.config_type)? + .path; let upload_result = upload_config_file( Path::new(config_file_path.as_str()), &config_upload_request.config_type, @@ -60,13 +63,19 @@ pub async fn handle_config_upload_request( ) .await; + let target_config_type = &config_upload_request.config_type; + match upload_result { Ok(upload_event_url) => { + info!("The configuration upload for '{target_config_type}' is successful."); + let successful_message = UploadConfigFileStatusMessage::successful(Some(upload_event_url))?; let () = mqtt_client.published.send(successful_message).await?; } Err(err) => { + error!("The configuration upload for '{target_config_type}' is failed.",); + let failed_message = UploadConfigFileStatusMessage::failed(err.to_string())?; let () = mqtt_client.published.send(failed_message).await?; } @@ -179,7 +188,7 @@ mod tests { }; let plugin_config = PluginConfig { - files: HashSet::from([FileEntry::new( + files: HashSet::from([FileEntry::new_with_path_and_type( "/some/test/config".to_string(), "config_type".to_string(), )]), -- cgit v1.2.3