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

Main tedge config refactoring #1916

Closed

Conversation

jarhodes314
Copy link
Contributor

@jarhodes314 jarhodes314 commented Apr 18, 2023

Proposed changes

TLDR for people that are interested in the changes but don't want to review masses of code: almost all the changes here arise from new_tedge_config.rs, run cargo doc -p tedge-config --open to see what the result of that is, and the screenshots below demonstrate the main user-facing changes.

This is the main chunk of work associated with #1812 (sorry it's so enormous). There are still a few things to be done in tedge_config, as well as public facing documentation about the updates.

If a user uses a key with lots of dots rather than the updated key, using tedge config will look something like this (I also renamed software.plugin.default -> software.default_plugin as the word order became particularly clunky after replacing the dot with an underscore):

image

This is what tedge config list --doc now outputs (assuming it's running in a tty, if stdout is piped elsewhere, the colours are disabled):

tedge config list --doc

All of the information shown here is derived from the doc comments, examples and notes in the various DTO structs. The colours and alignment are completely configurable, I just chose something that looked readable to me.

I haven't attempted yet to modify any crates other than tedge config and tedge.

I've attempted to add doc comments and examples for the major parts I've added. It's probably worth looking in the generated documentation (cargo doc -p tedge_config --open will get you there quickly if you want to try that), although I imagine people don't tend to run cargo doc an awful lot.

I also fixed some unrelated tests so they work on my machine.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Improvement (general improvements like code refactoring that doesn't explicitly fix a bug or add any new functionality)
  • Documentation Update (if none of the other choices apply)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Paste Link to the issue

#1812

Checklist

  • I have read the CONTRIBUTING doc
  • [ x I have signed the CLA (in all commits with git commit -s)
  • I ran cargo fmt as mentioned in CODING_GUIDELINES
  • I used cargo clippy as mentioned in CODING_GUIDELINES
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Further comments

As I discussed in the issue, the main goal here was to create a single source of truth for a couple of reasons:

  1. The keys that are accepted by tedge config are generally not quite the same as the keys in tedge.toml due to the manual input required.
  2. We had documentation for each key in both doc comments and descriptions.
  3. The level of indirection required to obtain a configuration value makes the library unnecessarily complex for new users.

Annoyingly, to achieve this, I did have to use some significant, albeit declarative, macros. The results are, I believe, quite clear in the rustdoc output, and with the odd exception, are named completely intuitively. I've documented the macro syntax with relevant comments in the invocation of the macro.

This builds on previous changes to TEdgeConfigDTO, adding auto generated documentation from
the doc comments, as well as a simpler means to access configuration via getters and update
values from both the type stored in the dto and a string representation.

This also migrates the tedge command to use the updated configuration, and reformats the
result of `tedge config list --doc` to be more colourful (and hopefully readable, too).

Signed-off-by: James Rhodes <jarhodes314@gmail.com>
Signed-off-by: James Rhodes <jarhodes314@gmail.com>
Copy link
Contributor

@Bravo555 Bravo555 left a comment

Choose a reason for hiding this comment

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

I like how the new tedge config list --doc looks. However, some comments:

///
/// # Ok::<_, TEdgeConfigError>(())
/// ```
pub struct NewTEdgeConfig {
Copy link
Contributor

Choose a reason for hiding this comment

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

The module itself is called new_tedge_config, so could we not repeat New here? As long as the old module is still in use, I think it's fine for code using new config to explicitly use tedge_config::tedge_config_cli::new_tedge_config::TEdgeConfig, or if we renamed the module as well, use tedge_config::tedge_config_cli::new::TEdgeConfig.

If we want this to be available directly under tedge_config, we could rename in lib.rs: pub use self::tedge_config_cli::old_tedge_config::TEdgeConfig as NewTEdgeConfig.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We could, although I'd prefer to keep the simple, unambiguous solution for now. I intend to update all the existing usage and completely replace OldTEdgeConfig with NewTEdgeConfig (and rename the struct and module to not contain new), albeit likely in a separate PR.


#[cfg(test)]
#[allow(clippy::expect_fun_call)]
mod tests {
Copy link
Contributor

@Bravo555 Bravo555 Apr 18, 2023

Choose a reason for hiding this comment

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

The file is a bit large, I think we can extract tests to tedge_config_cli/new_tedge_config/tests.rs? As the new_tedge_config.rs file will still be quite big with test extracted, it can remain under it's original name, instead of being moved into the directory and renamed to mod.rs.

@@ -4,8 +4,9 @@ pub mod tedge_config_cli;
pub use self::tedge_config_cli::config_setting::*;
pub use self::tedge_config_cli::error::*;
pub use self::tedge_config_cli::models::*;
pub use self::tedge_config_cli::new_tedge_config::*;
pub use self::tedge_config_cli::old_tedge_config::*;
Copy link
Contributor

Choose a reason for hiding this comment

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

We're already in tedge_config crate, could we also update module names to not include tedge_config in them? There would be less typing and we could revise whether or not do we want to reexport all these items as we do now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, although I think for this PR it's probably sensible to keep the major structural changes together, and deal with this naming issue separately.

SettingIsNotConfigurable {
key: &'static str,
message: &'static str,
},

#[error("An error occurred: {msg}")]
Other { msg: &'static str },

#[error("Unrecognised configuration key '{key}'")]
WriteUnrecognisedKey {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should use different error types for different operations. If there are different error variants for reading and writing, and write-related error will never be returned from a function doing the reading, we could have something like ReadSettingError and WriteSettingError. Would it be practical to include it in this PR or do you reckon it should be done separately?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should use different error types for different operations. If there are different error variants for reading and writing, and write-related error will never be returned from a function doing the reading, we could have something like ReadSettingError and WriteSettingError. Would it be practical to include it in this PR or do you reckon it should be done separately?

I appreciate the need to put apart the read and write errors, I would prefer to see that change in a different PR this one being already quite large.


#[derive(Copy, Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(transparent)]
/// A wrapper type to add implementations of [fake::Dummy] to `T` in cases where `T: !fake::Dummy`
Copy link
Contributor

Choose a reason for hiding this comment

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

Link here doesn't work. Could you also expand what this Dummy is and why we use it, ideally as a module doc but can also be here.

@@ -536,4 +547,18 @@ mod tests {
let err = get_gid_by_name("test").unwrap_err();
assert!(err.to_string().contains("Group not found"));
}

fn current_username() -> String {
users::get_current_username()
Copy link
Contributor

Choose a reason for hiding this comment

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

I looked up this function, and it looks like it fetches the GID of the running process and then looks up the name of the group by the GID. Vice-versa for users. Looking up the name can fail if there is no group with this ID, but it is because we just got it off the process, in this case the function should be infallible? If this is right, then please add a comment with this info so readers know why unwrap() is fine here.


pub(crate) fn get_connected_c8y_url(port: u16, host: String) -> Result<String, ConnectError> {
pub(crate) fn get_connected_c8y_url(port: u16, host: IpAddress) -> Result<String, ConnectError> {
Copy link
Contributor

Choose a reason for hiding this comment

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

As above, I think that may be incorrect because hosts can be also DNS names, but I'm not sure

Copy link
Contributor

Choose a reason for hiding this comment

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

@Bravo555 is correct we need to be able to give a host name.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If that's the case, the logic calling this that can only ever be an IpAddress is broken. I presume it's a case of us wanting to use the MQTT client address rather than the bind address? My gut feeling there is that the client address default should be the bind address (whereas the current default is just a copy of the bind address default)?


/// The configuration of thin-edge
///
/// This handles fetching values from [TedgeConfigDto](super::tedge_config_dto::TEdgeConfigDto) as well as from the system
Copy link
Contributor

Choose a reason for hiding this comment

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

I see there is some hard wrapping for most doc comments, but in some places rows are very short and in some places it looks like they're very wide. So you could some of the comments to some arbitrary threshold, but I don't think either thin-edge or rustdoc mandate it, so it's just a suggesstion. I personally wrap comments to 80 columns and use Rewrap extension for VSCode for automatic wrapping.

@Bravo555
Copy link
Contributor

If a user uses a key with lots of dots rather than the updated key, using tedge config will look something like this (I also renamed software.plugin.default -> software.default_plugin as the word order became particularly clunky after replacing the dot with an underscore):

So I have some concerns about simplifying TOML key names in order to accommodate env variable names. Sorry for not raising this earlier, but only now when implementing MQTT authentication, I see that I use up to 4 levels of nesting (mqtt.client.auth.cert_file) and I see that in this PR it was necessary to flatten out the TOML to be 2 levels deep.

So I wonder whether or not do we get kind of the worst of both worlds here. The advantage of TOML is that it can be deeply hierarchical, contain objects, arrays, etc. but we restrict it two two levels only so that we can better support pretty looking environment variable names.

Have we considered the option of completely disallowing underscores (_) in our TOML keys? They could be replaced with . and become "dotted keys". Example from TOML spec:

name = "Orange"
physical.color = "orange"
physical.shape = "round"
site."google.com" = true
  • closely related settings like mqtt.external.bind_address and mqtt.external.bind_interface would become mqtt.external.bind.address and mqtt.external.bind.interface respectively
  • in some settings we could eliminate underscores altogether like run.lock_files -> run.lockfiles
  • in some settings where we couldn't do the above we would introduce some unnecessary nesting, which would require some additional structs

This excess nesting sounds pretty painful, but I'd rather have that than only nesting 2 levels deep.

@jarhodes314
Copy link
Contributor Author

jarhodes314 commented Apr 20, 2023

If a user uses a key with lots of dots rather than the updated key, using tedge config will look something like this (I also renamed software.plugin.default -> software.default_plugin as the word order became particularly clunky after replacing the dot with an underscore):

So I have some concerns about simplifying TOML key names in order to accommodate env variable names. Sorry for not raising this earlier, but only now when implementing MQTT authentication, I see that I use up to 4 levels of nesting (mqtt.client.auth.cert_file) and I see that in this PR it was necessary to flatten out the TOML to be 2 levels deep.

So I wonder whether or not do we get kind of the worst of both worlds here. The advantage of TOML is that it can be deeply hierarchical, contain objects, arrays, etc. but we restrict it two two levels only so that we can better support pretty looking environment variable names.

Have we considered the option of completely disallowing underscores (_) in our TOML keys? They could be replaced with . and become "dotted keys". Example from TOML spec:

name = "Orange"
physical.color = "orange"
physical.shape = "round"
site."google.com" = true
* closely related settings like `mqtt.external.bind_address` and `mqtt.external.bind_interface` would become `mqtt.external.bind.address` and `mqtt.external.bind.interface` respectively

* in some settings we could eliminate underscores altogether like `run.lock_files` -> `run.lockfiles`

* in some settings where we couldn't do the above we would introduce some unnecessary nesting, which would require some additional structs

This excess nesting sounds pretty painful, but I'd rather have that than only nesting 2 levels deep.

There's no technical reason why there needs to only be one dot to support environment variable names, it just made the logic for translating them to the correct key much simpler as it can be done without any knowledge of what the keys are. So long as the keys are unambiguous (e.g. we don't have keys like c8y.root.cert.path and c8y.root_cert_path meaning separate things), we can map the environment variables to variables successfully. The reason why the TOML is two levels deep here is because that is what is was. I was trying to (mostly) preserve the existing tedge.toml format as a source of truth at least for now to avoid the complexity of migrating the TOML to a new structure (because even if we want to rename a bunch of fields to make the names make more sense in a non-hierarchical structure, we can just tell serde to deal with it).

If we do change the structure, we absolutely need to get the naming somewhat self consistent. From what I can tell, mqtt.port is currently the counterpart to mqtt.bind.address. I think we should definitely keep using underscores to some degree. Things like c8y.root.cert.path don't make any sense as a hierarchical structure. I agree it's a problem with mqtt, and most of the other keys are probably better as nested keys, but the impact of "fixing it" on the understandability of the TOML will be pretty small.

FWIW, my (pretty recent, but maybe not since the last toml_edit update recent) experience with trying to turn "normal" TOML tables into dotted keys with toml_edit was awkward, and there were a bunch of issues with how it handled comments (which I should check for the existence of and raise issues for), although that probably doesn't have much impact on us. It does, however, have an impact if we wish to update the TOML in a format-preserving manner at some point, which could be more possible if we've already written manual migration logic for old TOML files.

Copy link
Contributor

@didier-wenzek didier-wenzek left a comment

Choose a reason for hiding this comment

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

I still need some time to have an overall view of the impacts of the proposed changes.

Looking things independently, I saw many interesting points compared to the old version. However, I still have a feeling that this if by far too complex for the task. The configuration_keys! invocation highlights how many weirdness we have introduced over time. Dealing with all of them is a "tour de force" and I admire you ingenuity to cope with this legacy. However, I wonder if it wouldn't be simpler to give up maintaining backward compatibility on some of these points.

  • Do we really need to have a TedgeConfig and TedgeConfigDto?
  • Can this be simplified once the OldTedgeConfig fully deprecated?

Comment on lines +80 to +81
// TODO add a test to make sure people don't accidentally set the wrong meta name
if let Some(note) = ty.metas.get("note") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would not bother too much here. That said I don't understand why the note is accessed using ty.metas.get("note") while the example is accessed using ty.example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Doku has official support for examples, so we just use #[doku(example="value")], but for notes we have to use metas (#[doku(meta(note = "A note"))] ) which is doku's mechanism for arbitrary structured metadata.

Comment on lines 88 to 89
// TODO should this be client host? or should it really be bind_address
.with_internal_opts(config.mqtt_port(), config.mqtt_client_host())
Copy link
Contributor

Choose a reason for hiding this comment

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

This must be the mqtt.bind_address. The mqtt.client_host points to the same but can be a DNS name (therefore cannot be used as a bind address).

Suggested change
// TODO should this be client host? or should it really be bind_address
.with_internal_opts(config.mqtt_port(), config.mqtt_client_host())
.with_internal_opts(config.mqtt_port(), config.mqtt_bind_address())

figment = { version = "0.10", features = ["test"] }
rand = "0.8.5"
Copy link
Contributor

Choose a reason for hiding this comment

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

We agreed to not specify the PATCH number, letting cargo free to get the latest.

@@ -17,9 +17,10 @@ pub struct TEdgeConfigRepository {
pub trait ConfigRepository<T> {
type Error;
fn load(&self) -> Result<T, Self::Error>;
// TODO don't make this part of the trait. Do we even need the trait?
Copy link
Contributor

Choose a reason for hiding this comment

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

The trait is a vestige of a complex config setting with different configurations for root and the regular users.

It can be safely removed.

crates/common/tedge_utils/src/file.rs Show resolved Hide resolved
SettingIsNotConfigurable {
key: &'static str,
message: &'static str,
},

#[error("An error occurred: {msg}")]
Other { msg: &'static str },

#[error("Unrecognised configuration key '{key}'")]
WriteUnrecognisedKey {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should use different error types for different operations. If there are different error variants for reading and writing, and write-related error will never be returned from a function doing the reading, we could have something like ReadSettingError and WriteSettingError. Would it be practical to include it in this PR or do you reckon it should be done separately?

I appreciate the need to put apart the read and write errors, I would prefer to see that change in a different PR this one being already quite large.

Comment on lines +59 to +60
/// A cache for the device id, to save us re-reading certificates unneccessarily
device_id: OnceCell<String>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This wart is okay for now, but we might have to revise the whole idea of computed settings as the device.id. Indeed, in a containerized environment, this works only in the container running mosquitto.


Ok(match key {
ReadOnly(DeviceId) => Some(self.device_id()?.to_string()),
ReadOnly(HttpAddress) => Some(self.http_address().to_string()),
Copy link
Contributor

Choose a reason for hiding this comment

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

This wart (using the MQTT bind address as the HTTP bind address) will also have to be removed in a near future.

Comment on lines +116 to +119
/// Getter for `firmware.child_update_timeout`
pub fn firmware_child_update_timeout(&self) -> Duration {
self.stored.firmware().child_update_timeout()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a special case, here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a special case because it felt sensible for any Rust code accessing this to access this as a Duration, but tedge config needs to read and write this as a number of seconds. So when we read the string value, we get the number of seconds, but when we read the typed value, we get a Duration.

Copy link
Contributor

@didier-wenzek didier-wenzek Apr 27, 2023

Choose a reason for hiding this comment

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

I would prefer to address the issue at the level of TEdgeConfigDto, using #[serde(deserialize_with = "to_duration", serialize_with = "from_duration")].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In which case we actually would need a wrapper type because it needs ToString and FromStr too for tedge config

@jarhodes314
Copy link
Contributor Author

I still need some time to have an overall view of the impacts of the proposed changes.

Looking things independently, I saw many interesting points compared to the old version. However, I still have a feeling that this if by far too complex for the task. The configuration_keys! invocation highlights how many weirdness we have introduced over time. Dealing with all of them is a "tour de force" and I admire you ingenuity to cope with this legacy. However, I wonder if it wouldn't be simpler to give up maintaining backward compatibility on some of these points.

* Do we really need to have a `TedgeConfig` and `TedgeConfigDto`?

* Can this be simplified  once the `OldTedgeConfig` fully deprecated?

There are two opposing forces at play here:

  • We want to edit tedge.toml programmatically, and in doing so, get the benefits of using serde to its full potential, such as simple support for aliases and renaming fields. When we do that, we don't want to serialize default values in tedge.toml, it makes it harder to see what's configured and difficult/impossible to have logic deriving those defaults (such as deriving the mqtt client details from the bind details, or the device certificates from the configuration path).
  • We don't want consumers of tedge_config to have to care that values are optional if they have defaults.

The first point requires we represent the absence of values, but the second requires we don't. Initially, I thought we could just have getters on TEdgeConfigDto to alleviate this, but the default certificate paths are derived from TEdgeConfigLocation, so anyone accessing those variables would have to keep that around and somewhat arbitrarily pass it in (although we may be able to use (something like) https://docs.rs/figment/latest/figment/value/magic/struct.RelativePathBuf.html to fix that).

One nice feature of the existing solution that I'm completely stuck on how I'd implement without code generation is the errors for when data isn't supplied. At the moment, we quote what configuration key isn't set, and this makes it really simple for consuming code to fail with a clear instructions on how to fix the problem. Of course you could specify manually in each of the relevant getters what the key name is, but then we're back to relying on someone to get the thing right in the first place.

@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring branch from b2d0558 to 8edfdd6 Compare April 26, 2023 12:57
@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring branch from 8edfdd6 to 1c5045d Compare April 26, 2023 13:09
@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring branch from 79d6bb9 to 011c71a Compare April 26, 2023 16:30
@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring branch from 011c71a to 55743ac Compare April 26, 2023 16:34
@jarhodes314
Copy link
Contributor Author

jarhodes314 commented Apr 26, 2023

I've now updated the PR to include @Bravo555's changes. The behaviour should be identical to before, with the exception of the mqtt.client_auth.cert_file/mqtt.client_auth.key_file now including _s to match the dto (but the keys do work complete with the second dot, I've improved the logic to normalize the keys to allow for this). I don't think the added complexity of trying to update the toml manually (as opposed to modifying the dto and then serializing that and letting serde-derive do all the hard work) is worth it to save a few lines of pretty simple logic.

Now we have actually got an instance of a multi-level hierarchical configuration, it seems weird to limit this to this one configuration. I think it makes sense to update the toml to be more hierarchical, especially mqtt. This change probably doesn't belong in this PR, mainly because it doesn't need to and this PR is already rather large. We could introduce a version field to indicate whether the configuration is updated or not, and manually (or possibly automatically, like I do here for outdated key detection) modify a toml::Value to move e.g. external_port to external.port. If this is something we want to do, it would be worth disabling the "deprecated key" warnings until that change is completed as we would reverse many of the changes we are warning about.

On the subject of warnings, they're pretty coarse-grained at the moment, they don't have any knowledge of previous keys, so they will claim that any key with the right segments (e.g. c8y_root.cert_path) is deprecated (and point the user in the direction of the actual key). I don't think this is a huge issue, but there might be some unintended side effects that someone else can think of?

I've also attempted (not in anything I've pushed to here) to investigate what it would be like to replace this with a procedural macro that would replace most of new_tedge_config.rs and all of tedge_config_dto.rs, and I've got something that pretty much works. My proposed syntax is something like the following:

define_tedge_config! {
    mqtt: {
        #[tedge_config(default(function = "default_port"))]
        port: NonZeroU16,
        
        // I think this is a (slightly) useful feature for this one case.
        // If mqtt.port is set, but not mqtt.client_port, mqtt.client_port
        // should probably match it rather than defaulting to 1883.
        #[tedge_config(default(config_path = "mqtt.port"))]
        client_port: NonZeroU16,

        #[tedge_config(all_or_nothing)]
        client_auth: {
            cert_file: Utf8PathBuf,
            key_file: Utf8PathBuf,
        },
    },
   // ...
}

fn default_port() -> NonZeroU16 {
    1883.try_into().unwrap()
}

It still supports all the other attributes we're using from serde and doku, I omitted them here to focus on the new attributes. Using this would be something like:

let config: TEdgeConfig = ...;
assert_eq!(config.read().mqtt.port, default_port());
assert_eq!(config.read().mqtt.client_port, default_port());

match config.read().mqtt.client_auth {
     Ok(Some(auth)) => println!("{}, {}", auth.cert_file, auth.key_file),
     Ok(None) => println!("Client auth not set"),
     // It generates an appropriate error message if the config
     // is partially set
     Err(e) => println!("Invalid config: {e}"),
}

This solution has different trade offs. The parsing logic is mostly simpler to understand, and possible to unit test, but if the generated code is invalid, the error messages can be much more impenetrable. Usually with a declarative macro, rustc can point you at the bit of the macro that's failing, but emitting invalid syntax from a proc macro will generally give you no information at all as to where the error arose from. It would, however, simplify the code defining the configuration. I'll try and open a draft PR with those changes so you can have a look at the code, but you're more than welcome to go "no, this is even more complicated and I don't even want to consider it as something we have to support".

@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring branch from 40c1c17 to d8c6d83 Compare April 27, 2023 07:54
@github-actions
Copy link
Contributor

github-actions bot commented Apr 27, 2023

Robot Results

✅ Passed ❌ Failed ⏭️ Skipped Total Pass %
186 0 5 186 100

Passed Tests

Name ⏱️ Duration Suite
Define Child device 1 ID 0.004 s C8Y Child Alarms Rpi
Normal case when the child device does not exist on c8y cloud 1.792 s C8Y Child Alarms Rpi
Normal case when the child device already exists 0.744 s C8Y Child Alarms Rpi
Reconciliation when the new alarm message arrives, restart the mapper 1.643 s C8Y Child Alarms Rpi
Reconciliation when the alarm that is cleared 5.252 s C8Y Child Alarms Rpi
Prerequisite Parent 75.006 s Child Conf Mgmt Plugin
Prerequisite Child 0.405 s Child Conf Mgmt Plugin
Child device bootstrapping 11.896 s Child Conf Mgmt Plugin
Snapshot from device 61.218 s Child Conf Mgmt Plugin
Child device config update 62.258 s Child Conf Mgmt Plugin
Configuration types should be detected on file change (without restarting service) 108.666 s Inotify Crate
Check lock file existence in default folder 1.456 s Lock File
Check PID number in lock file 1.228 s Lock File
Check PID number in lock file after restarting the services 3.215 s Lock File
Check starting same service twice 1.3519999999999999 s Lock File
Switch off lock file creation 1.96 s Lock File
Set configuration when file exists 54.179 s Configuration Operation
Set configuration when file does not exist 4.959 s Configuration Operation
Set configuration with broken url 4.42 s Configuration Operation
Get configuration 5.228 s Configuration Operation
Get non existent configuration file 5.06 s Configuration Operation
Get non existent configuration type 4.319 s Configuration Operation
Update configuration plugin config via cloud 4.91 s Configuration Operation
Modify configuration plugin config via local filesystem modify inplace 4.233 s Configuration Operation
Modify configuration plugin config via local filesystem overwrite 4.425 s Configuration Operation
Update configuration plugin config via local filesystem copy 2.188 s Configuration Operation
Update configuration plugin config via local filesystem move (different directory) 1.9449999999999998 s Configuration Operation
Update configuration plugin config via local filesystem move (same directory) 2.366 s Configuration Operation
Successful firmware operation 67.397 s Firmware Operation
Install with empty firmware name 45.045 s Firmware Operation
Prerequisite Parent 16.263 s Firmware Operation Child Device
Prerequisite Child 7.62 s Firmware Operation Child Device
Child device firmware update 5.623 s Firmware Operation Child Device
Child device firmware update with cache 5.439 s Firmware Operation Child Device
Update Inventory data via inventory.json 3.064 s Inventory Update
Retrieve a JWT tokens 116.486 s Jwt Request
Main device registration 1.538 s Device Registration
Child device registration 1.742 s Device Registration
Supports restarting the device 117.186 s Restart Device
Update tedge version from previous using Cumulocity 62.696 s Tedge Self Update
Test if all c8y services are up 105.935 s Service Monitoring
Test if all c8y services are down 111.181 s Service Monitoring
Test if all c8y services are using configured service type 115.562 s Service Monitoring
Test if all c8y services using default service type when service type configured as empty 37.625 s Service Monitoring
Check health status of tedge-mapper-c8y service on broker stop start 33.263 s Service Monitoring
Check health status of tedge-mapper-c8y service on broker restart 35.307 s Service Monitoring
Check health status of child device service 25.599 s Service Monitoring
Successful shell command with output 3.078 s Shell Operation
Check Successful shell command with literal double quotes output 2.943 s Shell Operation
Execute multiline shell command 3.057 s Shell Operation
Failed shell command 3.073 s Shell Operation
Software list should be populated during startup 35.192 s Software
Install software via Cumulocity 50.009 s Software
Software list should only show currently installed software and not candidates 40.763 s Software
Child devices support sending simple measurements 1.146 s Child Device Telemetry
Child devices support sending custom measurements 1.008 s Child Device Telemetry
Child devices support sending custom events 0.999 s Child Device Telemetry
Child devices support sending custom events overriding the type 0.959 s Child Device Telemetry
Child devices support sending custom alarms #1699 3 s Child Device Telemetry
Child devices support sending inventory data via c8y topic 0.852 s Child Device Telemetry
Main device support sending inventory data via c8y topic 1.232 s Child Device Telemetry
Child device supports sending custom child device measurements directly to c8y 1.283 s Child Device Telemetry
Main device supports sending custom child device measurements directly to c8y 1.168 s Child Device Telemetry
Check retained alarms 39.049 s Raise Alarms
Validate updated data path used by tedge-agent 0.457 s Data Path Config
Validate updated data path used by c8y-firmware-plugin 10.296 s Data Path Config
Stop tedge-agent service 0.464 s Log Path Config
Customize the log path 0.21 s Log Path Config
Initialize tedge-agent 0.167 s Log Path Config
Check created folders 0.148 s Log Path Config
Remove created custom folders 0.094 s Log Path Config
Install thin-edge via apt 26.43 s Install Apt
Install latest via script (from current branch) 21.581 s Install Tedge
Install specific version via script (from current branch) 23.309 s Install Tedge
Install latest tedge via script (from main branch) 14.765 s Install Tedge
Install then uninstall latest tedge via script (from main branch) 31.41 s Install Tedge
Support starting and stopping services 43.649 s Service-Control
Supports a reconnect 56.526 s Test-Commands
Supports disconnect then connect 35.499 s Test-Commands
Update unknown setting 35.204 s Test-Commands
Update known setting 40.481 s Test-Commands
Stop c8y-configuration-plugin 0.103 s Health C8Y-Configuration-Plugin
Update the service file 0.11 s Health C8Y-Configuration-Plugin
Reload systemd files 0.21 s Health C8Y-Configuration-Plugin
Start c8y-configuration-plugin 0.057 s Health C8Y-Configuration-Plugin
Start watchdog service 10.088 s Health C8Y-Configuration-Plugin
Check PID of c8y-configuration-plugin 0.072 s Health C8Y-Configuration-Plugin
Kill the PID 0.137 s Health C8Y-Configuration-Plugin
Recheck PID of c8y-configuration-plugin 6.351 s Health C8Y-Configuration-Plugin
Compare PID change 0.001 s Health C8Y-Configuration-Plugin
Stop watchdog service 0.098 s Health C8Y-Configuration-Plugin
Remove entry from service file 0.087 s Health C8Y-Configuration-Plugin
Stop c8y-log-plugin 60.109 s Health C8Y-Log-Plugin
Update the service file 0.177 s Health C8Y-Log-Plugin
Reload systemd files 0.337 s Health C8Y-Log-Plugin
Start c8y-log-plugin 0.106 s Health C8Y-Log-Plugin
Start watchdog service 10.091 s Health C8Y-Log-Plugin
Check PID of c8y-log-plugin 0.084 s Health C8Y-Log-Plugin
Kill the PID 0.159 s Health C8Y-Log-Plugin
Recheck PID of c8y-log-plugin 6.491 s Health C8Y-Log-Plugin
Compare PID change 0.001 s Health C8Y-Log-Plugin
Stop watchdog service 0.118 s Health C8Y-Log-Plugin
Remove entry from service file 0.212 s Health C8Y-Log-Plugin
Stop tedge-mapper 0.103 s Health Tedge Mapper C8Y
Update the service file 0.109 s Health Tedge Mapper C8Y
Reload systemd files 0.266 s Health Tedge Mapper C8Y
Start tedge-mapper 0.093 s Health Tedge Mapper C8Y
Start watchdog service 10.098 s Health Tedge Mapper C8Y
Check PID of tedge-mapper 0.108 s Health Tedge Mapper C8Y
Kill the PID 0.177 s Health Tedge Mapper C8Y
Recheck PID of tedge-mapper 6.322 s Health Tedge Mapper C8Y
Compare PID change 0.001 s Health Tedge Mapper C8Y
Stop watchdog service 0.127 s Health Tedge Mapper C8Y
Remove entry from service file 0.158 s Health Tedge Mapper C8Y
Stop tedge-agent 0.083 s Health Tedge-Agent
Update the service file 0.088 s Health Tedge-Agent
Reload systemd files 0.245 s Health Tedge-Agent
Start tedge-agent 0.093 s Health Tedge-Agent
Start watchdog service 10.219 s Health Tedge-Agent
Check PID of tedge-mapper 0.084 s Health Tedge-Agent
Kill the PID 0.165 s Health Tedge-Agent
Recheck PID of tedge-agent 6.333 s Health Tedge-Agent
Compare PID change 0.001 s Health Tedge-Agent
Stop watchdog service 0.151 s Health Tedge-Agent
Remove entry from service file 0.157 s Health Tedge-Agent
Stop tedge-mapper-az 0.102 s Health Tedge-Mapper-Az
Update the service file 0.11 s Health Tedge-Mapper-Az
Reload systemd files 0.326 s Health Tedge-Mapper-Az
Start tedge-mapper-az 0.089 s Health Tedge-Mapper-Az
Start watchdog service 10.17 s Health Tedge-Mapper-Az
Check PID of tedge-mapper-az 0.088 s Health Tedge-Mapper-Az
Kill the PID 0.174 s Health Tedge-Mapper-Az
Recheck PID of tedge-agent 6.36 s Health Tedge-Mapper-Az
Compare PID change 0 s Health Tedge-Mapper-Az
Stop watchdog service 0.107 s Health Tedge-Mapper-Az
Remove entry from service file 0.08 s Health Tedge-Mapper-Az
Stop tedge-mapper-collectd 0.154 s Health Tedge-Mapper-Collectd
Update the service file 0.115 s Health Tedge-Mapper-Collectd
Reload systemd files 0.363 s Health Tedge-Mapper-Collectd
Start tedge-mapper-collectd 0.131 s Health Tedge-Mapper-Collectd
Start watchdog service 10.133 s Health Tedge-Mapper-Collectd
Check PID of tedge-mapper-collectd 0.141 s Health Tedge-Mapper-Collectd
Kill the PID 0.412 s Health Tedge-Mapper-Collectd
Recheck PID of tedge-mapper-collectd 6.657 s Health Tedge-Mapper-Collectd
Compare PID change 0.001 s Health Tedge-Mapper-Collectd
Stop watchdog service 0.121 s Health Tedge-Mapper-Collectd
Remove entry from service file 0.123 s Health Tedge-Mapper-Collectd
tedge-collectd-mapper health status 5.504 s Health Tedge-Mapper-Collectd
c8y-log-plugin health status 5.397 s MQTT health endpoints
c8y-configuration-plugin health status 5.987 s MQTT health endpoints
Publish on a local insecure broker 0.128 s Basic Pub Sub
Publish on a local secure broker 1.5470000000000002 s Basic Pub Sub
Publish on a local secure broker with client authentication 1.948 s Basic Pub Sub
Check remote mqtt broker #1773 2.073 s Remote Mqtt Broker
Wrong package name 0.108 s Improve Tedge Apt Plugin Error Messages
Wrong version 0.108 s Improve Tedge Apt Plugin Error Messages
Wrong type 0.323 s Improve Tedge Apt Plugin Error Messages
tedge_connect_test_positive 0.16 s Tedge Connect Test
tedge_connect_test_negative 0.366 s Tedge Connect Test
tedge_connect_test_sm_services 8.435 s Tedge Connect Test
tedge_disconnect_test_sm_services 2.157 s Tedge Connect Test
Install thin-edge.io 19.71 s Call Tedge
call tedge -V 0.095 s Call Tedge
call tedge -h 0.13 s Call Tedge
call tedge -h -V 0.135 s Call Tedge
call tedge help 0.075 s Call Tedge
tedge config list 0.163 s Call Tedge Config List
tedge config list --all 0.072 s Call Tedge Config List
set/unset device.type 0.61 s Call Tedge Config List
set/unset device.key.path 0.624 s Call Tedge Config List
set/unset device.cert.path 0.645 s Call Tedge Config List
set/unset c8y.root.cert.path 0.67 s Call Tedge Config List
set/unset c8y.smartrest.templates 1.076 s Call Tedge Config List
set/unset az.root.cert.path 0.584 s Call Tedge Config List
set/unset az.mapper.timestamp 0.624 s Call Tedge Config List
set/unset mqtt.bind_address 0.553 s Call Tedge Config List
set/unset mqtt.port 0.395 s Call Tedge Config List
set/unset tmp.path 0.398 s Call Tedge Config List
set/unset logs.path 0.611 s Call Tedge Config List
set/unset run.path 0.228 s Call Tedge Config List
Get Put Delete 4.548 s Http File Transfer Api
Set keys should return value on stdout 0.2 s Tedge Config Get
Unset keys should not return anything on stdout and warnings on stderr 0.413 s Tedge Config Get
Invalid keys should not return anything on stdout and warnings on stderr 0.512 s Tedge Config Get
Set configuration via environment variables 1.5550000000000002 s Tedge Config Get
Set unknown configuration via environment variables 0.204 s Tedge Config Get

@didier-wenzek
Copy link
Contributor

I've also attempted (not in anything I've pushed to here) to investigate what it would be like to replace this with a procedural macro that would replace most of new_tedge_config.rs and all of tedge_config_dto.rs, and I've got something that pretty much works. My proposed syntax is something like the following

The excerpt is appealing and I would be really interested by a draft PR. However, we need to save our time and energy.

  1. Submit a draft PR with what you have currently - even if not fully working. The point is to have an idea of the complexity of this procedural macro as well as the benefits.
  2. Estimate the work that still need to be done for this PR to be fully done (i.e. to remove the OldTedgeConfig) and to remove duplicated code (as for TEdgeConfig::mqtt_config() and NewTEdgeConfig::mqtt_config()).

// I think this is a (slightly) useful feature for this one case.
// If mqtt.port is set, but not mqtt.client_port, mqtt.client_port
// should probably match it rather than defaulting to 1883.
#[tedge_config(default(config_path = "mqtt.port"))]
client_port: NonZeroU16,

This can be indeed useful as several settings are closely related. We need to balance the cost with the actual help provided to users, though.

#[tedge_config(all_or_nothing)]
client_auth: {
cert_file: Utf8PathBuf,
key_file: Utf8PathBuf,
},

Here, I have a mix feeling to implement to implement this in procedural macro. As far as I know, this check is use in a single place. There are so many ways to get a configuration wrong that we cannot check all weird combinations.

@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request April 28, 2023 12:25 — with GitHub Actions Inactive
@jarhodes314 jarhodes314 mentioned this pull request Apr 28, 2023
11 tasks
@didier-wenzek
Copy link
Contributor

superseded by #1936

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants