Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #2383: Use "service" if service type missing #2385

Merged
merged 1 commit into from
Oct 31, 2023
Merged
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
64 changes: 58 additions & 6 deletions crates/core/c8y_api/src/smartrest/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,27 @@ pub fn child_device_creation_message(
device_name: Option<&str>,
device_type: Option<&str>,
ancestors: &[String],
) -> Message {
Message::new(
) -> Result<Message, InvalidValueError> {
if child_id.is_empty() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that the scope of this PR is limited to services only, but since you added this check here, why not extend it to device_name and device_type as well?

return Err(InvalidValueError {
field_name: "child_id".to_string(),
value: child_id.to_string(),
});
}
if let Some("") = device_name {
return Err(InvalidValueError {
field_name: "device_name".to_string(),
value: "".to_string(),
});
}
if let Some("") = device_type {
return Err(InvalidValueError {
field_name: "device_type".to_string(),
value: "".to_string(),
});
}

Ok(Message::new(
&publish_topic_from_ancestors(ancestors),
// XXX: if any arguments contain commas, output will be wrong
format!(
Expand All @@ -32,7 +51,7 @@ pub fn child_device_creation_message(
device_name.unwrap_or(child_id),
device_type.unwrap_or("thin-edge.io-child")
),
)
))
}

/// Create a SmartREST message for creating a service on device.
Expand All @@ -44,15 +63,41 @@ pub fn service_creation_message(
service_type: &str,
service_status: &str,
ancestors: &[String],
) -> Message {
Message::new(
) -> Result<Message, InvalidValueError> {
// TODO: most of this noise can be eliminated by implementing `Serialize`/`Deserialize` for smartrest format
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you said here, why not implementing the representation struct for SmartREST 101 and 102? We already have them for other message IDs, for operations.
https://github.com/thin-edge/thin-edge.io/blob/main/crates/core/c8y_api/src/smartrest/smartrest_serializer.rs

if service_id.is_empty() {
return Err(InvalidValueError {
field_name: "service_id".to_string(),
value: service_id.to_string(),
});
}
if service_name.is_empty() {
return Err(InvalidValueError {
field_name: "service_name".to_string(),
value: service_name.to_string(),
});
}
if service_type.is_empty() {
return Err(InvalidValueError {
field_name: "service_type".to_string(),
value: service_type.to_string(),
});
}
if service_status.is_empty() {
return Err(InvalidValueError {
field_name: "service_status".to_string(),
value: service_status.to_string(),
});
}

Ok(Message::new(
&publish_topic_from_ancestors(ancestors),
// XXX: if any arguments contain commas, output will be wrong
format!(
"102,{},{},{},{}",
service_id, service_type, service_name, service_status
),
)
))
}

/// Create a SmartREST message for updating service status.
Expand Down Expand Up @@ -81,3 +126,10 @@ pub fn service_status_update_message(external_ids: &[String], service_status: &s

Message::new(&topic, payload)
}

#[derive(thiserror::Error, Debug)]
#[error("Field `{field_name}` contains invalid value: {value:?}")]
pub struct InvalidValueError {
field_name: String,
value: String,
}
9 changes: 7 additions & 2 deletions crates/core/tedge_mapper/src/c8y/mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,12 @@ impl TEdgeComponent for CumulocityMapper {

pub fn service_monitor_client_config(tedge_config: &TEdgeConfig) -> Result<Config, anyhow::Error> {
let main_device_xid: EntityExternalId = tedge_config.device.id.try_read(tedge_config)?.into();
let service_type = tedge_config.service.ty.clone();
let service_type = &tedge_config.service.ty;
let service_type = if service_type.is_empty() {
"service".to_string()
} else {
service_type.to_string()
};

// FIXME: this will not work if `mqtt.device_topic_id` is not in default scheme

Expand All @@ -113,7 +118,7 @@ pub fn service_monitor_client_config(tedge_config: &TEdgeConfig) -> Result<Confi
service_type.as_str(),
"down",
&[],
);
)?;

let mqtt_config = tedge_config
.mqtt_config()?
Expand Down
2 changes: 1 addition & 1 deletion crates/extensions/c8y_mapper_ext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ homepage = { workspace = true }
repository = { workspace = true }

[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
c8y_api = { workspace = true }
c8y_auth_proxy = { workspace = true }
Expand Down Expand Up @@ -45,7 +46,6 @@ toml = { workspace = true }
tracing = { workspace = true }

[dev-dependencies]
anyhow = { workspace = true }
assert-json-diff = { workspace = true }
assert_matches = { workspace = true }
proptest = { workspace = true }
Expand Down
66 changes: 61 additions & 5 deletions crates/extensions/c8y_mapper_ext/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::actor::IdDownloadRequest;
use crate::dynamic_discovery::DiscoverOp;
use crate::error::ConversionError;
use crate::json;
use anyhow::Context;
use c8y_api::http_proxy::C8yEndPoint;
use c8y_api::json_c8y::C8yCreateEvent;
use c8y_api::json_c8y::C8yUpdateSoftwareListResponse;
Expand Down Expand Up @@ -190,7 +191,13 @@ impl CumulocityConverter {
let device_id = config.device_id.clone();
let device_topic_id = config.device_topic_id.clone();
let device_type = config.device_type.clone();
let service_type = config.service_type.clone();

let service_type = if config.service_type.is_empty() {
"service".to_string()
} else {
config.service_type.clone()
};
Comment on lines +195 to +199
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is defenitely fine as of now, especially with the restricted time limit for the new release.

However, I've got the feeling that the ideal solution is to change the tedge_config API; If user tries to set an empty string, it works the same as "unset". The "service" is already used as the default value of service.type. It's a little pity that we need to hard-code the same value here.


let c8y_host = config.c8y_host.clone();
let cfg_dir = config.config_dir.clone();

Expand Down Expand Up @@ -279,7 +286,8 @@ impl CumulocityConverter {
display_name,
display_type,
&ancestors_external_ids,
);
)
.context("Could not create device creation message")?;
Ok(vec![child_creation_message])
}
EntityType::Service => {
Expand All @@ -296,7 +304,8 @@ impl CumulocityConverter {
display_type.unwrap_or(&self.service_type),
"up",
&ancestors_external_ids,
);
)
.context("Could not create service creation message")?;
Ok(vec![service_creation_message])
}
}
Expand Down Expand Up @@ -1437,6 +1446,7 @@ pub(crate) mod tests {
use assert_json_diff::assert_json_eq;
use assert_json_diff::assert_json_include;
use assert_matches::assert_matches;
use c8y_api::smartrest::topic::SMARTREST_PUBLISH_TOPIC;
use c8y_auth_proxy::url::ProxyUrlGenerator;
use c8y_http_proxy::handle::C8YHttpProxy;
use c8y_http_proxy::messages::C8YRestRequest;
Expand Down Expand Up @@ -2708,12 +2718,48 @@ pub(crate) mod tests {
assert!(!second_registration_message_mapped);
}

#[tokio::test]
async fn handles_empty_service_type_2383() {
let tmp_dir = TempTedgeDir::new();
let mut config = c8y_converter_config(&tmp_dir);
config.service_type = String::new();

let (mut converter, _) = create_c8y_converter_from_config(config);

let service_health_message = Message::new(
&Topic::new_unchecked("te/device/main/service/service1/status/health"),
serde_json::to_string(&json!({"status": "up"})).unwrap(),
);

let output = converter.convert(&service_health_message).await;
let service_creation_message = output
.into_iter()
.find(|m| m.topic.name == SMARTREST_PUBLISH_TOPIC)
.expect("service creation message should be present");

let mut smartrest_fields = service_creation_message.payload_str().unwrap().split(',');

assert_eq!(smartrest_fields.next().unwrap(), "102");
assert_eq!(
smartrest_fields.next().unwrap(),
format!("{}:device:main:service:service1", converter.device_name)
);
assert_eq!(smartrest_fields.next().unwrap(), "service");
assert_eq!(smartrest_fields.next().unwrap(), "service1");
assert_eq!(smartrest_fields.next().unwrap(), "up");
Comment on lines +2735 to +2749
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use that helper.

Suggested change
let service_creation_message = output
.into_iter()
.find(|m| m.topic.name == SMARTREST_PUBLISH_TOPIC)
.expect("service creation message should be present");
let mut smartrest_fields = service_creation_message.payload_str().unwrap().split(',');
assert_eq!(smartrest_fields.next().unwrap(), "102");
assert_eq!(
smartrest_fields.next().unwrap(),
format!("{}:device:main:service:service1", converter.device_name)
);
assert_eq!(smartrest_fields.next().unwrap(), "service");
assert_eq!(smartrest_fields.next().unwrap(), "service1");
assert_eq!(smartrest_fields.next().unwrap(), "up");
assert_messages_matching(
&messages,
[
(
SMARTREST_PUBLISH_TOPIC,
"102,test-device:device:main:service:service1,service,service1,up".into(),
),
],
);

}

pub(crate) async fn create_c8y_converter(
tmp_dir: &TempTedgeDir,
) -> (
CumulocityConverter,
SimpleMessageBox<C8YRestRequest, C8YRestResult>,
) {
let config = c8y_converter_config(tmp_dir);
create_c8y_converter_from_config(config)
}

fn c8y_converter_config(tmp_dir: &TempTedgeDir) -> C8yMapperConfig {
tmp_dir.dir("operations").dir("c8y");
tmp_dir.dir("tedge").dir("agent");

Expand All @@ -2731,7 +2777,7 @@ pub(crate) mod tests {
topics.add_all(crate::log_upload::log_upload_topic_filter(&mqtt_schema));
topics.add_all(C8yMapperConfig::default_external_topic_filter());

let config = C8yMapperConfig::new(
C8yMapperConfig::new(
tmp_dir.to_path_buf(),
tmp_dir.utf8_path_buf(),
tmp_dir.utf8_path_buf().into(),
Expand All @@ -2745,15 +2791,25 @@ pub(crate) mod tests {
Capabilities::default(),
auth_proxy_addr,
auth_proxy_port,
);
)
}

fn create_c8y_converter_from_config(
config: C8yMapperConfig,
) -> (
CumulocityConverter,
SimpleMessageBox<C8YRestRequest, C8YRestResult>,
) {
let mqtt_builder: SimpleMessageBoxBuilder<MqttMessage, MqttMessage> =
SimpleMessageBoxBuilder::new("MQTT", 5);
let mqtt_publisher = LoggingSender::new("MQTT".into(), mqtt_builder.build().sender_clone());

let mut c8y_proxy_builder: SimpleMessageBoxBuilder<C8YRestRequest, C8YRestResult> =
SimpleMessageBoxBuilder::new("C8Y", 1);
let http_proxy = C8YHttpProxy::new("C8Y", &mut c8y_proxy_builder);

let auth_proxy_addr = config.auth_proxy_addr;
let auth_proxy_port = config.auth_proxy_port;
let auth_proxy = ProxyUrlGenerator::new(auth_proxy_addr, auth_proxy_port);

let downloader_builder: SimpleMessageBoxBuilder<IdDownloadResult, IdDownloadRequest> =
Expand Down
3 changes: 3 additions & 0 deletions crates/extensions/c8y_mapper_ext/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ pub enum ConversionError {

#[error(transparent)]
InvalidExternalIdError(#[from] InvalidExternalIdError),

#[error("Unexpected error")]
UnexpectedError(#[from] anyhow::Error),
Comment on lines +128 to +130
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error case is unused.

In all cases, I don't see what's your intent when you write Unexpected part is supposed to denote that the error was not related due to invalid input, or the user doing something wrong, but some unexpected conditions occured that makes it so that the request cannot be completed. Like a panic!, it can be used when there is a logic error on part of the programmer, but unlike panic! it doesn't crash the entire program.

ConversionError is already a ball of mug and I don't see how adding such an open case can help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is used in crates/extensions/c8y_mapper_ext/src/converter.rs:290 and crates/extensions/c8y_mapper_ext/src/converter.rs:308 via ?.

The motivation was the same as behind #2353 (comment): by adding a variant that has #[from] anyhow:Error, if in the future we have to handle another type of error, then we can just use .context() to transform it into an anyhow::Error and then ? to convert it into ConversionError::UnexpectedError.

Additional enum variants are only useful as long as the calling code can reasonably match on them. In this case, UnexpectedError doesn't fix ConversionError much, but hopefully it introduces an idea of an opaque error, that we can start using more in the future to stop adding more variants, and also perhaps turn some of the variants we're not matching on into opaque errors.

}

#[derive(thiserror::Error, Debug)]
Expand Down
6 changes: 5 additions & 1 deletion crates/extensions/tedge_health_ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ impl HealthMonitorBuilder {
mqtt: &mut (impl ServiceProvider<MqttMessage, MqttMessage, TopicFilter> + AsMut<MqttConfig>),
// TODO: pass it less annoying way
mqtt_schema: &MqttSchema,
service_type: String,
mut service_type: String,
) -> Self {
let service_topic_id = &service.service_topic_id;

Expand Down Expand Up @@ -71,6 +71,10 @@ impl HealthMonitorBuilder {
box_builder
.set_request_sender(mqtt.connect_consumer(subscriptions, box_builder.get_sender()));

if service_type.is_empty() {
service_type = "service".to_string()
}

let registration_message = EntityRegistrationMessage {
topic_id: service_topic_id.entity().clone(),
external_id: None,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ Check if a service using configured service type

Check if a service using configured service type as empty
[Arguments] ${service_name}
Skip msg=Skipping due to bug #2383
Execute Command tedge config set service.type ""
Custom Test Setup
ThinEdgeIO.Restart Service ${service_name}
Expand Down