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

Proc macro for tedge config #1936

Merged

Conversation

jarhodes314
Copy link
Contributor

@jarhodes314 jarhodes314 commented Apr 28, 2023

Proposed changes

PoC of alternative solution to #1916. This version uses a procedural macro to reduce boilerplate code in tedge config.

The syntax is currently (you can see the same example in crates/common/tedge_config_macros/src/lib.rs):

static DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";

define_tedge_config! {
    device: {
        /// Path where the device's private key is stored
        #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem", default(function = "default_device_key"))]
        #[doku(as = "PathBuf")]
        key_path: Utf8PathBuf,

        /// Path where the device's certificate is stored
        #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem", default(function = "default_device_cert"))]
        #[doku(as = "PathBuf")]
        cert_path: Utf8PathBuf,

        /// The default device type
        #[tedge_config(example = "thin-edge.io")]
        #[serde(rename = "type")]
        device_type: String,
    },

    #[serde(alias = "azure")] // for 0.1.0 compatibility
    az: {
        /// Endpoint URL of Azure IoT tenant
        #[tedge_config(example = "myazure.azure-devices.net")]
        url: ConnectUrl,

        /// The path where Azure IoT root certificate(s) are stared
        #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")]
        #[tedge_config(example = "/etc/tedge/az-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))]
        #[doku(as = "PathBuf")]
        root_cert_path: Utf8PathBuf,

        mapper: {
            /// Whether the Azure IoT mapper should add a timestamp or not
            #[tedge_config(example = "true")]
            #[tedge_config(default(value = true))]
            timestamp: bool,
        }
    },

    mqtt: {
        bind: {
            /// The address mosquitto binds to for internal use
            #[tedge_config(example = "127.0.0.1", default(variable = "Ipv4Addr::LOCALHOST"))]
            address: IpAddr,

            /// The port mosquitto binds to for internal use
            #[tedge_config(example = "1883", default(function = "default_mqtt_port"))]
            #[doku(as = "u16")]
            // This was originally u16, but I can't think of any way in which
            // tedge could actually connect to mosquitto if it bound to a random
            // free port, so I don't think 0 is *really* valid here
            port: NonZeroU16,
        },

        client: {
            /// The host that the thin-edge MQTT client should connect to
            #[tedge_config(example = "localhost", default(value = "localhost"))]
            host: String,

            /// The port that the thin-edge MQTT client should connect to
            #[tedge_config(default(from_path = "mqtt.bind.port"))]
            #[doku(as = "u16")]
            port: NonZeroU16,

            auth: {
                /// Path to the CA certificate used by MQTT clients to use when authenticating the MQTT broker
                #[tedge_config(example = "/etc/mosquitto/ca_certificates/ca.crt")]
                #[doku(as = "PathBuf")]
                #[serde(alias = "cafile")]
                ca_file: Utf8PathBuf,

                /// Path to the directory containing the CA certificates used by MQTT
                /// clients when authenticating the MQTT broker
                #[tedge_config(example = "/etc/mosquitto/ca_certificates")]
                #[doku(as = "PathBuf")]
                #[serde(alias = "capath")]
                ca_path: Utf8PathBuf,

                /// Path to the client certficate
                #[doku(as = "PathBuf")]
                #[serde(alias = "certfile")]
                cert_file: Utf8PathBuf,

                /// Path to the client private key
                #[doku(as = "PathBuf")]
                #[serde(alias = "keyfile")]
                key_file: Utf8PathBuf,
            }
        },

        external: {
            bind: {
                /// The port mosquitto binds to for external use
                #[tedge_config(example = "8883")]
                port: u16,

                /// The address mosquitto binds to for external use
                #[tedge_config(example = "0.0.0.0")]
                address: IpAddr,

                /// Name of the network interface which mosquitto limits incoming connections on
                #[tedge_config(example = "wlan0")]
                interface: String,
            },

            /// Path to a file containing the PEM encoded CA certificates that are
            /// trusted when checking incoming client certificates
            #[tedge_config(example = "/etc/ssl/certs")]
            #[doku(as = "PathBuf")]
            #[serde(alias = "capath")]
            ca_path: Utf8PathBuf,

            /// Path to the certificate file which is used by the external MQTT listener
            #[tedge_config(note = "This setting shall be used together with `mqtt.external.key_file` for external connections.")]
            #[tedge_config(example = "/etc/tedge/device-certs/tedge-certificate.pem")]
            #[doku(as = "PathBuf")]
            #[serde(alias = "certfile")]
            cert_file: Utf8PathBuf,

            /// Path to the key file which is used by the external MQTT listener
            #[tedge_config(note = "This setting shall be used together with `mqtt.external.cert_file` for external connections.")]
            #[tedge_config(example = "/etc/tedge/device-certs/tedge-private-key.pem")]
            #[doku(as = "PathBuf")]
            #[serde(alias = "keyfile")]
            key_file: Utf8PathBuf,
        }
    }
}

fn default_device_key(location: &TEdgeConfigLocation) -> Utf8PathBuf {
    location
        .tedge_config_root_path()
        .join("device-certs")
        .join("tedge-private-key.pem")
}

fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf {
    location
        .tedge_config_root_path()
        .join("device-certs")
        .join("tedge-certificate.pem")
}

fn default_mqtt_port() -> NonZeroU16 {
    NonZeroU16::try_from(1883).unwrap()
}

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


Checklist

  • I have read the CONTRIBUTING doc
  • 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

@jarhodes314 jarhodes314 changed the title POC proc macro for tedge config PoC proc macro for tedge config Apr 28, 2023
@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring-proc branch 2 times, most recently from 2dc44a9 to d893370 Compare April 28, 2023 17:48
@jarhodes314 jarhodes314 force-pushed the feat/tedge-config-refactoring-proc branch from d893370 to fb59fda Compare April 28, 2023 17:48
@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request April 28, 2023 17:59 — with GitHub Actions Inactive
Comment on lines +1 to +2
mod parse;
mod validate;
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't help but remember Parse, don’t validate ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ironically, that's pretty much what validate is doing. It could do with clearer naming to distinguish between "things that can be parsed with syn and darling" and "things that are meaningful for the rest of the application, after e.g. converting boolean flags into enum discriminants" (which is currently in the validate module, even though it's following the idea of "Parse, don't validate").

@jarhodes314
Copy link
Contributor Author

@didier-wenzek you had concerns with implementing all_or_nothing logic in the macro itself (I do too, now I've considered the impact). I think what we probably want here is an attribute to mark configurations as private, preventing other crates from accessing those configurations directly via the fields of TEdgeConfigReader, and we can add methods like the existing mqtt_config(&TEdgeConfigReader) -> MqttConfig for accessing these configurations.

With the OptionalConfig<T> type, it should be very simple to create already/add the required support for

fn all_or_nothing<T, U>(_: OptionalConfig<T>, _: OptionalConfig<U>) -> Result<Option<(T, U)>, SomeError>

(in "normal" Rust code) where SomeError is an informative error message stating that you have to provide both the relevant keys and which one isn't provided.

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 have not dive yet into the macro implementation. But the outcome is really appealing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I fail to understand how TEdgeConfigDefault is used and what's the point of this dummy TEdgeConfigLocation.

Copy link
Contributor Author

@jarhodes314 jarhodes314 May 2, 2023

Choose a reason for hiding this comment

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

TEdgeConfigLocation is a dummy implementation that will be replaced by the real implementation in tedge_config because there wasn't any value in copying the code for it, and I didn't want the macro crate to depend on tedge_config to keep compile times short while I was still developing. Because the location has to be passed into the from_dto method, it has to exist in order to call define_tedge_config!.

TEdgeConfigDefault is an abstraction over the possible functions we can use to generate a default value if one isn't populated. For instance, a NonZeroU16 has to be created via a function call, but for a default port, this doesn't depend on any input, it can be something equivalent to || NonZeroU16::try_from(1883).unwrap(). However some default (or read only) values may depend on other configurations, or the tedge config dir (such as the device certificate path, which is {config_root}/device-certs/tedge-certificate.pem by default. For this, we need a function like:

fn default_device_cert(location: &TEdgeConfigLocation) -> Utf8PathBuf {
    location
        .tedge_config_root_path()
        .join("device-certs")
        .join("tedge-certificate.pem")
}

The trait TEdgeConfigDefault<Output = T> is implemented by both fn() -> T and fn(&TEdgeConfigLocation) -> T, so this allows both of these functions to be called by the exact same (generated) code. This means the default functions just depend on whatever they need, keeping the config definition simple.

The idea (and implementation details) were borrowed from axum, there's a really good talk on it at https://youtu.be/7DOYtnCXucw.

Copy link
Contributor

Choose a reason for hiding this comment

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

TEdgeConfigLocation is a dummy implementation that will be replaced by the real implementation in tedge_config because there wasn't any value in copying the code for it.

It makes sense.

TEdgeConfigDefault is an abstraction over the possible functions we can use to generate a default value if one isn't populated ... so these default functions just depend on whatever they need, keeping the config definition simple.

Thank you. It's all clear now and that's indeed a great design.

The idea (and implementation details) were borrowed from axum, there's a really good talk on it at https://youtu.be/7DOYtnCXucw.

Thank you for the link.

crates/common/tedge_config_macros/src/lib.rs Outdated Show resolved Hide resolved
crates/common/tedge_config_macros/src/lib.rs Outdated Show resolved Hide resolved

static DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs";

define_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.

Just to confirm what I understand:

  • The define_tedge_config! defines a TEdgeConfigDto struct which hierarchy reflects the macro call structure.
  • This DTO manages read / write / types / docs / examples as well as default values.
  • The DTO structure is to be used to update the config but one needs to call TEdgeCsonfigReader::from_dto(&dto, &TEdgeConfigLocation) to get a TEdgeConfig where all the defaults have been resolved (or the errors raised).
  • The TEdgeConfig struct has the same hierarchy and be used as a regular struct with no method invocations to get specific settings however deep there are in the hierarchy.

Is this correct? If so this is really appealing!

Copy link
Contributor Author

@jarhodes314 jarhodes314 May 2, 2023

Choose a reason for hiding this comment

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

That is essentially correct, and it's encouraging that it could be understood (even if only following discussions on the other PR)!

I think at the moment I'm generating the documentation from the reader, so read-only configurations are treated in the same way there to read-write configurations, but that doesn't make a huge difference to generating them from the DTO (and we may just want to get rid of read-only configurations entirely).

The intention with the reader is to only expose an immutable borrow to other crates so they can only read the underlying configuration. I intend to enable writing to the configuration, e.g. to set c8y.url in tedge connect c8y in a similar way to the current setup, where we give the caller &mut TEdgeConfigDto and they just apply the necessary updates.

Copy link
Contributor

Choose a reason for hiding this comment

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

That is essentially correct, and it's encouraging that it could be understood (even if only following discussions on the other PR)!

Following the other PR definitely helps. But the tests too.

I think at the moment I'm generating the documentation from the reader, so read-only configurations are treated in the same way there to read-write configurations, but that doesn't make a huge difference to generating them from the DTO (and we may just want to get rid of read-only configurations entirely).

Just makes what it's simpler to maintain.

The intention with the reader is to only expose an immutable borrow to other crates so they can only read the underlying configuration.

That's perfect.

@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 2, 2023 13:08 — with GitHub Actions Inactive
@github-actions
Copy link
Contributor

github-actions bot commented May 2, 2023

Robot Results

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

Passed Tests

Name ⏱️ Duration Suite
Define Child device 1 ID 0.016 s C8Y Child Alarms Rpi
Normal case when the child device does not exist on c8y cloud 2.013 s C8Y Child Alarms Rpi
Normal case when the child device already exists 1.227 s C8Y Child Alarms Rpi
Reconciliation when the new alarm message arrives, restart the mapper 1.772 s C8Y Child Alarms Rpi
Reconciliation when the alarm that is cleared 6.036 s C8Y Child Alarms Rpi
Prerequisite Parent 19.2 s Child Conf Mgmt Plugin
Prerequisite Child 0.238 s Child Conf Mgmt Plugin
Child device bootstrapping 14.661 s Child Conf Mgmt Plugin
Snapshot from device 60.987 s Child Conf Mgmt Plugin
Child device config update 62.391 s Child Conf Mgmt Plugin
Configuration types should be detected on file change (without restarting service) 49.58 s Inotify Crate
Check lock file existence in default folder 2.052 s Lock File
Check PID number in lock file 3.065 s Lock File
Check PID number in lock file after restarting the services 3.144 s Lock File
Check starting same service twice 2.107 s Lock File
Switch off lock file creation 2.809 s Lock File
Set configuration when file exists 58.585 s Configuration Operation
Set configuration when file does not exist 5.327 s Configuration Operation
Set configuration with broken url 5.768 s Configuration Operation
Get configuration 5.951 s Configuration Operation
Get non existent configuration file 5.249 s Configuration Operation
Get non existent configuration type 4.782 s Configuration Operation
Update configuration plugin config via cloud 5.6530000000000005 s Configuration Operation
Modify configuration plugin config via local filesystem modify inplace 4.1370000000000005 s Configuration Operation
Modify configuration plugin config via local filesystem overwrite 3.649 s Configuration Operation
Update configuration plugin config via local filesystem copy 4.07 s Configuration Operation
Update configuration plugin config via local filesystem move (different directory) 3.6109999999999998 s Configuration Operation
Update configuration plugin config via local filesystem move (same directory) 2.719 s Configuration Operation
Successful firmware operation 67.463 s Firmware Operation
Install with empty firmware name 59.849 s Firmware Operation
Prerequisite Parent 20.848 s Firmware Operation Child Device
Prerequisite Child 7.922 s Firmware Operation Child Device
Child device firmware update 5.984 s Firmware Operation Child Device
Child device firmware update with cache 5.64 s Firmware Operation Child Device
Firmware plugin supports restart via service manager #1932 4.423 s Firmware Operation Child Device Retry
Update Inventory data via inventory.json 1.427 s Inventory Update
Retrieve a JWT tokens 56.243 s Jwt Request
Check running collectd 0.827 s Monitor Device Collectd
Is collectd publishing MQTT messages? 0.837 s Monitor Device Collectd
Check thin-edge monitoring 4.366 s Monitor Device Collectd
Check grouping of measurements 9.395 s Monitor Device Collectd
Update the custom operation dynamically 51.665 s Dynamically Reload Operation
Main device registration 1.7530000000000001 s Device Registration
Child device registration 2.19 s Device Registration
Supports restarting the device 65.242 s Restart Device
Update tedge version from previous using Cumulocity 105.619 s Tedge Self Update
Test if all c8y services are up 51.145 s Service Monitoring
Test if all c8y services are down 53.331 s Service Monitoring
Test if all c8y services are using configured service type 62.379 s Service Monitoring
Test if all c8y services using default service type when service type configured as empty 110.733 s Service Monitoring
Check health status of tedge-mapper-c8y service on broker stop start 23.82 s Service Monitoring
Check health status of tedge-mapper-c8y service on broker restart 26.839 s Service Monitoring
Check health status of child device service 22.805 s Service Monitoring
Successful shell command with output 3.584 s Shell Operation
Check Successful shell command with literal double quotes output 3.2800000000000002 s Shell Operation
Execute multiline shell command 3.459 s Shell Operation
Failed shell command 3.4939999999999998 s Shell Operation
Software list should be populated during startup 53.197 s Software
Install software via Cumulocity 68.517 s Software
Software list should only show currently installed software and not candidates 57.487 s Software
Child devices support sending simple measurements 1.18 s Child Device Telemetry
Child devices support sending custom measurements 1.188 s Child Device Telemetry
Child devices support sending custom events 1.057 s Child Device Telemetry
Child devices support sending custom events overriding the type 3.12 s Child Device Telemetry
Child devices support sending custom alarms #1699 1.093 s Child Device Telemetry
Child devices support sending inventory data via c8y topic 1.146 s Child Device Telemetry
Child device supports sending custom child device measurements directly to c8y 1.549 s Child Device Telemetry
Check retained alarms 51.157 s Raise Alarms
Thin-edge devices support sending simple measurements 1.6 s Thin-Edge Device Telemetry
Thin-edge devices support sending simple measurements with custom type 1.315 s Thin-Edge Device Telemetry
Thin-edge devices support sending custom measurements 1.791 s Thin-Edge Device Telemetry
Thin-edge devices support sending custom events 1.096 s Thin-Edge Device Telemetry
Thin-edge devices support sending custom events overriding the type 1.333 s Thin-Edge Device Telemetry
Thin-edge devices support sending custom alarms #1699 1.116 s Thin-Edge Device Telemetry
Thin-edge device supports sending custom Thin-edge device measurements directly to c8y 1.474 s Thin-Edge Device Telemetry
Thin-edge device support sending inventory data via c8y topic 1.418 s Thin-Edge Device Telemetry
Validate updated data path used by tedge-agent 1.008 s Data Path Config
Validate updated data path used by c8y-firmware-plugin 11.57 s Data Path Config
Stop tedge-agent service 0.347 s Log Path Config
Customize the log path 0.196 s Log Path Config
Initialize tedge-agent 0.254 s Log Path Config
Check created folders 0.206 s Log Path Config
Remove created custom folders 0.236 s Log Path Config
Install thin-edge via apt 60.359 s Install Apt
Install latest via script (from current branch) 32.066 s Install Tedge
Install specific version via script (from current branch) 24.386 s Install Tedge
Install latest tedge via script (from main branch) 31.722 s Install Tedge
Install then uninstall latest tedge via script (from main branch) 65.21 s Install Tedge
Support starting and stopping services 47.541 s Service-Control
Supports a reconnect 62.075 s Test-Commands
Supports disconnect then connect 58.654 s Test-Commands
Update unknown setting 34.92 s Test-Commands
Update known setting 26.126 s Test-Commands
Stop c8y-configuration-plugin 0.233 s Health C8Y-Configuration-Plugin
Update the service file 0.281 s Health C8Y-Configuration-Plugin
Reload systemd files 1.09 s Health C8Y-Configuration-Plugin
Start c8y-configuration-plugin 0.219 s Health C8Y-Configuration-Plugin
Start watchdog service 10.259 s Health C8Y-Configuration-Plugin
Check PID of c8y-configuration-plugin 0.11 s Health C8Y-Configuration-Plugin
Kill the PID 0.461 s Health C8Y-Configuration-Plugin
Recheck PID of c8y-configuration-plugin 6.6370000000000005 s Health C8Y-Configuration-Plugin
Compare PID change 0.001 s Health C8Y-Configuration-Plugin
Stop watchdog service 0.129 s Health C8Y-Configuration-Plugin
Remove entry from service file 0.175 s Health C8Y-Configuration-Plugin
Stop c8y-log-plugin 0.213 s Health C8Y-Log-Plugin
Update the service file 0.143 s Health C8Y-Log-Plugin
Reload systemd files 1.284 s Health C8Y-Log-Plugin
Start c8y-log-plugin 0.291 s Health C8Y-Log-Plugin
Start watchdog service 10.393 s Health C8Y-Log-Plugin
Check PID of c8y-log-plugin 0.179 s Health C8Y-Log-Plugin
Kill the PID 0.327 s Health C8Y-Log-Plugin
Recheck PID of c8y-log-plugin 6.529 s Health C8Y-Log-Plugin
Compare PID change 0.001 s Health C8Y-Log-Plugin
Stop watchdog service 0.271 s Health C8Y-Log-Plugin
Remove entry from service file 0.199 s Health C8Y-Log-Plugin
Stop tedge-mapper 0.265 s Health Tedge Mapper C8Y
Update the service file 0.278 s Health Tedge Mapper C8Y
Reload systemd files 1.092 s Health Tedge Mapper C8Y
Start tedge-mapper 0.256 s Health Tedge Mapper C8Y
Start watchdog service 10.394 s Health Tedge Mapper C8Y
Check PID of tedge-mapper 0.098 s Health Tedge Mapper C8Y
Kill the PID 0.337 s Health Tedge Mapper C8Y
Recheck PID of tedge-mapper 6.619 s Health Tedge Mapper C8Y
Compare PID change 0.001 s Health Tedge Mapper C8Y
Stop watchdog service 0.475 s Health Tedge Mapper C8Y
Remove entry from service file 0.224 s Health Tedge Mapper C8Y
Stop tedge-agent 0.372 s Health Tedge-Agent
Update the service file 0.314 s Health Tedge-Agent
Reload systemd files 0.713 s Health Tedge-Agent
Start tedge-agent 0.256 s Health Tedge-Agent
Start watchdog service 10.412 s Health Tedge-Agent
Check PID of tedge-mapper 0.31 s Health Tedge-Agent
Kill the PID 0.463 s Health Tedge-Agent
Recheck PID of tedge-agent 6.6690000000000005 s Health Tedge-Agent
Compare PID change 0.001 s Health Tedge-Agent
Stop watchdog service 0.303 s Health Tedge-Agent
Remove entry from service file 0.212 s Health Tedge-Agent
Stop tedge-mapper-az 0.201 s Health Tedge-Mapper-Az
Update the service file 0.272 s Health Tedge-Mapper-Az
Reload systemd files 0.878 s Health Tedge-Mapper-Az
Start tedge-mapper-az 0.207 s Health Tedge-Mapper-Az
Start watchdog service 10.392 s Health Tedge-Mapper-Az
Check PID of tedge-mapper-az 0.107 s Health Tedge-Mapper-Az
Kill the PID 0.272 s Health Tedge-Mapper-Az
Recheck PID of tedge-agent 6.852 s Health Tedge-Mapper-Az
Compare PID change 0.001 s Health Tedge-Mapper-Az
Stop watchdog service 0.125 s Health Tedge-Mapper-Az
Remove entry from service file 0.134 s Health Tedge-Mapper-Az
Stop tedge-mapper-collectd 0.235 s Health Tedge-Mapper-Collectd
Update the service file 0.197 s Health Tedge-Mapper-Collectd
Reload systemd files 0.904 s Health Tedge-Mapper-Collectd
Start tedge-mapper-collectd 0.171 s Health Tedge-Mapper-Collectd
Start watchdog service 10.311 s Health Tedge-Mapper-Collectd
Check PID of tedge-mapper-collectd 0.156 s Health Tedge-Mapper-Collectd
Kill the PID 0.389 s Health Tedge-Mapper-Collectd
Recheck PID of tedge-mapper-collectd 6.602 s Health Tedge-Mapper-Collectd
Compare PID change 0.001 s Health Tedge-Mapper-Collectd
Stop watchdog service 0.199 s Health Tedge-Mapper-Collectd
Remove entry from service file 0.183 s Health Tedge-Mapper-Collectd
tedge-collectd-mapper health status 5.692 s Health Tedge-Mapper-Collectd
c8y-log-plugin health status 5.796 s MQTT health endpoints
c8y-configuration-plugin health status 5.618 s MQTT health endpoints
Publish on a local insecure broker 0.301 s Basic Pub Sub
Publish on a local secure broker 3.35 s Basic Pub Sub
Publish on a local secure broker with client authentication 2.308 s Basic Pub Sub
Check remote mqtt broker #1773 2.219 s Remote Mqtt Broker
Wrong package name 0.129 s Improve Tedge Apt Plugin Error Messages
Wrong version 0.125 s Improve Tedge Apt Plugin Error Messages
Wrong type 0.321 s Improve Tedge Apt Plugin Error Messages
tedge_connect_test_positive 0.242 s Tedge Connect Test
tedge_connect_test_negative 0.583 s Tedge Connect Test
tedge_connect_test_sm_services 6.8309999999999995 s Tedge Connect Test
tedge_disconnect_test_sm_services 0.417 s Tedge Connect Test
Install thin-edge.io 16.922 s Call Tedge
call tedge -V 0.169 s Call Tedge
call tedge -h 0.201 s Call Tedge
call tedge -h -V 0.199 s Call Tedge
call tedge help 0.16 s Call Tedge
tedge config list 0.089 s Call Tedge Config List
tedge config list --all 0.084 s Call Tedge Config List
set/unset device.type 0.296 s Call Tedge Config List
set/unset device.key_path 0.305 s Call Tedge Config List
set/unset device.cert_path 0.291 s Call Tedge Config List
set/unset c8y.root_cert_path 0.302 s Call Tedge Config List
set/unset c8y.smartrest.templates 0.393 s Call Tedge Config List
set/unset az.root_cert_path 0.326 s Call Tedge Config List
set/unset az.mapper.timestamp 0.378 s Call Tedge Config List
set/unset mqtt.bind.address 0.392 s Call Tedge Config List
set/unset mqtt.bind.port 0.37 s Call Tedge Config List
set/unset tmp.path 0.343 s Call Tedge Config List
set/unset logs.path 0.524 s Call Tedge Config List
set/unset run.path 0.466 s Call Tedge Config List
Get Put Delete 1.8639999999999999 s Http File Transfer Api
Set keys should return value on stdout 0.116 s Tedge Config Get
Unset keys should not return anything on stdout and warnings on stderr 0.319 s Tedge Config Get
Invalid keys should not return anything on stdout and warnings on stderr 0.201 s Tedge Config Get
Set configuration via environment variables 0.8 s Tedge Config Get
Set unknown configuration via environment variables 0.153 s Tedge Config Get

@didier-wenzek didier-wenzek requested a review from Bravo555 May 2, 2023 15:40
@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 3, 2023 17:27 — with GitHub Actions Inactive
Comment on lines 3 to 6
pub fn all_or_nothing<T, U>(
t: OptionalConfig<T>,
u: OptionalConfig<U>,
) -> Result<Option<(T, U)>, String> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely simpler and better to have this defined as a regular function instead of a macro.

Beyond this specific case, I see this simplification as a better separation of the responsibilities. Adding constraints on what makes sense should be done at the application level while the config should be focus on a more syntactic level (what is set, what is missing, what is ill-formed).

Comment on lines 168 to 169
let dto = TEdgeConfigDto::default();
let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
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 add this to the example, as this is that straight use the generated structs that let me prefer this proposal.

Suggested change
let dto = TEdgeConfigDto::default();
let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
let mut dto = TEdgeConfigDto::default();
dto.mqtt.bind.address = Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));
let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation);
let host: IpAddress = config.mqtt.bind.address;
let port: NonZeroU16 = config.mqtt.bind.port;
println!("mqtt = {}:{}", host, port);

Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't lines 172 and 173 move values out of config? Is this intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't lines 172 and 173 move values out of config? Is this intended?

No, because IpAddress and NonZeroU16 both implement Copy.

@jarhodes314
Copy link
Contributor Author

I haven't updated the examples to make this obvious yet, partially because I'm still playing around with the implementation, but the read-only configurations are slightly cumbersome to use. In order to depend on other configuration values, the input for generating these configurations is &TEdgeConfigReader.

In an ideal world, the fields for these configurations would just hold once_cell::sync::Lazy values, which will automatically load as they are first used. The problem with this is that it would require TEdgeConfigReader to be self-referential.

The solution I've come up with for this (which doesn't quite match what's currently pushed):

let config = TEdgeConfigReader::from_dto(...);

// We can read "normal" configurations simply by doing
assert_eq!(config.mqtt.port, 1883);

// But read-only configurations need `config` passing in as an argument
assert_eq!(config.device.id.try_read(&config).unwrap(), "device-id");

This will have a tiny impact as very few configurations are read-only, but it is slightly annoying. It does make it much easier, however, to deal with the fact the device id computation is fallible, as it means the error is an owned value.

@didier-wenzek
Copy link
Contributor

config.device.id.try_read(&config).unwrap()

That's indeed unusual. But not a big deal as this will be used in really few places. This can even be abstracted with a get_device_id() function provided by tedge_config.

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 also don't have much comments regarding macro implementation, because TBH it's a bit hard to wrap my head around it all, and because I didn't really learn macros, especially procedural ones. But I do get the general gist of a macro, generating a DTO, in turn generating a Reader, that will be used by most of the code that just reads from the configuration. So I have just a few questions and perhaps nitpicks regarding the usage of the macro.

/// Path to the CA certificate used by MQTT clients to use when authenticating the MQTT broker
#[tedge_config(example = "/etc/mosquitto/ca_certificates/ca.crt")]
#[doku(as = "PathBuf")]
#[serde(alias = "cafile")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really related to the macro stuff, but I'd rather see #[serde(rename = ...)] used here and for other options with _ in rust name but no _ in the actual TOML

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'm using alias here to cope with the existing keys (i.e. to accept them in tedge config), but I was trying to make the keys more consistent than they were previously (mainly putting _ anywhere we have words that should be delimited but not by .. If we want all of cafile, certfile etc. to remain as is, we can just modify the field name directly to match tedge config.

#[doku(as = "u16")]
port: NonZeroU16,

auth: {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm affraid I don't completely understand how optional settings work in this proposal. Can only final settings (mqtt.client.auth.cafile, mqtt.client.auth.certfile, etc.) be made optional and entire groups like mqtt.client.auth cannot? How are settings marked as optional? I understand that when #[tedge_config(default = ...)] is used, a default can be used in case a setting is not set, but what if there is no reasonable default?

Copy link
Contributor

Choose a reason for hiding this comment

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

That's a good point. My understanding is that will have to be manage at the application layer using functions like all_or_nothing to interpret (None,None) as None.

Copy link
Contributor Author

@jarhodes314 jarhodes314 May 5, 2023

Choose a reason for hiding this comment

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

If there is no default, the field in the reader is optional. It possibly makes the code slightly less clear here than if you provide an explicit Option<_> for each field that needs it, but I originally tried to implement it that way, and it's frankly just a pain to deal with in the macro, compared to deciding whether a field is optional based on the presence of a default or not.

Groups are never optional (but they are only serialized if they contain values for one or more fields) because this drastically increases the complexity involved in reading and updating the configuration. all_or_nothing is a tool to try and deal with this problem. My intention is that where we need custom logic, like mqtt_config() at the moment, we should just add that as methods on the reader as, like with much of this stuff, most of the configurations don't need especially complex handling.


/// The default device type
#[tedge_config(example = "thin-edge.io")]
#[tedge_config(rename = "type")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this just proxy to #[serde(rename)]? Because e.g. on line 84 we use #[serde(alias = ...)], so when should we use one or the other?

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 does proxy to #[serde(rename)]. Long story short, I couldn't find a nice way of parsing a (strict) subset of serde attributes that we do something special with and allowing the rest to just pass through to the underlying structs.

For rename specifically, this not only affects the TOML deserialisation, but it also affects the key used in tedge config commands, and this is why tedge_config cares about renaming. This generally is only needed for fields like type, where we want to avoid conflicting with Rust keywords as raw identifiers are difficult to read. The macro invocation will fail to compile if you try and use #[serde(rename)] to prevent confusion. Looking back on how much effort goes into establishing if this exists, we could just as easily parse #[serde(rename)] instead.

Aliasing fields, on the other hand, is used to migrate a key from one name to another, e.g. to add or remove underscores. This is used, via doku, to work out if we have renamed the field in the past to allow tedge config to parse the key successfully (with a warning that the key is deprecated). Because the define_tedge_config macro doesn't interact with this attribute directly, it doesn't need to parse it so we can leave it as #[serde(alias = ...)].

I think this question points to a larger possibly-faulty design decision I took. I assumed that it would be simple to be transparent and just use serde attributes where possible, but maybe it might be clearer to replace the common cases (i.e. these two) with tedge_config specific attributes, and give them names more relevant to our specific use case (e.g. #[tedge_config(deprecated_name = "cafile")] instead of #[serde(alias = "cafile)]). Alternatively, the existing solution might be the way, but with "which attribute do I use where" (more clearly) documented.

@Bravo555 @didier-wenzek, what are your opinions on those possible solutions?

Copy link
Contributor

Choose a reason for hiding this comment

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

@jarhodes314 The need for a specific #[tedge_config(rename)] vs #[serde(rename)] makes sense.

About alias, I think the confusion is beyond just a lack of documentation. It's not obvious that #[serde(alias = ...)] has a specific meaning for the tedge_config and that this meaning is added thanks to doku.

So if things were free I would vote for #[tedge_config(deprecated_name = "cafile")].

Copy link
Contributor Author

@jarhodes314 jarhodes314 May 11, 2023

Choose a reason for hiding this comment

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

I've now modified this to match your suggestion. I've also added #[tedge_config(deprecated_key = "mqtt.port")]. The difference between deprecated_name and deprecated_key is that deprecated_name is for renaming a field/group, but deprecated_key is for mapping one key to a different structure (e.g. mqtt.port -> mqtt.bind.port), which cannot just translate to #[serde(alias)]. The macro will emit compiler errors if the user uses the wrong one (by checking if the input contains ., if it does, it can only be a key, and if it doesn't, it can only be a (field/group) name as all fields are nested in at least one group).

I think having different attributes and a compiler error is easier to grasp than one attribute that changes its behaviour dynamically depending on whether the input contains ., but I'm happy to change the behaviour if you disagree with this.

use syn::parse_macro_input;

#[proc_macro]
/// Defines the necessary structures to create a tedge config struct
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO if we do end up basically moving all the complexity into this proc macro, then this necessitates some really really comprehensive documentation of the macro. I'm talking some summary, enumeration of all possible attributes, ideally with examples, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh for sure, I wanted to focus on writing the macro itself first to ensure there weren't major unforeseen issues that will severely affect the usability of the solution.

@@ -0,0 +1,87 @@
use crate::OptionalConfig;

pub fn all_or_nothing<T, U>(
Copy link
Contributor

Choose a reason for hiding this comment

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

If I'm understanding correctly, the function takes only two arguments, what would it look like if we wanted to use it with 3 or 4 arguments? Do we duplicate the function however many times we need or is there a way to solve this more generically, which we'll use when we need it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

At the moment I'm trying to keep this bit simple as this is an area where we have a lot of scope for solving problems we don't really have. I will refactor this to a single tuple argument via a trait so we at least don't need to create a different all_or_nothing function for each cardinality, but for now, I'm going to avoid coming up with any particularly clever solutions to generalising this, as I'm not convinced we really have a use case.

struct_field_paths(None, &fields)
});

impl TEdgeTomlVersion {
Copy link
Contributor

@didier-wenzek didier-wenzek May 11, 2023

Choose a reason for hiding this comment

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

So you propose to manage config versions and to have an automatic conversion. It's a good idea. We will have to double check with @reubenmiller for the proposed changes, though.

My questions are more about how this works.

  • In a migration step like mv("mqtt.port", MqttBindPort), what is this MqttBindPort. I failed to find a definition in the code.
  • Do we have to maintain two versions of the config. In the current state of the PR, we have both but what are the plan?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

MqttBindPort is WritableKey::MqttBindPort. WritableKey is an enum generated by define_tedge_config!, and I imported WritableKey::* in this method to reduce noise.

The idea is that the configuration will be read, the version will be checked, and then migrations carried out if necessary and written back to tedge.toml. If we've applied any migrations, we re-read the configuration before creating the TEdgeConfigReader from it.

I'm not expecting there to be a great burden from maintaining support for existing configuration keys and outdated toml files in future tedge versions, but it should be pretty simple to remove the relevant code if we wish to. As far as I understand, this logic should continue to just work for people upgrading tedge from whatever version, and tedge config will continue to accept (but warn about) the deprecated keys (there are far fewer of those in this PR than the previous PR due to the original key structure being largely preserved here). It uses a toml::Value based representation to ensure that the migration doesn't corrupt any existing data if the version field somehow disappears from the config file.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's clear.

@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 11, 2023 10:09 — with GitHub Actions Inactive
@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 12, 2023 13:02 — with GitHub Actions Inactive
@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 12, 2023 18:43 — with GitHub Actions Inactive
@jarhodes314 jarhodes314 temporarily deployed to Test Pull Request May 16, 2023 07:22 — with GitHub Actions Inactive
@didier-wenzek didier-wenzek changed the title PoC proc macro for tedge config Proc macro for tedge config May 22, 2023
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.

Approved

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