diff --git a/Cargo.lock b/Cargo.lock index be2ca2cd12d..3b8b2d79396 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1110,8 +1110,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944" +dependencies = [ + "darling_core 0.20.1", + "darling_macro 0.20.1", ] [[package]] @@ -1128,17 +1138,42 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2 1.0.56", + "quote 1.0.26", + "strsim", + "syn 2.0.15", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", "quote 1.0.26", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" +dependencies = [ + "darling_core 0.20.1", + "quote 1.0.26", + "syn 2.0.15", +] + [[package]] name = "data-encoding" version = "2.3.3" @@ -1215,9 +1250,9 @@ checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" [[package]] name = "doku" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "966b1227ac4d9d77f4d7e9507dd01c56ceaec6e35888661b54319123da47b159" +checksum = "b738a9a7942e6996e6df51b8a58db17105fe5a448112add828fc9ff93fcd6c8e" dependencies = [ "doku-derive", "serde", @@ -1226,11 +1261,11 @@ dependencies = [ [[package]] name = "doku-derive" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "252ec56116f931b050b5d80512c2c76f4807a297dd95a93f37593dd7650868a5" +checksum = "4dd9ccf9dd3a20ad0a08c3a099693490eb243be3287330d77bd3c9c06225babb" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2 1.0.56", "quote 1.0.26", "syn 1.0.109", @@ -2460,6 +2495,15 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -3604,13 +3648,16 @@ dependencies = [ "anyhow", "assert_cmd", "assert_matches", + "atty", "base64 0.13.1", "camino", "certificate", "clap 3.2.25", + "doku", "hyper", "mockito", "mqtt_tests", + "pad", "pem", "predicates 2.1.5", "reqwest", @@ -3629,6 +3676,7 @@ dependencies = [ "tracing", "url", "which", + "yansi", ] [[package]] @@ -3788,9 +3836,11 @@ dependencies = [ "doku", "figment", "mqtt_channel", + "once_cell", "serde", "serde_ignored", "strum_macros", + "tedge_config_macros", "tedge_test_utils", "tedge_utils", "tempfile", @@ -3802,6 +3852,41 @@ dependencies = [ "url", ] +[[package]] +name = "tedge_config_macros" +version = "0.1.0" +dependencies = [ + "camino", + "doku", + "once_cell", + "serde", + "tedge_config_macros-macro", + "thiserror", + "tracing", + "url", +] + +[[package]] +name = "tedge_config_macros-impl" +version = "0.1.0" +dependencies = [ + "darling 0.20.1", + "heck", + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 2.0.15", +] + +[[package]] +name = "tedge_config_macros-macro" +version = "0.1.0" +dependencies = [ + "proc-macro2 1.0.56", + "quote 1.0.26", + "syn 1.0.109", + "tedge_config_macros-impl", +] + [[package]] name = "tedge_downloader_ext" version = "0.10.0" diff --git a/ci/configure_bridge.sh b/ci/configure_bridge.sh index 628110f3295..9fd06f178f4 100755 --- a/ci/configure_bridge.sh +++ b/ci/configure_bridge.sh @@ -31,11 +31,11 @@ sudo tedge cert show sudo tedge config set c8y.url "$URL" -sudo tedge config set c8y.root.cert.path /etc/ssl/certs +sudo tedge config set c8y.root_cert_path /etc/ssl/certs sudo tedge config set az.url "$IOTHUBNAME.azure-devices.net" -sudo tedge config set az.root.cert.path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem +sudo tedge config set az.root_cert_path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem sudo tedge config list diff --git a/crates/common/tedge_config/Cargo.toml b/crates/common/tedge_config/Cargo.toml index 05cd0948210..6074621ac57 100644 --- a/crates/common/tedge_config/Cargo.toml +++ b/crates/common/tedge_config/Cargo.toml @@ -11,12 +11,14 @@ repository = { workspace = true } [dependencies] camino = { version = "1.1.4", features = ["serde", "serde1"] } certificate = { path = "../certificate" } -doku = "0.20.0" +doku = "0.21" figment = { version = "0.10", features = ["env", "toml"] } mqtt_channel = { path = "../mqtt_channel" } +once_cell = "1.17" serde = { version = "1.0", features = ["derive"] } serde_ignored = "0.1" strum_macros = { version = "0.24" } +tedge_config_macros = { path = "../tedge_config_macros" } tedge_utils = { path = "../tedge_utils" } thiserror = "1.0" toml = "0.7" diff --git a/crates/common/tedge_config/src/lib.rs b/crates/common/tedge_config/src/lib.rs index fac817e454f..63c6c152871 100644 --- a/crates/common/tedge_config/src/lib.rs +++ b/crates/common/tedge_config/src/lib.rs @@ -5,10 +5,10 @@ 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; pub use self::tedge_config_cli::settings::*; pub use self::tedge_config_cli::tedge_config::*; pub use self::tedge_config_cli::tedge_config_defaults::*; -use self::tedge_config_cli::tedge_config_dto::*; pub use self::tedge_config_cli::tedge_config_location::*; pub use self::tedge_config_cli::tedge_config_repository::*; pub use camino::Utf8Path as Path; diff --git a/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs b/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs index a62357e8676..d8e493930a4 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/config_setting.rs @@ -75,4 +75,7 @@ pub enum ConfigSettingError { #[error("An error occurred: {msg}")] Other { msg: &'static str }, + + #[error(transparent)] + Write(#[from] crate::new::WriteError), } diff --git a/crates/common/tedge_config/src/tedge_config_cli/figment.rs b/crates/common/tedge_config/src/tedge_config_cli/figment.rs index 7b90bc222b4..3ef6d256930 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/figment.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/figment.rs @@ -1,12 +1,16 @@ use std::borrow::Cow; +use std::collections::HashSet; use std::fmt::Display; use std::path::Path; use std::path::PathBuf; +use std::sync::Mutex; use figment::providers::Format; use figment::providers::Toml; +use figment::value::Uncased; use figment::Figment; use figment::Metadata; +use once_cell::sync::Lazy; use serde::de::DeserializeOwned; use crate::TEdgeConfigError; @@ -28,10 +32,22 @@ impl ConfigSources for FileOnly { const INCLUDE_ENVIRONMENT: bool = false; } +#[derive(Default, Debug, PartialEq, Eq)] +#[must_use] +pub struct UnusedValueWarnings(Vec<String>); + +impl UnusedValueWarnings { + pub fn emit(self) { + for warning in self.0 { + tracing::warn!("{warning}"); + } + } +} + /// Extract the configuration data from the provided TOML path and `TEDGE_` prefixed environment variables pub fn extract_data<T: DeserializeOwned, Sources: ConfigSources>( path: impl AsRef<Path>, -) -> Result<T, TEdgeConfigError> { +) -> Result<(T, UnusedValueWarnings), TEdgeConfigError> { let env = TEdgeEnv::default(); let figment = Figment::new().merge(Toml::file(path)); @@ -43,14 +59,18 @@ pub fn extract_data<T: DeserializeOwned, Sources: ConfigSources>( let data = extract_exact(&figment, &env); - for warning in unused_value_warnings::<T>(&figment, &env) + let warnings = unused_value_warnings::<T>(&figment, &env) .ok() - .unwrap_or_default() - { - tracing::warn!("{warning}"); + .map(UnusedValueWarnings) + .unwrap_or_default(); + + match data { + Ok(data) => Ok((data, warnings)), + Err(e) => { + warnings.emit(); + Err(e) + } } - - data } fn unused_value_warnings<T: DeserializeOwned>( @@ -172,9 +192,27 @@ impl TEdgeEnv { } fn provider(&self) -> figment::providers::Env { - let pattern = self.separator; - figment::providers::Env::prefixed(self.prefix) - .map(move |name| name.as_str().replacen(pattern, ".", 1).into()) + static WARNINGS: Lazy<Mutex<HashSet<String>>> = Lazy::new(<_>::default); + figment::providers::Env::prefixed(self.prefix).map(move |name| { + let lowercase_name = name.as_str().to_ascii_lowercase(); + Uncased::new( + tracing::subscriber::with_default( + tracing::subscriber::NoSubscriber::default(), + || lowercase_name.parse::<crate::new::WritableKey>(), + ) + .map(|key| key.as_str().to_owned()) + .map_err(|err| { + let is_read_only_key = matches!(err, crate::new::ParseKeyError::ReadOnly(_)); + if is_read_only_key && !WARNINGS.lock().unwrap().insert(lowercase_name.clone()) { + tracing::error!( + "Failed to configure tedge with environment variable `TEDGE_{name}`: {}", + err.to_string().replace('\n', " ") + ) + } + }) + .unwrap_or(lowercase_name), + ) + }) } } @@ -212,6 +250,7 @@ mod tests { assert_eq!( extract_data::<Config, FileAndEnvironment>(&PathBuf::from("tedge.toml")) .unwrap() + .0 .c8y .url, "override.c8y.io" @@ -298,7 +337,7 @@ mod tests { jail.set_env(variable_name, "environment"); let data = extract_data::<Config, FileOnly>("tedge.toml").unwrap(); - assert_eq!(data.value, "config"); + assert_eq!(data.0.value, "config"); Ok(()) }) } diff --git a/crates/common/tedge_config/src/tedge_config_cli/mod.rs b/crates/common/tedge_config/src/tedge_config_cli/mod.rs index 4cfd7de9af5..4e9ad211a1b 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/mod.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/mod.rs @@ -3,9 +3,9 @@ pub mod error; pub mod settings; pub mod tedge_config; pub mod tedge_config_defaults; -pub mod tedge_config_dto; pub mod tedge_config_location; pub mod tedge_config_repository; mod figment; pub mod models; +pub mod new; diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs b/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs index 8b1f80313b9..aa50ecd4208 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/models/connect_url.rs @@ -1,4 +1,6 @@ use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; use url::Host; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] @@ -29,6 +31,26 @@ impl TryFrom<String> for ConnectUrl { } } +impl FromStr for ConnectUrl { + type Err = InvalidConnectUrl; + + fn from_str(input: &str) -> Result<Self, Self::Err> { + ConnectUrl::try_from(input.to_string()) + } +} + +impl fmt::Display for ConnectUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.input.fmt(f) + } +} + +impl doku::Document for ConnectUrl { + fn ty() -> doku::Type { + String::ty() + } +} + impl TryFrom<&str> for ConnectUrl { type Error = InvalidConnectUrl; diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs b/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs index d35ba7b0587..57e98d05c13 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/models/host_port.rs @@ -2,7 +2,9 @@ use crate::ConnectUrl; use crate::Port; use serde::Deserialize; use serde::Serialize; +use std::fmt; use std::num::ParseIntError; +use std::str::FromStr; use url::Host; /// A combination of a host and a port number. @@ -72,21 +74,41 @@ impl<const P: u16> From<HostPort<P>> for String { } } +impl<const P: u16> fmt::Display for HostPort<P> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.input.fmt(f) + } +} + +impl<const P: u16> FromStr for HostPort<P> { + type Err = ParseHostPortError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Self::try_from(s.to_owned()) + } +} + +impl<const P: u16> doku::Document for HostPort<P> { + fn ty() -> doku::Type { + String::ty() + } +} + impl<const P: u16> TryFrom<String> for HostPort<P> { type Error = ParseHostPortError; - fn try_from(s: String) -> Result<Self, Self::Error> { - let (hostname, port) = if let Some((hostname, port)) = s.split_once(':') { + fn try_from(input: String) -> Result<Self, Self::Error> { + let (hostname, port) = if let Some((hostname, port)) = input.split_once(':') { let port = Port(port.parse()?); let hostname: Host<String> = Host::parse(hostname)?; (hostname, port) } else { - let hostname: Host<String> = Host::parse(&s)?; + let hostname: Host<String> = Host::parse(&input)?; (hostname, Port(P)) }; Ok(HostPort { - input: s, + input, hostname, port, }) diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs b/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs index 354aa519512..ee27412341b 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/models/seconds.rs @@ -1,8 +1,14 @@ use std::convert::TryFrom; use std::convert::TryInto; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; -#[derive(Debug, Copy, Clone, Eq, PartialEq)] -pub struct Seconds(pub u64); +#[derive( + Copy, Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq, doku::Document, +)] +#[serde(transparent)] +pub struct Seconds(pub(crate) u64); #[derive(thiserror::Error, Debug)] #[error("Invalid seconds number: '{input}'.")] @@ -30,12 +36,38 @@ impl TryInto<String> for Seconds { } } +impl FromStr for Seconds { + type Err = <u64 as FromStr>::Err; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + u64::from_str(s).map(Self) + } +} + +impl fmt::Display for Seconds { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Seconds { + pub fn duration(self) -> Duration { + Duration::from_secs(self.0) + } +} + impl From<Seconds> for u64 { fn from(val: Seconds) -> Self { val.0 } } +impl From<u64> for Seconds { + fn from(value: u64) -> Self { + Self(value) + } +} + #[cfg(test)] use assert_matches::*; #[test] diff --git a/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs b/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs index 92b69e589e1..2862de50007 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/models/templates_set.rs @@ -1,4 +1,6 @@ +use std::convert::Infallible; use std::convert::TryInto; +use std::str::FromStr; /// Represents a set of smartrest templates. /// @@ -7,6 +9,12 @@ use std::convert::TryInto; #[serde(from = "FromTomlOrCli")] pub struct TemplatesSet(pub Vec<String>); +impl doku::Document for TemplatesSet { + fn ty() -> doku::Type { + Vec::<String>::ty() + } +} + #[derive(serde::Deserialize)] #[serde(from = "String")] struct CommaDelimited(Vec<String>); @@ -39,12 +47,8 @@ impl From<FromTomlOrCli> for TemplatesSet { } } -#[derive(thiserror::Error, Debug)] -#[error("TemplateSet to String conversion failed: {0:?}")] -pub struct TemplatesSetToStringConversionFailure(String); - impl TryFrom<Vec<String>> for TemplatesSet { - type Error = TemplatesSetToStringConversionFailure; + type Error = Infallible; fn try_from(value: Vec<String>) -> Result<Self, Self::Error> { Ok(TemplatesSet(value)) @@ -52,7 +56,7 @@ impl TryFrom<Vec<String>> for TemplatesSet { } impl TryFrom<Vec<&str>> for TemplatesSet { - type Error = TemplatesSetToStringConversionFailure; + type Error = Infallible; fn try_from(value: Vec<&str>) -> Result<Self, Self::Error> { Ok(TemplatesSet( @@ -62,9 +66,9 @@ impl TryFrom<Vec<&str>> for TemplatesSet { } impl TryInto<Vec<String>> for TemplatesSet { - type Error = TemplatesSetToStringConversionFailure; + type Error = Infallible; - fn try_into(self) -> Result<Vec<String>, TemplatesSetToStringConversionFailure> { + fn try_into(self) -> Result<Vec<String>, Self::Error> { Ok(self.0) } } @@ -77,11 +81,24 @@ impl From<TemplatesSet> for String { impl From<String> for TemplatesSet { fn from(val: String) -> Self { + Self::from(val.as_str()) + } +} + +impl<'a> From<&'a str> for TemplatesSet { + fn from(val: &'a str) -> Self { let strings = val.split(',').map(|ss| ss.into()).collect(); TemplatesSet(strings) } } +impl FromStr for TemplatesSet { + type Err = Infallible; + fn from_str(value: &str) -> Result<Self, Self::Err> { + Ok(Self::from(value)) + } +} + impl std::fmt::Display for TemplatesSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) diff --git a/crates/common/tedge_config/src/tedge_config_cli/new.rs b/crates/common/tedge_config/src/tedge_config_cli/new.rs new file mode 100644 index 00000000000..2494618484f --- /dev/null +++ b/crates/common/tedge_config/src/tedge_config_cli/new.rs @@ -0,0 +1,687 @@ +use crate::ConnectUrl; +use crate::HostPort; +use crate::Seconds; +use crate::TEdgeConfigLocation; +use crate::TemplatesSet; +use crate::HTTPS_PORT; +use crate::MQTT_TLS_PORT; +use camino::Utf8PathBuf; +use certificate::CertificateError; +use certificate::PemCertificate; +use doku::Document; +use once_cell::sync::Lazy; +use std::borrow::Cow; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::num::NonZeroU16; +use std::path::PathBuf; +use tedge_config_macros::define_tedge_config; +use tedge_config_macros::struct_field_aliases; +use tedge_config_macros::struct_field_paths; +pub use tedge_config_macros::ConfigNotSet; +use tedge_config_macros::OptionalConfig; +use toml::Table; + +const DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs"; + +pub trait OptionalConfigError<T> { + fn or_err(&self) -> Result<&T, ReadError>; +} + +impl<T> OptionalConfigError<T> for OptionalConfig<T> { + fn or_err(&self) -> Result<&T, ReadError> { + self.or_config_not_set().map_err(ReadError::from) + } +} + +pub struct TEdgeConfig(TEdgeConfigReader); + +impl std::ops::Deref for TEdgeConfig { + type Target = TEdgeConfigReader; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl TEdgeConfig { + pub fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> Self { + Self(TEdgeConfigReader::from_dto(dto, location)) + } +} + +#[derive(serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq, Eq, Debug)] +#[serde(into = "&'static str", try_from = "String")] +/// A version of tedge.toml, used to manage migrations (see [Self::migrations]) +pub enum TEdgeTomlVersion { + One, + Two, +} + +impl Default for TEdgeTomlVersion { + fn default() -> Self { + Self::One + } +} + +impl TryFrom<String> for TEdgeTomlVersion { + type Error = String; + + fn try_from(value: String) -> Result<Self, Self::Error> { + match value.as_str() { + "1" => Ok(Self::One), + "2" => Ok(Self::Two), + _ => todo!(), + } + } +} + +impl From<TEdgeTomlVersion> for &'static str { + fn from(value: TEdgeTomlVersion) -> Self { + match value { + TEdgeTomlVersion::One => "1", + TEdgeTomlVersion::Two => "2", + } + } +} + +impl From<TEdgeTomlVersion> for toml::Value { + fn from(value: TEdgeTomlVersion) -> Self { + let str: &str = value.into(); + toml::Value::String(str.to_owned()) + } +} + +pub enum TomlMigrationStep { + UpdateFieldValue { + key: &'static str, + value: toml::Value, + }, + + MoveKey { + original: &'static str, + target: &'static str, + }, + + RemoveTableIfEmpty { + key: &'static str, + }, +} + +impl TomlMigrationStep { + pub fn apply_to(self, mut toml: toml::Value) -> toml::Value { + match self { + TomlMigrationStep::MoveKey { original, target } => { + let mut doc = &mut toml; + let (tables, field) = original.rsplit_once('.').unwrap(); + for key in tables.split('.') { + if doc.as_table().map(|table| table.contains_key(key)) == Some(true) { + doc = &mut doc[key]; + } else { + return toml; + } + } + let value = doc.as_table_mut().unwrap().remove(field); + + if let Some(value) = value { + let mut doc = &mut toml; + let (tables, field) = target.rsplit_once('.').unwrap(); + for key in tables.split('.') { + let table = doc.as_table_mut().unwrap(); + if !table.contains_key(key) { + table.insert(key.to_owned(), toml::Value::Table(Table::new())); + } + doc = &mut doc[key]; + } + let table = doc.as_table_mut().unwrap(); + // TODO if this returns Some, something is going wrong? Maybe this could be an error, or maybe it doesn't matter + table.insert(field.to_owned(), value); + } + } + TomlMigrationStep::UpdateFieldValue { key, value } => { + let mut doc = &mut toml; + let (tables, field) = key.rsplit_once('.').unwrap(); + for key in tables.split('.') { + let table = doc.as_table_mut().unwrap(); + if !table.contains_key(key) { + table.insert(key.to_owned(), toml::Value::Table(Table::new())); + } + doc = &mut doc[key]; + } + let table = doc.as_table_mut().unwrap(); + // TODO if this returns Some, something is going wrong? Maybe this could be an error, or maybe it doesn't matter + table.insert(field.to_owned(), value); + } + TomlMigrationStep::RemoveTableIfEmpty { key } => { + let mut doc = &mut toml; + let (parents, target) = key.rsplit_once('.').unwrap(); + for key in parents.split('.') { + let table = doc.as_table_mut().unwrap(); + if !table.contains_key(key) { + table.insert(key.to_owned(), toml::Value::Table(Table::new())); + } + doc = &mut doc[key]; + } + let table = doc.as_table_mut().unwrap(); + if let Some(table) = table.get(target) { + let table = table.as_table().unwrap(); + // TODO make sure this is covered in toml migration test + if !table.is_empty() { + return toml; + } + } + table.remove(target); + } + } + + toml + } +} + +/// The keys that can be read from the configuration +pub static READABLE_KEYS: Lazy<Vec<(Cow<'static, str>, doku::Type)>> = Lazy::new(|| { + let ty = TEdgeConfigReader::ty(); + let doku::TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) }; + let doku::Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)}; + struct_field_paths(None, &fields) +}); + +impl TEdgeTomlVersion { + fn next(self) -> Self { + match self { + Self::One => Self::Two, + Self::Two => Self::Two, + } + } + + /// The migrations to upgrade `tedge.toml` from its current version to the + /// next version. + /// + /// If this returns `None`, the version of `tedge.toml` is the latest + /// version, and no migrations need to be applied. + pub fn migrations(self) -> Option<Vec<TomlMigrationStep>> { + use WritableKey::*; + let mv = |original, target: WritableKey| TomlMigrationStep::MoveKey { + original, + target: target.as_str(), + }; + let update_version_field = || TomlMigrationStep::UpdateFieldValue { + key: "config.version", + value: self.next().into(), + }; + let rm = |key| TomlMigrationStep::RemoveTableIfEmpty { key }; + + match self { + Self::One => Some(vec![ + mv("mqtt.port", MqttBindPort), + mv("mqtt.bind_address", MqttBindAddress), + mv("mqtt.client_host", MqttClientHost), + mv("mqtt.client_port", MqttClientPort), + mv("mqtt.client_ca_file", MqttClientAuthCaFile), + mv("mqtt.client_ca_path", MqttClientAuthCaDir), + mv("mqtt.client_auth.cert_file", MqttClientAuthCertFile), + mv("mqtt.client_auth.key_file", MqttClientAuthKeyFile), + rm("mqtt.client_auth"), + mv("mqtt.external_port", MqttExternalBindPort), + mv("mqtt.external_bind_address", MqttExternalBindAddress), + mv("mqtt.external_bind_interface", MqttExternalBindInterface), + mv("mqtt.external_capath", MqttExternalCaPath), + mv("mqtt.external_certfile", MqttExternalCertFile), + mv("mqtt.external_keyfile", MqttExternalKeyFile), + mv("az.mapper_timestamp", AzMapperTimestamp), + mv("aws.mapper_timestamp", AwsMapperTimestamp), + mv("http.port", HttpBindPort), + mv("http.bind_address", HttpBindAddress), + mv("software.default_plugin_type", SoftwarePluginDefault), + mv("run.lock_files", RunLockFiles), + mv("firmware.child_update_timeout", FirmwareChildUpdateTimeout), + mv("c8y.smartrest_templates", C8ySmartrestTemplates), + update_version_field(), + ]), + Self::Two => None, + } + } +} + +define_tedge_config! { + #[tedge_config(reader(skip))] + config: { + #[tedge_config(default(variable = "TEdgeTomlVersion::One"))] + version: TEdgeTomlVersion, + }, + + device: { + /// Identifier of the device within the fleet. It must be globally + /// unique and is derived from the device certificate. + #[tedge_config(readonly( + write_error = "\ + The device id is read from the device certificate and cannot be set directly.\n\ + To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.", + function = "device_id", + ))] + #[tedge_config(example = "Raspberrypi-4d18303a-6d3a-11eb-b1a6-175f6bb72665")] + #[tedge_config(note = "This setting is derived from the device certificate and is therefore read only.")] + #[doku(as = "String")] + id: Result<String, ReadError>, + + /// 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", default(value = "thin-edge.io"))] + #[tedge_config(rename = "type")] + ty: String, + }, + + c8y: { + /// Endpoint URL of Cumulocity tenant + #[tedge_config(example = "your-tenant.cumulocity.com")] + #[tedge_config(reader(private))] + url: ConnectUrl, + + /// The path where Cumulocity 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, + + smartrest: { + /// Set of SmartREST template IDs the device should subscribe to + #[tedge_config(example = "templateId1,templateId2", default(function = "TemplatesSet::default"))] + templates: TemplatesSet, + }, + + + /// HTTP Endpoint for the Cumulocity tenant, with optional port. + #[tedge_config(example = "http.your-tenant.cumulocity.com:1234")] + #[tedge_config(default(from_optional_key = "c8y.url"))] + http: HostPort<HTTPS_PORT>, + + /// MQTT Endpoint for the Cumulocity tenant, with optional port. + #[tedge_config(example = "mqtt.your-tenant.cumulocity.com:1234")] + #[tedge_config(default(from_optional_key = "c8y.url"))] + mqtt: HostPort<MQTT_TLS_PORT>, + + }, + + #[tedge_config(deprecated_name = "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, + } + }, + + aws: { + /// Endpoint URL of AWS IoT tenant + #[tedge_config(example = "your-endpoint.amazonaws.com")] + url: ConnectUrl, + + /// The path where AWS 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/aws-trusted-root-certificates.pem", default(variable = "DEFAULT_ROOT_CERT_PATH"))] + #[doku(as = "PathBuf")] + root_cert_path: Utf8PathBuf, + + mapper: { + /// Whether the AWS 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"), deprecated_key = "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_key = "mqtt.bind.port"))] + #[doku(as = "u16")] + port: NonZeroU16, + + #[tedge_config(reader(private))] + 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")] + #[tedge_config(deprecated_name = "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")] + #[tedge_config(deprecated_name = "cadir")] + ca_dir: Utf8PathBuf, + + /// Path to the client certficate + #[doku(as = "PathBuf")] + #[tedge_config(example = "/etc/mosquitto/auth_certificates/cert.pem")] + #[tedge_config(deprecated_name = "certfile")] + cert_file: Utf8PathBuf, + + /// Path to the client private key + #[doku(as = "PathBuf")] + #[tedge_config(example = "/etc/mosquitto/auth_certificates/key.pem")] + #[tedge_config(deprecated_name = "keyfile")] + key_file: Utf8PathBuf, + } + }, + + external: { + bind: { + /// The port mosquitto binds to for external use + #[tedge_config(example = "8883", deprecated_key = "mqtt.external.port")] + 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")] + #[tedge_config(deprecated_key = "mqtt.external.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")] + #[tedge_config(deprecated_key = "mqtt.external.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")] + #[tedge_config(deprecated_key = "mqtt.external.keyfile")] + key_file: Utf8PathBuf, + } + }, + + http: { + bind: { + /// Http server port used by the File Transfer Service + #[tedge_config(example = "8000", deprecated_key = "http.port")] + port: u16, + + /// Http server address used by the File Transfer Service + #[tedge_config(default(function = "default_http_address"), deprecated_key = "http.address")] + #[tedge_config(example = "127.0.0.1", example = "192.168.1.2")] + address: IpAddr, + }, + }, + + software: { + plugin: { + /// The default software plugin to be used for software management on the device + #[tedge_config(example = "apt")] + default: String, + } + }, + + run: { + /// The directory used to store runtime information, such as file locks + #[doku(as = "PathBuf")] + #[tedge_config(example = "/run", default(value = "/run"))] + path: Utf8PathBuf, + + /// Whether to create a lock file or not + #[tedge_config(example = "true", default(value = true))] + lock_files: bool, + }, + + logs: { + /// The directory used to store logs + #[tedge_config(example = "/var/log", default(value = "/var/log"))] + #[doku(as = "PathBuf")] + path: Utf8PathBuf, + }, + + tmp: { + /// The temporary directory used to download files to the device + #[tedge_config(example = "/tmp", default(value = "/tmp"))] + #[doku(as = "PathBuf")] + path: Utf8PathBuf, + }, + + data: { + /// The directory used to store data like cached files, runtime metadata, etc. + #[tedge_config(example = "/var/tedge", default(value = "/var/tedge"))] + #[doku(as = "PathBuf")] + path: Utf8PathBuf, + }, + + firmware: { + child: { + update: { + /// The timeout limit in seconds for firmware update operations on child devices + #[tedge_config(example = "3600", default(value = 3600_u64))] + timeout: Seconds, + } + } + }, + + service: { + /// The thin-edge.io service's service type + #[tedge_config(rename = "type", example = "systemd", default(value = "service"))] + ty: String, + }, +} + +fn default_http_address(dto: &TEdgeConfigDto) -> IpAddr { + let external_address = dto.mqtt.external.bind.address; + external_address + .or(dto.mqtt.bind.address) + .unwrap_or(Ipv4Addr::LOCALHOST.into()) +} + +fn device_id(reader: &TEdgeConfigReader) -> Result<String, ReadError> { + let pem = PemCertificate::from_pem_file(&reader.device.cert_path) + .map_err(|err| cert_error_into_config_error(ReadOnlyKey::DeviceId.as_str(), err))?; + let device_id = pem + .subject_common_name() + .map_err(|err| cert_error_into_config_error(ReadOnlyKey::DeviceId.as_str(), err))?; + Ok(device_id) +} + +fn cert_error_into_config_error(key: &'static str, err: CertificateError) -> ReadError { + match &err { + CertificateError::IoError(io_err) => match io_err.kind() { + std::io::ErrorKind::NotFound => ReadError::ReadOnlyNotFound { key, + message: concat!( + "The device id is read from the device certificate.\n", + "To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.", + ), + }, + _ => ReadError::DerivationFailed { + key, + cause: format!("{}", err), + }, + }, + _ => ReadError::DerivationFailed { + key, + cause: format!("{}", err), + }, + } +} + +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() +} + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), + + #[error("Config value {key}, cannot be read: {message} ")] + ReadOnlyNotFound { + key: &'static str, + message: &'static str, + }, + + #[error("Derivation for `{key}` failed: {cause}")] + DerivationFailed { key: &'static str, cause: String }, +} + +/// An abstraction over the possible default functions for tedge config values +/// +/// Some configuration defaults are relative to the config location, and +/// this trait allows us to pass that in, or the DTO, both, or neither! +pub trait TEdgeConfigDefault<T, Args> { + type Output; + fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output; +} + +impl<F, Out, T> TEdgeConfigDefault<T, ()> for F +where + F: FnOnce() -> Out + Clone, +{ + type Output = Out; + fn call(self, _: &T, _: &TEdgeConfigLocation) -> Self::Output { + (self)() + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, &T> for F +where + F: FnOnce(&T) -> Out + Clone, +{ + type Output = Out; + fn call(self, data: &T, _location: &TEdgeConfigLocation) -> Self::Output { + (self)(data) + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, (&TEdgeConfigLocation,)> for F +where + F: FnOnce(&TEdgeConfigLocation) -> Out + Clone, +{ + type Output = Out; + fn call(self, _data: &T, location: &TEdgeConfigLocation) -> Self::Output { + (self)(location) + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, (&T, &TEdgeConfigLocation)> for F +where + F: FnOnce(&T, &TEdgeConfigLocation) -> Out + Clone, +{ + type Output = Out; + fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output { + (self)(data, location) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test_case::test_case("device.id")] + #[test_case::test_case("device.type")] + #[test_case::test_case("device.key.path")] + #[test_case::test_case("device.cert.path")] + #[test_case::test_case("c8y.url")] + #[test_case::test_case("c8y.root.cert.path")] + #[test_case::test_case("c8y.smartrest.templates")] + #[test_case::test_case("az.url")] + #[test_case::test_case("az.root.cert.path")] + #[test_case::test_case("aws.url")] + #[test_case::test_case("aws.root.cert.path")] + #[test_case::test_case("aws.mapper.timestamp")] + #[test_case::test_case("az.mapper.timestamp")] + #[test_case::test_case("mqtt.bind_address")] + #[test_case::test_case("http.address")] + #[test_case::test_case("mqtt.client.host")] + #[test_case::test_case("mqtt.client.port")] + #[test_case::test_case("mqtt.client.auth.cafile")] + #[test_case::test_case("mqtt.client.auth.cadir")] + #[test_case::test_case("mqtt.client.auth.certfile")] + #[test_case::test_case("mqtt.client.auth.keyfile")] + #[test_case::test_case("mqtt.port")] + #[test_case::test_case("http.port")] + #[test_case::test_case("mqtt.external.port")] + #[test_case::test_case("mqtt.external.bind_address")] + #[test_case::test_case("mqtt.external.bind_interface")] + #[test_case::test_case("mqtt.external.capath")] + #[test_case::test_case("mqtt.external.certfile")] + #[test_case::test_case("mqtt.external.keyfile")] + #[test_case::test_case("software.plugin.default")] + #[test_case::test_case("tmp.path")] + #[test_case::test_case("logs.path")] + #[test_case::test_case("run.path")] + #[test_case::test_case("data.path")] + #[test_case::test_case("firmware.child.update.timeout")] + #[test_case::test_case("service.type")] + #[test_case::test_case("run.lock_files")] + fn all_0_10_keys_can_be_deserialised(key: &str) { + key.parse::<ReadableKey>().unwrap(); + } + + #[test] + fn missing_c8y_http_directs_user_towards_setting_c8y_url() { + let dto = TEdgeConfigDto::default(); + + let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); + + assert_eq!(reader.c8y.http.key(), "c8y.url"); + } +} diff --git a/crates/common/tedge_config/src/tedge_config_cli/settings.rs b/crates/common/tedge_config/src/tedge_config_cli/settings.rs index 18f0f315eb9..a5ed5764031 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/settings.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/settings.rs @@ -43,7 +43,7 @@ impl ConfigSetting for DeviceTypeSetting { pub struct DeviceKeyPathSetting; impl ConfigSetting for DeviceKeyPathSetting { - const KEY: &'static str = "device.key.path"; + const KEY: &'static str = "device.key_path"; const DESCRIPTION: &'static str = "Path to the private key file. Example: /home/user/.tedge/tedge-private-key.pem"; @@ -60,7 +60,7 @@ impl ConfigSetting for DeviceKeyPathSetting { pub struct DeviceCertPathSetting; impl ConfigSetting for DeviceCertPathSetting { - const KEY: &'static str = "device.cert.path"; + const KEY: &'static str = "device.cert_path"; const DESCRIPTION: &'static str = "Path to the certificate file. Example: /home/user/.tedge/tedge-certificate.crt"; @@ -123,7 +123,7 @@ impl ConfigSetting for C8yMqttSetting { pub struct C8yRootCertPathSetting; impl ConfigSetting for C8yRootCertPathSetting { - const KEY: &'static str = "c8y.root.cert.path"; + const KEY: &'static str = "c8y.root_cert_path"; const DESCRIPTION: &'static str = concat!( "Path where Cumulocity root certificate(s) are located. ", @@ -180,7 +180,7 @@ impl ConfigSetting for AzureUrlSetting { pub struct AzureRootCertPathSetting; impl ConfigSetting for AzureRootCertPathSetting { - const KEY: &'static str = "az.root.cert.path"; + const KEY: &'static str = "az.root_cert_path"; const DESCRIPTION: &'static str = concat!( "Path where Azure IoT root certificate(s) are located. ", @@ -253,7 +253,7 @@ impl ConfigSetting for AwsUrlSetting { pub struct AwsRootCertPathSetting; impl ConfigSetting for AwsRootCertPathSetting { - const KEY: &'static str = "aws.root.cert.path"; + const KEY: &'static str = "aws.root_cert_path"; const DESCRIPTION: &'static str = concat!( "Path where AWS IoT root certificate(s) are located. ", @@ -295,7 +295,7 @@ impl ConfigSetting for MqttClientPortSetting { pub struct MqttClientCafileSetting; impl ConfigSetting for MqttClientCafileSetting { - const KEY: &'static str = "mqtt.client.auth.cafile"; + const KEY: &'static str = "mqtt.client.auth.ca_file"; const DESCRIPTION: &'static str = concat!( "Path to the CA certificate used by MQTT clients to use when ", @@ -309,7 +309,7 @@ impl ConfigSetting for MqttClientCafileSetting { pub struct MqttClientCapathSetting; impl ConfigSetting for MqttClientCapathSetting { - const KEY: &'static str = "mqtt.client.auth.cadir"; + const KEY: &'static str = "mqtt.client.auth.ca_dir"; const DESCRIPTION: &'static str = concat!( "Path to the directory containing CA certificate used by MQTT clients ", @@ -324,7 +324,7 @@ impl ConfigSetting for MqttClientCapathSetting { pub struct MqttClientAuthCertSetting; impl ConfigSetting for MqttClientAuthCertSetting { - const KEY: &'static str = "mqtt.client.auth.certfile"; + const KEY: &'static str = "mqtt.client.auth.cert_file"; const DESCRIPTION: &'static str = "Path to the client certificate used for client authentication"; @@ -336,7 +336,7 @@ impl ConfigSetting for MqttClientAuthCertSetting { pub struct MqttClientAuthKeySetting; impl ConfigSetting for MqttClientAuthKeySetting { - const KEY: &'static str = "mqtt.client.auth.keyfile"; + const KEY: &'static str = "mqtt.client.auth.key_file"; const DESCRIPTION: &'static str = "Path to the client private key used for client authentication"; @@ -348,7 +348,7 @@ impl ConfigSetting for MqttClientAuthKeySetting { pub struct MqttPortSetting; impl ConfigSetting for MqttPortSetting { - const KEY: &'static str = "mqtt.port"; + const KEY: &'static str = "mqtt.bind.port"; const DESCRIPTION: &'static str = concat!( "Mqtt broker port, which is used by the local mqtt clients to publish or subscribe. ", @@ -389,7 +389,7 @@ impl ConfigSetting for HttpBindAddressSetting { pub struct MqttBindAddressSetting; impl ConfigSetting for MqttBindAddressSetting { - const KEY: &'static str = "mqtt.bind_address"; + const KEY: &'static str = "mqtt.bind.address"; const DESCRIPTION: &'static str = concat!( "Mqtt bind address, which is used by the mqtt clients to publish or subscribe. ", @@ -403,7 +403,7 @@ impl ConfigSetting for MqttBindAddressSetting { pub struct MqttExternalPortSetting; impl ConfigSetting for MqttExternalPortSetting { - const KEY: &'static str = "mqtt.external.port"; + const KEY: &'static str = "mqtt.external.bind.port"; const DESCRIPTION: &'static str = concat!( "Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. ", @@ -417,7 +417,7 @@ impl ConfigSetting for MqttExternalPortSetting { pub struct MqttExternalBindAddressSetting; impl ConfigSetting for MqttExternalBindAddressSetting { - const KEY: &'static str = "mqtt.external.bind_address"; + const KEY: &'static str = "mqtt.external.bind.address"; const DESCRIPTION: &'static str = concat!( "IP address / hostname, which the mqtt broker limits incoming connections on. ", @@ -431,7 +431,7 @@ impl ConfigSetting for MqttExternalBindAddressSetting { pub struct MqttExternalBindInterfaceSetting; impl ConfigSetting for MqttExternalBindInterfaceSetting { - const KEY: &'static str = "mqtt.external.bind_interface"; + const KEY: &'static str = "mqtt.external.bind.interface"; const DESCRIPTION: &'static str = concat!( "Name of network interface, which the mqtt broker limits incoming connections on. ", @@ -445,7 +445,7 @@ impl ConfigSetting for MqttExternalBindInterfaceSetting { pub struct MqttExternalCAPathSetting; impl ConfigSetting for MqttExternalCAPathSetting { - const KEY: &'static str = "mqtt.external.capath"; + const KEY: &'static str = "mqtt.external.ca_path"; const DESCRIPTION: &'static str = concat!( "Path to a file containing the PEM encoded CA certificates ", @@ -461,12 +461,12 @@ impl ConfigSetting for MqttExternalCAPathSetting { pub struct MqttExternalCertfileSetting; impl ConfigSetting for MqttExternalCertfileSetting { - const KEY: &'static str = "mqtt.external.certfile"; + const KEY: &'static str = "mqtt.external.cert_file"; const DESCRIPTION: &'static str = concat!( "Path to the certificate file, which is used by external MQTT listener", "Example: /etc/tedge/device-certs/tedge-certificate.pem", - "Note: This setting shall be used together with `mqtt.external.keyfile` for external connections." + "Note: This setting shall be used together with `mqtt.external.key_file` for external connections." ); type Value = Utf8PathBuf; @@ -476,12 +476,12 @@ impl ConfigSetting for MqttExternalCertfileSetting { pub struct MqttExternalKeyfileSetting; impl ConfigSetting for MqttExternalKeyfileSetting { - const KEY: &'static str = "mqtt.external.keyfile"; + const KEY: &'static str = "mqtt.external.key_file"; const DESCRIPTION: &'static str = concat!( "Path to the private key file, which is used by external MQTT listener", "Example: /etc/tedge/device-certs/tedge-private-key.pem", - "Note: This setting shall be used together with `mqtt.external.certfile` for external connections." + "Note: This setting shall be used together with `mqtt.external.cert_file` for external connections." ); type Value = Utf8PathBuf; diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs index 15faf929009..67298e42f5a 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config.rs @@ -1,4 +1,3 @@ -use crate::seconds::Seconds; use crate::*; use camino::Utf8PathBuf; use certificate::CertificateError; @@ -19,7 +18,7 @@ pub fn get_tedge_config() -> Result<TEdgeConfig, TEdgeConfigError> { /// #[derive(Debug)] pub struct TEdgeConfig { - pub(crate) data: TEdgeConfigDto, + pub(crate) data: new::TEdgeConfigDto, pub(crate) config_defaults: TEdgeConfigDefaults, } @@ -48,7 +47,7 @@ impl ConfigSettingAccessor<DeviceTypeSetting> for TEdgeConfig { let device_type = self .data .device - .device_type + .ty .clone() .unwrap_or_else(|| self.config_defaults.default_device_type.clone()); Ok(device_type) @@ -59,12 +58,12 @@ impl ConfigSettingAccessor<DeviceTypeSetting> for TEdgeConfig { _setting: DeviceTypeSetting, value: <DeviceTypeSetting as ConfigSetting>::Value, ) -> ConfigSettingResult<()> { - self.data.device.device_type = Some(value); + self.data.device.ty = Some(value); Ok(()) } fn unset(&mut self, _setting: DeviceTypeSetting) -> ConfigSettingResult<()> { - self.data.device.device_type = None; + self.data.device.ty = None; Ok(()) } } @@ -82,7 +81,7 @@ fn http_bind_address_read_only_error() -> ConfigSettingError { ConfigSettingError::ReadonlySetting { message: concat!( "The http address cannot be set directly. It is read from the mqtt bind address.\n", - "To set 'http.bind_address' to some <address>, you can `tedge config set mqtt.bind_address <address>`.", + "To set 'http.bind_address' to some <address>, you can `tedge config set mqtt.bind.address <address>`.", ), } } @@ -238,7 +237,8 @@ impl ConfigSettingAccessor<C8ySmartRestTemplates> for TEdgeConfig { Ok(self .data .c8y - .smartrest_templates + .smartrest + .templates .clone() .unwrap_or_else(|| self.config_defaults.default_c8y_smartrest_templates.clone())) } @@ -248,12 +248,12 @@ impl ConfigSettingAccessor<C8ySmartRestTemplates> for TEdgeConfig { _setting: C8ySmartRestTemplates, value: TemplatesSet, ) -> ConfigSettingResult<()> { - self.data.c8y.smartrest_templates = Some(value); + self.data.c8y.smartrest.templates = Some(value); Ok(()) } fn unset(&mut self, _setting: C8ySmartRestTemplates) -> ConfigSettingResult<()> { - self.data.c8y.smartrest_templates = None; + self.data.c8y.smartrest.templates = None; Ok(()) } } @@ -363,18 +363,19 @@ impl ConfigSettingAccessor<AzureMapperTimestamp> for TEdgeConfig { Ok(self .data .az - .mapper_timestamp + .mapper + .timestamp .map(Flag) .unwrap_or_else(|| self.config_defaults.default_mapper_timestamp.clone())) } fn update(&mut self, _setting: AzureMapperTimestamp, value: Flag) -> ConfigSettingResult<()> { - self.data.az.mapper_timestamp = Some(value.into()); + self.data.az.mapper.timestamp = Some(value.into()); Ok(()) } fn unset(&mut self, _setting: AzureMapperTimestamp) -> ConfigSettingResult<()> { - self.data.az.mapper_timestamp = None; + self.data.az.mapper.timestamp = None; Ok(()) } } @@ -384,18 +385,19 @@ impl ConfigSettingAccessor<AwsMapperTimestamp> for TEdgeConfig { Ok(self .data .aws - .mapper_timestamp + .mapper + .timestamp .map(Flag) .unwrap_or_else(|| self.config_defaults.default_mapper_timestamp.clone())) } fn update(&mut self, _setting: AwsMapperTimestamp, value: Flag) -> ConfigSettingResult<()> { - self.data.aws.mapper_timestamp = Some(value.into()); + self.data.aws.mapper.timestamp = Some(value.into()); Ok(()) } fn unset(&mut self, _setting: AwsMapperTimestamp) -> ConfigSettingResult<()> { - self.data.aws.mapper_timestamp = None; + self.data.aws.mapper.timestamp = None; Ok(()) } } @@ -430,7 +432,8 @@ impl ConfigSettingAccessor<MqttClientHostSetting> for TEdgeConfig { Ok(self .data .mqtt - .client_host + .client + .host .clone() .unwrap_or(self.config_defaults.default_mqtt_client_host.clone())) } @@ -440,12 +443,12 @@ impl ConfigSettingAccessor<MqttClientHostSetting> for TEdgeConfig { _setting: MqttClientHostSetting, value: String, ) -> ConfigSettingResult<()> { - self.data.mqtt.client_host = Some(value); + self.data.mqtt.client.host = Some(value); Ok(()) } fn unset(&mut self, _setting: MqttClientHostSetting) -> ConfigSettingResult<()> { - self.data.mqtt.client_host = None; + self.data.mqtt.client.host = None; Ok(()) } } @@ -455,7 +458,8 @@ impl ConfigSettingAccessor<MqttClientPortSetting> for TEdgeConfig { Ok(self .data .mqtt - .client_port + .client + .port .map(|p| Port(p.into())) .unwrap_or_else(|| self.config_defaults.default_mqtt_port)) } @@ -465,12 +469,12 @@ impl ConfigSettingAccessor<MqttClientPortSetting> for TEdgeConfig { let port: NonZeroU16 = port.try_into().map_err(|_| ConfigSettingError::Other { msg: "Can't use 0 for a client port", })?; - self.data.mqtt.client_port = Some(port); + self.data.mqtt.client.port = Some(port); Ok(()) } fn unset(&mut self, _setting: MqttClientPortSetting) -> ConfigSettingResult<()> { - self.data.mqtt.client_port = None; + self.data.mqtt.client.port = None; Ok(()) } } @@ -479,10 +483,12 @@ impl ConfigSettingAccessor<MqttClientCafileSetting> for TEdgeConfig { fn query(&self, _setting: MqttClientCafileSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .client_ca_file + .client + .auth + .ca_file .clone() .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.cafile", + key: "mqtt.client.auth.ca_file", }) } @@ -491,12 +497,12 @@ impl ConfigSettingAccessor<MqttClientCafileSetting> for TEdgeConfig { _setting: MqttClientCafileSetting, ca_file: Utf8PathBuf, ) -> ConfigSettingResult<()> { - self.data.mqtt.client_ca_file = Some(ca_file); + self.data.mqtt.client.auth.ca_file = Some(ca_file); Ok(()) } fn unset(&mut self, _setting: MqttClientCafileSetting) -> ConfigSettingResult<()> { - self.data.mqtt.client_ca_file = None; + self.data.mqtt.client.auth.ca_file = None; Ok(()) } } @@ -505,10 +511,12 @@ impl ConfigSettingAccessor<MqttClientCapathSetting> for TEdgeConfig { fn query(&self, _setting: MqttClientCapathSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .client_ca_path + .client + .auth + .ca_dir .clone() .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.cadir", + key: "mqtt.client.auth.ca_dir", }) } @@ -517,12 +525,12 @@ impl ConfigSettingAccessor<MqttClientCapathSetting> for TEdgeConfig { _setting: MqttClientCapathSetting, cafile: Utf8PathBuf, ) -> ConfigSettingResult<()> { - self.data.mqtt.client_ca_path = Some(cafile); + self.data.mqtt.client.auth.ca_dir = Some(cafile); Ok(()) } fn unset(&mut self, _setting: MqttClientCapathSetting) -> ConfigSettingResult<()> { - self.data.mqtt.client_ca_path = None; + self.data.mqtt.client.auth.ca_dir = None; Ok(()) } } @@ -531,17 +539,12 @@ impl ConfigSettingAccessor<MqttClientAuthCertSetting> for TEdgeConfig { fn query(&self, _setting: MqttClientAuthCertSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .client_auth - .as_ref() - .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.certfile", - })? + .client + .auth .cert_file .clone() - // TODO (Marcel): remove unnecessary Options once tedge_config is - // refactored .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.certfile", + key: "mqtt.client.auth.cert_file", }) } @@ -550,24 +553,14 @@ impl ConfigSettingAccessor<MqttClientAuthCertSetting> for TEdgeConfig { _setting: MqttClientAuthCertSetting, cafile: Utf8PathBuf, ) -> ConfigSettingResult<()> { - let client_auth_config = self - .data - .mqtt - .client_auth - .get_or_insert(MqttClientAuthConfig { - cert_file: None, - key_file: None, - }); - - client_auth_config.cert_file = Some(cafile); + self.data.mqtt.client.auth.cert_file = Some(cafile); Ok(()) } fn unset(&mut self, _setting: MqttClientAuthCertSetting) -> ConfigSettingResult<()> { - if let Some(client_auth) = self.data.mqtt.client_auth.as_mut() { - client_auth.cert_file = None; - } + self.data.mqtt.client.auth.cert_file = None; + Ok(()) } } @@ -576,43 +569,28 @@ impl ConfigSettingAccessor<MqttClientAuthKeySetting> for TEdgeConfig { fn query(&self, _setting: MqttClientAuthKeySetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .client_auth - .as_ref() - .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.keyfile", - })? + .client + .auth .key_file .clone() - // TODO (Marcel): remove unnecessary Options once tedge_config is - // refactored .ok_or(ConfigSettingError::ConfigNotSet { - key: "mqtt.client.auth.keyfile", + key: "mqtt.client.auth.key_file", }) } fn update( &mut self, _setting: MqttClientAuthKeySetting, - cafile: Utf8PathBuf, + key_file: Utf8PathBuf, ) -> ConfigSettingResult<()> { - let client_auth_config = self - .data - .mqtt - .client_auth - .get_or_insert(MqttClientAuthConfig { - cert_file: None, - key_file: None, - }); - - client_auth_config.key_file = Some(cafile); + self.data.mqtt.client.auth.key_file = Some(key_file); Ok(()) } fn unset(&mut self, _setting: MqttClientAuthKeySetting) -> ConfigSettingResult<()> { - if let Some(client_auth) = self.data.mqtt.client_auth.as_mut() { - client_auth.key_file = None; - } + self.data.mqtt.client.auth.key_file = None; + Ok(()) } } @@ -622,18 +600,23 @@ impl ConfigSettingAccessor<MqttPortSetting> for TEdgeConfig { Ok(self .data .mqtt + .bind .port + .map(u16::from) .map(Port) .unwrap_or_else(|| self.config_defaults.default_mqtt_port)) } fn update(&mut self, _setting: MqttPortSetting, value: Port) -> ConfigSettingResult<()> { - self.data.mqtt.port = Some(value.into()); + self.data.mqtt.bind.port = + Some(NonZeroU16::new(value.0).ok_or(ConfigSettingError::Other { + msg: "mqtt.bind.port cannot be set to 0", + })?); Ok(()) } fn unset(&mut self, _setting: MqttPortSetting) -> ConfigSettingResult<()> { - self.data.mqtt.port = None; + self.data.mqtt.bind.port = None; Ok(()) } } @@ -643,18 +626,19 @@ impl ConfigSettingAccessor<HttpPortSetting> for TEdgeConfig { Ok(self .data .http + .bind .port .map(Port) .unwrap_or_else(|| self.config_defaults.default_http_port)) } fn update(&mut self, _setting: HttpPortSetting, value: Port) -> ConfigSettingResult<()> { - self.data.http.port = Some(value.into()); + self.data.http.bind.port = Some(value.into()); Ok(()) } fn unset(&mut self, _setting: HttpPortSetting) -> ConfigSettingResult<()> { - self.data.http.port = None; + self.data.http.bind.port = None; Ok(()) } } @@ -690,8 +674,9 @@ impl ConfigSettingAccessor<MqttBindAddressSetting> for TEdgeConfig { Ok(self .data .mqtt - .bind_address - .clone() + .bind + .address + .map(IpAddress) .unwrap_or_else(|| self.config_defaults.default_mqtt_bind_address.clone())) } @@ -700,12 +685,12 @@ impl ConfigSettingAccessor<MqttBindAddressSetting> for TEdgeConfig { _setting: MqttBindAddressSetting, value: IpAddress, ) -> ConfigSettingResult<()> { - self.data.mqtt.bind_address = Some(value); + self.data.mqtt.bind.address = Some(value.0); Ok(()) } fn unset(&mut self, _setting: MqttBindAddressSetting) -> ConfigSettingResult<()> { - self.data.mqtt.bind_address = None; + self.data.mqtt.bind.address = None; Ok(()) } } @@ -714,7 +699,9 @@ impl ConfigSettingAccessor<MqttExternalPortSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalPortSetting) -> ConfigSettingResult<Port> { self.data .mqtt - .external_port + .external + .bind + .port .map(Port) .ok_or(ConfigSettingError::ConfigNotSet { key: MqttExternalPortSetting::KEY, @@ -726,25 +713,23 @@ impl ConfigSettingAccessor<MqttExternalPortSetting> for TEdgeConfig { _setting: MqttExternalPortSetting, value: Port, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_port = Some(value.into()); + self.data.mqtt.external.bind.port = Some(value.into()); Ok(()) } fn unset(&mut self, _setting: MqttExternalPortSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_port = None; + self.data.mqtt.external.bind.port = None; Ok(()) } } impl ConfigSettingAccessor<MqttExternalBindAddressSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalBindAddressSetting) -> ConfigSettingResult<IpAddress> { - self.data - .mqtt - .external_bind_address - .clone() - .ok_or(ConfigSettingError::ConfigNotSet { + self.data.mqtt.external.bind.address.map(IpAddress).ok_or( + ConfigSettingError::ConfigNotSet { key: MqttExternalBindAddressSetting::KEY, - }) + }, + ) } fn update( @@ -752,12 +737,12 @@ impl ConfigSettingAccessor<MqttExternalBindAddressSetting> for TEdgeConfig { _setting: MqttExternalBindAddressSetting, value: IpAddress, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_bind_address = Some(value); + self.data.mqtt.external.bind.address = Some(value.0); Ok(()) } fn unset(&mut self, _setting: MqttExternalBindAddressSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_bind_address = None; + self.data.mqtt.external.bind.address = None; Ok(()) } } @@ -766,7 +751,9 @@ impl ConfigSettingAccessor<MqttExternalBindInterfaceSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalBindInterfaceSetting) -> ConfigSettingResult<String> { self.data .mqtt - .external_bind_interface + .external + .bind + .interface .clone() .ok_or(ConfigSettingError::ConfigNotSet { key: MqttExternalBindInterfaceSetting::KEY, @@ -778,12 +765,12 @@ impl ConfigSettingAccessor<MqttExternalBindInterfaceSetting> for TEdgeConfig { _setting: MqttExternalBindInterfaceSetting, value: String, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_bind_interface = Some(value); + self.data.mqtt.external.bind.interface = Some(value); Ok(()) } fn unset(&mut self, _setting: MqttExternalBindInterfaceSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_bind_interface = None; + self.data.mqtt.external.bind.interface = None; Ok(()) } } @@ -792,7 +779,8 @@ impl ConfigSettingAccessor<MqttExternalCAPathSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalCAPathSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .external_capath + .external + .ca_path .clone() .ok_or(ConfigSettingError::ConfigNotSet { key: MqttExternalCAPathSetting::KEY, @@ -804,12 +792,12 @@ impl ConfigSettingAccessor<MqttExternalCAPathSetting> for TEdgeConfig { _setting: MqttExternalCAPathSetting, value: Utf8PathBuf, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_capath = Some(value); + self.data.mqtt.external.ca_path = Some(value); Ok(()) } fn unset(&mut self, _setting: MqttExternalCAPathSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_capath = None; + self.data.mqtt.external.ca_path = None; Ok(()) } } @@ -818,7 +806,8 @@ impl ConfigSettingAccessor<MqttExternalCertfileSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalCertfileSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .external_certfile + .external + .cert_file .clone() .ok_or(ConfigSettingError::ConfigNotSet { key: MqttExternalCertfileSetting::KEY, @@ -830,12 +819,12 @@ impl ConfigSettingAccessor<MqttExternalCertfileSetting> for TEdgeConfig { _setting: MqttExternalCertfileSetting, value: Utf8PathBuf, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_certfile = Some(value); + self.data.mqtt.external.cert_file = Some(value); Ok(()) } fn unset(&mut self, _setting: MqttExternalCertfileSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_certfile = None; + self.data.mqtt.external.cert_file = None; Ok(()) } } @@ -844,7 +833,8 @@ impl ConfigSettingAccessor<MqttExternalKeyfileSetting> for TEdgeConfig { fn query(&self, _setting: MqttExternalKeyfileSetting) -> ConfigSettingResult<Utf8PathBuf> { self.data .mqtt - .external_keyfile + .external + .key_file .clone() .ok_or(ConfigSettingError::ConfigNotSet { key: MqttExternalKeyfileSetting::KEY, @@ -856,12 +846,12 @@ impl ConfigSettingAccessor<MqttExternalKeyfileSetting> for TEdgeConfig { _setting: MqttExternalKeyfileSetting, value: Utf8PathBuf, ) -> ConfigSettingResult<()> { - self.data.mqtt.external_keyfile = Some(value); + self.data.mqtt.external.key_file = Some(value); Ok(()) } fn unset(&mut self, _setting: MqttExternalKeyfileSetting) -> ConfigSettingResult<()> { - self.data.mqtt.external_keyfile = None; + self.data.mqtt.external.key_file = None; Ok(()) } } @@ -870,7 +860,8 @@ impl ConfigSettingAccessor<SoftwarePluginDefaultSetting> for TEdgeConfig { fn query(&self, _setting: SoftwarePluginDefaultSetting) -> ConfigSettingResult<String> { self.data .software - .default_plugin_type + .plugin + .default .clone() .ok_or(ConfigSettingError::ConfigNotSet { key: SoftwarePluginDefaultSetting::KEY, @@ -882,12 +873,12 @@ impl ConfigSettingAccessor<SoftwarePluginDefaultSetting> for TEdgeConfig { _setting: SoftwarePluginDefaultSetting, value: String, ) -> ConfigSettingResult<()> { - self.data.software.default_plugin_type = Some(value); + self.data.software.plugin.default = Some(value); Ok(()) } fn unset(&mut self, _setting: SoftwarePluginDefaultSetting) -> ConfigSettingResult<()> { - self.data.software.default_plugin_type = None; + self.data.software.plugin.default = None; Ok(()) } } @@ -919,18 +910,18 @@ impl ConfigSettingAccessor<TmpPathSetting> for TEdgeConfig { Ok(self .data .tmp - .dir_path + .path .clone() .unwrap_or_else(|| self.config_defaults.default_tmp_path.clone())) } fn update(&mut self, _setting: TmpPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> { - self.data.tmp.dir_path = Some(value); + self.data.tmp.path = Some(value); Ok(()) } fn unset(&mut self, _setting: TmpPathSetting) -> ConfigSettingResult<()> { - self.data.tmp.dir_path = None; + self.data.tmp.path = None; Ok(()) } } @@ -940,18 +931,18 @@ impl ConfigSettingAccessor<LogPathSetting> for TEdgeConfig { Ok(self .data .logs - .dir_path + .path .clone() .unwrap_or_else(|| self.config_defaults.default_logs_path.clone())) } fn update(&mut self, _setting: LogPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> { - self.data.logs.dir_path = Some(value); + self.data.logs.path = Some(value); Ok(()) } fn unset(&mut self, _setting: LogPathSetting) -> ConfigSettingResult<()> { - self.data.logs.dir_path = None; + self.data.logs.path = None; Ok(()) } } @@ -961,18 +952,18 @@ impl ConfigSettingAccessor<RunPathSetting> for TEdgeConfig { Ok(self .data .run - .dir_path + .path .clone() .unwrap_or_else(|| self.config_defaults.default_run_path.clone())) } fn update(&mut self, _setting: RunPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> { - self.data.run.dir_path = Some(value); + self.data.run.path = Some(value); Ok(()) } fn unset(&mut self, _setting: RunPathSetting) -> ConfigSettingResult<()> { - self.data.run.dir_path = None; + self.data.run.path = None; Ok(()) } } @@ -982,18 +973,18 @@ impl ConfigSettingAccessor<DataPathSetting> for TEdgeConfig { Ok(self .data .data - .dir_path + .path .clone() .unwrap_or_else(|| self.config_defaults.default_data_path.clone())) } fn update(&mut self, _setting: DataPathSetting, value: Utf8PathBuf) -> ConfigSettingResult<()> { - self.data.data.dir_path = Some(value); + self.data.data.path = Some(value); Ok(()) } fn unset(&mut self, _setting: DataPathSetting) -> ConfigSettingResult<()> { - self.data.data.dir_path = None; + self.data.data.path = None; Ok(()) } } @@ -1024,8 +1015,10 @@ impl ConfigSettingAccessor<FirmwareChildUpdateTimeoutSetting> for TEdgeConfig { Ok(self .data .firmware - .child_update_timeout - .map(Seconds) + .child + .update + .timeout + .map(|s| Seconds(s.duration().as_secs())) .unwrap_or(self.config_defaults.default_firmware_child_update_timeout)) } @@ -1034,16 +1027,16 @@ impl ConfigSettingAccessor<FirmwareChildUpdateTimeoutSetting> for TEdgeConfig { _setting: FirmwareChildUpdateTimeoutSetting, value: Seconds, ) -> ConfigSettingResult<()> { - self.data.firmware.child_update_timeout = Some(value.into()); + self.data.firmware.child.update.timeout = Some(Seconds(value.into())); Ok(()) } fn unset(&mut self, _setting: FirmwareChildUpdateTimeoutSetting) -> ConfigSettingResult<()> { - self.data.firmware.child_update_timeout = Some( + self.data.firmware.child.update.timeout = Some(Seconds( self.config_defaults .default_firmware_child_update_timeout .into(), - ); + )); Ok(()) } } @@ -1053,18 +1046,18 @@ impl ConfigSettingAccessor<ServiceTypeSetting> for TEdgeConfig { Ok(self .data .service - .service_type + .ty .clone() .unwrap_or_else(|| self.config_defaults.default_service_type.clone())) } fn update(&mut self, _setting: ServiceTypeSetting, value: String) -> ConfigSettingResult<()> { - self.data.service.service_type = Some(value); + self.data.service.ty = Some(value); Ok(()) } fn unset(&mut self, _setting: ServiceTypeSetting) -> ConfigSettingResult<()> { - self.data.service.service_type = Some(self.config_defaults.default_service_type.clone()); + self.data.service.ty = Some(self.config_defaults.default_service_type.clone()); Ok(()) } } diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs deleted file mode 100644 index 5bcc83cb716..00000000000 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_dto.rs +++ /dev/null @@ -1,337 +0,0 @@ -//! Crate-private plain-old data-type used for serialization. - -use std::num::NonZeroU16; -use std::path::PathBuf; - -use crate::*; -use camino::Utf8PathBuf; -use doku::Document; -use serde::Deserialize; -use serde::Serialize; - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct TEdgeConfigDto { - /// Captures the device specific configurations - #[serde(default)] - pub(crate) device: DeviceConfigDto, - - /// Captures the configurations required to connect to Cumulocity - #[serde(default)] - pub(crate) c8y: CumulocityConfigDto, - - #[serde(default, alias = "azure")] // for version 0.1.0 compatibility - pub(crate) az: AzureConfigDto, - - #[serde(default)] - pub(crate) aws: AwsConfigDto, - - #[serde(default)] - pub(crate) mqtt: MqttConfigDto, - - #[serde(default)] - pub(crate) http: HttpConfigDto, - - #[serde(default)] - pub(crate) software: SoftwareConfigDto, - - #[serde(default)] - pub(crate) tmp: PathConfigDto, - - #[serde(default)] - pub(crate) logs: PathConfigDto, - - #[serde(default)] - pub(crate) run: PathConfigDto, - - #[serde(default)] - pub(crate) data: PathConfigDto, - - #[serde(default)] - pub(crate) firmware: FirmwareConfigDto, - - #[serde(default)] - pub(crate) service: ServiceTypeConfigDto, -} - -/// Represents the device specific configurations defined in the [device] -/// section of the thin edge configuration TOML file -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct DeviceConfigDto { - /// Path where the device's private key is stored - #[doku(example = "/etc/tedge/device-certs/tedge-private-key.pem")] - #[doku(as = "PathBuf")] - pub(crate) key_path: Option<Utf8PathBuf>, - - /// Path where the device's certificate is stored - #[doku(example = "/etc/tedge/device-certs/tedge-certificate.pem")] - #[doku(as = "PathBuf")] - pub(crate) cert_path: Option<Utf8PathBuf>, - - /// The default device type - #[serde(rename = "type")] - #[doku(example = "thin-edge.io")] - pub(crate) device_type: Option<String>, -} - -/// Represents the Cumulocity specific configurations defined in the -/// [c8y] section of the thin edge configuration TOML file -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct CumulocityConfigDto { - /// Endpoint URL of the Cumulocity tenant - #[doku(example = "your-tenant.cumulocity.com", as = "String")] - pub(crate) url: Option<ConnectUrl>, - - /// HTTP Endpoint for the Cumulocity tenant, with optional port. - #[doku(example = "http.your-tenant.cumulocity.com:1234", as = "String")] - pub(crate) http: Option<HostPort<HTTPS_PORT>>, - - /// MQTT Endpoint for the Cumulocity tenant, with optional port. - #[doku(example = "mqtt.your-tenant.cumulocity.com:1234", as = "String")] - pub(crate) mqtt: Option<HostPort<MQTT_TLS_PORT>>, - - /// The path where Cumulocity root certificate(s) are stored. The value can - /// be a directory path as well as the path of the direct certificate file. - #[doku(example = "/etc/tedge/c8y-trusted-root-certificates.pem")] - #[doku(as = "PathBuf")] - pub(crate) root_cert_path: Option<Utf8PathBuf>, - - /// Set of c8y template IDs used for subscriptions - #[doku(literal_example = "templateId1,templateId2", as = "String")] - pub(crate) smartrest_templates: Option<TemplatesSet>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct AzureConfigDto { - /// Endpoint URL of Azure IoT tenant - #[doku(example = "myazure.azure-devices.net", as = "String")] - pub(crate) url: Option<ConnectUrl>, - - /// The path where Azure root certificate(s) are stored - #[doku(example = "/etc/tedge/az-trusted-root-certificates.pem")] - #[doku(as = "PathBuf")] - pub(crate) root_cert_path: Option<Utf8PathBuf>, - - /// Whether Azure mapper should add timestamp or not - #[doku(example = "true")] - pub(crate) mapper_timestamp: Option<bool>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct AwsConfigDto { - /// Endpoint URL of AWS instance - #[doku(example = "your-endpoint.amazonaws.com", as = "String")] - pub(crate) url: Option<ConnectUrl>, - - /// The path where AWS root certificate(s) are stored - #[doku(example = "/etc/tedge/aws-trusted-root-certificates.pem")] - #[doku(as = "PathBuf")] - pub(crate) root_cert_path: Option<Utf8PathBuf>, - - /// Whether Azure mapper should add timestamp or not - #[doku(example = "true")] - pub(crate) mapper_timestamp: Option<bool>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct MqttConfigDto { - /// The address mosquitto binds to for internal use - pub(crate) bind_address: Option<IpAddress>, - // TODO example - /// The port mosquitto binds to for internal use - pub(crate) port: Option<u16>, - - pub(crate) client_host: Option<String>, - - /// Mqtt broker port, which is used by the mqtt clients to publish or - /// subscribe - #[doku(example = "1883", as = "u16")] - // When connecting to a host, port 0 is invalid. When binding, however, port - // 0 is accepted and understood by the system to dynamically assign any free - // port to the process. The process then needs to take notice of what port - // it received, which I'm not sure if we're doing. - // - // If we don't want to allow binding to port 0, then we can also use - // `NonZeroU16` there as well, which because it can never be 0, can make the - // `Option` completely free, because Option can use 0x0000 value for the - // `None` variant. - pub(crate) client_port: Option<NonZeroU16>, - - /// Path to the trusted CA certificate file used by MQTT clients when - /// authenticating the MQTT broker. - #[doku(example = "/etc/mosquitto/ca_certificates/ca.crt", as = "PathBuf")] - pub(crate) client_ca_file: Option<Utf8PathBuf>, - - /// Path to the directory containing trusted CA certificates used by MQTT - /// clients when authenticating the MQTT broker. - #[doku(example = "/etc/mosquitto/ca_certificates", as = "PathBuf")] - pub(crate) client_ca_path: Option<Utf8PathBuf>, - - /// MQTT client authentication configuration, containing a path to a client - /// certificate and a private key. - pub(crate) client_auth: Option<MqttClientAuthConfig>, - - /// The port mosquitto binds to for external use - #[doku(example = "1883")] - pub(crate) external_port: Option<u16>, - - /// The address mosquitto binds to for external use - #[doku(example = "0.0.0.0")] - pub(crate) external_bind_address: Option<IpAddress>, - - /// The interface mosquitto listens on for external use - #[doku(example = "wlan0")] - pub(crate) external_bind_interface: Option<String>, - - // All the paths relating to mosquitto are strings as they need to be safe - // to write to a configuration file (i.e. probably valid utf-8 at the least) - /// Path to a file containing the PEM encoded CA certificates that are - /// trusted when checking incoming client certificates - #[doku(example = "/etc/ssl/certs", as = "PathBuf")] - pub(crate) external_capath: Option<Utf8PathBuf>, - - /// Path to the certificate file which is used by the external MQTT listener - #[doku( - example = "/etc/tedge/device-certs/tedge-certificate.pem", - as = "PathBuf" - )] - pub(crate) external_certfile: Option<Utf8PathBuf>, - - /// Path to the key file which is used by the external MQTT listener - #[doku(example = "/etc/tedge/device-certs/tedge-private-key.pem")] - #[doku(as = "PathBuf")] - pub(crate) external_keyfile: Option<Utf8PathBuf>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct HttpConfigDto { - /// HTTP server port used by the File Transfer Service - #[doku(example = "8000")] - #[serde(alias = "bind_port")] - pub(crate) port: Option<u16>, - - /// HTTP bind address used by the File Transfer service - #[doku(example = "127.0.0.1")] - #[doku(example = "192.168.1.2")] - pub(crate) bind_address: Option<IpAddress>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct SoftwareConfigDto { - pub(crate) default_plugin_type: Option<String>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct PathConfigDto { - #[serde(rename = "path")] - #[doku(as = "PathBuf")] - pub(crate) dir_path: Option<Utf8PathBuf>, - - /// Whether create lock file or not - pub(crate) lock_files: Option<bool>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct FirmwareConfigDto { - pub(crate) child_update_timeout: Option<u64>, -} - -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub struct ServiceTypeConfigDto { - #[serde(rename = "type")] - pub(crate) service_type: Option<String>, -} - -/// Contains MQTT client authentication configuration. -/// -// Despite both cert_file and key_file being required for client authentication, -// fields in this struct are optional because `tedge config set` needs to -// successfully parse the configuration, update it in memory, and then save -// deserialized object. If the upcoming configuration refactor discussed in [1] -// ends up supporting partial updates to such objects, then these fields could -// be made non-optional. -// -// [1]: https://github.com/thin-edge/thin-edge.io/issues/1812 -#[derive(Debug, Default, Deserialize, Serialize, Document)] -pub(crate) struct MqttClientAuthConfig { - /// Path to the client certificate - #[doku(example = "/path/to/client.crt", as = "PathBuf")] - pub cert_file: Option<Utf8PathBuf>, - - /// Path to the client private key - #[doku(example = "/path/to/client.key", as = "PathBuf")] - pub key_file: Option<Utf8PathBuf>, -} - -#[cfg(test)] -mod tests { - use core::panic; - use std::borrow::Cow; - - use figment::providers::Format; - use serde::de::DeserializeOwned; - - use super::*; - - #[test] - fn example_values_can_be_deserialised() { - let ty = TEdgeConfigDto::ty(); - let doku::TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) }; - let doku::Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)}; - for (key, ty) in struct_field_paths(None, &fields) { - verify_examples_for::<TEdgeConfigDto>(&key, ty) - } - } - - fn verify_examples_for<Dto>(key: &str, ty: doku::Type) - where - Dto: Default + Serialize + DeserializeOwned, - { - for example in ty.example.iter().flat_map(|e| e.iter()) { - println!("Testing {key}={example}"); - figment::Jail::expect_with(|jail| { - jail.set_env(key, example); - let figment = figment::Figment::new() - .merge(figment::providers::Toml::string( - &toml::to_string(&Dto::default()).unwrap(), - )) - .merge(figment::providers::Env::raw().split(".")); - - figment.extract::<Dto>().unwrap_or_else(|_| { - panic!("\n\nFailed to deserialize example data: {key}={example}\n\n") - }); - - Ok(()) - }); - } - } - - fn key_name(prefix: Option<&str>, name: &'static str) -> Cow<'static, str> { - match prefix { - Some(prefix) => Cow::Owned(format!("{prefix}.{name}")), - None => Cow::Borrowed(name), - } - } - - fn struct_field_paths( - prefix: Option<&str>, - fields: &[(&'static str, doku::Field)], - ) -> Vec<(Cow<'static, str>, doku::Type)> { - fields - .iter() - .flat_map(|(name, field)| match named_fields(&field.ty.kind) { - Some(fields) => struct_field_paths(Some(&key_name(prefix, name)), fields), - None => vec![(key_name(prefix, name), field.ty.clone())], - }) - .collect() - } - - fn named_fields(kind: &doku::TypeKind) -> Option<&[(&'static str, doku::Field)]> { - match kind { - doku::TypeKind::Struct { - fields: doku::Fields::Named { fields }, - transparent: false, - } => Some(fields), - _ => None, - } - } -} diff --git a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs index aa75221a4a5..0690a232b4c 100644 --- a/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs +++ b/crates/common/tedge_config/src/tedge_config_cli/tedge_config_repository.rs @@ -1,11 +1,14 @@ use crate::*; +use camino::Utf8Path; +use serde::Serialize; use std::fs; -use std::path::PathBuf; use tedge_utils::fs::atomically_write_file_sync; use super::figment::ConfigSources; use super::figment::FileAndEnvironment; use super::figment::FileOnly; +use super::figment::UnusedValueWarnings; +use super::new; /// TEdgeConfigRepository is responsible for loading and storing TEdgeConfig entities. #[derive(Debug, Clone)] @@ -27,9 +30,8 @@ impl ConfigRepository<TEdgeConfig> for TEdgeConfigRepository { type Error = TEdgeConfigError; fn load(&self) -> Result<TEdgeConfig, TEdgeConfigError> { - let config = self.read_file_or_default::<FileAndEnvironment>( - self.config_location.tedge_config_file_path().into(), - )?; + let config = + self.make_tedge_config(self.load_dto::<FileAndEnvironment>(self.toml_path())?)?; Ok(config) } @@ -37,16 +39,28 @@ impl ConfigRepository<TEdgeConfig> for TEdgeConfigRepository { &self, update: &impl Fn(&mut TEdgeConfig) -> ConfigSettingResult<()>, ) -> Result<(), Self::Error> { - let mut config = self.read_file_or_default::<FileOnly>( - self.config_location.tedge_config_file_path().into(), - )?; + let mut config = self.read_file_or_default::<FileOnly>(self.toml_path())?; update(&mut config)?; - self.store(&config) + self.store(&config.data) } } impl TEdgeConfigRepository { + pub fn update_toml_new( + &self, + update: &impl Fn(&mut new::TEdgeConfigDto) -> ConfigSettingResult<()>, + ) -> Result<(), TEdgeConfigError> { + let mut config = self.load_dto::<FileOnly>(self.toml_path())?; + update(&mut config)?; + + self.store(&config) + } + + fn toml_path(&self) -> &Utf8Path { + self.config_location.tedge_config_file_path() + } + pub fn new(config_location: TEdgeConfigLocation) -> Self { let config_defaults = TEdgeConfigDefaults::from(&config_location); Self::new_with_defaults(config_location, config_defaults) @@ -62,20 +76,67 @@ impl TEdgeConfigRepository { } } + pub fn load_new(&self) -> Result<new::TEdgeConfig, TEdgeConfigError> { + let dto = self.load_dto::<FileAndEnvironment>(self.toml_path())?; + Ok(new::TEdgeConfig::from_dto(&dto, &self.config_location)) + } + + fn load_dto<Sources: ConfigSources>( + &self, + path: &Utf8Path, + ) -> Result<new::TEdgeConfigDto, TEdgeConfigError> { + let (dto, warnings) = self.load_dto_with_warnings::<Sources>(path)?; + + warnings.emit(); + + Ok(dto) + } + + fn load_dto_with_warnings<Sources: ConfigSources>( + &self, + path: &Utf8Path, + ) -> Result<(new::TEdgeConfigDto, UnusedValueWarnings), TEdgeConfigError> { + let (mut dto, mut warnings): (new::TEdgeConfigDto, _) = + super::figment::extract_data::<_, Sources>(path)?; + + if let Some(migrations) = dto.config.version.unwrap_or_default().migrations() { + 'migrate_toml: { + let Ok(config) = std::fs::read_to_string(self.toml_path()) else { break 'migrate_toml }; + + tracing::info!("Migrating tedge.toml configuration to version 2"); + + let toml = toml::de::from_str(&config)?; + let migrated_toml = migrations + .into_iter() + .fold(toml, |toml, migration| migration.apply_to(toml)); + + self.store(&migrated_toml)?; + + // Reload DTO to get the settings in the right place + (dto, warnings) = super::figment::extract_data::<_, Sources>(self.toml_path())?; + } + } + + Ok((dto, warnings)) + } + pub fn get_config_location(&self) -> &TEdgeConfigLocation { &self.config_location } fn read_file_or_default<Sources: ConfigSources>( &self, - path: PathBuf, + path: &Utf8Path, ) -> Result<TEdgeConfig, TEdgeConfigError> { - let data: TEdgeConfigDto = super::figment::extract_data::<_, Sources>(path)?; + let dto = self.load_dto::<Sources>(path)?; - self.make_tedge_config(data) + self.make_tedge_config(dto) } - fn make_tedge_config(&self, data: TEdgeConfigDto) -> Result<TEdgeConfig, TEdgeConfigError> { + fn make_tedge_config( + &self, + data: new::TEdgeConfigDto, + ) -> Result<TEdgeConfig, TEdgeConfigError> { Ok(TEdgeConfig { data, config_defaults: self.config_defaults.clone(), @@ -83,18 +144,119 @@ impl TEdgeConfigRepository { } // TODO: Explicitly set the file permissions in this function and file ownership! - fn store(&self, config: &TEdgeConfig) -> Result<(), TEdgeConfigError> { - let toml = toml::to_string_pretty(&config.data)?; + fn store<S: Serialize>(&self, config: &S) -> Result<(), TEdgeConfigError> { + let toml = toml::to_string_pretty(&config)?; // Create `$HOME/.tedge` or `/etc/tedge` directory in case it does not exist yet if !self.config_location.tedge_config_root_path.exists() { fs::create_dir(self.config_location.tedge_config_root_path())?; } - atomically_write_file_sync( - self.config_location.tedge_config_file_path(), - toml.as_bytes(), - )?; + atomically_write_file_sync(self.toml_path(), toml.as_bytes())?; Ok(()) } } + +#[cfg(test)] +mod tests { + use tedge_test_utils::fs::TempTedgeDir; + + use crate::new::TEdgeConfigReader; + + use super::*; + + #[test] + fn old_toml_can_be_read_in_its_entirety() { + let toml = r#"[device] +key_path = "/tedge/device-key.pem" +cert_path = "/tedge/device-cert.pem" +type = "a-device" + +[c8y] +url = "something.latest.stage.c8y.io" +root_cert_path = "/c8y/root-cert.pem" +smartrest_templates = [ + "id1", + "id2", +] + +[az] +url = "something.azure.com" +root_cert_path = "/az/root-cert.pem" +mapper_timestamp = true + +[aws] +url = "something.amazonaws.com" +root_cert_path = "/aws/root-cert.pem" +mapper_timestamp = false + +[mqtt] +bind_address = "192.168.0.1" +port = 1886 +client_host = "192.168.0.1" +client_port = 1885 +client_ca_file = "/mqtt/ca.crt" +client_ca_path = "/mqtt/ca" +external_port = 8765 +external_bind_address = "0.0.0.0" +external_bind_interface = "wlan0" +external_capath = "/mqtt/external/ca.pem" +external_certfile = "/mqtt/external/cert.pem" +external_keyfile = "/mqtt/external/key.pem" + +[mqtt.client_auth] +cert_file = "/mqtt/auth/cert.pem" +key_file = "/mqtt/auth/key.pem" + +[http] +port = 1234 + +[software] +default_plugin_type = "my-plugin" + +[tmp] +path = "/tmp-path" + +[logs] +path = "/logs-path" + +[run] +path = "/run-path" +lock_files = false + +[data] +path = "/data-path" + +[firmware] +child_update_timeout = 3429 + +[service] +type = "a-service-type""#; + let (_tempdir, config_location) = create_temp_tedge_config(toml).unwrap(); + let toml_path = config_location.tedge_config_file_path(); + let (dto, warnings) = TEdgeConfigRepository::new(config_location.clone()) + .load_dto_with_warnings::<FileOnly>(toml_path) + .unwrap(); + + // Figment will warn us if we're not using a field. If we've migrated + // everything successfully, then no warnings will be emitted + assert_eq!(warnings, UnusedValueWarnings::default()); + + let reader = TEdgeConfigReader::from_dto(&dto, &config_location); + + assert_eq!(reader.device.cert_path, "/tedge/device-cert.pem"); + assert_eq!(reader.device.key_path, "/tedge/device-key.pem"); + assert_eq!(reader.device.ty, "a-device"); + assert_eq!(u16::from(reader.mqtt.bind.port), 1886); + assert_eq!(u16::from(reader.mqtt.client.port), 1885); + } + + fn create_temp_tedge_config( + content: &str, + ) -> std::io::Result<(TempTedgeDir, TEdgeConfigLocation)> { + let dir = TempTedgeDir::new(); + dir.file("tedge.toml").with_raw_content(content); + let config_location = TEdgeConfigLocation::from_custom_root(dir.path()); + Ok((dir, config_location)) + } +} diff --git a/crates/common/tedge_config/tests/test_tedge_config.rs b/crates/common/tedge_config/tests/test_tedge_config.rs index 1d8b4b9f199..43dbd5a49c2 100644 --- a/crates/common/tedge_config/tests/test_tedge_config.rs +++ b/crates/common/tedge_config/tests/test_tedge_config.rs @@ -673,7 +673,7 @@ fn test_parse_config_empty_file() -> Result<(), TEdgeConfigError> { ); assert_eq!( config.query(FirmwareChildUpdateTimeoutSetting)?, - Seconds(3600) + Seconds::from(3600) ); assert_eq!(config.query(ServiceTypeSetting)?, "service".to_string()); assert_eq!(config.query(TmpPathSetting)?, Utf8PathBuf::from("/tmp")); @@ -727,7 +727,7 @@ fn test_parse_config_no_config_file() -> Result<(), TEdgeConfigError> { #[test] fn test_invalid_mqtt_port() -> Result<(), TEdgeConfigError> { let toml_conf = r#" -[mqtt] +[mqtt.bind] port = "1883" "#; @@ -735,7 +735,7 @@ port = "1883" let toml_path = config_location.tedge_config_file_path().to_string(); let result = TEdgeConfigRepository::new(config_location).load(); - let expected_err = format!("invalid type: found string \"1883\", expected u16 for key \"mqtt.port\" in {toml_path} TOML file"); + let expected_err = format!("invalid type: found string \"1883\", expected a nonzero u16 for key \"mqtt.bind.port\" in {toml_path} TOML file"); match result { Err(error @ TEdgeConfigError::Figment(_)) => assert_eq!(error.to_string(), expected_err), @@ -1050,7 +1050,7 @@ fn dummy_tedge_config_defaults() -> TEdgeConfigDefaults { default_mqtt_bind_address: IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)), default_http_bind_address: IpAddress(IpAddr::V4(Ipv4Addr::LOCALHOST)), default_c8y_smartrest_templates: TemplatesSet::default(), - default_firmware_child_update_timeout: Seconds(3600), + default_firmware_child_update_timeout: Seconds::from(3600), default_service_type: String::from("service"), default_lock_files: Flag(true), } diff --git a/crates/common/tedge_config_macros/Cargo.toml b/crates/common/tedge_config_macros/Cargo.toml new file mode 100644 index 00000000000..f15e7ece9ad --- /dev/null +++ b/crates/common/tedge_config_macros/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "tedge_config_macros" +version = "0.1.0" +edition = "2021" +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +camino = { version = "*", features = ["serde1"] } +doku = "0.21" +once_cell = "1.17" +serde = "1.0" +tedge_config_macros-macro = { path = "macro" } +thiserror = "1.0" +url = "2.3" + +[dev-dependencies] +tracing = "0.1" diff --git a/crates/common/tedge_config_macros/examples/macro.rs b/crates/common/tedge_config_macros/examples/macro.rs new file mode 100644 index 00000000000..15b76a15332 --- /dev/null +++ b/crates/common/tedge_config_macros/examples/macro.rs @@ -0,0 +1,213 @@ +use camino::Utf8PathBuf; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::num::NonZeroU16; +use std::path::PathBuf; +use tedge_config_macros::*; + +static DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs"; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), + #[error("Something went wrong: {0}")] + GenericError(String), +} + +define_tedge_config! { + #[tedge_config(reader(skip))] + config: { + #[tedge_config(default(value = 1))] + version: u32, + }, + + device: { + #[tedge_config(readonly( + write_error = "\ + The device id is read from the device certificate and cannot be set directly.\n\ + To set 'device.id' to some <id>, you can use `tedge cert create --device-id <id>`.", + function = "device_id", + ))] + #[doku(as = "String")] + id: Result<String, ReadError>, + + /// 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")] + #[tedge_config(rename = "type")] + device_type: String, + }, + + #[tedge_config(deprecated_name = "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, + } + }, + + c8y: { + #[tedge_config(reader(private))] + url: String, + + http: { + #[tedge_config(default(from_optional_key = "c8y.url"))] + url: String, + } + }, + + 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")] + #[tedge_config(deprecated_key = "mqtt.bind.port")] + // 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_key = "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")] + #[tedge_config(deprecated_name = "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")] + #[tedge_config(deprecated_name = "capath")] + ca_path: Utf8PathBuf, + + /// Path to the client certficate + #[doku(as = "PathBuf")] + #[tedge_config(deprecated_name = "certfile")] + cert_file: Utf8PathBuf, + + /// Path to the client private key + #[doku(as = "PathBuf")] + #[tedge_config(deprecated_name = "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")] + #[tedge_config(deprecated_name = "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")] + #[tedge_config(deprecated_name = "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")] + #[tedge_config(deprecated_name = "keyfile")] + key_file: Utf8PathBuf, + } + } +} + +fn device_id(_reader: &TEdgeConfigReader) -> Result<String, ReadError> { + Ok("dummy-device-id".to_owned()) +} + +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() +} + +fn main() { + 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); + + // Typed reads + println!( + "Device id is {}.", + // We have to pass the config into try_read to avoid TEdgeConfigReader being + // self-referential + config.device.id.try_read(&config).as_ref().unwrap() + ); + assert_eq!(u16::from(config.mqtt.bind.port), 1883); + assert_eq!(config.mqtt.external.bind.port.or_none(), None); + assert_eq!( + config.read_string(ReadableKey::DeviceId).unwrap(), + "dummy-device-id" + ); +} diff --git a/crates/common/tedge_config_macros/examples/renaming_keys.rs b/crates/common/tedge_config_macros/examples/renaming_keys.rs new file mode 100644 index 00000000000..27281775fa6 --- /dev/null +++ b/crates/common/tedge_config_macros/examples/renaming_keys.rs @@ -0,0 +1,56 @@ +use camino::Utf8PathBuf; +use std::path::PathBuf; +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), + #[error("Something went wrong: {0}")] + GenericError(String), +} + +define_tedge_config! { + device: { + // Fields and groups can be renamed using #[tedge_config(rename)] + // this is useful + #[tedge_config(rename = "type")] + ty: String, + + }, + + mqtt: { + client: { + auth: { + // Where we have renamed a field, we can use + // #[tedge_config(deprecated_name)] to create an alias that both serde and `tedge config` + // will understand + #[tedge_config(deprecated_name = "cafile")] + #[doku(as = "PathBuf")] + ca_file: Utf8PathBuf, + } + }, + + bind: { + // If we've moved a field, we can use + // #[tedge_config(deprecated_key)] to provide an alternative key for + // `tedge config` to accept. NB: this changes necessitates a toml + // migration as we cannot use serde aliases for a structural change + // like this + #[tedge_config(deprecated_key = "mqtt.port")] + port: u16, + } + } +} + +fn main() { + let parsed_deprecated_key = "mqtt.port".parse::<ReadableKey>().unwrap(); + let parsed_current_key = "mqtt.bind.port".parse::<ReadableKey>().unwrap(); + assert_eq!(parsed_deprecated_key, parsed_current_key); + assert_eq!(parsed_deprecated_key.as_str(), "mqtt.bind.port"); + + let parsed_deprecated_key = "mqtt.client.auth.cafile".parse::<WritableKey>().unwrap(); + let parsed_current_key = "mqtt.client.auth.ca_file".parse::<WritableKey>().unwrap(); + assert_eq!(parsed_deprecated_key, parsed_current_key); + assert_eq!(parsed_deprecated_key.as_str(), "mqtt.client.auth.ca_file") +} diff --git a/crates/common/tedge_config_macros/impl/Cargo.toml b/crates/common/tedge_config_macros/impl/Cargo.toml new file mode 100644 index 00000000000..6d3f8706998 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "tedge_config_macros-impl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +darling = "0.20" +heck = "0.4.1" +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/crates/common/tedge_config_macros/impl/src/dto.rs b/crates/common/tedge_config_macros/impl/src/dto.rs new file mode 100644 index 00000000000..a2336e3e210 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/dto.rs @@ -0,0 +1,120 @@ +use proc_macro2::TokenStream; +use quote::quote; +use syn::parse_quote_spanned; +use syn::spanned::Spanned; + +use crate::input::FieldOrGroup; +use crate::prefixed_type_name; + +pub fn generate( + name: proc_macro2::Ident, + items: &[FieldOrGroup], + doc_comment: &str, +) -> TokenStream { + let mut idents = Vec::new(); + let mut tys = Vec::<syn::Type>::new(); + let mut sub_dtos = Vec::new(); + let mut preserved_attrs: Vec<Vec<&syn::Attribute>> = Vec::new(); + let mut extra_attrs = Vec::new(); + + for item in items { + match item { + FieldOrGroup::Field(field) => { + if !field.dto().skip && field.read_only().is_none() { + idents.push(field.ident()); + tys.push({ + let ty = field.ty(); + parse_quote_spanned!(ty.span()=> Option<#ty>) + }); + sub_dtos.push(None); + preserved_attrs.push(field.attrs().iter().filter(is_preserved).collect()); + extra_attrs.push(quote! {}); + } + } + FieldOrGroup::Group(group) => { + if !group.dto.skip { + let sub_dto_name = prefixed_type_name(&name, group); + let is_default = format!("{sub_dto_name}::is_default"); + idents.push(&group.ident); + tys.push(parse_quote_spanned!(group.ident.span()=> #sub_dto_name)); + sub_dtos.push(Some(generate(sub_dto_name, &group.contents, ""))); + preserved_attrs.push(group.attrs.iter().filter(is_preserved).collect()); + extra_attrs.push(quote! { + #[serde(default)] + #[serde(skip_serializing_if = #is_default)] + }); + } + } + } + } + + quote! { + #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + // We will add more configurations in the future, so this is + // non_exhaustive (see + // https://doc.rust-lang.org/reference/attributes/type_system.html) + #[non_exhaustive] + #[doc = #doc_comment] + pub struct #name { + #( + // The fields are pub as that allows people to easily modify the + // dto via a mutable borrow + #(#preserved_attrs)* + #extra_attrs + pub #idents: #tys, + )* + } + + impl #name { + fn is_default(&self) -> bool { + self == &Self::default() + } + } + + #(#sub_dtos)* + } +} + +fn is_preserved(attr: &&syn::Attribute) -> bool { + match &attr.meta { + // Maybe cfg is useful. Certainly seems sensible to preserve it + syn::Meta::List(list) => list.path.is_ident("serde") || list.path.is_ident("cfg"), + syn::Meta::NameValue(nv) => nv.path.is_ident("doc"), + _ => false, + } +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn doc_comments_are_preserved() { + assert!(is_preserved(&&parse_quote!( + /// Test + ))) + } + + #[test] + fn serde_attributes_are_preserved() { + assert!(is_preserved(&&parse_quote!( + #[serde(alias = "something")] + ))) + } + + #[test] + fn unrecognised_attributes_are_not_preserved() { + assert!(!is_preserved(&&parse_quote!( + #[unknown_crate(unknown_bool)] + ))) + } + + #[test] + fn unrecognised_attributes_of_the_wrong_type_are_not_preserved() { + assert!(!is_preserved(&&parse_quote!( + #[unknown_attribute = "some value"] + ))) + } +} diff --git a/crates/common/tedge_config_macros/impl/src/error.rs b/crates/common/tedge_config_macros/impl/src/error.rs new file mode 100644 index 00000000000..53923ab1d02 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/error.rs @@ -0,0 +1,80 @@ +use crate::optional_error::OptionalError; + +pub fn combine_errors<T>( + items: impl Iterator<Item = Result<T, syn::Error>>, +) -> Result<Vec<T>, syn::Error> { + let mut error = OptionalError::default(); + let mut successful_values = Vec::new(); + for item in items { + match item { + Ok(value) => successful_values.push(value), + Err(e) => error.combine(e), + } + } + error.try_throw().and(Ok(successful_values)) +} + +// Based on https://stackoverflow.com/a/56264023 +pub fn extract_type_from_result(ty: &syn::Type) -> Option<(&syn::Type, &syn::Type)> { + use syn::GenericArgument; + use syn::Path; + use syn::PathArguments; + use syn::PathSegment; + + fn extract_type_path(ty: &syn::Type) -> Option<&Path> { + match *ty { + syn::Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path), + _ => None, + } + } + + fn extract_result_segment(path: &Path) -> Option<&PathSegment> { + let idents_of_path = path.segments.iter().fold(String::new(), |mut acc, v| { + acc.push_str(&v.ident.to_string()); + acc.push('|'); + acc + }); + vec!["Result|", "std|result|Result|", "core|result|Result|"] + .into_iter() + .find(|s| idents_of_path == *s) + .and_then(|_| path.segments.last()) + } + + extract_type_path(ty) + .and_then(extract_result_segment) + .and_then(|path_seg| { + let type_params = &path_seg.arguments; + // It should have only on angle-bracketed param ("<String>"): + match *type_params { + PathArguments::AngleBracketed(ref params) => { + Some((params.args.first()?, params.args.last()?)) + } + _ => None, + } + }) + .and_then(|generic_arg| match generic_arg { + (GenericArgument::Type(ok), GenericArgument::Type(err)) => Some((ok, err)), + _ => None, + }) +} + +#[test] +fn extract_type_from_different_results() { + use syn::parse_quote; + assert_eq!( + extract_type_from_result(&parse_quote!(Result<String, Error>)), + Some((&parse_quote!(String), &parse_quote!(Error))) + ); + assert_eq!( + extract_type_from_result(&parse_quote!(::std::result::Result<String, Error>)), + Some((&parse_quote!(String), &parse_quote!(Error))) + ); + assert_eq!( + extract_type_from_result(&parse_quote!(std::result::Result<String, Error>)), + Some((&parse_quote!(String), &parse_quote!(Error))) + ); + assert_eq!( + extract_type_from_result(&parse_quote!(core::result::Result<String, Error>)), + Some((&parse_quote!(String), &parse_quote!(Error))) + ); +} diff --git a/crates/common/tedge_config_macros/impl/src/input/mod.rs b/crates/common/tedge_config_macros/impl/src/input/mod.rs new file mode 100644 index 00000000000..84a58dc4015 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/input/mod.rs @@ -0,0 +1,4 @@ +mod parse; +mod validate; + +pub use validate::*; diff --git a/crates/common/tedge_config_macros/impl/src/input/parse.rs b/crates/common/tedge_config_macros/impl/src/input/parse.rs new file mode 100644 index 00000000000..9e421127696 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/input/parse.rs @@ -0,0 +1,173 @@ +//! The inital parsing logic +//! +//! This is designed to take a [proc_macro2::TokenStream] and turn it into +//! something useful with the aid of [syn]. +use darling::util::SpannedValue; +use darling::FromAttributes; +use darling::FromField; +use darling::FromMeta; +use syn::parse::Parse; +use syn::punctuated::Punctuated; +use syn::Attribute; +use syn::Token; + +#[derive(Debug)] +pub struct Configuration { + pub groups: Punctuated<FieldOrGroup, Token![,]>, +} + +impl Parse for Configuration { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + Ok(Self { + groups: input.parse_terminated(<_>::parse, Token![,])?, + }) + } +} + +#[derive(FromAttributes)] +#[darling(attributes(tedge_config))] +pub struct ConfigurationAttributes { + #[darling(default)] + pub dto: GroupDtoSettings, + #[darling(default)] + pub reader: ReaderSettings, + #[darling(default, multiple, rename = "deprecated_name")] + pub deprecated_names: Vec<SpannedValue<String>>, + #[darling(default)] + pub rename: Option<SpannedValue<String>>, +} + +#[derive(Debug)] +pub struct ConfigurationGroup { + pub attrs: Vec<syn::Attribute>, + pub dto: GroupDtoSettings, + pub reader: ReaderSettings, + pub deprecated_names: Vec<SpannedValue<String>>, + pub rename: Option<SpannedValue<String>>, + pub ident: syn::Ident, + pub colon_token: Token![:], + pub brace: syn::token::Brace, + pub content: Punctuated<FieldOrGroup, Token![,]>, +} + +impl Parse for ConfigurationGroup { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let content; + let attributes = input.call(Attribute::parse_outer)?; + let known_attributes = ConfigurationAttributes::from_attributes(&attributes)?; + Ok(ConfigurationGroup { + attrs: attributes.into_iter().filter(not_tedge_config).collect(), + dto: known_attributes.dto, + reader: known_attributes.reader, + deprecated_names: known_attributes.deprecated_names, + rename: known_attributes.rename, + ident: input.parse()?, + colon_token: input.parse()?, + brace: syn::braced!(content in input), + content: content.parse_terminated(<_>::parse, Token![,])?, + }) + } +} + +fn not_tedge_config(attr: &syn::Attribute) -> bool { + let is_tedge_config = match &attr.meta { + syn::Meta::List(list) => list.path.is_ident("tedge_config"), + _ => false, + }; + + !is_tedge_config +} + +#[derive(Debug)] +pub enum FieldOrGroup { + Field(ConfigurableField), + Group(ConfigurationGroup), +} + +impl Parse for FieldOrGroup { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + let fork = input.fork(); + + fork.call(Attribute::parse_outer)?; + fork.parse::<syn::Ident>()?; + fork.parse::<Token![:]>()?; + + let lookahead = fork.lookahead1(); + if lookahead.peek(syn::token::Brace) { + input.parse().map(Self::Group) + } else { + input.parse().map(Self::Field) + } + } +} + +#[derive(FromField, Debug)] +#[darling(attributes(tedge_config), forward_attrs)] +pub struct ConfigurableField { + pub attrs: Vec<syn::Attribute>, + #[darling(default)] + pub readonly: Option<ReadonlySettings>, + #[darling(default)] + pub dto: FieldDtoSettings, + #[darling(default)] + pub rename: Option<SpannedValue<String>>, + #[darling(multiple, rename = "deprecated_key")] + pub deprecated_keys: Vec<SpannedValue<String>>, + #[darling(multiple, rename = "deprecated_name")] + pub deprecated_names: Vec<SpannedValue<String>>, + #[darling(default)] + // TODO remove this or separate it from the group ones + pub reader: ReaderSettings, + #[darling(default)] + pub default: Option<FieldDefault>, + #[darling(default)] + pub note: Option<SpannedValue<String>>, + #[darling(multiple, rename = "example")] + pub examples: Vec<SpannedValue<String>>, + pub ident: Option<syn::Ident>, + pub ty: syn::Type, +} + +#[derive(Debug, FromMeta, PartialEq, Eq)] +pub enum FieldDefault { + Variable(syn::Path), + Function(syn::Expr), + FromKey(Punctuated<syn::Ident, syn::Token![.]>), + FromOptionalKey(Punctuated<syn::Ident, syn::Token![.]>), + Value(syn::Lit), + None, +} + +impl Parse for ConfigurableField { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + Ok(Self::from_field(&input.call(syn::Field::parse_named)?)?) + } +} + +#[derive(FromMeta, Debug, Default)] +pub struct GroupDtoSettings { + #[darling(default)] + pub skip: bool, + #[darling(default)] + pub flatten: bool, +} + +#[derive(FromMeta, Debug, Default)] +pub struct FieldDtoSettings { + #[darling(default)] + pub skip: bool, +} + +#[derive(FromMeta, Debug, Default)] +pub struct ReaderSettings { + #[darling(default)] + pub private: bool, + #[darling(default)] + pub skip: bool, +} + +#[derive(FromMeta, Debug)] +pub struct ReadonlySettings { + pub write_error: String, + pub function: syn::Path, +} diff --git a/crates/common/tedge_config_macros/impl/src/input/validate.rs b/crates/common/tedge_config_macros/impl/src/input/validate.rs new file mode 100644 index 00000000000..099f9b3b3fc --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/input/validate.rs @@ -0,0 +1,609 @@ +use std::borrow::Cow; + +use darling::export::NestedMeta; +use darling::util::SpannedValue; +use heck::ToUpperCamelCase; +use quote::format_ident; +use syn::parse_quote_spanned; +use syn::spanned::Spanned; + +use crate::error::combine_errors; +use crate::optional_error::OptionalError; +use crate::optional_error::SynResultExt; + +pub use super::parse::FieldDefault; +pub use super::parse::FieldDtoSettings; +pub use super::parse::GroupDtoSettings; +pub use super::parse::ReaderSettings; +use super::parse::ReadonlySettings; + +#[derive(Debug)] +pub struct Configuration { + pub groups: Vec<FieldOrGroup>, +} + +impl syn::parse::Parse for Configuration { + fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { + super::parse::Configuration::parse(input)?.try_into() + } +} + +impl TryFrom<super::parse::Configuration> for Configuration { + type Error = syn::Error; + + fn try_from(value: super::parse::Configuration) -> Result<Self, Self::Error> { + Ok(Self { + groups: combine_errors(value.groups.into_iter().map(<_>::try_from))?, + }) + } +} + +#[derive(Debug)] +pub struct ConfigurationGroup { + pub attrs: Vec<syn::Attribute>, + pub rename: Option<SpannedValue<String>>, + pub dto: GroupDtoSettings, + pub reader: ReaderSettings, + pub ident: syn::Ident, + pub contents: Vec<FieldOrGroup>, +} + +impl TryFrom<super::parse::ConfigurationGroup> for ConfigurationGroup { + type Error = syn::Error; + + fn try_from(mut value: super::parse::ConfigurationGroup) -> Result<Self, Self::Error> { + deny_attribute( + &value.attrs, + "serde", + "rename", + "tedge_config(rename)", + "rename a group", + )?; + deny_attribute( + &value.attrs, + "serde", + "alias", + "tedge_config(deprecated_name)", + "supply an alias for a group", + )?; + + for name in value.deprecated_names { + // TODO similar errors to fields? + let name_str = name.as_str(); + value + .attrs + .push(parse_quote_spanned! {name.span() => #[serde(alias = #name_str)]}) + } + + Ok(Self { + attrs: value.attrs, + rename: value.rename, + dto: value.dto, + reader: value.reader, + ident: value.ident, + contents: combine_errors(value.content.into_iter().map(<_>::try_from))?, + }) + } +} + +#[derive(Debug)] +pub enum FieldOrGroup { + Field(ConfigurableField), + Group(ConfigurationGroup), +} + +impl FieldOrGroup { + pub fn name(&self) -> Cow<str> { + let rename = match self { + Self::Group(group) => group.rename.as_ref().map(|s| s.as_str()), + Self::Field(field) => field.rename(), + }; + + rename.map_or_else(|| Cow::Owned(self.ident().to_string()), Cow::Borrowed) + } + + pub fn ident(&self) -> &syn::Ident { + match self { + Self::Field(field) => field.ident(), + Self::Group(group) => &group.ident, + } + } + + pub fn is_called(&self, target: &syn::Ident) -> bool { + self.ident() == target + || match self { + Self::Field(field) => field.rename().map_or(false, |rename| *target == rename), + // Groups don't support renaming at the moment + Self::Group(_) => false, + } + } + + pub fn field(&self) -> Option<&ConfigurableField> { + match self { + Self::Field(field) => Some(field), + Self::Group(..) => None, + } + } + + pub fn reader(&self) -> &ReaderSettings { + match self { + Self::Field(field) => field.reader(), + Self::Group(group) => &group.reader, + } + } +} + +impl TryFrom<super::parse::FieldOrGroup> for FieldOrGroup { + type Error = syn::Error; + fn try_from(value: super::parse::FieldOrGroup) -> Result<Self, Self::Error> { + match value { + super::parse::FieldOrGroup::Field(field) => field.try_into().map(Self::Field), + super::parse::FieldOrGroup::Group(group) => group.try_into().map(Self::Group), + } + } +} + +#[derive(Debug)] +pub enum ConfigurableField { + ReadOnly(ReadOnlyField), + ReadWrite(ReadWriteField), +} + +#[derive(Debug)] +pub struct ReadOnlyField { + pub attrs: Vec<syn::Attribute>, + pub deprecated_keys: Vec<SpannedValue<String>>, + pub readonly: ReadonlySettings, + pub rename: Option<SpannedValue<String>>, + pub dto: FieldDtoSettings, + pub reader: ReaderSettings, + pub ident: syn::Ident, + pub ty: syn::Type, +} + +impl ReadOnlyField { + pub fn lazy_reader_name(&self, parents: &[syn::Ident]) -> syn::Ident { + format_ident!( + "LazyReader{}{}", + parents + .iter() + .map(|p| p.to_string().to_upper_camel_case()) + .collect::<Vec<_>>() + .join("."), + self.rename() + .map(<_>::to_owned) + .unwrap_or_else(|| self.ident.to_string()) + .to_upper_camel_case() + ) + } + + pub fn rename(&self) -> Option<&str> { + Some(self.rename.as_ref()?.as_str()) + } +} + +#[derive(Debug)] +pub struct ReadWriteField { + pub attrs: Vec<syn::Attribute>, + pub deprecated_keys: Vec<SpannedValue<String>>, + pub rename: Option<SpannedValue<String>>, + pub dto: FieldDtoSettings, + pub reader: ReaderSettings, + pub examples: Vec<SpannedValue<String>>, + pub ident: syn::Ident, + pub ty: syn::Type, + pub default: FieldDefault, +} + +impl ConfigurableField { + pub fn attrs(&self) -> &[syn::Attribute] { + match self { + Self::ReadOnly(ReadOnlyField { attrs, .. }) + | Self::ReadWrite(ReadWriteField { attrs, .. }) => attrs, + } + } + + pub fn has_guaranteed_default(&self) -> bool { + match self { + Self::ReadWrite(_) => !self.is_optional(), + Self::ReadOnly(..) => false, + } + } + + pub fn is_optional(&self) -> bool { + matches!( + self, + Self::ReadWrite(ReadWriteField { + default: FieldDefault::FromOptionalKey(_) | FieldDefault::None, + .. + }) + ) + } + + pub fn ident(&self) -> &syn::Ident { + match self { + Self::ReadOnly(ReadOnlyField { ident, .. }) + | Self::ReadWrite(ReadWriteField { ident, .. }) => ident, + } + } + + pub fn rename(&self) -> Option<&str> { + match self { + Self::ReadOnly(ReadOnlyField { rename, .. }) + | Self::ReadWrite(ReadWriteField { rename, .. }) => Some(rename.as_ref()?.as_str()), + } + } + + pub fn ty(&self) -> &syn::Type { + match self { + Self::ReadOnly(ReadOnlyField { ty, .. }) + | Self::ReadWrite(ReadWriteField { ty, .. }) => ty, + } + } + + pub fn dto(&self) -> &FieldDtoSettings { + match self { + Self::ReadOnly(ReadOnlyField { dto, .. }) + | Self::ReadWrite(ReadWriteField { dto, .. }) => dto, + } + } + + #[allow(unused)] + pub fn reader(&self) -> &ReaderSettings { + match self { + Self::ReadOnly(ReadOnlyField { reader, .. }) + | Self::ReadWrite(ReadWriteField { reader, .. }) => reader, + } + } + + pub fn read_write(&self) -> Option<&ReadWriteField> { + match self { + Self::ReadWrite(field) => Some(field), + Self::ReadOnly(_) => None, + } + } + + pub fn read_only(&self) -> Option<&ReadOnlyField> { + match self { + Self::ReadOnly(field) => Some(field), + Self::ReadWrite(_) => None, + } + } + + pub fn deprecated_keys(&self) -> impl Iterator<Item = &str> { + let keys = match self { + Self::ReadOnly(field) => &field.deprecated_keys, + Self::ReadWrite(field) => &field.deprecated_keys, + }; + keys.iter().map(|key| key.as_str()) + } +} + +impl TryFrom<super::parse::ConfigurableField> for ConfigurableField { + type Error = syn::Error; + fn try_from(mut value: super::parse::ConfigurableField) -> Result<Self, Self::Error> { + let mut custom_errors = OptionalError::default(); + + let attrs = &value.attrs; + deny_attribute( + attrs, + "serde", + "rename", + "tedge_config(rename)", + "rename a field", + ) + .append_err_to(&mut custom_errors); + deny_attribute( + attrs, + "serde", + "alias", + "tedge_config(deprecated_name)", + "create an alias for a field", + ) + .append_err_to(&mut custom_errors); + deny_attribute( + attrs, + "doku", + "example", + "tedge_config(example)", + "supply an example value for a field", + ) + .append_err_to(&mut custom_errors); + + if let Some(renamed_to) = &value.rename { + let span = renamed_to.span(); + let literal = renamed_to.as_str(); + value + .attrs + .push(parse_quote_spanned!(span=> #[serde(rename = #literal)])) + } + + for name in value.deprecated_names { + let name_str = name.as_str(); + if name.contains('.') { + custom_errors.combine(syn::Error::new( + name.span(), + format!("this a path rather than a field or group name. Did you mean to use #[tedge_config(deprecated_key = \"{name_str}\")] instead?") + )); + } + value + .attrs + .push(parse_quote_spanned! {name.span()=> #[serde(alias = #name_str)]}) + } + + for key in &value.deprecated_keys { + if !key.contains('.') { + custom_errors.combine(syn::Error::new( + key.span(), + format!("this is just a field or group name, not a key (which would contain one or more `.`s). Did you mean to use #[tedge_config(deprecated_name = \"{}\"] instead?", key.as_str()) + )); + } + } + + for example in &value.examples { + let example_str = example.as_str(); + value + .attrs + .push(parse_quote_spanned! {example.span()=> #[doku(example = #example_str)]}); + } + + if let Some(note) = value.note { + value.attrs.push(tedge_note_to_doku_meta(¬e)); + } + + custom_errors.try_throw()?; + + if let Some(readonly) = value.readonly { + Ok(Self::ReadOnly(ReadOnlyField { + attrs: value.attrs, + deprecated_keys: value.deprecated_keys, + rename: value.rename, + ident: value.ident.unwrap(), + readonly, + ty: value.ty, + dto: value.dto, + reader: value.reader, + })) + } else { + Ok(Self::ReadWrite(ReadWriteField { + attrs: value.attrs, + deprecated_keys: value.deprecated_keys, + rename: value.rename, + examples: value.examples, + ident: value.ident.unwrap(), + ty: value.ty, + dto: value.dto, + reader: value.reader, + default: value.default.unwrap_or(FieldDefault::None), + })) + } + } +} + +fn deny_attribute( + attrs: &[syn::Attribute], + krate: &str, + attribute: &str, + our_name: &str, + action: &str, +) -> Result<(), syn::Error> { + attrs + .iter() + .filter(|attr| attr.path().is_ident(krate)) + .filter_map(|attr| attr.meta.require_list().ok()) + .filter_map(|attr| darling::ast::NestedMeta::parse_meta_list(attr.tokens.clone()).ok()) + .flatten() + .filter_map(|attr| match attr { + NestedMeta::Meta(m) => Some(m), + _ => None, + }) + .filter_map(|meta| Some(meta.require_name_value().ok()?.to_owned())) + .filter(|attr| attr.path.is_ident(attribute)) + .map(|attr| { + syn::Error::new( + attr.span(), + format!("use #[{our_name}] instead of #[{krate}({attribute})] to {action}"), + ) + }) + .fold(OptionalError::default(), |errors, e| { + errors.combine_owned(e) + }) + .try_throw() +} + +fn tedge_note_to_doku_meta(note: &SpannedValue<String>) -> syn::Attribute { + let meta = format!("note = {}", note.as_str()); + parse_quote_spanned!(note.span()=> #[doku(meta(#meta))]) +} + +#[cfg(test)] +mod tests { + use super::*; + use proc_macro2::Span; + use quote::quote; + use syn::parse_quote; + + #[test] + fn doku_examples_are_denied() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + device: { + #[doku(example = "test")] + id: String, + }, + }) + .unwrap(); + + assert!(Configuration::try_from(input).is_err()); + } + + #[test] + fn tedge_note_is_converted_to_doku_meta() { + let note = SpannedValue::new("A note".to_owned(), Span::call_site()); + assert_eq!( + tedge_note_to_doku_meta(¬e), + parse_quote!( + #[doku(meta("note = A note"))] + ) + ); + } + + #[test] + fn serde_rename_is_denied_for_fields() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + device: { + #[serde(rename = "type")] + ty: String, + }, + }) + .unwrap(); + + let error = Configuration::try_from(input).unwrap_err(); + assert!(error.to_string().contains("#[tedge_config(rename)]")) + } + + #[test] + fn serde_alias_is_denied_for_fields() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + device: { + #[serde(alias = "type")] + ty: String, + }, + }) + .unwrap(); + + let error = Configuration::try_from(input).unwrap_err(); + assert!(error + .to_string() + .contains("#[tedge_config(deprecated_name)]")) + } + + #[test] + fn serde_alias_is_denied_for_groups() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + #[serde(alias = "dev")] + device: { + ty: String, + }, + }) + .unwrap(); + + let error = Configuration::try_from(input).unwrap_err(); + assert!(error + .to_string() + .contains("#[tedge_config(deprecated_name)]")) + } + + #[test] + fn deprecated_key_accepts_valid_keys_for_fields() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + mqtt: { + bind: { + #[tedge_config(deprecated_key = "mqtt.port")] + port: u16, + } + }, + }) + .unwrap(); + + Configuration::try_from(input).unwrap(); + } + + #[test] + fn deprecated_name_accepts_valid_names_for_fields() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + mqtt: { + auth: { + #[tedge_config(deprecated_name = "cafile")] + ca_file: u16, + } + }, + }) + .unwrap(); + + Configuration::try_from(input).unwrap(); + } + + #[test] + fn rename_accepts_valid_keys_for_groups() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + mqtt: { + #[tedge_config(rename = "notbind")] + bind: { + port: u16, + } + }, + }) + .unwrap(); + + Configuration::try_from(input).unwrap(); + } + + #[test] + fn deprecated_name_accepts_valid_names_for_groups() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + mqtt: { + #[tedge_config(deprecated_name = "old_auth")] + auth: { + ca_file: u16, + } + }, + }) + .unwrap(); + + Configuration::try_from(input).unwrap(); + } + + #[test] + fn group_name_is_derived_from_ident_if_not_renamed() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + c8y: { + url: String, + } + }) + .unwrap(); + + let configuration = Configuration::try_from(input).unwrap(); + + assert_eq!(configuration.groups[0].name(), "c8y") + } + + #[test] + fn group_can_be_renamed() { + let input: super::super::parse::Configuration = syn::parse2(quote! { + #[tedge_config(rename = "cumulocity")] + c8y: { + url: String, + } + }) + .unwrap(); + + let configuration = Configuration::try_from(input).unwrap(); + + assert_eq!(configuration.groups[0].name(), "cumulocity") + } + + #[test] + fn field_name_is_derived_from_ident_if_not_renamed() { + let input: super::super::parse::ConfigurableField = syn::parse2(quote! { + ty: String + }) + .unwrap(); + + let field = FieldOrGroup::Field(ConfigurableField::try_from(input).unwrap()); + + assert_eq!(field.name(), "ty") + } + + #[test] + fn field_can_be_renamed() { + let input: super::super::parse::ConfigurableField = syn::parse2(quote! { + #[tedge_config(rename = "type")] + ty: String + }) + .unwrap(); + + let field = FieldOrGroup::Field(ConfigurableField::try_from(input).unwrap()); + + assert_eq!(field.name(), "type") + } +} diff --git a/crates/common/tedge_config_macros/impl/src/lib.rs b/crates/common/tedge_config_macros/impl/src/lib.rs new file mode 100644 index 00000000000..39a2ddafaaf --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/lib.rs @@ -0,0 +1,191 @@ +//! This crate implements the macro for `tedge_config_macros` and should not be used directly. + +use heck::ToUpperCamelCase; +use optional_error::OptionalError; +use proc_macro2::Span; +use proc_macro2::TokenStream; +use quote::quote; +use quote::quote_spanned; + +mod dto; +mod error; +mod input; +mod optional_error; +mod query; +mod reader; + +#[doc(hidden)] +pub fn generate_configuration(tokens: TokenStream) -> Result<TokenStream, syn::Error> { + let input: input::Configuration = syn::parse2(tokens)?; + + let mut error = OptionalError::default(); + let fields_with_keys = input + .groups + .iter() + .flat_map(|group| match group { + input::FieldOrGroup::Group(group) => unfold_group(Vec::new(), group), + input::FieldOrGroup::Field(field) => { + error.combine(syn::Error::new( + field.ident().span(), + "top level fields are not supported", + )); + vec![] + } + }) + .collect::<Vec<_>>(); + error.try_throw()?; + + let example_tests = fields_with_keys + .iter() + .filter_map(|(key, field)| Some((key, field.read_write()?))) + .flat_map(|(key, field)| { + let ty = &field.ty; + field.examples.iter().enumerate().map(move |(n, example)| { + let name = quote::format_ident!( + "example_value_can_be_deserialized_for_{}_example_{n}", + key.join("_") + ); + let span = example.span(); + let example = example.as_ref(); + let expect_message = format!( + "Example value {example:?} for '{}' could not be deserialized", + key.join(".") + ); + quote_spanned! {span=> + #[test] + fn #name() { + #example.parse::<#ty>().expect(#expect_message); + } + } + }) + }) + .collect::<Vec<_>>(); + + let reader_name = proc_macro2::Ident::new("TEdgeConfigReader", Span::call_site()); + let dto_doc_comment = format!( + "A data-transfer object, designed for reading and writing to + `tedge.toml` +\n\ + All the configurations inside this are optional to represent whether + the value is or isn't configured in the TOML file. Any defaults are + populated when this is converted to [{reader_name}] (via + [from_dto]({reader_name}::from_dto)). +\n\ + For simplicity when using this struct, only the fields are optional. + Any configuration groups (e.g. `device`, `c8y`, `mqtt.external`) are + always present. Groups that have no value set will be omitted in the + serialized output to avoid polluting `tedge.toml`." + ); + + let dto = dto::generate( + proc_macro2::Ident::new("TEdgeConfigDto", Span::call_site()), + &input.groups, + &dto_doc_comment, + ); + + let reader_doc_comment = "A struct to read configured values from, designed to be accessed only + via an immutable borrow +\n\ + The configurations inside this struct are optional only if the field + does not have a default value configured. This ensures that thin-edge + code only needs to handle possible errors where a field may not be + set. +\n\ + Where fields are optional, they are stored using [OptionalConfig] to + produce a descriptive error message that directs the user to set the + relevant key."; + let reader = reader::try_generate(reader_name, &input.groups, reader_doc_comment)?; + + let enums = query::generate_writable_keys(&input.groups); + + Ok(quote! { + #(#example_tests)* + #dto + #reader + #enums + }) +} + +fn unfold_group( + mut name: Vec<String>, + group: &input::ConfigurationGroup, +) -> Vec<(Vec<String>, &input::ConfigurableField)> { + let mut output = Vec::new(); + name.push(group.ident.to_string()); + for field_or_group in &group.contents { + match field_or_group { + input::FieldOrGroup::Field(field) => { + let mut name = name.clone(); + name.push( + field + .rename() + .map(<_>::to_owned) + .unwrap_or_else(|| field.ident().to_string()), + ); + output.push((name, field)) + } + input::FieldOrGroup::Group(group) => { + output.append(&mut unfold_group(name.clone(), group)); + } + } + } + + output +} + +fn prefixed_type_name( + start: &proc_macro2::Ident, + group: &input::ConfigurationGroup, +) -> proc_macro2::Ident { + quote::format_ident!( + "{start}{}", + group.ident.to_string().to_upper_camel_case(), + span = group.ident.span() + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + // TODO should these move to parse + #[test] + fn parse_basic_configuration_with_attributes() { + assert!(generate_configuration(quote! { + device: { + /// The id of the device + #[tedge_config(readonly(write_error = "Device id is derived from the certificate and cannot be written to", function = "device_id"))] + id: String, + /// The key path + #[tedge_config(example = "test")] + #[tedge_config(example = "tes2")] + key_path: Utf8Path, + } + }) + .is_ok()); + } + + #[test] + fn parse_nested_groups() { + assert!(generate_configuration(quote! { + device: { + nested: { + #[tedge_config(rename = "type")] + ty: String, + }, + }, + }) + .is_ok()); + } + + #[test] + fn serde_rename_is_not_allowd() { + assert!(generate_configuration(quote! { + device: { + #[serde(rename = "type")] + ty: String, + }, + }) + .is_err()); + } +} diff --git a/crates/common/tedge_config_macros/impl/src/optional_error.rs b/crates/common/tedge_config_macros/impl/src/optional_error.rs new file mode 100644 index 00000000000..a58eb61b338 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/optional_error.rs @@ -0,0 +1,87 @@ +use syn::Error; + +/// Represents an `Option<syn::Error>`. +#[derive(Default)] +pub struct OptionalError(Option<Error>); + +impl OptionalError { + /// Removes the contained [error](Error) and returns it, if any. + pub fn take(&mut self) -> Option<Error> { + self.0.take() + } + + /// Combine the given [error](Error) with the existing one, + /// initializing it if none currently exists. + pub fn combine(&mut self, error: Error) { + match self.0 { + None => self.0 = Some(error), + Some(ref mut prev) => prev.combine(error), + } + } + + /// Combine the given [error](Error) with the existing one, + /// initializing it if none currently exists. + pub fn combine_owned(mut self, error: Error) -> Self { + match self.0 { + None => self.0 = Some(error), + Some(ref mut prev) => prev.combine(error), + }; + self + } + + /// Returns a [`Result`] with the contained [error](Error), if any. + /// + /// This can be used for quick and easy early returns. + pub fn try_throw(self) -> Result<(), Error> { + match self.0 { + None => Ok(()), + Some(err) => Err(err), + } + } +} + +pub trait SynResultExt: Sized { + fn append_err_to(self, errors: &mut OptionalError); +} + +impl SynResultExt for Result<(), syn::Error> { + fn append_err_to(self, errors: &mut OptionalError) { + if let Err(error) = self { + errors.combine(error); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proc_macro2::Span; + + #[test] + fn should_combine() { + let mut collector = OptionalError(Some(Error::new(Span::call_site(), "First Error"))); + collector.combine(Error::new(Span::call_site(), "Second Error")); + + let expected = r#":: core :: compile_error ! { "First Error" } :: core :: compile_error ! { "Second Error" }"#; + let received = collector + .try_throw() + .expect_err("expected error") + .to_compile_error() + .to_string(); + assert_eq!(expected, received); + } + + #[test] + fn should_take() { + let mut collector = OptionalError(Some(Error::new(Span::call_site(), "First Error"))); + let existing = collector.take(); + + let expected = r#":: core :: compile_error ! { "First Error" }"#; + let received = existing + .expect("expected error") + .to_compile_error() + .to_string(); + assert_eq!(expected, received); + assert!(collector.try_throw().is_ok()); + } +} diff --git a/crates/common/tedge_config_macros/impl/src/query.rs b/crates/common/tedge_config_macros/impl/src/query.rs new file mode 100644 index 00000000000..3ddf3ba5d9c --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/query.rs @@ -0,0 +1,410 @@ +use heck::ToUpperCamelCase; +use proc_macro2::TokenStream; +use quote::quote; +use quote::quote_spanned; +use std::collections::VecDeque; +use syn::parse_quote; + +use crate::error::extract_type_from_result; +use crate::input::ConfigurableField; +use crate::input::FieldOrGroup; + +pub fn generate_writable_keys(items: &[FieldOrGroup]) -> TokenStream { + let paths = configuration_paths_from(items); + let (readonly_variant, write_error) = paths + .iter() + .filter_map(|field| { + Some(( + variant_name(field), + field + .back()? + .field()? + .read_only()? + .readonly + .write_error + .as_str(), + )) + }) + .unzip::<_, _, Vec<_>, Vec<_>>(); + let readable_args = configuration_strings(paths.iter()); + let readonly_args = configuration_strings(paths.iter().filter(|path| !is_read_write(path))); + let writable_args = configuration_strings(paths.iter().filter(|path| is_read_write(path))); + let readable_keys = keys_enum(parse_quote!(ReadableKey), &readable_args, "read from"); + let readonly_keys = keys_enum( + parse_quote!(ReadOnlyKey), + &readonly_args, + "read from, but not written to,", + ); + let writable_keys = keys_enum(parse_quote!(WritableKey), &writable_args, "written to"); + let fromstr_readable = generate_fromstr_readable(parse_quote!(ReadableKey), &readable_args); + let fromstr_readonly = generate_fromstr_readable(parse_quote!(ReadOnlyKey), &readonly_args); + let fromstr_writable = generate_fromstr_writable(parse_quote!(WritableKey), &writable_args); + let read_string = generate_string_readers(&paths); + let write_string = generate_string_writers( + &paths + .iter() + .filter(|path| is_read_write(path)) + .cloned() + .collect::<Vec<_>>(), + ); + let (static_alias, updated_key) = deprecated_keys(paths.iter()); + + quote! { + #readable_keys + #readonly_keys + #writable_keys + #fromstr_readable + #fromstr_readonly + #fromstr_writable + #read_string + #write_string + + #[derive(::thiserror::Error, Debug)] + /// An error encountered when writing to a configuration value from a + /// string + pub enum WriteError { + #[error("Failed to parse input")] + ParseValue(#[from] Box<dyn ::std::error::Error + Send + Sync>), + } + + impl ReadOnlyKey { + fn write_error(self) -> &'static str { + match self { + #( + Self::#readonly_variant => #write_error, + )* + } + } + } + + #[derive(Debug, ::thiserror::Error)] + /// An error encountered when parsing a configuration key from a string + pub enum ParseKeyError { + #[error("{}", .0.write_error())] + ReadOnly(ReadOnlyKey), + #[error("Unknown key: '{0}'")] + Unrecognised(String), + } + + fn replace_aliases(key: String) -> String { + use ::once_cell::sync::Lazy; + use ::std::borrow::Cow; + use ::std::collections::HashMap; + use ::doku::*; + + static ALIASES: Lazy<HashMap<Cow<'static, str>, Cow<'static, str>>> = Lazy::new(|| { + let ty = TEdgeConfigReader::ty(); + let TypeKind::Struct { fields, transparent: false } = ty.kind else { panic!("Expected struct but got {:?}", ty.kind) }; + let Fields::Named { fields } = fields else { panic!("Expected named fields but got {:?}", fields)}; + let mut aliases = struct_field_aliases(None, &fields); + #( + if let Some(alias) = aliases.insert(Cow::Borrowed(#static_alias), Cow::Borrowed(ReadableKey::#updated_key.as_str())) { + panic!("Duplicate configuration alias for '{}'. It maps to both '{}' and '{}'. Perhaps you provided an incorrect `deprecated_key` for one of these configurations?", #static_alias, alias, ReadableKey::#updated_key.as_str()); + } + )* + aliases + }); + + ALIASES + .get(&Cow::Borrowed(key.as_str())) + .map(|c| c.clone().into_owned()) + .unwrap_or(key) + } + + fn warn_about_deprecated_key(deprecated_key: String, updated_key: &'static str) { + use ::once_cell::sync::Lazy; + use ::std::sync::Mutex; + use ::std::collections::HashSet; + + static WARNINGS: Lazy<Mutex<HashSet<String>>> = Lazy::new(<_>::default); + + let warning = format!("The key '{}' is deprecated. Use '{}' instead.", deprecated_key, updated_key); + if WARNINGS.lock().unwrap().insert(deprecated_key) { + ::tracing::warn!("{}", warning); + } + } + } +} + +fn configuration_strings<'a>( + variants: impl Iterator<Item = &'a VecDeque<&'a FieldOrGroup>>, +) -> (Vec<String>, Vec<syn::Ident>) { + variants + .map(|segments| { + ( + segments + .iter() + .map(|variant| variant.name()) + .collect::<Vec<_>>() + .join("."), + variant_name(segments), + ) + }) + .unzip() +} + +fn deprecated_keys<'a>( + variants: impl Iterator<Item = &'a VecDeque<&'a FieldOrGroup>>, +) -> (Vec<&'a str>, Vec<syn::Ident>) { + variants + .flat_map(|segments| { + segments + .back() + .unwrap() + .field() + .unwrap() + .deprecated_keys() + .map(|key| (key, variant_name(segments))) + }) + .unzip() +} + +fn generate_fromstr( + type_name: syn::Ident, + (configuration_string, variant_name): &(Vec<String>, Vec<syn::Ident>), + error_case: syn::Arm, +) -> TokenStream { + let simplified_configuration_string = configuration_string + .iter() + .map(|s| s.replace('.', "_")) + .zip(variant_name.iter()) + .map(|(s, v)| quote_spanned!(v.span()=> #s)); + + quote! { + impl ::std::str::FromStr for #type_name { + type Err = ParseKeyError; + fn from_str(value: &str) -> Result<Self, Self::Err> { + // If we get an unreachable pattern, it means we have the same key twice + #[deny(unreachable_patterns)] + match replace_aliases(value.to_owned()).replace(".", "_").as_str() { + #( + #simplified_configuration_string => { + if (value != #configuration_string) { + warn_about_deprecated_key(value.to_owned(), #configuration_string); + } + Ok(Self::#variant_name) + }, + )* + #error_case + } + } + } + } +} + +fn generate_fromstr_readable( + type_name: syn::Ident, + fields: &(Vec<String>, Vec<syn::Ident>), +) -> TokenStream { + generate_fromstr( + type_name, + fields, + parse_quote! { _ => Err(ParseKeyError::Unrecognised(value.to_owned())) }, + ) +} + +// TODO test the error messages actually appear +fn generate_fromstr_writable( + type_name: syn::Ident, + fields: &(Vec<String>, Vec<syn::Ident>), +) -> TokenStream { + generate_fromstr( + type_name, + fields, + parse_quote! { + _ => if let Ok(key) = <ReadOnlyKey as ::std::str::FromStr>::from_str(value) { + Err(ParseKeyError::ReadOnly(key)) + } else { + Err(ParseKeyError::Unrecognised(value.to_owned())) + }, + }, + ) +} + +fn keys_enum( + type_name: syn::Ident, + (configuration_string, variant_name): &(Vec<String>, Vec<syn::Ident>), + doc_fragment: &'static str, +) -> TokenStream { + let as_str_example = variant_name + .iter() + .zip(configuration_string.iter()) + .map(|(ident, value)| format!("assert_eq!({type_name}::{ident}.as_str(), \"{value}\");\n")) + .take(10) + .collect::<Vec<_>>(); + let as_str_example = (!as_str_example.is_empty()).then(|| { + quote! { + /// ```compile_fail + /// // This doctest is compile_fail because we have no way import the + /// // current type, but the example is still valuable + #( + #[doc = #as_str_example] + )* + /// ``` + } + }); + let type_name_str = type_name.to_string(); + + quote! { + #[derive(Copy, Clone, Debug, PartialEq, Eq)] + #[non_exhaustive] + #[allow(unused)] + #[doc = concat!("A key that can be *", #doc_fragment, "* the configuration\n\n")] + #[doc = concat!("This can be converted to `&'static str` using [`", #type_name_str, "::as_str`], and")] + #[doc = "parsed using [`FromStr`](::std::str::FromStr). The `FromStr` implementation also"] + #[doc = "automatically emits warnings about deprecated keys. It also implements [Display](std::fmt::Display),"] + #[doc = "so you can also use it in format strings."] + pub enum #type_name { + #( + #[doc = concat!("`", #configuration_string, "`")] + #variant_name, + )* + } + + impl #type_name { + /// Converts this key to the canonical key used by `tedge config` and `tedge.toml` + #as_str_example + pub fn as_str(self) -> &'static str { + match self { + #( + Self::#variant_name => #configuration_string, + )* + } + } + + /// Iterates through all the variants of this enum + pub fn iter() -> impl Iterator<Item = Self> { + [ + #( + Self::#variant_name, + )* + ].into_iter() + } + } + + impl ::std::fmt::Display for #type_name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + self.as_str().fmt(f) + } + } + } +} + +fn generate_string_readers(paths: &[VecDeque<&FieldOrGroup>]) -> TokenStream { + let variant_names = paths.iter().map(variant_name); + let arms = paths + .iter() + .zip(variant_names) + .map(|(path, variant_name)| -> syn::Arm { + let field = path + .back() + .expect("Path must have a back as it is nonempty") + .field() + .expect("Back of path is guaranteed to be a field"); + let segments = path.iter().map(|thing| thing.ident()); + if field.read_only().is_some() { + if extract_type_from_result(field.ty()).is_some() { + parse_quote! { + // Probably where the compiler error appears + // TODO why do we need to unwrap + ReadableKey::#variant_name => Ok(self.#(#segments).*.try_read(self)?.to_string()), + } + } else { + parse_quote! { + // Probably where the compiler error appears + // TODO why do we need to unwrap + ReadableKey::#variant_name => Ok(self.#(#segments).*.read(self).to_string()), + } + } + } else if field.has_guaranteed_default() { + parse_quote! { + ReadableKey::#variant_name => Ok(self.#(#segments).*.to_string()), + } + } else { + parse_quote! { + ReadableKey::#variant_name => Ok(self.#(#segments).*.or_config_not_set()?.to_string()), + } + } + }); + quote! { + impl TEdgeConfigReader { + pub fn read_string(&self, key: ReadableKey) -> Result<String, ReadError> { + match key { + #(#arms)* + } + } + } + } +} + +fn generate_string_writers(paths: &[VecDeque<&FieldOrGroup>]) -> TokenStream { + let variant_names = paths.iter().map(variant_name); + let (update_arms, unset_arms): (Vec<syn::Arm>, Vec<syn::Arm>) = paths + .iter() + .zip(variant_names) + .map(|(path, variant_name)| { + let segments = path.iter().map(|thing| thing.ident()).collect::<Vec<_>>(); + + ( + // TODO this should probably be spanned to the field type + parse_quote! { + WritableKey::#variant_name => self.#(#segments).* = Some(value.parse().map_err(|e| WriteError::ParseValue(Box::new(e)))?), + }, + parse_quote! { + WritableKey::#variant_name => self.#(#segments).* = None, + } + ) + }).unzip(); + quote! { + impl TEdgeConfigDto { + pub fn try_update_str(&mut self, key: WritableKey, value: &str) -> Result<(), WriteError> { + match key { + #(#update_arms)* + }; + Ok(()) + } + + pub fn unset_key(&mut self, key: WritableKey) { + match key { + #(#unset_arms)* + } + } + } + } +} + +fn variant_name(segments: &VecDeque<&FieldOrGroup>) -> syn::Ident { + syn::Ident::new( + &segments + .iter() + .map(|segment| segment.name().to_upper_camel_case()) + .collect::<String>(), + segments.iter().last().unwrap().ident().span(), + ) +} + +/// Generates a list of the toml paths for each of the keys in the provided +/// configuration +fn configuration_paths_from(items: &[FieldOrGroup]) -> Vec<VecDeque<&FieldOrGroup>> { + let mut res = vec![]; + for item in items.iter().filter(|item| !item.reader().skip) { + match item { + FieldOrGroup::Field(_) => res.push(VecDeque::from([item])), + FieldOrGroup::Group(group) => { + for mut fields in configuration_paths_from(&group.contents) { + fields.push_front(item); + res.push(fields); + } + } + } + } + res +} + +/// Checks if the field for the given path is read write +fn is_read_write(path: &VecDeque<&FieldOrGroup>) -> bool { + matches!( + path.back(), // the field + Some(FieldOrGroup::Field(ConfigurableField::ReadWrite(_))), + ) +} diff --git a/crates/common/tedge_config_macros/impl/src/reader.rs b/crates/common/tedge_config_macros/impl/src/reader.rs new file mode 100644 index 00000000000..acfac9827d1 --- /dev/null +++ b/crates/common/tedge_config_macros/impl/src/reader.rs @@ -0,0 +1,357 @@ +//! Generation for the configuration readers +//! +//! When reading the configuration, we want to see default values if nothing has +//! been configured +use std::iter; + +use proc_macro2::TokenStream; +use quote::quote; +use quote::quote_spanned; +use syn::parse_quote; +use syn::parse_quote_spanned; +use syn::punctuated::Punctuated; +use syn::spanned::Spanned; +use syn::Token; + +use crate::error::extract_type_from_result; +use crate::input::ConfigurableField; +use crate::input::FieldDefault; +use crate::input::FieldOrGroup; +use crate::optional_error::OptionalError; +use crate::prefixed_type_name; + +pub fn try_generate( + root_name: proc_macro2::Ident, + items: &[FieldOrGroup], + doc_comment: &str, +) -> syn::Result<TokenStream> { + let structs = generate_structs(&root_name, items, Vec::new(), doc_comment)?; + let conversions = generate_conversions(&root_name, items, vec![], items)?; + Ok(quote! { + #structs + #conversions + }) +} + +fn generate_structs( + name: &proc_macro2::Ident, + items: &[FieldOrGroup], + parents: Vec<syn::Ident>, + doc_comment: &str, +) -> syn::Result<TokenStream> { + let mut idents = Vec::new(); + let mut tys = Vec::<syn::Type>::new(); + let mut sub_readers = Vec::new(); + let mut attrs: Vec<Vec<syn::Attribute>> = Vec::new(); + let mut lazy_readers = Vec::new(); + let mut vis: Vec<syn::Visibility> = Vec::new(); + + for item in items { + match item { + FieldOrGroup::Field(field) => { + let ty = field.ty(); + attrs.push(field.attrs().to_vec()); + idents.push(field.ident()); + if field.is_optional() { + tys.push(parse_quote_spanned!(ty.span()=> OptionalConfig<#ty>)); + } else if let Some(field) = field.read_only() { + let name = field.lazy_reader_name(&parents); + tys.push(parse_quote_spanned!(field.ty.span()=> #name)); + lazy_readers.push((name, &field.ty, &field.readonly.function)); + } else { + tys.push(ty.to_owned()); + } + sub_readers.push(None); + vis.push(match field.reader().private { + true => parse_quote!(), + false => parse_quote!(pub), + }); + } + FieldOrGroup::Group(group) if !group.reader.skip => { + let sub_reader_name = prefixed_type_name(name, group); + idents.push(&group.ident); + tys.push(parse_quote_spanned!(group.ident.span()=> #sub_reader_name)); + let mut parents = parents.clone(); + parents.push(group.ident.clone()); + sub_readers.push(Some(generate_structs( + &sub_reader_name, + &group.contents, + parents, + "", + )?)); + attrs.push(group.attrs.to_vec()); + vis.push(match group.reader.private { + true => parse_quote!(), + false => parse_quote!(pub), + }); + } + FieldOrGroup::Group(_) => { + // Skipped + } + } + } + + let lazy_reader_impls = lazy_readers + .iter() + .map(|(name, ty, function)| -> syn::ItemImpl { + if let Some((ok, err)) = extract_type_from_result(ty) { + parse_quote_spanned! {name.span()=> + impl #name { + // TODO don't just guess we're called tedgeconfigreader + pub fn try_read(&self, reader: &TEdgeConfigReader) -> Result<&#ok, #err> { + self.0.get_or_try_init(|| #function(reader)) + } + } + } + } else { + parse_quote_spanned! {name.span()=> + impl #name { + // TODO don't just guess we're called tedgeconfigreader + pub fn read(&self, reader: &TEdgeConfigReader) -> &#ty { + self.0.get_or_init(|| #function(reader)) + } + } + } + } + }); + + let (lr_names, lr_tys): (Vec<_>, Vec<_>) = lazy_readers + .iter() + .map(|(name, ty, _)| match extract_type_from_result(ty) { + Some((ok, _err)) => (name, ok), + None => (name, *ty), + }) + .unzip(); + + Ok(quote! { + #[derive(::doku::Document, ::serde::Serialize, Debug)] + #[non_exhaustive] + #[doc = #doc_comment] + pub struct #name { + #( + #(#attrs)* + #vis #idents: #tys, + )* + } + + #( + #[derive(::serde::Serialize, Clone, Debug, Default)] + #[serde(into = "()")] + pub struct #lr_names(::once_cell::sync::OnceCell<#lr_tys>); + + impl From<#lr_names> for () { + fn from(_: #lr_names) -> () {} + } + + #lazy_reader_impls + )* + + #(#sub_readers)* + }) +} + +fn find_field<'a>( + mut fields: &'a [FieldOrGroup], + key: &Punctuated<syn::Ident, Token![.]>, +) -> syn::Result<&'a ConfigurableField> { + let mut current_field = None; + for (i, segment) in key.iter().enumerate() { + let target = fields + .iter() + .find(|field| field.is_called(segment)) + .ok_or_else(|| { + syn::Error::new( + segment.span(), + format!( + "no field named `{segment}` {}", + current_field.map_or_else( + || "at top level of configuration".to_owned(), + |field: &FieldOrGroup| format!("in {}", field.ident()) + ) + ), + ) + })?; + + let is_last_segment = i == key.len() - 1; + match target { + FieldOrGroup::Group(group) => fields = &group.contents, + FieldOrGroup::Field(_) if is_last_segment => (), + _ => { + let string_path = key.iter().map(<_>::to_string).collect::<Vec<_>>(); + let (successful_segments, subfields) = string_path.split_at(i + 1); + let successful_segments = successful_segments.join("."); + let subfields = subfields.join("."); + return Err(syn::Error::new( + segment.span(), + format!("cannot access `{subfields}` because `{successful_segments}` is a configuration field, not a group"), + )); + } + }; + current_field = Some(target); + } + + match current_field { + // TODO test this appears + None => Err(syn::Error::new(key.span(), "key is empty")), + Some(FieldOrGroup::Group(_)) => Err(syn::Error::new( + key.span(), + // TODO test this too + "path points to a group of fields, not a single field", + )), + Some(FieldOrGroup::Field(f)) => Ok(f), + } +} + +fn reader_value_for_field<'a>( + field: &'a ConfigurableField, + parents: &[syn::Ident], + root_fields: &[FieldOrGroup], + mut observed_keys: Vec<&'a Punctuated<syn::Ident, Token![.]>>, +) -> syn::Result<TokenStream> { + let name = field.ident(); + Ok(match field { + ConfigurableField::ReadWrite(field) => { + let key = parents + .iter() + .map(|p| p.to_string()) + .chain(iter::once(name.to_string())) + .collect::<Vec<_>>() + .join("."); + match &field.default { + FieldDefault::None => quote! { + match &dto.#(#parents).*.#name { + None => OptionalConfig::Empty(#key), + Some(value) => OptionalConfig::Present { value: value.clone(), key: #key }, + } + }, + FieldDefault::FromKey(key) if observed_keys.contains(&key) => { + let string_paths = observed_keys + .iter() + .map(|path| { + path.iter() + .map(<_>::to_string) + .collect::<Vec<_>>() + .join(".") + }) + .collect::<Vec<_>>(); + let error = + format!("this path's default is part of a cycle ({string_paths:?})"); + // Safe to unwrap the error since observed_paths.len() >= 1 + return Err(observed_keys + .into_iter() + .map(|path| syn::Error::new(path.span(), &error)) + .fold(OptionalError::default(), |mut errors, error| { + errors.combine(error); + errors + }) + .take() + .unwrap()); + } + FieldDefault::FromKey(default_key) | FieldDefault::FromOptionalKey(default_key) => { + observed_keys.push(default_key); + let default = reader_value_for_field( + find_field(root_fields, default_key)?, + &default_key + .iter() + .take(default_key.len() - 1) + .map(<_>::to_owned) + .collect::<Vec<_>>(), + root_fields, + observed_keys, + )?; + + let (default, value) = + if matches!(&field.default, FieldDefault::FromOptionalKey(_)) { + ( + quote!(#default.map(|v| v.into())), + quote!(OptionalConfig::Present { value: value.clone(), key: #key }), + ) + } else { + (quote!(#default.into()), quote!(value.clone())) + }; + + quote_spanned! {name.span()=> + match &dto.#(#parents).*.#name { + Some(value) => #value, + None => #default, + } + } + } + FieldDefault::Function(function) => quote_spanned! {function.span()=> + match &dto.#(#parents).*.#name { + None => TEdgeConfigDefault::<TEdgeConfigDto, _>::call(#function, dto, location), + Some(value) => value.clone(), + } + }, + FieldDefault::Value(default) => quote_spanned! {name.span()=> + match &dto.#(#parents).*.#name { + None => #default.into(), + Some(value) => value.clone(), + } + }, + FieldDefault::Variable(default) => quote_spanned! {name.span()=> + match &dto.#(#parents).*.#name { + None => #default.into(), + Some(value) => value.clone(), + } + }, + } + } + ConfigurableField::ReadOnly(field) => { + let name = field.lazy_reader_name(parents); + quote! { + #name::default() + } + } + }) +} + +/// Generate the conversion methods from DTOs to Readers +fn generate_conversions( + name: &proc_macro2::Ident, + items: &[FieldOrGroup], + parents: Vec<syn::Ident>, + root_fields: &[FieldOrGroup], +) -> syn::Result<TokenStream> { + let mut field_conversions = Vec::new(); + let mut rest = Vec::new(); + + for item in items { + match item { + FieldOrGroup::Field(field) => { + let name = field.ident(); + let value = reader_value_for_field(field, &parents, root_fields, Vec::new())?; + field_conversions.push(quote!(#name: #value)); + } + FieldOrGroup::Group(group) if !group.reader.skip => { + let sub_reader_name = prefixed_type_name(name, group); + let name = &group.ident; + + let mut parents = parents.clone(); + parents.push(group.ident.clone()); + field_conversions.push(quote!(#name: #sub_reader_name::from_dto(dto, location))); + let sub_conversions = + generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)?; + rest.push(sub_conversions); + } + FieldOrGroup::Group(_) => { + // Skipped + } + } + } + + Ok(quote! { + impl #name { + #[allow(unused, clippy::clone_on_copy, clippy::useless_conversion)] + #[automatically_derived] + /// Converts the provided [TEdgeConfigDto] into a reader + pub fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> Self { + Self { + #(#field_conversions),* + } + } + } + + #(#rest)* + }) +} diff --git a/crates/common/tedge_config_macros/macro/Cargo.toml b/crates/common/tedge_config_macros/macro/Cargo.toml new file mode 100644 index 00000000000..0480ce03d3a --- /dev/null +++ b/crates/common/tedge_config_macros/macro/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "tedge_config_macros-macro" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +tedge_config_macros-impl = { version = "=0.1.0", path = "../impl" } +syn = { version = "1", features = ["full", "extra-traits"] } +quote = "1" +proc-macro2 = "1" diff --git a/crates/common/tedge_config_macros/macro/src/lib.rs b/crates/common/tedge_config_macros/macro/src/lib.rs new file mode 100644 index 00000000000..e74340ffcdc --- /dev/null +++ b/crates/common/tedge_config_macros/macro/src/lib.rs @@ -0,0 +1,15 @@ +//! This crate implements the macro for `tedge_config_macros` and should not be used directly. +extern crate proc_macro; + +use proc_macro::TokenStream; +use syn::parse_macro_input; + +#[proc_macro] +pub fn define_tedge_config(item: TokenStream) -> TokenStream { + let item = parse_macro_input!(item as proc_macro2::TokenStream); + + match tedge_config_macros_impl::generate_configuration(item) { + Ok(tokens) => tokens.into(), + Err(err) => TokenStream::from(err.to_compile_error()), + } +} diff --git a/crates/common/tedge_config_macros/src/all_or_nothing.rs b/crates/common/tedge_config_macros/src/all_or_nothing.rs new file mode 100644 index 00000000000..6eb9e8f17d0 --- /dev/null +++ b/crates/common/tedge_config_macros/src/all_or_nothing.rs @@ -0,0 +1,172 @@ +use crate::OptionalConfig; + +/// An abstraction over "all or nothing" configurations +/// +/// This is designed to be used with [all_or_nothing] to generate helpful error +/// messages in cases where configuration values are mutually optional. See +/// [all_or_nothing] for more information. +pub trait MultiOption { + type Output; + fn extract_all(self) -> Result<Option<Self::Output>, PartialConfiguration>; +} + +/// The keys which were and weren't provided as part of an all or nothing group +pub struct PartialConfiguration { + present: Vec<&'static str>, + missing: Vec<&'static str>, +} + +impl PartialConfiguration { + fn error_message(&self) -> String { + let mut all_settings = self.present.clone(); + all_settings.append(&mut self.missing.clone()); + let present = &self.present; + let missing = &self.missing; + + format!( + "The thin-edge configuration is invalid. The settings {all_settings:?} \ + must either all be configured, or all unset. Currently {present:?} are \ + set, and {missing:?} are unset." + ) + } +} + +impl<T, U> MultiOption for (OptionalConfig<T>, OptionalConfig<U>) { + type Output = (T, U); + fn extract_all(self) -> Result<Option<Self::Output>, PartialConfiguration> { + use OptionalConfig::*; + match self { + (Present { value: t, .. }, Present { value: u, .. }) => Ok(Some((t, u))), + (Empty(..), Empty(..)) => Ok(None), + (t, u) => { + let present = [t.key_if_present(), u.key_if_present()] + .into_iter() + .flatten() + .collect::<Vec<_>>(); + let missing = [t.key_if_empty(), u.key_if_empty()] + .into_iter() + .flatten() + .collect::<Vec<_>>(); + Err(PartialConfiguration { present, missing }) + } + } + } +} + +/// Combine a set of optional configurations into a single option +/// +/// # Errors +/// This will fail in the case that some, but not all the configurations are provided. +/// +/// ``` +/// use tedge_config_macros::*; +/// use camino::Utf8PathBuf; +/// use std::path::PathBuf; +/// +/// #[derive(thiserror::Error, Debug)] +/// pub enum ReadError { +/// #[error(transparent)] +/// ConfigNotSet(#[from] ConfigNotSet), +/// } +/// +/// define_tedge_config! { +/// mqtt: { +/// auth: { +/// #[doku(as = "PathBuf")] +/// cert_file: Utf8PathBuf, +/// +/// #[doku(as = "PathBuf")] +/// key_file: Utf8PathBuf, +/// } +/// } +/// } +/// +/// let mut dto = TEdgeConfigDto::default(); +/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +/// assert!(matches!( +/// all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())), +/// Ok(None) +/// )); +/// +/// dto.mqtt.auth.cert_file = Some("/etc/tedge/mqtt-certs/auth.crt".into()); +/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +/// assert!(matches!( +/// all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())), +/// Err(_) +/// )); +/// +/// dto.mqtt.auth.key_file = Some("/etc/tedge/mqtt-certs/key.cert".into()); +/// let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +/// assert!(matches!( +/// all_or_nothing((reader.mqtt.auth.cert_file.as_ref(), reader.mqtt.auth.key_file.as_ref())), +/// Ok(Some((_, _))) +/// )); +/// ``` +pub fn all_or_nothing<Configs: MultiOption>( + input: Configs, +) -> Result<Option<Configs::Output>, String> { + input + .extract_all() + .map_err(|partial_config| partial_config.error_message()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_or_nothing_returns_some_when_both_values_are_configured() { + assert_eq!( + all_or_nothing(( + OptionalConfig::Present { + value: "first", + key: "test.key" + }, + OptionalConfig::Present { + value: "second", + key: "test.key2" + } + )), + Ok(Some(("first", "second"))) + ) + } + + #[test] + fn all_or_nothing_returns_none_when_both_values_when_neither_value_is_configured() { + assert_eq!( + all_or_nothing(( + OptionalConfig::<String>::Empty("first.key"), + OptionalConfig::<String>::Empty("second.key") + )), + Ok(None) + ) + } + + #[test] + fn all_or_nothing_returns_an_error_if_only_the_first_value_is_configured() { + assert!(matches!( + all_or_nothing(( + OptionalConfig::Present { + value: "test", + key: "first.key" + }, + OptionalConfig::<String>::Empty("second.key") + )), + Err(_) + )) + } + + #[test] + fn all_or_nothing_returns_an_error_if_only_the_second_value_is_configured() { + assert!(matches!( + all_or_nothing(( + OptionalConfig::<String>::Empty("first.key"), + OptionalConfig::Present { + value: "test", + key: "second.key" + }, + )), + Err(_) + )) + } +} diff --git a/crates/common/tedge_config_macros/src/connect_url.rs b/crates/common/tedge_config_macros/src/connect_url.rs new file mode 100644 index 00000000000..c3a59f4da62 --- /dev/null +++ b/crates/common/tedge_config_macros/src/connect_url.rs @@ -0,0 +1,99 @@ +use std::convert::TryFrom; +use std::fmt; +use std::str::FromStr; +use url::Host; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +pub struct ConnectUrl { + input: String, + host: Host, +} + +impl doku::Document for ConnectUrl { + fn ty() -> doku::Type { + String::ty() + } +} + +#[derive(thiserror::Error, Debug)] +#[error( + "Provided URL: '{input}' contains scheme or port. + Provided URL should contain only domain, eg: 'subdomain.cumulocity.com'." +)] +pub struct InvalidConnectUrl { + input: String, + error: url::ParseError, +} + +impl TryFrom<String> for ConnectUrl { + type Error = InvalidConnectUrl; + + fn try_from(input: String) -> Result<Self, Self::Error> { + match Host::parse(&input) { + Ok(host) => Ok(Self { input, host }), + Err(error) => Err(InvalidConnectUrl { input, error }), + } + } +} + +impl FromStr for ConnectUrl { + type Err = InvalidConnectUrl; + + fn from_str(input: &str) -> Result<Self, Self::Err> { + ConnectUrl::try_from(input.to_string()) + } +} + +impl ConnectUrl { + pub fn as_str(&self) -> &str { + self.input.as_str() + } +} + +impl fmt::Display for ConnectUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.input.fmt(f) + } +} + +impl From<ConnectUrl> for String { + fn from(val: ConnectUrl) -> Self { + val.input + } +} + +impl From<ConnectUrl> for Host { + fn from(val: ConnectUrl) -> Self { + val.host + } +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_port() { + let input = "test.address.com:8883"; + + assert!(ConnectUrl::from_str(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_scheme_http() { + let input = "http://test.address.com"; + + assert!(ConnectUrl::from_str(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_port_and_http() { + let input = "http://test.address.com:8883"; + + assert!(ConnectUrl::from_str(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_string() { + let input = "test.address.com"; + let expected = "test.address.com"; + + assert_eq!(ConnectUrl::from_str(input).unwrap().as_str(), expected); +} diff --git a/crates/common/tedge_config_macros/src/default.rs b/crates/common/tedge_config_macros/src/default.rs new file mode 100644 index 00000000000..74c97a8a487 --- /dev/null +++ b/crates/common/tedge_config_macros/src/default.rs @@ -0,0 +1,63 @@ +use camino::Utf8Path; + +/// An abstraction over the possible default functions for tedge config values +/// +/// Some configuration defaults are relative to the config location, and +/// this trait allows us to pass that in, or the DTO, both, or neither! +pub trait TEdgeConfigDefault<T, Args> { + type Output; + fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output; +} + +/// A dummy tedge config location +/// +/// In practice, we use `TEdgeConfigLocation` from the `tedge_config` crate. +/// This is defined simply to allow us to create examples in this crate. +#[derive(Default)] +pub struct TEdgeConfigLocation; + +impl TEdgeConfigLocation { + pub fn tedge_config_root_path(&self) -> &Utf8Path { + "/etc/tedge".into() + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, ()> for F +where + F: FnOnce() -> Out + Clone, +{ + type Output = Out; + fn call(self, _: &T, _: &TEdgeConfigLocation) -> Self::Output { + (self)() + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, &T> for F +where + F: FnOnce(&T) -> Out + Clone, +{ + type Output = Out; + fn call(self, data: &T, _location: &TEdgeConfigLocation) -> Self::Output { + (self)(data) + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, (&TEdgeConfigLocation,)> for F +where + F: FnOnce(&TEdgeConfigLocation) -> Out + Clone, +{ + type Output = Out; + fn call(self, _data: &T, location: &TEdgeConfigLocation) -> Self::Output { + (self)(location) + } +} + +impl<F, Out, T> TEdgeConfigDefault<T, (&T, &TEdgeConfigLocation)> for F +where + F: FnOnce(&T, &TEdgeConfigLocation) -> Out + Clone, +{ + type Output = Out; + fn call(self, data: &T, location: &TEdgeConfigLocation) -> Self::Output { + (self)(data, location) + } +} diff --git a/crates/common/tedge_config_macros/src/define_tedge_config_docs.md b/crates/common/tedge_config_macros/src/define_tedge_config_docs.md new file mode 100644 index 00000000000..11b351fd76c --- /dev/null +++ b/crates/common/tedge_config_macros/src/define_tedge_config_docs.md @@ -0,0 +1,577 @@ +Defines the necessary structures to create a tedge config struct + +For a complete example of its usage, see the `macros.rs` file inside the +examples folder of this crate. + +# Output +This macro outputs a few different types: +- `TEdgeConfigDto` ([example](example::TEdgeConfigDto)) --- A data-transfer + object, used for reading and writing to toml +- `TEdgeConfigReader` ([example](example::TEdgeConfigReader)) --- A struct to + read configured values from, populating values with defaults if they exist +- `ReadableKey` ([example](example::ReadableKey)) --- An enum of all the + possible keys that can be read from the configuration, with + [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) + implementations. +- `WritableKey` ([example](example::WritableKey)) --- An enum of all the + possible keys that can be written to the configuration, with + [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) + implementations. +- `ReadOnlyKey` ([example](example::ReadOnlyKey)) --- An enum of all the + possible keys that can be read from, but not written to, the configuration, + with [`FromStr`](std::str::FromStr) and [`Display`](std::fmt::Display) + implementations. + +# Attributes +## `#[tedge_config(...)]` attributes +| Attribute | Supported for | Summary | +| --------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- | +| [`rename`](#rename) | fields/groups | Renames a field or group in serde and the `tedge config` key | +| [`deprecated_name`](#dep-name) | fields/groups | Adds an alias for a field or group in serde/`tedge config` | +| [`deprecated_key`](#dep-key) | fields | Adds an alias for the field or group in serde/`tedge config` | +| **Doc comments** | [fields](#docs-fields)/[groups](#docs-groups) | Adds a description of a key in `tedge config` docs | +| [`example`](#examples) | fields | Adds an example value to `tedge config` docs | +| [`note`](#notes) | fields | Adds a highlighted note to `tedge config` docs | +| [`reader(skip)`](#reader-skip) | groups | Omits a group from the reader struct entirely | +| [`reader(private)`](#reader-priv) | fields/groups | Stops the field from the reader struct being marked with `pub` | +| [`default(value)`](#default-lit) | fields | Sets the default value for a field from a literal | +| [`default(variable)`](#default-var) | fields | Sets the default value for a field from a variable | +| [`default(from_key)`](#from-key) | fields | Sets the default value for a field to the value of another field | +| [`default(from_optional_key)`](#from-opt-key) | fields | Sets the default value for a field to the value of another field | +| [`default(function)`](#default-fn) | fields | Specifies a function that will be used to compute a field's default value | +| [`readonly(...)`](#readonly) | fields | Marks a field as read-only | + +## Other attributes +### `#[doku(as = "...")]` +Some types are not known to [`doku`], and therefore do not implement +[`doku::Document`] (which is required for all fields). In order to resolve this +error, you can use a `#[doku(as = "...")]` attribute like so: + +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +use camino::Utf8PathBuf; +use std::path::PathBuf; + +define_tedge_config! { + device: { + #[doku(as = "PathBuf")] + cert_path: Utf8PathBuf, + } +} +``` + +The actual type information isn't currently used for anything, but it's usually +possible to find a close match. Custom types can also implement the trait very +easily. + +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +#[serde(transparent)] +pub struct ConnectUrl(String); + +impl doku::Document for ConnectUrl { + fn ty() -> doku::Type { + // Just call `ty()` on a similar enough type + String::ty() + } +} + +define_tedge_config! { + c8y: { + // We now don't need `#[doku(as = "String")]` + url: ConnectUrl, + } +} +# +# impl std::str::FromStr for ConnectUrl { +# type Err = std::convert::Infallible; +# fn from_str(s: &str) -> Result<Self, Self::Err> { Ok(Self(s.to_owned())) } +# } +# impl std::fmt::Display for ConnectUrl { +# fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +# self.0.fmt(f) +# } +# } +``` + +## Denied attributes +Some `tedge_config` attributes are translated under the hood to `serde` or +`doku` attributes, as well as adding some additional behaviour (e.g. [example +tests](#example-tests)). To avoid these attributes being used directly, +`define_tedge_config!` will emit a compiler error if you attempt to use these +attributes. + +```rust compile_fail +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + device: { + #[serde(rename = "type")] + ty: String, + } +} +``` + +## Naming +### <a name="rename"></a>Customising config keys: `#[tedge_config(rename = "new_name")]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + device: { + #[tedge_config(rename = "type")] + ty: String, + } +} + +assert!("device.ty".parse::<WritableKey>().is_err()); +assert!("device.type".parse::<WritableKey>().is_ok()); +``` + +Fields and groups can be renamed (i.e. such that the field or group name as +observed by serde and `tedge config` does not match the Rust identifier) using +the `rename` attribute. This is generally useful for keys like `device.type`, +where the `type` field conflicts with the Rust `type` keyword. Instead we can +call the field `ty` and rename it to `type` as shown above (which is more +ergonomic and idiomatic than using a [raw +identifier](https://doc.rust-lang.org/rust-by-example/compatibility/raw_identifiers.html) +to solve the problem instead). + +### <a name="dep-name"></a>Deprecated names: `#[tedge_config(deprecated_name = "old_name")]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + group: { + #[tedge_config(deprecated_name = "old_name")] + new_name: String, + } +} + +assert_eq!( + "group.old_name".parse::<WritableKey>().unwrap(), + "group.new_name".parse::<WritableKey>().unwrap(), +); +``` + +Fields and groups can be updated in a backwards compatible manner (i.e. the name +can be changed between different thin-edge.io versions) with the +`deprecated_name` attribute. This is useful for e.g. renaming the `azure` group +to `az`, or renaming a field to include an `_` when it previously didn't. + +The alias that is created will apply to keys supplied to `tedge config`, values +read from `tedge.toml` and `TEDGE_` environment variables, and the field will be +automatically renamed in `tedge.toml` next time `tedge` writes to `tedge.toml`. + +### <a name="dep-key"></a>Deprecated keys: `#[tedge_config(deprecated_key = "some.old.key")]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + mqtt: { + bind: { + #[tedge_config(deprecated_key = "mqtt.port")] + port: u16, + } + } +} + +assert_eq!( + "mqtt.port".parse::<WritableKey>().unwrap(), + "mqtt.bind.port".parse::<WritableKey>().unwrap(), +); +``` +More complex field name updates can be carried out with the `deprecated_key` +attribute. This is required when a field is moved to a different group, e.g. +renaming `mqtt.port` to `mqtt.bind.port`. + +#### Note +The `deprecated_key` attribute only adds aliases that will be handled by `tedge +config` and `TEDGE_` environment variables, you will also need to add a TOML +migration step if making a change like this to ensure the field is moved in +`tedge.toml` too. + +## Documentation +### <a></a>Doc comments: `/// Comment` or `#[doc = "Comment"]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + /// You can document groups too, but this won't be shown to users + az: { + /// Endpoint URL of Azure IoT tenant + url: ConnectUrl, + } +} +``` + +<a name="docs-fields"></a>Doc comments for **fields** are automatically +converted to documentation in `tedge config list --doc`. All line breaks are +removed before this. They are also preserved on the generated `Reader` and `Dto` +structs. + +<a name="docs-groups"></a>Doc comments for **groups** are preserved on the +`Reader` and `Dto` structs, and ignored by `tedge config list --doc`. + +### <a name="examples"></a>Examples: `#[tedge_config(example = "value")]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + az: { + #[tedge_config(example = "myazure.azure-devices.net")] + url: ConnectUrl, + + mapper: { + // Examples are always specified using string literals + #[tedge_config(example = "true")] + timestamp: bool, + } + } +} +``` + +Examples can be added using `#[tedge_config(example = "value")]`. The value +provided must be a string literal. + +#### Example tests +A test will automatically be generated for each example field to check that the +provided value can be deserialised to the field's type (i.e. the example would +work if used with `tedge config set`) + +For the code above, `define_tedge_config!` will create a test to verify +`"myazure.azure-devices.net".parse::<ConnectUrl>().is_ok()` and another test +will verify `"true".parse::<bool>().is_ok()`. + +### <a name="notes"></a>Notes +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +use std::path::PathBuf; +use camino::Utf8PathBuf; + +define_tedge_config! { + c8y: { + #[tedge_config(note = "The value can be a directory path as well as the path of the direct certificate file.")] + #[doku(as = "PathBuf")] + root_cert_path: Utf8PathBuf, + } +} +``` + +Additional notes about fields can be added using `#[tedge_config(note = +"content")]`. This will be added to `tedge config list --doc` on a seperate +line, with a coloured heading to make it more distinctive. + +## Reader: `#[tedge_config(reader(...))]` +There are some options to customise the fields in the generated `Reader` struct. + +### <a name="reader-skip"></a> Skipping fields: `#[tedge_config(reader(skip))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + #[tedge_config(reader(skip))] + config: { + version: u32, + } +} + +assert!("config.version".parse::<WritableKey>().is_err()); +``` + +Groups can be omitted from the `Reader` struct entirely. This was added to +support the `config.version` field, which is used to manage `tedge.toml` +migrations. Since this is just a detail about the `tedge.toml` version, it must +not be exposed in `tedge config` or to other tedge crates. Omitting the group +from the reader using this attribute + +### <a name="reader-priv"></a> Private fields: `#[tedge_config(reader(private))]` +```rust compile_fail +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +mod tedge_config { + define_tedge_config! { + #[tedge_config(reader(skip))] + config: { + version: u32, + } + } +} + +use tedge_config::*; + +let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default()); +println!("{}", tedge_config::reader.config.version); // compile error! The field is not public +``` + +Occasionally, you may want to add custom logic, such as +[`all_or_nothing`](`crate::all_or_nothing::all_or_nothing`) to field accesses. +This attribute prevents the relevant `Reader` struct field from being marked +with `pub`, which prevents other crates from accessing the value directly. The +value is still exposed via `tedge config` to read from and write to. + +## Defaults +There are a variety of ways to specify default values for fields. Supplying a +default value for a field will result in the reader field being non-optional +(with the exception of [`from_optional_key`](#from-opt-key)). + +### <a name="default-lit"></a> Literals: `#[tedge_config(default(value = ...))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + mqtt: { + bind: { + #[tedge_config(default(value = 1883_u16))] + port: u16, + } + } +} + +let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default()); +assert_eq!(reader.mqtt.bind.port, 1883); +``` +If the value can be specified as a literal (e.g. for `bool`, primitive numeric +types (`u16`, `i32`, etc.), and strings), you can use the `value` specifier for +a default value. + +The implematation calls `.into()` on the provided value, so any field with a +type that implements `From<T>`, where `T` is the type of the literal, can be +filled using this method. As shown above, numeric literals may have to specify +their type [using a +suffix](https://doc.rust-lang.org/rust-by-example/primitives/literals.html). As +the value is a simply a literal, it does not have to be quoted. + +### <a name="default-var"></a> Variables/constants: `#[tedge_config(default(variable = "..."))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +use std::net::IpAddr; +use std::net::Ipv4Addr; +use camino::Utf8PathBuf; +use std::path::PathBuf; + +const DEFAULT_CERT_PATH: &str = "/etc/ssl/certs"; + +define_tedge_config! { + mqtt: { + bind: { + #[tedge_config(default(variable = "Ipv4Addr::LOCALHOST"))] + address: IpAddr, + } + }, + c8y: { + // This will default to `DEFAULT_CERT_PATH.into()`, so we + // can use a `&str` const as the default for this field + #[tedge_config(default(variable = "DEFAULT_CERT_PATH"))] + #[doku(as = "PathBuf")] + root_cert_path: Utf8PathBuf, + } +} + +let reader = TEdgeConfigReader::from_dto(&TEdgeConfigDto::default(), &TEdgeConfigLocation::default()); +assert_eq!(reader.mqtt.bind.address, Ipv4Addr::LOCALHOST); +``` + +Instead of providing a value as a literal, you can a reference a `const` value +using the `variable` specifier. Like `value`, the generated implematation calls +`.into()` on the constant. + +### <a name="from-key"></a> Fallback keys/derived keys `#[tedge_config(default(from_key = "..."))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + mqtt: { + bind: { + #[tedge_config(default(value = 1883_u16))] + port: u16 + }, + client: { + #[tedge_config(default(from_key = "mqtt.bind.port"))] + port: u16, + } + } +} + +let mut dto = TEdgeConfigDto::default(); +let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +assert_eq!(reader.mqtt.client.port, reader.mqtt.bind.port); + +dto.mqtt.bind.port = Some(2387); +let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +assert_eq!(reader.mqtt.client.port, reader.mqtt.bind.port); +``` + +Using `from_key`, the default value can be derived from the value of another +field. This allows the provided key to act as the default/fallback value for the +field. + +| `dto.mqtt.bind.port` | `dto.mqtt.client.port` | `reader.mqtt.bind.port` | `reader.mqtt.client.port` | +| -------------------- | ---------------------- | ----------------------- | ------------------------- | +| `None` | `None` | `1883` | `1883` | +| `Some(5678)` | `None` | `5678` | `5678` | +| `None` | `Some(1234)` | `1883` | `1234` | +| `Some(5678)` | `Some(1234)` | `1234` | `1234` | + +### <a name="from-opt-key"></a> Optional fallback keys `#[tedge_config(default(from_optional_key = "..."))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +# +define_tedge_config! { + c8y: { + url: ConnectUrl, + + /// The endpoint used for HTTP communication with Cumulocity + #[tedge_config(note = "This will fall back to the value of 'c8y.url' if it is not set")] + #[tedge_config(default(from_optional_key = "c8y.url"))] + http: ConnectUrl, + } +} + +let mut dto = TEdgeConfigDto::default(); +let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +assert_eq!(reader.c8y.url, reader.c8y.http); + +let not_set_err = reader.c8y.http.or_config_not_set().unwrap_err(); +assert!(not_set_err.to_string().contains("'c8y.url'")); + +dto.c8y.url = Some("test.cumulocity.com".parse().unwrap()); +let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation::default()); +assert_eq!(reader.c8y.url, reader.c8y.http); + +``` + +Using `from_optional_key`, the default value can be derived from the value of an +optional field (i.e. a field without a default value of its own). + +The type of the resulting reader field will be +[`OptionalConfig`](crate::OptionalConfig), with `Empty("fallback.key.here")` if +the value is not set. + +| `dto.c8y.url` | `dto.c8y.http` | `reader.c8y.url` | `reader.c8y.http` | +| --------------------- | -------------------------- | ------------------------ | ----------------------------- | +| `None` | `None` | `Empty("c8y.url")` | `Empty("c8y.url")` | +| `Some("example.com")` | `None` | `Present("example.com")` | `Present("example.com")` | +| `None` | `Some("http.example.com")` | `Empty("c8y.url")` | `Present("http.example.com")` | +| `Some("example.com")` | `Some("http.example.com")` | `Present("example.com")` | `Present("http.example.com")` | + +### <a name="default-fn"></a> Default functions: `#[tedge_config(default(function = "..."))]` +```rust +# use tedge_config_macros::*; +# #[derive(::thiserror::Error, Debug)] +# pub enum ReadError { #[error(transparent)] NotSet(#[from] ConfigNotSet)} +use std::num::NonZeroU16; +use camino::Utf8PathBuf; + +define_tedge_config! { + device: { + #[tedge_config(default(function = "default_device_cert_path"))] + #[doku(as = "std::path::PathBuf")] + cert_path: Utf8PathBuf, + }, + mqtt: { + bind: { + #[tedge_config(default(function = "default_mqtt_port"))] + #[doku(as = "u16")] + port: NonZeroU16, + + #[tedge_config(default(function = "|| NonZeroU16::try_from(1883).unwrap()"))] + #[doku(as = "u16")] + another_port: NonZeroU16, + }, + }, +} + +fn default_device_cert_path(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() +} + +``` + +The `function` sub-attribute allows a function to be supplied as the source for +a default value. The function can be supplied as either the name of a function, +or as a closure (both are demonstrated above). Through the magic of +[`TEdgeConfigDefault`], these functions can depend on `&TEdgeConfigDto`, +`&TEdgeConfigLocation`, neither, or both. The relevant values will be +automatically passed in. + + +## <a name="readonly"></a> Read only settings: `#[tedge_config(readonly(...))]` +The `readonly` attribute marks a field as read only, and has two required sub +attributes: + +- `#[tedge_config(readonly(function = "..."))]` specifies the function that is + called to populate the field in the reader, this is called lazily using + [`once_cell::sync::Lazy`] +- `#[tedge_config(readonly(write_error = "..."))]` specifies the error that will + be displayed if someone attempts to `tedge config set` the field + +```rust +use tedge_config_macros::*; + +#[derive(::thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + NotSet(#[from] ConfigNotSet), + + #[error("Couldn't read certificate")] + CertificateParseFailure(#[from] Box<dyn std::error::Error>), +} + +define_tedge_config! { + device: { + #[tedge_config(readonly( + function = "try_read_device_id", + write_error = "This setting is derived from the device certificate and is therefore read only.", + ))] + #[doku(as = "String")] + id: Result<String, ReadError>, + } +} + +fn try_read_device_id(_reader: &TEdgeConfigReader) -> Result<String, ReadError> { + unimplemented!() +} +``` +The `function` sub attribute is more restrictive than +[`default(function)`](#default-fn). The function must be passed in by name, and +must have a single argument of type `&TEdgeConfigReader`. diff --git a/crates/common/tedge_config_macros/src/doku_aliases.rs b/crates/common/tedge_config_macros/src/doku_aliases.rs new file mode 100644 index 00000000000..281344cd58b --- /dev/null +++ b/crates/common/tedge_config_macros/src/doku_aliases.rs @@ -0,0 +1,116 @@ +use std::borrow::Cow; +use std::collections::HashMap; + +fn dot_separate(prefix: Option<&str>, field: &str, sub_path: &str) -> Cow<'static, str> { + Cow::Owned( + prefix + .into_iter() + .chain([field, sub_path]) + .collect::<Vec<_>>() + .join("."), + ) +} + +/// Creates a map from aliases to canonical keys +pub fn struct_field_aliases( + prefix: Option<&str>, + fields: &[(&'static str, doku::Field)], +) -> HashMap<Cow<'static, str>, Cow<'static, str>> { + fields + .iter() + .flat_map(|(field_name, field)| match named_fields(&field.ty.kind) { + Some(fields) => { + // e.g. normal_field.alias + struct_field_aliases(Some(&key_name(prefix, field_name)), fields) + .into_iter() + // e.g. alias.normal_field + .chain(conventional_sub_paths(field, prefix, field_name, fields)) + // e.g. alias.other_alias + .chain(aliased_sub_paths(field, prefix, field_name, fields)) + .collect::<HashMap<_, _>>() + } + None => field + .aliases + .iter() + .map(|alias| (key_name(prefix, alias), key_name(prefix, field_name))) + .collect(), + }) + .collect() +} + +fn aliased_sub_paths( + field: &doku::Field, + prefix: Option<&str>, + field_name: &str, + sub_fields: &[(&'static str, doku::Field)], +) -> Vec<(Cow<'static, str>, Cow<'static, str>)> { + field + .aliases + .iter() + .flat_map(|alias| { + // e.g. alias.another_alias + struct_field_aliases(None, sub_fields).into_iter().map( + move |(nested_alias, resolved_subpath)| { + ( + dot_separate(prefix, alias, &nested_alias), + dot_separate(prefix, field_name, &resolved_subpath), + ) + }, + ) + }) + .collect() +} + +fn conventional_sub_paths( + field: &doku::Field, + prefix: Option<&str>, + name: &str, + sub_fields: &[(&'static str, doku::Field)], +) -> Vec<(Cow<'static, str>, Cow<'static, str>)> { + field + .aliases + .iter() + .flat_map(|alias| { + // e.g. alias.normal_field + struct_field_paths(None, sub_fields) + .into_iter() + .map(move |(path, _ty)| { + ( + dot_separate(prefix, alias, &path), + dot_separate(prefix, name, &path), + ) + }) + }) + .collect() +} + +/// Creates a "map" from keys to their doku type information +pub fn struct_field_paths( + prefix: Option<&str>, + fields: &[(&'static str, doku::Field)], +) -> Vec<(Cow<'static, str>, doku::Type)> { + fields + .iter() + .flat_map(|(name, field)| match named_fields(&field.ty.kind) { + Some(fields) => struct_field_paths(Some(&key_name(prefix, name)), fields), + None => vec![(key_name(prefix, name), field.ty.clone())], + }) + .collect() +} + +fn key_name(prefix: Option<&str>, name: &'static str) -> Cow<'static, str> { + match prefix { + Some(prefix) => Cow::Owned(format!("{}.{}", prefix, name)), + None => Cow::Borrowed(name), + } +} + +fn named_fields(kind: &doku::TypeKind) -> Option<&[(&'static str, doku::Field)]> { + match kind { + doku::TypeKind::Struct { + fields: doku::Fields::Named { fields }, + transparent: false, + } => Some(fields), + _ => None, + } +} diff --git a/crates/common/tedge_config_macros/src/example.rs b/crates/common/tedge_config_macros/src/example.rs new file mode 100644 index 00000000000..44e24656b8e --- /dev/null +++ b/crates/common/tedge_config_macros/src/example.rs @@ -0,0 +1,93 @@ +//! An example invocation of [define_tedge_config] to demonstrate what +//! it expands to in `cargo doc` output. +use super::*; +use camino::Utf8PathBuf; +use std::path::PathBuf; + +#[derive(thiserror::Error, Debug)] +/// *Not macro generated!* An error that can be encountered when reading values +/// from the configuration +/// +/// As custom logic (e.g. for read-only values) needs to interact with this, +/// this is left to the consuming module to define. It must include a case with +/// `#[from] ConfigNotSet`, for instance: +/// +/// ``` +/// use tedge_config_macros::ConfigNotSet; +/// +/// #[derive(thiserror::Error, Debug)] +/// pub enum ReadError { +/// #[error(transparent)] +/// ConfigNotSet(#[from] ConfigNotSet), +/// +/// // Add more cases, such as errors from inferring the device id, here +/// } +/// +/// ``` +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), +} + +/// A trait defined to conveniently emit [ReadError]s from [OptionalConfig] +/// values +/// +/// Since this depends on [ReadError], this is not macro generated. +/// +/// ``` +/// # #[derive(thiserror::Error, Debug)] +/// # pub enum ReadError { +/// # #[error(transparent)] +/// # ConfigNotSet(#[from] ConfigNotSet), +/// # } +/// +/// # pub trait OptionalConfigError<T> { +/// # fn or_err(&self) -> Result<&T, ReadError>; +/// # } +/// +/// # impl<T> OptionalConfigError<T> for OptionalConfig<T> { +/// # fn or_err(&self) -> Result<&T, ReadError> { +/// # self.or_config_not_set().map_err(ReadError::from) +/// # } +/// # } +/// +/// use tedge_config_macros::*; +/// +/// define_tedge_config! { +/// c8y: { +/// url: ConnectUrl, +/// } +/// } +/// +/// fn connect_to_c8y(reader: &TEdgeConfigReader) -> Result<(), ReadError> { +/// // If we fail here, the error message will tell the user that 'c8y.url' is unset +/// let url = reader.c8y.url.or_err()?; +/// println!("Connecting to Cumulocity: {url}"); +/// Ok(()) +/// } +/// ``` +pub trait OptionalConfigError<T> { + fn or_err(&self) -> Result<&T, ReadError>; +} + +impl<T> OptionalConfigError<T> for OptionalConfig<T> { + fn or_err(&self) -> Result<&T, ReadError> { + self.or_config_not_set().map_err(ReadError::from) + } +} + +define_tedge_config! { + /// The device settings. Group doc comments are not used in tedge config, but they are copied to the Reader and DTO. + device: { + /// The root certificate path + #[doku(as = "PathBuf")] + root_cert_path: Utf8PathBuf, + + #[doku(as = "PathBuf")] + #[tedge_config(default(from_optional_key = "device.root_cert_path"))] + root_cert_path2: Utf8PathBuf, + + #[tedge_config(rename = "type")] + ty: String, + } +} diff --git a/crates/common/tedge_config_macros/src/lib.rs b/crates/common/tedge_config_macros/src/lib.rs new file mode 100644 index 00000000000..f6e0515f31a --- /dev/null +++ b/crates/common/tedge_config_macros/src/lib.rs @@ -0,0 +1,18 @@ +#[doc(inline)] +#[doc = include_str!("define_tedge_config_docs.md")] +pub use tedge_config_macros_macro::define_tedge_config; + +pub use all_or_nothing::*; +#[doc(hidden)] +pub use connect_url::*; +pub use default::*; +pub use doku_aliases::*; +pub use option::*; + +mod all_or_nothing; +mod connect_url; +mod default; +mod doku_aliases; +#[cfg(doc)] +pub mod example; +mod option; diff --git a/crates/common/tedge_config_macros/src/option.rs b/crates/common/tedge_config_macros/src/option.rs new file mode 100644 index 00000000000..70004e78e3d --- /dev/null +++ b/crates/common/tedge_config_macros/src/option.rs @@ -0,0 +1,122 @@ +//! Handling for optional configuration values +//! +//! This module provides types used to represent the presence or absence of +//! values, but with the addition of metadata (such as the relevant +//! configuration key) to aid in producing informative error messages. + +#[derive(serde::Serialize, Clone, Copy, PartialEq, Eq, Debug)] +#[serde(into = "Option<T>", bound = "T: Clone + serde::Serialize")] +/// The value for an optional configuration (i.e. one without a default value) +/// +/// ``` +/// use tedge_config_macros::*; +/// +/// assert_eq!( +/// OptionalConfig::Present { value: "test", key: "device.type" }.or_none(), +/// Some(&"test"), +/// ); +/// ``` +pub enum OptionalConfig<T> { + /// Equivalent to `Some(T)` + Present { value: T, key: &'static str }, + /// Equivalent to `None`, but stores the configuration key to create a + /// better error message + Empty(&'static str), +} + +impl<T> From<OptionalConfig<T>> for Option<T> { + fn from(value: OptionalConfig<T>) -> Self { + match value { + OptionalConfig::Present { value, .. } => Some(value), + OptionalConfig::Empty(_key_name) => None, + } + } +} + +#[derive(thiserror::Error, Debug)] +#[error( + r#"A value for '{key}' is missing.\n\ + A value can be set with `tedge config set {key} <value>`"# +)] +/// A descriptive error for missing configurations +/// +/// When a configuration is missing, it can be converted to this via +/// [OptionalConfig::or_config_not_set], and this will convert to a descriptive +/// error message telling the user which key to set. +pub struct ConfigNotSet { + key: &'static str, +} + +impl<T> OptionalConfig<T> { + /// Converts the value to an [Option] + /// + /// ``` + /// use tedge_config_macros::*; + /// + /// assert_eq!( + /// OptionalConfig::Present { value: "test", key: "device.type" }.or_none(), + /// Some(&"test"), + /// ); + /// + /// assert_eq!(OptionalConfig::Empty::<&str>("device.type").or_none(), None); + /// ``` + pub fn or_none(&self) -> Option<&T> { + match self { + Self::Present { value, .. } => Some(value), + Self::Empty(_) => None, + } + } + + /// Converts the value to a [Result] with an error that contains the missing + /// key name + pub fn or_config_not_set(&self) -> Result<&T, ConfigNotSet> { + match self { + Self::Present { value, .. } => Ok(value), + Self::Empty(key) => Err(ConfigNotSet { key }), + } + } + + pub fn key(&self) -> &'static str { + match self { + Self::Present { key, .. } => key, + Self::Empty(key) => key, + } + } + + pub fn key_if_present(&self) -> Option<&'static str> { + match self { + Self::Present { key, .. } => Some(key), + Self::Empty(..) => None, + } + } + + pub fn key_if_empty(&self) -> Option<&'static str> { + match self { + Self::Empty(key) => Some(key), + Self::Present { .. } => None, + } + } + + pub fn as_ref(&self) -> OptionalConfig<&T> { + match self { + Self::Present { value, key } => OptionalConfig::Present { value, key }, + Self::Empty(key) => OptionalConfig::Empty(key), + } + } + + pub fn map<U>(self, f: impl FnOnce(T) -> U) -> OptionalConfig<U> { + match self { + Self::Present { value, key } => OptionalConfig::Present { + value: f(value), + key, + }, + Self::Empty(key) => OptionalConfig::Empty(key), + } + } +} + +impl<T: doku::Document> doku::Document for OptionalConfig<T> { + fn ty() -> doku::Type { + Option::<T>::ty() + } +} diff --git a/crates/common/tedge_config_macros/tests/aliases.rs b/crates/common/tedge_config_macros/tests/aliases.rs new file mode 100644 index 00000000000..3249366073c --- /dev/null +++ b/crates/common/tedge_config_macros/tests/aliases.rs @@ -0,0 +1,37 @@ +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), +} + +define_tedge_config! { + #[tedge_config(deprecated_name = "azure")] + az: { + mapper: { + timestamp: bool, + } + }, + device: { + #[tedge_config(rename = "type")] + ty: bool, + } +} + +#[test] +fn aliases_can_be_parsed_to_writable_keys() { + let _: WritableKey = "az.mapper.timestamp".parse().unwrap(); + let _: WritableKey = "azure.mapper.timestamp".parse().unwrap(); +} + +#[test] +fn aliases_can_be_parsed_to_readable_keys() { + let _: ReadableKey = "az.mapper.timestamp".parse().unwrap(); + let _: ReadableKey = "azure.mapper.timestamp".parse().unwrap(); +} + +#[test] +fn renamed_fields_can_be_parsed_to_writable_keys() { + let _: WritableKey = "device.type".parse().unwrap(); +} diff --git a/crates/common/tedge_config_macros/tests/defaults.rs b/crates/common/tedge_config_macros/tests/defaults.rs new file mode 100644 index 00000000000..f97c54b7f79 --- /dev/null +++ b/crates/common/tedge_config_macros/tests/defaults.rs @@ -0,0 +1,109 @@ +use camino::Utf8PathBuf; +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), +} + +#[test] +fn root_cert_path_default() { + const DEFAULT_ROOT_CERT_PATH: &str = "/etc/ssl/certs"; + + define_tedge_config! { + az: { + #[tedge_config(default(variable = "DEFAULT_ROOT_CERT_PATH"))] + #[doku(as = "std::path::PathBuf")] + root_cert_path: Utf8PathBuf, + } + } + + let dto = TEdgeConfigDto::default(); + let reader = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation); + assert_eq!(reader.az.root_cert_path, "/etc/ssl/certs"); +} + +#[test] +fn default_from_key_uses_the_correct_default() { + #![allow(unused_variables)] + define_tedge_config! { + test: { + #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))] + one: String, + #[tedge_config(default(from_key = "test.one"))] + two: String, + } + } + let dto = TEdgeConfigDto::default(); + assert_eq!( + TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation) + .test + .two, + "DEFAULT_VALUE_FOR_ONE" + ); +} + +#[test] +fn default_from_key_uses_the_value_of_other_field_if_set() { + #![allow(unused_variables)] + define_tedge_config! { + test: { + #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))] + one: String, + #[tedge_config(default(from_key = "test.one"))] + two: String, + } + } + let mut dto = TEdgeConfigDto::default(); + dto.test.one = Some("UPDATED_VALUE".into()); + assert_eq!( + TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation) + .test + .two, + "UPDATED_VALUE" + ); +} + +#[test] +fn default_from_key_uses_its_own_value_if_both_are_set() { + #![allow(unused_variables)] + define_tedge_config! { + test: { + #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))] + one: String, + #[tedge_config(default(from_key = "test.one"))] + two: String, + } + } + let mut dto = TEdgeConfigDto::default(); + dto.test.one = Some("UPDATED_VALUE".into()); + dto.test.two = Some("OWN_VALUE".into()); + assert_eq!( + TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation) + .test + .two, + "OWN_VALUE" + ); +} + +#[test] +fn default_from_key_uses_its_own_value_if_only_it_is_set() { + #![allow(unused_variables)] + define_tedge_config! { + test: { + #[tedge_config(default(value = "DEFAULT_VALUE_FOR_ONE"))] + one: String, + #[tedge_config(default(from_key = "test.one"))] + two: String, + } + } + let mut dto = TEdgeConfigDto::default(); + dto.test.two = Some("OWN_VALUE".into()); + assert_eq!( + TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation) + .test + .two, + "OWN_VALUE" + ); +} diff --git a/crates/common/tedge_config_macros/tests/examples.rs b/crates/common/tedge_config_macros/tests/examples.rs new file mode 100644 index 00000000000..f8b65eda9e4 --- /dev/null +++ b/crates/common/tedge_config_macros/tests/examples.rs @@ -0,0 +1,18 @@ +use camino::Utf8PathBuf; +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), +} + +// The macro invocation generates tests of its own for each example value +define_tedge_config! { + device: { + #[tedge_config(example = "/test/cert/path")] + #[tedge_config(example = "/test/cert/path2")] + #[doku(as = "std::path::PathBuf")] + root_cert_path: Utf8PathBuf, + } +} diff --git a/crates/common/tedge_config_macros/tests/optional.rs b/crates/common/tedge_config_macros/tests/optional.rs new file mode 100644 index 00000000000..e86fc3c9989 --- /dev/null +++ b/crates/common/tedge_config_macros/tests/optional.rs @@ -0,0 +1,28 @@ +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), +} + +#[test] +fn vacant_optional_configurations_contain_the_relevant_key() { + define_tedge_config! { + mqtt: { + external: { + bind: { + port: u16, + } + } + } + } + + let dto = TEdgeConfigDto::default(); + let config = TEdgeConfigReader::from_dto(&dto, &TEdgeConfigLocation); + + assert_eq!( + config.mqtt.external.bind.port, + OptionalConfig::Empty("mqtt.external.bind.port") + ); +} diff --git a/crates/core/tedge/Cargo.toml b/crates/core/tedge/Cargo.toml index ce6db3ea23a..4e5a795515e 100644 --- a/crates/core/tedge/Cargo.toml +++ b/crates/core/tedge/Cargo.toml @@ -16,11 +16,14 @@ maintainer-scripts = "../../../configuration/debian/tedge" [dependencies] anyhow = "1.0" +atty = "0.2" base64 = "0.13" camino = "1.1.4" certificate = { path = "../../common/certificate" } clap = { version = "3", features = ["cargo", "derive"] } +doku = "0.21" hyper = { version = "0.14", default-features = false } +pad = "0.1" reqwest = { version = "0.11", default-features = false, features = [ "blocking", "json", @@ -39,6 +42,7 @@ toml = "0.5" tracing = { version = "0.1", features = ["attributes", "log"] } url = "2.2" which = "4.2" +yansi = "0.5" [dev-dependencies] assert_cmd = "2.0" diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index 0a0066299da..883eb29984f 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -1,3 +1,5 @@ +use tedge_config::new::OptionalConfigError; + use super::create::CreateCertCmd; use super::remove::RemoveCertCmd; use super::show::ShowCertCmd; @@ -8,8 +10,6 @@ use crate::command::BuildContext; use crate::command::Command; use crate::ConfigError; -use tedge_config::*; - #[derive(clap::Subcommand, Debug)] pub enum TEdgeCertCli { /// Create a self-signed device certificate @@ -32,29 +32,29 @@ pub enum TEdgeCertCli { impl BuildCommand for TEdgeCertCli { fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> { - let config = context.config_repository.load()?; + let config = context.config_repository.load_new()?; let cmd = match self { TEdgeCertCli::Create { id } => { let cmd = CreateCertCmd { id, - cert_path: config.query(DeviceCertPathSetting)?, - key_path: config.query(DeviceKeyPathSetting)?, + cert_path: config.device.cert_path.clone(), + key_path: config.device.key_path.clone(), }; cmd.into_boxed() } TEdgeCertCli::Show => { let cmd = ShowCertCmd { - cert_path: config.query(DeviceCertPathSetting)?, + cert_path: config.device.cert_path.clone(), }; cmd.into_boxed() } TEdgeCertCli::Remove => { let cmd = RemoveCertCmd { - cert_path: config.query(DeviceCertPathSetting)?, - key_path: config.query(DeviceKeyPathSetting)?, + cert_path: config.device.cert_path.clone(), + key_path: config.device.key_path.clone(), }; cmd.into_boxed() } @@ -62,9 +62,9 @@ impl BuildCommand for TEdgeCertCli { TEdgeCertCli::Upload(cmd) => { let cmd = match cmd { UploadCertCli::C8y { username } => UploadCertCmd { - device_id: config.query(DeviceIdSetting)?, - path: config.query(DeviceCertPathSetting)?, - host: config.query(C8yHttpSetting)?, + device_id: config.device.id.try_read(&config)?.clone(), + path: config.device.cert_path.clone(), + host: config.c8y.http.or_err()?.to_owned(), username, }, }; diff --git a/crates/core/tedge/src/cli/certificate/error.rs b/crates/core/tedge/src/cli/certificate/error.rs index 1c83df69c1d..34c0ed23a6a 100644 --- a/crates/core/tedge/src/cli/certificate/error.rs +++ b/crates/core/tedge/src/cli/certificate/error.rs @@ -46,10 +46,10 @@ pub enum CertError { #[error("I/O error")] IoError(#[from] std::io::Error), - #[error("Invalid device.cert.path path: {0}")] + #[error("Invalid device.cert_path path: {0}")] CertPathError(PathsError), - #[error("Invalid device.key.path path: {0}")] + #[error("Invalid device.key_path path: {0}")] KeyPathError(PathsError), #[error(transparent)] diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index 067e7693f28..6b3da820e00 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -1,21 +1,21 @@ use crate::cli::config::commands::*; -use crate::cli::config::config_key::*; use crate::command::*; use crate::ConfigError; -use tedge_config::ConfigRepository; +use tedge_config::new::ReadableKey; +use tedge_config::new::WritableKey; #[derive(clap::Subcommand, Debug)] pub enum ConfigCmd { /// Get the value of the provided configuration key Get { /// Configuration key. Run `tedge config list --doc` for available keys - key: ConfigKey, + key: ReadableKey, }, /// Set or update the provided configuration key with the given value Set { /// Configuration key. Run `tedge config list --doc` for available keys - key: ConfigKey, + key: WritableKey, /// Configuration value. value: String, @@ -24,7 +24,7 @@ pub enum ConfigCmd { /// Unset the provided configuration key Unset { /// Configuration key. Run `tedge config list --doc` for available keys - key: ConfigKey, + key: WritableKey, }, /// Print the configuration keys and their values @@ -41,31 +41,29 @@ pub enum ConfigCmd { impl BuildCommand for ConfigCmd { fn build_command(self, context: BuildContext) -> Result<Box<dyn Command>, ConfigError> { - let config = context.config_repository.load()?; let config_repository = context.config_repository; match self { ConfigCmd::Get { key } => Ok(GetConfigCommand { - config_key: key, - config, + key, + config: config_repository.load_new()?, } .into_boxed()), ConfigCmd::Set { key, value } => Ok(SetConfigCommand { - config_key: key, + key, value, config_repository, } .into_boxed()), ConfigCmd::Unset { key } => Ok(UnsetConfigCommand { - config_key: key, + key, config_repository, } .into_boxed()), ConfigCmd::List { is_all, is_doc } => Ok(ListConfigCommand { is_all, is_doc, - config_keys: ConfigKey::list_all(), - config, + config: config_repository.load_new()?, } .into_boxed()), } diff --git a/crates/core/tedge/src/cli/config/commands/get.rs b/crates/core/tedge/src/cli/config/commands/get.rs index 82053e73767..ecffb89f56f 100644 --- a/crates/core/tedge/src/cli/config/commands/get.rs +++ b/crates/core/tedge/src/cli/config/commands/get.rs @@ -1,35 +1,27 @@ -use crate::cli::config::ConfigKey; +use tedge_config::tedge_config_cli::new::ReadableKey; + use crate::command::Command; pub struct GetConfigCommand { - pub config_key: ConfigKey, - pub config: tedge_config::TEdgeConfig, + pub key: ReadableKey, + pub config: tedge_config::new::TEdgeConfig, } impl Command for GetConfigCommand { fn description(&self) -> String { - format!( - "get the configuration value for key: {}", - self.config_key.key - ) + format!("get the configuration value for key: '{}'", self.key) } fn execute(&self) -> anyhow::Result<()> { - match (self.config_key.get)(&self.config) { + match self.config.read_string(self.key) { Ok(value) => { println!("{}", value); } - Err(tedge_config::ConfigSettingError::ConfigNotSet { .. }) => { - eprintln!( - "The provided config key: '{}' is not set", - self.config_key.key - ); + Err(tedge_config::new::ReadError::ConfigNotSet { .. }) => { + eprintln!("The provided config key: '{}' is not set", self.key); } - Err(tedge_config::ConfigSettingError::SettingIsNotConfigurable { .. }) => { - eprintln!( - "The provided config key: '{}' is not configurable", - self.config_key.key - ); + Err(tedge_config::new::ReadError::ReadOnlyNotFound { message, key }) => { + eprintln!("The provided config key: '{key}' is not configured: {message}",); } Err(err) => return Err(err.into()), } diff --git a/crates/core/tedge/src/cli/config/commands/list.rs b/crates/core/tedge/src/cli/config/commands/list.rs index 8645e7ecb3c..0ef8804cdd1 100644 --- a/crates/core/tedge/src/cli/config/commands/list.rs +++ b/crates/core/tedge/src/cli/config/commands/list.rs @@ -1,13 +1,14 @@ -use crate::cli::config::config_key::*; use crate::command::Command; use crate::ConfigError; -use tedge_config::*; +use pad::PadStr; +use tedge_config::new::ReadableKey; +use tedge_config::new::TEdgeConfig; +use tedge_config::new::READABLE_KEYS; pub struct ListConfigCommand { pub is_all: bool, pub is_doc: bool, pub config: TEdgeConfig, - pub config_keys: Vec<ConfigKey>, } impl Command for ListConfigCommand { @@ -17,31 +18,25 @@ impl Command for ListConfigCommand { fn execute(&self) -> anyhow::Result<()> { if self.is_doc { - print_config_doc(&self.config_keys); + print_config_doc(); } else { - print_config_list(&self.config_keys, &self.config, self.is_all)?; + print_config_list(&self.config, self.is_all)?; } Ok(()) } } -fn print_config_list( - config_keys: &[ConfigKey], - config: &TEdgeConfig, - all: bool, -) -> Result<(), ConfigError> { - let mut keys_without_values: Vec<String> = Vec::new(); - for config_key in config_keys { - match (config_key.get)(config) { - Ok(value) => { - println!("{}={}", config_key.key, value); +fn print_config_list(config: &TEdgeConfig, all: bool) -> Result<(), ConfigError> { + let mut keys_without_values = Vec::new(); + for config_key in ReadableKey::iter() { + match config.read_string(config_key).ok() { + Some(value) => { + println!("{}={}", config_key, value); } - Err(tedge_config::ConfigSettingError::ConfigNotSet { .. }) - | Err(tedge_config::ConfigSettingError::SettingIsNotConfigurable { .. }) => { - keys_without_values.push(config_key.key.into()); + None => { + keys_without_values.push(config_key); } - Err(err) => return Err(err.into()), } } if all && !keys_without_values.is_empty() { @@ -53,8 +48,73 @@ fn print_config_list( Ok(()) } -fn print_config_doc(config_keys: &[ConfigKey]) { - for config_key in config_keys { - println!("{:<30} {}", config_key.key, config_key.description); +fn print_config_doc() { + if atty::isnt(atty::Stream::Stdout) { + yansi::Paint::disable(); + } + + let max_length = ReadableKey::iter() + .map(|c| c.as_str().len()) + .max() + .unwrap_or_default(); + + for (key, ty) in READABLE_KEYS.iter() { + let docs = ty + .comment + .map(|c| { + let mut comment = c.replace('\n', " "); + if !comment.ends_with('.') { + comment.push('.'); + }; + comment.push(' '); + comment + }) + .unwrap_or_default(); + + println!( + "{} {}", + yansi::Paint::yellow( + key.pad_to_width_with_alignment(max_length, pad::Alignment::Right) + ), + yansi::Paint::default(docs).italic() + ); + + // TODO add a test to make sure people don't accidentally set the wrong meta name + if let Some(note) = ty.metas.get("note") { + println!( + "{} {} {note}", + "".pad_to_width(max_length), + yansi::Paint::blue("Note:") + ); + } + + match ty.example { + Some(doku::Example::Simple(val)) | Some(doku::Example::Literal(val)) => { + println!( + "{} {} {}", + "".pad_to_width(max_length), + yansi::Paint::green("Example:"), + val + ); + } + Some(doku::Example::Compound(val)) => { + let vals = val + .iter() + .map(|v| v.to_string()) + .collect::<Vec<_>>() + .join(", "); + println!( + "{} {} {}", + "".pad_to_width(max_length), + yansi::Paint::green("Examples:"), + vals + ); + } + None => (), + }; + + if atty::isnt(atty::Stream::Stdout) { + println!(); + } } } diff --git a/crates/core/tedge/src/cli/config/commands/set.rs b/crates/core/tedge/src/cli/config/commands/set.rs index bd4b277382c..5275a0ebcbe 100644 --- a/crates/core/tedge/src/cli/config/commands/set.rs +++ b/crates/core/tedge/src/cli/config/commands/set.rs @@ -1,9 +1,9 @@ -use crate::cli::config::ConfigKey; use crate::command::Command; +use tedge_config::new::WritableKey; use tedge_config::*; pub struct SetConfigCommand { - pub config_key: ConfigKey, + pub key: WritableKey, pub value: String, pub config_repository: TEdgeConfigRepository, } @@ -11,14 +11,17 @@ pub struct SetConfigCommand { impl Command for SetConfigCommand { fn description(&self) -> String { format!( - "set the configuration key: {} with value: {}.", - self.config_key.key, self.value + "set the configuration key: '{}' with value: {}.", + self.key.as_str(), + self.value ) } fn execute(&self) -> anyhow::Result<()> { - self.config_repository - .update_toml(&|config| (self.config_key.set)(config, self.value.to_string()))?; + self.config_repository.update_toml_new(&|dto| { + dto.try_update_str(self.key, &self.value) + .map_err(|e| e.into()) + })?; Ok(()) } } diff --git a/crates/core/tedge/src/cli/config/commands/unset.rs b/crates/core/tedge/src/cli/config/commands/unset.rs index 14d61b0452d..5f2279e43bc 100644 --- a/crates/core/tedge/src/cli/config/commands/unset.rs +++ b/crates/core/tedge/src/cli/config/commands/unset.rs @@ -1,22 +1,22 @@ -use crate::cli::config::ConfigKey; use crate::command::Command; +use tedge_config::new::WritableKey; use tedge_config::*; pub struct UnsetConfigCommand { - pub config_key: ConfigKey, + pub key: WritableKey, pub config_repository: TEdgeConfigRepository, } impl Command for UnsetConfigCommand { fn description(&self) -> String { - format!( - "unset the configuration value for key: {}", - self.config_key.key - ) + format!("unset the configuration value for '{}'", self.key) } fn execute(&self) -> anyhow::Result<()> { - self.config_repository.update_toml(&self.config_key.unset)?; + self.config_repository.update_toml_new(&|dto| { + dto.unset_key(self.key); + Ok(()) + })?; Ok(()) } } diff --git a/crates/core/tedge/src/cli/connect/bridge_config_aws.rs b/crates/core/tedge/src/cli/connect/bridge_config_aws.rs index 806466fee39..2b462d35b05 100644 --- a/crates/core/tedge/src/cli/connect/bridge_config_aws.rs +++ b/crates/core/tedge/src/cli/connect/bridge_config_aws.rs @@ -25,7 +25,7 @@ impl From<BridgeConfigAwsParams> for BridgeConfig { bridge_keyfile, } = params; - let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port); + let address = format!("{}:{}", connect_url, mqtt_tls_port); let user_name = remote_clientid.to_string(); // telemetry/command topics for use by the user diff --git a/crates/core/tedge/src/cli/connect/bridge_config_azure.rs b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs index 6efb68f6e3f..d2b1e727174 100644 --- a/crates/core/tedge/src/cli/connect/bridge_config_azure.rs +++ b/crates/core/tedge/src/cli/connect/bridge_config_azure.rs @@ -25,11 +25,10 @@ impl From<BridgeConfigAzureParams> for BridgeConfig { bridge_keyfile, } = params; - let address = format!("{}:{}", connect_url.as_str(), mqtt_tls_port); + let address = format!("{}:{}", connect_url, mqtt_tls_port); let user_name = format!( "{}/{}/?api-version=2018-06-30", - connect_url.as_str(), - remote_clientid + connect_url, remote_clientid ); let pub_msg_topic = format!("messages/events/# out 1 az/ devices/{}/", remote_clientid); let sub_msg_topic = format!( diff --git a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs index 668a9e14279..c11408a0209 100644 --- a/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs +++ b/crates/core/tedge/src/cli/connect/c8y_direct_connection.rs @@ -84,13 +84,13 @@ pub fn create_device_with_direct_connection( { if let AlertDescription::CertificateUnknown = alert_description { // Either the device cert is not uploaded to c8y or - // another cert is set in device.cert.path + // another cert is set in device.cert_path eprintln!("The device certificate is not trusted by Cumulocity."); return Err(ConnectError::ConnectionCheckError); } else if let AlertDescription::HandshakeFailure = alert_description { - // Non-paired private key is set in device.key.path + // Non-paired private key is set in device.key_path eprintln!( - "The private key is not paired with the certificate. Check your 'device.key.path'." + "The private key is not paired with the certificate. Check your 'device.key_path'." ); return Err(ConnectError::ConnectionCheckError); } @@ -108,7 +108,7 @@ pub fn create_device_with_direct_connection( Some(Error::InvalidCertificateData(description)) if description == "invalid peer certificate: UnknownIssuer" => { - eprintln!("Cumulocity certificate is not trusted by the device. Check your 'c8y.root.cert.path'."); + eprintln!("Cumulocity certificate is not trusted by the device. Check your 'c8y.root_cert_path'."); } _ => { eprintln!("ERROR: {:?}", err); diff --git a/crates/core/tedge/src/error.rs b/crates/core/tedge/src/error.rs index 14341cbe909..1f87474c285 100644 --- a/crates/core/tedge/src/error.rs +++ b/crates/core/tedge/src/error.rs @@ -24,4 +24,7 @@ pub enum TEdgeError { #[error(transparent)] FromSystemServiceError(#[from] tedge_config::system_services::SystemServiceError), + + #[error(transparent)] + FromTEdgeConfigRead(#[from] tedge_config::new::ReadError), } diff --git a/crates/core/tedge/src/main.rs b/crates/core/tedge/src/main.rs index ef8f8554235..fc577eaa27a 100644 --- a/crates/core/tedge/src/main.rs +++ b/crates/core/tedge/src/main.rs @@ -20,10 +20,10 @@ const BROKER_USER: &str = "mosquitto"; const BROKER_GROUP: &str = "mosquitto"; fn main() -> anyhow::Result<()> { - let opt = cli::Opt::parse(); - set_log_level(tracing::Level::WARN); + let opt = cli::Opt::parse(); + if opt.init { initialize_tedge(&opt.config_dir) .with_context(|| "Failed to initialize tedge. You have to run tedge with sudo.")?; diff --git a/crates/core/tedge/tests/main.rs b/crates/core/tedge/tests/main.rs index 50952d92750..131561a26e4 100644 --- a/crates/core/tedge/tests/main.rs +++ b/crates/core/tedge/tests/main.rs @@ -52,7 +52,7 @@ mod tests { home_dir, "config", "set", - "device.cert.path", + "device.cert_path", &cert_path, ])?; let mut set_key_path_cmd = tedge_command_with_test_home([ @@ -60,7 +60,7 @@ mod tests { home_dir, "config", "set", - "device.key.path", + "device.key_path", &key_path, ])?; @@ -88,7 +88,7 @@ mod tests { get_device_id_cmd .assert() .success() - .stderr(predicate::str::contains("'device.id' is not configurable")); + .stderr(predicate::str::contains("'device.id' is not configured")); // The create command created a certificate create_cmd.assert().success(); @@ -187,7 +187,7 @@ mod tests { "The provided config key: \'c8y.url\' is not set\n", false )] - #[test_case("mqtt.port", "8880", "1883", true)] + #[test_case("mqtt.bind.port", "8880", "1883", true)] fn run_config_set_get_unset_read_write_key( config_key: &str, config_value: &str, @@ -294,7 +294,7 @@ mod tests { test_home_str, "config", "get", - "device.cert.path", + "device.cert_path", ])?; get_cert_path_cmd @@ -307,7 +307,7 @@ mod tests { test_home_str, "config", "get", - "device.key.path", + "device.key_path", ])?; get_key_path_cmd @@ -335,7 +335,7 @@ mod tests { test_home_str, "config", "get", - "c8y.root.cert.path", + "c8y.root_cert_path", ])?; get_c8y_root_cert_path_cmd @@ -358,22 +358,21 @@ mod tests { let output = assert.get_output().clone(); let output_str = String::from_utf8(output.stdout).unwrap(); - let key_path = extract_config_value(&output_str, "device.key.path"); + let key_path = extract_config_value(&output_str, "device.key_path"); assert!(key_path.ends_with("tedge-private-key.pem")); assert!(key_path.contains(test_home_str)); - let cert_path = extract_config_value(&output_str, "device.cert.path"); + let cert_path = extract_config_value(&output_str, "device.cert_path"); assert!(cert_path.ends_with("tedge-certificate.pem")); assert!(cert_path.contains(test_home_str)); } - fn extract_config_value(output: &str, key: &str) -> String { + fn extract_config_value<'a>(output: &'a str, key: &str) -> &'a str { output .lines() .map(|line| line.splitn(2, '=').collect::<Vec<_>>()) .find(|pair| pair[0] == key) - .unwrap()[1] - .into() + .unwrap_or_else(|| panic!("couldn't find config value for '{key}'"))[1] } #[test] @@ -401,16 +400,19 @@ mod tests { let output = assert.get_output(); let output_str = String::from_utf8(output.clone().stdout).unwrap(); - let key_path = extract_config_value(&output_str, "device.key.path"); + let key_path = extract_config_value(&output_str, "device.key_path"); assert!(key_path.ends_with("tedge-private-key.pem")); assert!(key_path.contains(test_home_str)); - let cert_path = extract_config_value(&output_str, "device.cert.path"); + let cert_path = extract_config_value(&output_str, "device.cert_path"); assert!(cert_path.ends_with("tedge-certificate.pem")); assert!(cert_path.contains(test_home_str)); for key in get_tedge_config_keys() { - assert!(output_str.contains(key)); + assert!( + output_str.contains(key), + "couldn't find '{key}' in output of tedge config list --all" + ); } } @@ -432,7 +434,10 @@ mod tests { let output_str = String::from_utf8(output.stdout).unwrap(); for key in get_tedge_config_keys() { - assert!(output_str.contains(key)); + assert!( + output_str.contains(key), + "couldn't find '{key}' in output of tedge config list --doc" + ); } assert!(output_str.contains("Example")); } @@ -455,10 +460,10 @@ mod tests { fn get_tedge_config_keys() -> Vec<&'static str> { let vec = vec![ "device.id", - "device.key.path", - "device.cert.path", + "device.key_path", + "device.cert_path", "c8y.url", - "c8y.root.cert.path", + "c8y.root_cert_path", ]; vec } diff --git a/docs/src/howto-guides/006_config.md b/docs/src/howto-guides/006_config.md index f20ca561535..6c52a8c96cf 100644 --- a/docs/src/howto-guides/006_config.md +++ b/docs/src/howto-guides/006_config.md @@ -56,9 +56,9 @@ The names for these environment variables are prefixed with `TEDGE_` to avoid co | Setting | Environment variable | | ------------------- | ------------------------- | | `c8y.url` | `TEDGE_C8Y_URL` | -| `device.key.path` | `TEDGE_DEVICE_KEY_PATH` | -| `device.cert.path` | `TEDGE_DEVICE_CERT_PATH` | -| `mqtt.bind_address` | `TEDGE_MQTT_BIND_ADDRESS` | +| `device.key_path` | `TEDGE_DEVICE_KEY_PATH` | +| `device.cert_path` | `TEDGE_DEVICE_CERT_PATH` | +| `mqtt.bind.address` | `TEDGE_MQTT_BIND_ADDRESS` | You can also use `tedge config` to inspect the value that is set, which may prove useful if you are using a mix of toml configuration and environment variables. If you had tedge.toml file set as shown [above](#tedgetoml), you could run: diff --git a/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md b/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md index 585fb5ed6e6..46bde422d09 100644 --- a/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md +++ b/docs/src/howto-guides/008_config_local_mqtt_bind_address_and_port.md @@ -3,7 +3,7 @@ Configuring a mosquitto port and bind address in thin-edge.io is a three-step process. ```admonish note -The mqtt.port and the mqtt.bind_address can be set/unset independently. +The mqtt.bind.port and the mqtt.bind.address can be set/unset independently. ``` ## Step 1: Disconnect thin-edge.io edge device @@ -16,14 +16,14 @@ tedge disconnect c8y/az ## Step 2: Set and verify the new mqtt port and bind address -Use the `tedge` command to set the mqtt.port and mqtt.bind_address with a desired port and bind address as below. +Use the `tedge` command to set the mqtt.bind.port and mqtt.bind.address with a desired port and bind address as below. ```shell -tedge config set mqtt.port 1024 +tedge config set mqtt.bind.port 1024 ``` ```shell -tedge config set mqtt.bind_address 127.0.0.1 +tedge config set mqtt.bind.address 127.0.0.1 ``` ```admonish note @@ -39,11 +39,11 @@ has been set once the device is connected to the cloud as in step 3. Use the `tedge` command to print the mqtt port and bind address that has been set as below. ```shell -tedge config get mqtt.port +tedge config get mqtt.bind.port ``` ```shell -tedge config get mqtt.bind_address +tedge config get mqtt.bind.address ``` ## Step 3: Connect the thin edge device to cloud @@ -72,11 +72,11 @@ Note: The step 1 and 2 can be followed in any order. Use the `tedge` command to set the default port (1883) and default bind address (localhost) as below. ```shell -tedge config unset mqtt.port +tedge config unset mqtt.bind.port ``` ```shell -tedge config unset mqtt.bind_address +tedge config unset mqtt.bind.address ``` Once the port or the bind address is reverted to default, the [step 1](#Step-3:-Connect-the-thin-edge-device-to-cloud) @@ -87,9 +87,9 @@ and 3 has to be followed to use the default port or the default bind address. The below example shows that we cannot set a string value for the port number. ```shell -tedge config set mqtt.port '"1234"' +tedge config set mqtt.bind.port '"1234"' -Error: failed to set the configuration key: mqtt.port with value: "1234". +Error: failed to set the configuration key: mqtt.bind.port with value: "1234". Caused by: Conversion from String failed diff --git a/docs/src/howto-guides/013_connect_external_device.md b/docs/src/howto-guides/013_connect_external_device.md index 5efd8ba1c5e..989fac5a7b6 100644 --- a/docs/src/howto-guides/013_connect_external_device.md +++ b/docs/src/howto-guides/013_connect_external_device.md @@ -12,13 +12,13 @@ External devices connection can be setup by using the `tedge` cli tool making so The following configurations option are available for you if you want to add an external listener to thin-edge.io: -`mqtt.external.port` Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. Example: 8883 -`mqtt.external.bind_address` IP address / hostname, which the mqtt broker limits incoming connections on. Example: 0.0.0.0 -`mqtt.external.bind_interface` Name of network interface, which the mqtt broker limits incoming connections on. Example: wlan0 +`mqtt.external.bind.port` Mqtt broker port, which is used by the external mqtt clients to publish or subscribe. Example: 8883 +`mqtt.external.bind.address` IP address / hostname, which the mqtt broker limits incoming connections on. Example: 0.0.0.0 +`mqtt.external.bind.interface` Name of network interface, which the mqtt broker limits incoming connections on. Example: wlan0 -`mqtt.external.capath` Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates. Example: /etc/ssl/certs -`mqtt.external.certfile` Path to the certificate file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-certificate.pem -`mqtt.external.keyfile` Path to the private key file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-private-key.pem +`mqtt.external.ca_path` Path to a file containing the PEM encoded CA certificates that are trusted when checking incoming client certificates. Example: /etc/ssl/certs +`mqtt.external.cert_file` Path to the certificate file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-certificate.pem +`mqtt.external.key_file` Path to the private key file, which is used by external MQTT listener. Example: /etc/tedge/server-certs/tedge-private-key.pem ```admonish note If none of these options is set, then no external listener is set. @@ -31,35 +31,35 @@ These settings can be considered in 2 groups, listener configuration and TLS con ### Configure basic listener To configure basic listener you should provide port and/or bind address which will use default interface. -To change the default interface you can use mqtt.external.bind_interface configuration option. +To change the default interface you can use mqtt.external.bind.interface configuration option. To set them you can use `tedge config` as so: ```shell -tedge config set mqtt.external.port 8883 +tedge config set mqtt.external.bind.port 8883 ``` To allow connections from all IP addresses on the interface: ```shell -tedge config set mqtt.external.bind_address 0.0.0.0 +tedge config set mqtt.external.bind.address 0.0.0.0 ``` ### Configure TLS on the listener -To configure the external listener with TLS additional settings are available: `mqtt.external.capath` `mqtt.external.certfile` `mqtt.external.keyfile` +To configure the external listener with TLS additional settings are available: `mqtt.external.ca_path` `mqtt.external.cert_file` `mqtt.external.key_file` To enable MQTT over TLS, a server side certificate must be configured using the 2 following settings: ```shell -tedge config set mqtt.external.certfile /etc/tedge/server-certs/tedge-certificate.pem -tedge config set mqtt.external.keyfile /etc/tedge/server-certs/tedge-private-key.pem +tedge config set mqtt.external.cert_file /etc/tedge/server-certs/tedge-certificate.pem +tedge config set mqtt.external.key_file /etc/tedge/server-certs/tedge-private-key.pem ``` To fully enable TLS authentication clients, client side certificate validation can be enabled: ```shell -tedge config set mqtt.external.capath /etc/ssl/certs +tedge config set mqtt.external.ca_path /etc/ssl/certs ``` ```admonish note diff --git a/docs/src/howto-guides/029_mqtt_local_broker_authentication.md b/docs/src/howto-guides/029_mqtt_local_broker_authentication.md index a2a5c0d6ae3..21258d3c0f3 100644 --- a/docs/src/howto-guides/029_mqtt_local_broker_authentication.md +++ b/docs/src/howto-guides/029_mqtt_local_broker_authentication.md @@ -90,8 +90,8 @@ keyfile PATH_TO_CLIENT_CA_CERTIFICATE ### Step 2: Configure thin-edge.io to use a client certificate and private key ```sh -tedge config set mqtt.client.auth.certfile PATH_TO_CLIENT_CERTIFICATE -tedge config set mqtt.client.auth.keyfile PATH_TO_CLIENT_PRIVATE_KEY +tedge config set mqtt.client.auth.cert_file PATH_TO_CLIENT_CERTIFICATE +tedge config set mqtt.client.auth.key_file PATH_TO_CLIENT_PRIVATE_KEY ``` Both `certfile` and `keyfile` are required to enable client authentication. diff --git a/docs/src/howto-guides/child_device_config_management_agent.md b/docs/src/howto-guides/child_device_config_management_agent.md index 3c72574119c..a038330f7ba 100644 --- a/docs/src/howto-guides/child_device_config_management_agent.md +++ b/docs/src/howto-guides/child_device_config_management_agent.md @@ -31,7 +31,7 @@ MQTT messages before and after the configuration file is uploaded/downloaded via Since child device agents typically run on an external device and not on the thin-edge device itself, the MQTT and HTTP APIs of thin-edge need to be accessed over the network using its IP address, -which is configured using the tedge configuration settings `mqtt.external.bind_address` or `mqtt.bind_address`. +which is configured using the tedge configuration settings `mqtt.external.bind.address` or `mqtt.bind.address`. The MQTT APIs are exposed via port 1883 and the HTTP APIs are exposed via port 8000. In rare cases, where the child device agent is installed alongside thin-edge on the same device, these APIs can be accessed via a local IP or even `127.0.0.1`. @@ -64,7 +64,7 @@ The child device agent needs to upload this file to thin-edge with an HTTP PUT r to the URL: `http://{tedge-ip}:8000/tedge/file-transfer/{child-id}/c8y-configuration-plugin` * `{tedge-ip}` is the IP of the thin-edge device which is configured as -`mqtt.external.bind_address` or `mqtt.bind_address` or `127.0.0.1` if neither is configured. +`mqtt.external.bind.address` or `mqtt.bind.address` or `127.0.0.1` if neither is configured. * `{child-id}` is the child-device-id Once the upload is complete, the agent should notify thin-edge about the upload by sending the following MQTT message: diff --git a/docs/src/references/c8y-configuration-management.md b/docs/src/references/c8y-configuration-management.md index 0d6e6c33708..cd3b72c0250 100644 --- a/docs/src/references/c8y-configuration-management.md +++ b/docs/src/references/c8y-configuration-management.md @@ -257,8 +257,8 @@ Note that: The behavior of the `c8y-configuration-plugin` is also controlled by the configuration of thin-edge: -* `tedge config get mqtt.bind_address`: the address of the local MQTT bus. -* `tedge config get mqtt.port`: the TCP port of the local MQTT bus. +* `tedge config get mqtt.bind.address`: the address of the local MQTT bus. +* `tedge config get mqtt.bind.port`: the TCP port of the local MQTT bus. * `tedge config get tmp.path`: the directory where the files are updated before being copied atomically to their targets. @@ -339,11 +339,11 @@ From a TCP point of view, the child devices act as clients and all the connections to thin-edge are established by the child devices. * Thin-edge opens two ports for MQTT and HTTP over the local network. These ports are controlled on the main device `tedge config`: - * `mqtt.external.bind_address` - * `mqtt.external.port` + * `mqtt.external.bind.address` + * `mqtt.external.bind.port` * `http.external.port` * The child devices must know the main device IP address. - * This is the address set for `mqtt.external.bind_address` on the main device. + * This is the address set for `mqtt.external.bind.address` on the main device. * For the very specific case, where the child-device agent runs on the main device, this connection address can be the `localhost`. * On start, the child-device agent for configuration management, diff --git a/docs/src/references/tedge-file-transfer-service.md b/docs/src/references/tedge-file-transfer-service.md index 2bd459761cd..75e200aeaf9 100644 --- a/docs/src/references/tedge-file-transfer-service.md +++ b/docs/src/references/tedge-file-transfer-service.md @@ -12,11 +12,11 @@ Files can be uploaded, downloaded and deleted from this repository via the follo The `tedge-ip` is derived from the following tedge configurations: -* mqtt.bind_address -* mqtt.external.bind_address +* mqtt.bind.address +* mqtt.external.bind.address -If the `mqtt.external.bind_address` is configured, then the `tedge-ip` is set to that value, -else the `mqtt.bind_address` is used with the default value of `127.0.0.1`. +If the `mqtt.external.bind.address` is configured, then the `tedge-ip` is set to that value, +else the `mqtt.bind.address` is used with the default value of `127.0.0.1`. The files uploaded to this repository are stored at `/var/tedge/file-transfer` directory. The `{path}/{to}/{resource}` specified in the URL is replicated under this directory. diff --git a/docs/src/tutorials/child-device-config-management.md b/docs/src/tutorials/child-device-config-management.md index 2b81e386b0d..a0ff898e084 100644 --- a/docs/src/tutorials/child-device-config-management.md +++ b/docs/src/tutorials/child-device-config-management.md @@ -61,7 +61,7 @@ The rest of the MQTT and HTTP interactions would remain the same. Since child-device connectors typically run on thin-edge.io device itself, these APIs can be accessed via a local IP or even 127.0.0.1. In cases where the child-device connector is deployed on the external child-device itself, the MQTT and HTTP APIs of thin-edge.io need to be accessed over the network using its IP address, -which is configured using the thin-edge.io configuration settings `mqtt.external.bind_address` or `mqtt.bind_address`. +which is configured using the thin-edge.io configuration settings `mqtt.external.bind.address` or `mqtt.bind.address`. The MQTT APIs are exposed via port 1883 and the HTTP APIs are exposed via port 8000. In this tutorial 127.0.0.1. is used. @@ -145,7 +145,7 @@ Follow these steps to bootstrap the child device: The child-device connector needs to upload this file to thin-edge.io with an HTTP PUT request to the URL: `http://{tedge-ip}:8000/tedge/file-transfer/{child-id}/c8y-configuration-plugin` - * `{tedge-ip}` is the IP of the thin-edge.io device which is configured as `mqtt.external.bind_address` or `mqtt.bind_address` or + * `{tedge-ip}` is the IP of the thin-edge.io device which is configured as `mqtt.external.bind.address` or `mqtt.bind.address` or `127.0.0.1` if neither is configured. * `{child-id}` is the child-device-id. diff --git a/docs/src/tutorials/connect-aws.md b/docs/src/tutorials/connect-aws.md index 3f26feba7a2..2971e56f9ef 100644 --- a/docs/src/tutorials/connect-aws.md +++ b/docs/src/tutorials/connect-aws.md @@ -91,7 +91,7 @@ The URL is unique to the AWS account and region that is used, and can be found i Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`. ```shell -sudo tedge config set aws.root.cert.path /etc/ssl/certs/AmazonRootCA1.pem +sudo tedge config set aws.root_cert_path /etc/ssl/certs/AmazonRootCA1.pem ``` This will set the root certificate path of the AWS IoT Hub. In most of the Linux flavors, the certificate will be diff --git a/docs/src/tutorials/connect-azure.md b/docs/src/tutorials/connect-azure.md index c0729b6d528..6ebb72f6d7c 100644 --- a/docs/src/tutorials/connect-azure.md +++ b/docs/src/tutorials/connect-azure.md @@ -94,7 +94,7 @@ The URL/Hostname can be found in the Azure web portal, clicking on the overview Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`. ```shell -sudo tedge config set az.root.cert.path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem +sudo tedge config set az.root_cert_path /etc/ssl/certs/Baltimore_CyberTrust_Root.pem ``` This will set the root certificate path of the Azure IoT Hub. diff --git a/docs/src/tutorials/connect-c8y.md b/docs/src/tutorials/connect-c8y.md index 19472a82d32..4c2b4ed843d 100644 --- a/docs/src/tutorials/connect-c8y.md +++ b/docs/src/tutorials/connect-c8y.md @@ -38,7 +38,7 @@ sudo tedge config set c8y.url your-tenant.cumulocity.com Set the path to the root certificate if necessary. The default is `/etc/ssl/certs`. ``` -sudo tedge config set c8y.root.cert.path /etc/ssl/certs +sudo tedge config set c8y.root_cert_path /etc/ssl/certs ``` This will set the root certificate path of the Cumulocity IoT. @@ -49,10 +49,10 @@ If not found download it from [here](https://www.identrust.com/dst-root-ca-x3). ## Connecting to Cumulocity server signed with self-signed certificate If the Cumulocity IoT instance that you're connecting to, is signed with a self-signed certificate(eg: Cumulocity IoT Edge instance), -then the path to that server certificate must be set as the c8y.root.cert.path as follows: +then the path to that server certificate must be set as the c8y.root_cert_path as follows: ``` -sudo tedge config set c8y.root.cert.path /path/to/the/self-signed/certificate +sudo tedge config set c8y.root_cert_path /path/to/the/self-signed/certificate ``` ```admonish warning diff --git a/plugins/c8y_firmware_plugin/src/main.rs b/plugins/c8y_firmware_plugin/src/main.rs index a810699d58c..e231cbc0b3e 100644 --- a/plugins/c8y_firmware_plugin/src/main.rs +++ b/plugins/c8y_firmware_plugin/src/main.rs @@ -28,7 +28,7 @@ During a successful operation, `c8y-firmware-plugin` updates the installed firmw The thin-edge `CONFIG_DIR` is used to find where: * to store temporary files on download: `tedge config get tmp.path`, * to log operation errors and progress: `tedge config get log.path`, - * to connect the MQTT bus: `tedge config get mqtt.port`, + * to connect the MQTT bus: `tedge config get mqtt.bind.port`, * to timeout pending operations: `tedge config get firmware.child.update.timeout"#; #[derive(Debug, clap::Parser)] diff --git a/tests/PySys/misc_features/invalid_device_id/run.py b/tests/PySys/misc_features/invalid_device_id/run.py index a9e674689e1..a5c4120704f 100644 --- a/tests/PySys/misc_features/invalid_device_id/run.py +++ b/tests/PySys/misc_features/invalid_device_id/run.py @@ -39,7 +39,7 @@ def setup(self): self.tedge, "config", "set", - "device.cert.path", + "device.cert_path", "/tmp/test-device-certs/tedge-certificate.pem", ], stdouterr="set_cert_path", @@ -52,7 +52,7 @@ def setup(self): self.tedge, "config", "set", - "device.key.path", + "device.key_path", "/tmp/test-device-certs/tedge-private-key.pem", ], stdouterr="set_key_path", @@ -80,17 +80,17 @@ def validate(self): self.assertGrep("cert_create.err", "DeviceID Error", contains=True) def device_id_cleanup(self): - # unset the custom device.cert.path + # unset the custom device.cert_path unset_cert_path = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "device.cert.path"], + arguments=[self.tedge, "config", "unset", "device.cert_path"], stdouterr="unset_cert_path", ) - # unset the custom device.key.path + # unset the custom device.key_path unset_key_path = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "device.key.path"], + arguments=[self.tedge, "config", "unset", "device.key_path"], stdouterr="unset_key_path", ) diff --git a/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py b/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py index ae4bcd185d3..73a3ecc1d1d 100644 --- a/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py +++ b/tests/PySys/misc_features/mqtt_port_change_connection_fails/run.py @@ -8,7 +8,7 @@ Validate changing the mqtt port using the tedge command that fails without restarting the mqtt server Given a configured system, that is configured with certificate created and registered in a cloud -When `tedge mqtt.port set` with `sudo` +When `tedge mqtt.bind.port set` with `sudo` When the `sudo tedge mqtt sub` tries to subscribe for a topic and fails to connect to mqtt server When the `sudo tedge mqtt pub` tries to publish a message and fails to connect to mqtt server @@ -26,7 +26,7 @@ def execute(self): # set a new mqtt port for local communication mqtt_port = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "set", "mqtt.port", "8880"], + arguments=[self.tedge, "config", "set", "mqtt.bind.port", "8880"], stdouterr="mqtt_port_set", ) @@ -57,6 +57,6 @@ def mqtt_cleanup(self): # unset a new mqtt port, falls back to default port (1883) mqtt_port = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "mqtt.port"], + arguments=[self.tedge, "config", "unset", "mqtt.bind.port"], stdouterr="mqtt_port_unset", ) diff --git a/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py b/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py index 4eab578e73c..c9a575f9639 100644 --- a/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py +++ b/tests/PySys/misc_features/mqtt_port_change_connection_works/run.py @@ -10,7 +10,7 @@ Given a configured system, that is configured with certificate created and registered in a cloud When the thin edge device is disconnected from cloud, `sudo tedge disconnect c8y` -When `tedge mqtt.port set` with sudo +When `tedge mqtt.bind.port set` with sudo When the thin edge device is connected to cloud, `sudo tedge connect c8y` Now validate the services that use the mqtt port Validate tedge mqtt pub/sub @@ -32,7 +32,7 @@ def setup(self): # set a new mqtt port for local communication mqtt_port = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "set", "mqtt.port", "8889"], + arguments=[self.tedge, "config", "set", "mqtt.bind.port", "8889"], stdouterr="mqtt_port", ) self.addCleanupFunction(self.mqtt_cleanup) @@ -177,7 +177,7 @@ def mqtt_cleanup(self): # unset a new mqtt port, falls back to default port (1883) mqtt_port = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "mqtt.port"], + arguments=[self.tedge, "config", "unset", "mqtt.bind.port"], stdouterr="mqtt_port_unset", ) diff --git a/tests/PySys/misc_features/valid_device_id/run.py b/tests/PySys/misc_features/valid_device_id/run.py index 264ad768f60..74203f6cd22 100644 --- a/tests/PySys/misc_features/valid_device_id/run.py +++ b/tests/PySys/misc_features/valid_device_id/run.py @@ -43,7 +43,7 @@ def setup(self): self.tedge, "config", "set", - "device.cert.path", + "device.cert_path", "/tmp/test-device-certs/tedge-certificate.pem", ], stdouterr="set_cert_path", @@ -56,7 +56,7 @@ def setup(self): self.tedge, "config", "set", - "device.key.path", + "device.key_path", "/tmp/test-device-certs/tedge-private-key.pem", ], stdouterr="set_key_path", @@ -115,14 +115,14 @@ def device_id_cleanup(self): # unset the device certificate path unset_cert_path = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "device.cert.path"], + arguments=[self.tedge, "config", "unset", "device.cert_path"], stdouterr="unset_cert_path", ) # unset the device key path unset_key_path = self.startProcess( command=self.sudo, - arguments=[self.tedge, "config", "unset", "device.key.path"], + arguments=[self.tedge, "config", "unset", "device.key_path"], stdouterr="unset_key_path", ) diff --git a/tests/PySys/tedge/call_tedge_config_list/run.py b/tests/PySys/tedge/call_tedge_config_list/run.py index 945b8236bce..6ffec212571 100644 --- a/tests/PySys/tedge/call_tedge_config_list/run.py +++ b/tests/PySys/tedge/call_tedge_config_list/run.py @@ -33,12 +33,12 @@ """ configdict = { - "device.key.path": "", - "device.cert.path": "", + "device.key_path": "", + "device.cert_path": "", "c8y.url": "", - "c8y.root.cert.path": "", + "c8y.root_cert_path": "", "az.url": "", - "az.root.cert.path": "", + "az.root_cert_path": "", } DEFAULTKEYPATH = "/etc/tedge/device-certs/tedge-private-key.pem" @@ -118,21 +118,21 @@ def execute(self): valueread = self.get_config_key(key) self.log.debug(f"Key: {key} Value: {valueread}") # Some values have defaults that are set instead of nothing: - if key == "device.key.path": + if key == "device.key_path": self.assertThat( "expect == valueread", expect=DEFAULTKEYPATH, valueread=valueread ) - elif key == "device.cert.path": + elif key == "device.cert_path": self.assertThat( "expect == valueread", expect=DEFAULTCERTPATH, valueread=valueread ) - elif key == "c8y.root.cert.path": + elif key == "c8y.root_cert_path": self.assertThat( "expect == valueread", expect=DEFAULTROOTCERTPATH, valueread=valueread, ) - elif key == "az.root.cert.path": + elif key == "az.root_cert_path": self.assertThat( "expect == valueread", expect=DEFAULTROOTCERTPATH, diff --git a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py index 6a3049ec0e4..7e72949c264 100644 --- a/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py +++ b/tests/RobotFramework/libraries/ThinEdgeIO/ThinEdgeIO.py @@ -404,7 +404,7 @@ def assert_service_health_status_equal( return self._assert_health_status(service, status=status) def _assert_health_status(self, service: str, status: str) -> Dict[str, Any]: - # if mqtt.client.auth.cafile or mqtt.client.auth.cadir is set, we pass setting + # if mqtt.client.auth.ca_file or mqtt.client.auth.ca_dir is set, we pass setting # value to mosquitto_sub mqtt_config_options = self.execute_command(f"tedge config list", stdout=True, @@ -413,11 +413,11 @@ def _assert_health_status(self, service: str, status: str) -> Dict[str, Any]: ) server_auth = "" - if "mqtt.client.auth.ca" in mqtt_config_options: + if "mqtt.client.auth.ca_file" in mqtt_config_options: server_auth = "--cafile /etc/mosquitto/ca_certificates/ca.crt" client_auth = "" - if "mqtt.client.auth.certfile" in mqtt_config_options: + if "mqtt.client.auth.cert_file" in mqtt_config_options: client_auth = "--cert /setup/client.crt --key /setup/client.key" message = self.execute_command( diff --git a/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot b/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot index 9f325b1a6ef..2104e9c2997 100644 --- a/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot +++ b/tests/RobotFramework/tests/config_management/child_conf_mgmt_plugin.robot @@ -1,81 +1,83 @@ +*** Comments *** #PRECONDITION: #Device CH_DEV_CONF_MGMT is existing on tenant, if not #use -v DeviceID:xxxxxxxxxxx in the command line to use your existing device + *** Settings *** -Resource ../../resources/common.resource -Library ThinEdgeIO -Library Cumulocity -Library JSONLibrary -Library Collections +Resource ../../resources/common.resource +Library ThinEdgeIO +Library Cumulocity +Library JSONLibrary +Library Collections -Test Tags theme:configuration theme:childdevices -Suite Setup Custom Setup -Suite Teardown Get Logs name=${PARENT_SN} +Suite Setup Custom Setup +Suite Teardown Get Logs name=${PARENT_SN} -*** Variables *** +Force Tags theme:configuration theme:childdevices + +*** Variables *** ${PARENT_IP} -${HTTP_PORT} 8000 +${HTTP_PORT} 8000 -${config} "files = [\n\t { path = '/home/pi/config1', type = 'config1' },\n ]\n" +${config} "files = [\n\t { path = '/home/pi/config1', type = 'config1' },\n ]\n" ${PARENT_SN} ${CHILD_SN} -${topic_snap} /commands/res/config_snapshot" -${topic_upd} /commands/res/config_update" -${payl_notify} '{"status": null, "path": "", "type":"c8y-configuration-plugin", "reason": null}' -${payl_exec} '{"status": "executing", "path": "/home/pi/config1", "type": "config1", "reason": null}' -${payl_succ} '{"status": "successful", "path": "/home/pi/config1", "type": "config1", "reason": null}' +${topic_snap} /commands/res/config_snapshot" +${topic_upd} /commands/res/config_update" +${payl_notify} '{"status": null, "path": "", "type":"c8y-configuration-plugin", "reason": null}' +${payl_exec} '{"status": "executing", "path": "/home/pi/config1", "type": "config1", "reason": null}' +${payl_succ} '{"status": "successful", "path": "/home/pi/config1", "type": "config1", "reason": null}' ${CHILD_CONFIG}= SEPARATOR=\n - ... files = [ - ... { path = '/home/pi/config1', type = 'config1' }, - ... ] +... files = [ +... { path = '/home/pi/config1', type = 'config1' }, +... ] -*** Test Cases *** +*** Test Cases *** Prerequisite Parent - Set Device Context ${PARENT_SN} #Creates ssh connection to the parent device + Set Device Context ${PARENT_SN} #Creates ssh connection to the parent device Execute Command sudo tedge disconnect c8y - Delete child related content #Delete any previous created child related configuration files/folders on the parent device - Check for child related content #Checks if folders that will trigger child device creation are existing - Set external MQTT bind address #Setting external MQTT bind address which child will use for communication - Set external MQTT port #Setting external MQTT port which child will use for communication Default:1883 + Delete child related content #Delete any previous created child related configuration files/folders on the parent device + Check for child related content #Checks if folders that will trigger child device creation are existing + Set external MQTT bind address #Setting external MQTT bind address which child will use for communication + Set external MQTT port #Setting external MQTT port which child will use for communication Default:1883 Sleep 3s Execute Command sudo tedge connect c8y - Restart Configuration plugin #Stop and Start c8y-configuration-plugin + Restart Configuration plugin #Stop and Start c8y-configuration-plugin Cumulocity.Log Device Info Prerequisite Child - Child device delete configuration files #Delete any previous created child related configuration files/folders on the child device + Child device delete configuration files #Delete any previous created child related configuration files/folders on the child device Child device bootstrapping - Startup child device #Setting up/Bootstrapping of a child device - Validate child Name #This is to check the existence of the bug: https://github.com/thin-edge/thin-edge.io/issues/1569 + Startup child device #Setting up/Bootstrapping of a child device + Validate child Name #This is to check the existence of the bug: https://github.com/thin-edge/thin-edge.io/issues/1569 Snapshot from device - Request snapshot from child device #Using the cloud command: "Get snapshot from device" - Child device response on snapshot request #Child device is sending 'executing' and 'successful' MQTT responses - No response from child device on snapshot request #Tests the failing of request after timeout of 10s + Request snapshot from child device #Using the cloud command: "Get snapshot from device" + Child device response on snapshot request #Child device is sending 'executing' and 'successful' MQTT responses + No response from child device on snapshot request #Tests the failing of request after timeout of 10s Child device config update - Send configuration to device #Using the cloud command: "Send configuration to device" - Child device response on update request #Child device is sending 'executing' and 'successful' MQTT responses - No response from child device on config update #Tests the failing of request after timeout of 10s + Send configuration to device #Using the cloud command: "Send configuration to device" + Child device response on update request #Child device is sending 'executing' and 'successful' MQTT responses + No response from child device on config update #Tests the failing of request after timeout of 10s *** Keywords *** - Set external MQTT bind address Set Device Context ${PARENT_SN} - Execute Command sudo tedge config set mqtt.external.bind_address ${PARENT_IP} + Execute Command sudo tedge config set mqtt.external.bind.address ${PARENT_IP} Set external MQTT port Set Device Context ${PARENT_SN} - Execute Command sudo tedge config set mqtt.external.port 1883 + Execute Command sudo tedge config set mqtt.external.bind.port 1883 Check for child related content Set Device Context ${CHILD_SN} @@ -84,9 +86,9 @@ Check for child related content Directory Should Not Have Sub Directories /var/tedge Delete child related content - Execute Command sudo rm -rf /etc/tedge/operations/c8y/TST* #if folder exists, child device will be created + Execute Command sudo rm -rf /etc/tedge/operations/c8y/TST* #if folder exists, child device will be created Execute Command sudo rm -f c8y-configuration-plugin.toml - Execute Command sudo rm -rf /etc/tedge/c8y/TST* #if folder exists, child device will be created + Execute Command sudo rm -rf /etc/tedge/c8y/TST* #if folder exists, child device will be created Execute Command sudo rm -rf /var/tedge/* Check parent child relationship @@ -110,16 +112,17 @@ Child device delete configuration files Validate child Name Device Should Exist ${CHILD_SN} ${child_mo}= Cumulocity.Device Should Have Fragments name - Should Be Equal device_${PARENT_SN} ${child_mo["owner"]} # The parent is the owner of the child + Should Be Equal device_${PARENT_SN} ${child_mo["owner"]} # The parent is the owner of the child Startup child device - Sleep 5s reason=The registration of child devices is flakey + Sleep 5s reason=The registration of child devices is flakey Set Device Context ${CHILD_SN} Execute Command printf ${config} > c8y-configuration-plugin - Execute Command curl -X PUT http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/c8y-configuration-plugin --data-binary "${CHILD_CONFIG}" + Execute Command + ... curl -X PUT http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/c8y-configuration-plugin --data-binary "${CHILD_CONFIG}" - Sleep 5s reason=The registration of child devices is flakey + Sleep 5s reason=The registration of child devices is flakey Execute Command sudo apt-get install mosquitto-clients -y Execute Command mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_notify} -q 1 @@ -131,7 +134,9 @@ Request snapshot from child device Set Device Context ${PARENT_SN} @{listen}= Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/config_snapshot date_from=-5s - Should Be Equal @{listen} {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"} + Should Be Equal + ... @{listen} + ... {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"} #CHECK OPERATION Cumulocity.Operation Should Be PENDING ${operation} @@ -140,7 +145,8 @@ Child device response on snapshot request Set Device Context ${CHILD_SN} Execute Command mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_exec} - Execute Command curl -X PUT --data-binary @/home/pi/config1 http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1 + Execute Command + ... curl -X PUT --data-binary @/home/pi/config1 http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1 Sleep 5s Execute Command mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_snap} -m ${payl_succ} @@ -151,13 +157,18 @@ Child device response on snapshot request Cumulocity.Operation Should Be SUCCESSFUL ${operation} Send configuration to device - ${file_url}= Create Inventory Binary test-config.toml config1 contents=Dummy config - ${operation}= Set Configuration config1 ${file_url} description=Send configuration snapshot config1 of configuration type config1 to device ${CHILD_SN} + ${file_url}= Create Inventory Binary test-config.toml config1 contents=Dummy config + ${operation}= Set Configuration + ... config1 + ... ${file_url} + ... description=Send configuration snapshot config1 of configuration type config1 to device ${CHILD_SN} Set Suite Variable $operation Set Device Context ${PARENT_SN} - @{listen}= Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/config_update date_from=-5s - Should Be Equal @{listen} {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"} + @{listen}= Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/config_update date_from=-5s + Should Be Equal + ... @{listen} + ... {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"} #CHECK OPERATION Operation Should Be DELIVERED ${operation} @@ -166,9 +177,10 @@ Child device response on update request Set Device Context ${CHILD_SN} Execute Command mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_upd} -m ${payl_exec} - Execute Command curl http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1 --output config1 + Execute Command + ... curl http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1 --output config1 - # Sleep 5s #Enable if tests starts to fail + # Sleep 5s #Enable if tests starts to fail Execute Command mosquitto_pub -h ${PARENT_IP} -t "tedge/${CHILD_SN}${topic_upd} -m ${payl_succ} Cumulocity.Operation Should Be SUCCESSFUL ${operation} @@ -178,21 +190,31 @@ No response from child device on snapshot request Set Device Context ${PARENT_SN} @{listen}= Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/config_snapshot - Should Be Equal @{listen} {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"} + Should Be Equal + ... @{listen} + ... {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_snapshot/config1","path":"/home/pi/config1","type":"config1"} #CHECK TIMEOUT MESSAGE - Cumulocity.Operation Should Be FAILED ${operation} failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1 timeout=60 + Cumulocity.Operation Should Be FAILED + ... ${operation} + ... failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1 + ... timeout=60 No response from child device on config update - ${file_url}= Create Inventory Binary test-config.toml config1 contents=Dummy config + ${file_url}= Create Inventory Binary test-config.toml config1 contents=Dummy config ${operation}= Set Configuration config1 ${file_url} Set Device Context ${PARENT_SN} @{listen}= Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/config_update - Should Be Equal @{listen} {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"} + Should Be Equal + ... @{listen} + ... {"url":"http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/config_update/config1","path":"/home/pi/config1","type":"config1"} #CHECK OPERATION - Cumulocity.Operation Should Be FAILED ${operation} failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1 timeout=60 + Cumulocity.Operation Should Be FAILED + ... ${operation} + ... failure_reason=Timeout due to lack of response from child device: ${CHILD_SN} for config type: config1 + ... timeout=60 Custom Setup # Parent @@ -200,8 +222,8 @@ Custom Setup Set Suite Variable $PARENT_SN ${parent_sn} ${parent_ip}= Get IP Address - Set Suite Variable $PARENT_IP ${parent_ip} + Set Suite Variable $PARENT_IP ${parent_ip} # Child ${child_sn}= Setup skip_bootstrap=True - Set Suite Variable $CHILD_SN ${child_sn} + Set Suite Variable $CHILD_SN ${child_sn} diff --git a/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot b/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot index 6c0a514a55e..e22e86414d3 100644 --- a/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot +++ b/tests/RobotFramework/tests/cumulocity/firmware/firmware_operation_child_device.robot @@ -1,19 +1,19 @@ *** Settings *** -Resource ../../../resources/common.resource -Library Cumulocity -Library ThinEdgeIO -Library JSONLibrary -Library String +Resource ../../../resources/common.resource +Library Cumulocity +Library ThinEdgeIO +Library JSONLibrary +Library String -Suite Setup Custom Setup -Test Teardown Get Logs name=${PARENT_SN} +Suite Setup Custom Setup +Test Teardown Get Logs name=${PARENT_SN} -Force Tags theme:firmware theme:childdevices +Force Tags theme:firmware theme:childdevices -*** Variables *** +*** Variables *** ${PARENT_IP} -${HTTP_PORT} 8000 +${HTTP_PORT} 8000 ${PARENT_SN} ${CHILD_SN} @@ -24,6 +24,7 @@ ${file_url} ${file_transfer_url} ${file_creation_time} + *** Test Cases *** Prerequisite Parent Set Device Context ${PARENT_SN} #Creates ssh connection to the parent device @@ -59,10 +60,11 @@ Child device firmware update with cache Validate Cumulocity operation status and MO Validate if file is not newly downloaded + *** Keywords *** Delete child related content - Execute Command sudo rm -rf /etc/tedge/operations/c8y/TST* #if folder exists, child device will be created - Execute Command sudo rm -rf /etc/tedge/c8y/TST* #if folder exists, child device will be created + Execute Command sudo rm -rf /etc/tedge/operations/c8y/TST* #if folder exists, child device will be created + Execute Command sudo rm -rf /etc/tedge/c8y/TST* #if folder exists, child device will be created Execute Command sudo rm -rf /var/tedge/file-transfer/* Execute Command sudo rm -rf /var/tedge/cache/* Execute Command sudo rm -rf /var/tedge/firmware/* @@ -77,11 +79,11 @@ Check for child related content Set external MQTT bind address Set Device Context ${PARENT_SN} - Execute Command sudo tedge config set mqtt.external.bind_address ${PARENT_IP} + Execute Command sudo tedge config set mqtt.external.bind.address ${PARENT_IP} Set external MQTT port Set Device Context ${PARENT_SN} - Execute Command sudo tedge config set mqtt.external.port 1883 + Execute Command sudo tedge config set mqtt.external.bind.port 1883 Restart Firmware plugin ThinEdgeIO.Restart Service c8y-firmware-plugin.service @@ -103,7 +105,7 @@ Create child device Validate child Name ${child_mo}= Cumulocity.Device Should Have Fragments name - Should Be Equal device_${PARENT_SN} ${child_mo["owner"]} # The parent is the owner of the child + Should Be Equal device_${PARENT_SN} ${child_mo["owner"]} # The parent is the owner of the child Get timestamp of cache Set Device Context ${PARENT_SN} @@ -111,7 +113,7 @@ Get timestamp of cache Set Suite Variable $file_creation_time Upload binary to Cumulocity - ${file_url}= Cumulocity.Create Inventory Binary firmware1.txt firmware1 file=${CURDIR}/firmware1.txt + ${file_url}= Cumulocity.Create Inventory Binary firmware1.txt firmware1 file=${CURDIR}/firmware1.txt Set Suite Variable $file_url Create c8y_Firmware operation @@ -126,7 +128,9 @@ Validate if file is not newly downloaded Validate firmware update request Set Device Context ${PARENT_SN} - ${listen}= ThinEdgeIO.Should Have MQTT Messages topic=tedge/${CHILD_SN}/commands/req/firmware_update date_from=-5s + ${listen}= ThinEdgeIO.Should Have MQTT Messages + ... topic=tedge/${CHILD_SN}/commands/req/firmware_update + ... date_from=-5s ${message}= JSONLibrary.Convert String To Json ${listen[0]} Should Not Be Empty ${message["id"]} @@ -134,9 +138,14 @@ Validate firmware update request Should Be Equal ${message["name"]} firmware1 Should Be Equal ${message["version"]} 1.0 Should Be Equal ${message["sha256"]} 4b0126519dfc1a3023851bfcc5b312b20fc80452256f7f40a5d8722765500ba9 - Should Match Regexp ${message["url"]} ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/[0-9A-Za-z]+$ + Should Match Regexp + ... ${message["url"]} + ... ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/[0-9A-Za-z]+$ - ${cache_key}= Get Regexp Matches ${message["url"]} ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/([0-9A-Za-z]+)$ 1 + ${cache_key}= Get Regexp Matches + ... ${message["url"]} + ... ^http://${PARENT_IP}:${HTTP_PORT}/tedge/file-transfer/${CHILD_SN}/firmware_update/([0-9A-Za-z]+)$ + ... 1 Set Suite Variable $op_id ${message["id"]} Set Suite Variable $file_transfer_url ${message["url"]} @@ -164,4 +173,4 @@ Custom Setup # Child ${child_sn}= Setup skip_bootstrap=True - Set Suite Variable $CHILD_SN ${child_sn} + Set Suite Variable $CHILD_SN ${child_sn} diff --git a/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot b/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot index c87df3cfb07..240dab00dea 100644 --- a/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot +++ b/tests/RobotFramework/tests/mqtt/basic_pub_sub.robot @@ -10,22 +10,23 @@ Force Tags theme:mqtt *** Test Cases *** Publish on a local insecure broker - Start Service mosquitto + Start Service mosquitto Execute Command tedge mqtt pub topic message Publish on a local secure broker Set up broker server authentication - Restart Service mosquitto + Restart Service mosquitto tedge configure MQTT server authentication Execute Command tedge mqtt pub topic message Publish on a local secure broker with client authentication Set up broker with server and client authentication - Restart Service mosquitto + Restart Service mosquitto tedge configure MQTT server authentication tedge configure MQTT client authentication Execute Command tedge mqtt pub topic message + *** Keywords *** Custom Setup Setup skip_bootstrap=True @@ -33,22 +34,24 @@ Custom Setup Set up broker server authentication Transfer To Device ${CURDIR}/mosquitto-server-auth.conf /etc/tedge/mosquitto-conf/ - Execute Command mv /etc/tedge/mosquitto-conf/mosquitto-server-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf + Execute Command + ... mv /etc/tedge/mosquitto-conf/mosquitto-server-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf Transfer To Device ${CURDIR}/gen_certs.sh /root/gen_certs.sh - Execute Command chmod u+x /root/gen_certs.sh - Execute Command /root/gen_certs.sh + Execute Command chmod u+x /root/gen_certs.sh + Execute Command /root/gen_certs.sh Set up broker with server and client authentication Transfer To Device ${CURDIR}/mosquitto-client-auth.conf /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf - Execute Command mv /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf + Execute Command + ... mv /etc/tedge/mosquitto-conf/mosquitto-client-auth.conf /etc/tedge/mosquitto-conf/tedge-mosquitto.conf Transfer To Device ${CURDIR}/gen_certs.sh /root/gen_certs.sh - Execute Command chmod u+x /root/gen_certs.sh - Execute Command /root/gen_certs.sh + Execute Command chmod u+x /root/gen_certs.sh + Execute Command /root/gen_certs.sh tedge configure MQTT server authentication Execute Command tedge config set mqtt.client.port 8883 - Execute Command tedge config set mqtt.client.auth.cafile /etc/mosquitto/ca_certificates/ca.crt + Execute Command tedge config set mqtt.client.auth.ca_file /etc/mosquitto/ca_certificates/ca.crt tedge configure MQTT client authentication - Execute Command tedge config set mqtt.client.auth.certfile client.crt - Execute Command tedge config set mqtt.client.auth.keyfile client.key + Execute Command tedge config set mqtt.client.auth.cert_file client.crt + Execute Command tedge config set mqtt.client.auth.key_file client.key diff --git a/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot b/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot index f1baef0f623..a8172f5593c 100644 --- a/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot +++ b/tests/RobotFramework/tests/mqtt/remote_mqtt_broker.robot @@ -1,16 +1,19 @@ *** Settings *** -Resource ../../resources/common.resource -Library ThinEdgeIO -Library Cumulocity +Resource ../../resources/common.resource +Library ThinEdgeIO +Library Cumulocity + +Suite Setup Custom Setup +Suite Teardown Get Logs name=${CONTAINER_1} + +Force Tags theme:mqtt theme:c8y adapter:docker -Test Tags theme:mqtt theme:c8y adapter:docker -Suite Setup Custom Setup -Suite Teardown Get Logs name=${CONTAINER_1} *** Variables *** ${CONTAINER_1} ${CONTAINER_2} + *** Test Cases *** Check remote mqtt broker #1773 [Documentation] The test relies on two containers functioning as one logical device. 1 container (CONTAINER_1) @@ -50,8 +53,8 @@ Check remote mqtt broker #1773 Cumulocity.Should Have Services name=c8y-firmware-plugin status=up Cumulocity.Should Have Services name=c8y-log-plugin status=up -*** Keywords *** +*** Keywords *** Custom Setup # Container 1 running mqtt host and mapper ${CONTAINER_1}= Setup skip_bootstrap=${True} @@ -61,7 +64,7 @@ Custom Setup Disconnect Mapper c8y Execute Command sudo tedge config set mqtt.client.host ${CONTAINER_1_IP} Execute Command sudo tedge config set mqtt.client.port 1883 - Execute Command sudo tedge config set mqtt.bind_address ${CONTAINER_1_IP} + Execute Command sudo tedge config set mqtt.bind.address ${CONTAINER_1_IP} Connect Mapper c8y Restart Service mqtt-logger @@ -72,21 +75,21 @@ Custom Setup # Copy files form one device to another (use base64 encoding to prevent quoting issues) ${tedge_toml_encoded}= Execute Command cat /etc/tedge/tedge.toml | base64 - ${pem}= Execute Command cat "$(tedge config get device.cert.path)" + ${pem}= Execute Command cat "$(tedge config get device.cert_path)" # container 2 running all other services ${CONTAINER_2}= Setup skip_bootstrap=${True} Execute Command ./bootstrap.sh --no-connect --no-bootstrap --no-secure Set Suite Variable $CONTAINER_2 - + # Stop services that don't need to be running on the second device - Stop Service mosquitto - Stop Service tedge-mapper-c8y + Stop Service mosquitto + Stop Service tedge-mapper-c8y Execute Command echo -n "${tedge_toml_encoded}" | base64 --decode | sudo tee /etc/tedge/tedge.toml Execute Command sudo tedge config set mqtt.client.host ${CONTAINER_1_IP} Execute Command sudo tedge config set mqtt.client.port 1883 - Execute Command echo "${pem}" | sudo tee "$(tedge config get device.cert.path)" + Execute Command echo "${pem}" | sudo tee "$(tedge config get device.cert_path)" Restart Service c8y-firmware-plugin Restart Service c8y-log-plugin Restart Service c8y-configuration-plugin diff --git a/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot b/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot index 6a313707006..37354721cb8 100644 --- a/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot +++ b/tests/RobotFramework/tests/tedge/call_tedge_config_list.robot @@ -1,25 +1,26 @@ *** Settings *** -Documentation Purpose of this test is to verify that the tedge config list and tedge config list --all -... will result with Return Code 0 -... Set new device type and return to default value -... Set new kay path and return to default value -... Set new cert path and return to default value -... Set new c8y.root.cert.path and return to default value -... Set new c8y.smartrest.templates and return to default value -... Set new az.root.cert.path and return to default value -... Set new az.mapper.timestamp and return to default value -... Set new mqtt.bind_address and return to default value -... Set new mqtt.port and return to default value -... Set new tmp.path and return to default value -... Set new logs.path and return to default value -... Set new run.path and return to default value - -Resource ../../resources/common.resource -Library ThinEdgeIO - -Test Tags theme:cli theme:configuration -Suite Setup Setup -Suite Teardown Get Logs +Documentation Purpose of this test is to verify that the tedge config list and tedge config list --all +... will result with Return Code 0 +... Set new device type and return to default value +... Set new kay path and return to default value +... Set new cert path and return to default value +... Set new c8y.root_cert_path and return to default value +... Set new c8y.smartrest.templates and return to default value +... Set new az.root_cert_path and return to default value +... Set new az.mapper.timestamp and return to default value +... Set new mqtt.bind.address and return to default value +... Set new mqtt.bind.port and return to default value +... Set new tmp.path and return to default value +... Set new logs.path and return to default value +... Set new run.path and return to default value + +Resource ../../resources/common.resource +Library ThinEdgeIO + +Suite Setup Setup +Suite Teardown Get Logs + +Force Tags theme:cli theme:configuration *** Test Cases *** @@ -31,108 +32,136 @@ tedge config list --all set/unset device.type Execute Command sudo tedge config set device.type changed-type #Changing device.type to "changed-type" - ${set} Execute Command tedge config list + ${set} Execute Command tedge config list Should Contain ${set} device.type=changed-type - Execute Command sudo tedge config unset device.type #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset device.type + ${unset} Execute Command tedge config list Should Contain ${unset} device.type=thin-edge.io -set/unset device.key.path - Execute Command sudo tedge config set device.key.path /etc/tedge/device-certs1/tedge-private-key.pem #Changing device.key.path - ${set} Execute Command tedge config list - Should Contain ${set} device.key.path=/etc/tedge/device-certs1/tedge-private-key.pem - - Execute Command sudo tedge config unset device.key.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} device.key.path=/etc/tedge/device-certs/tedge-private-key.pem - -set/unset device.cert.path - Execute Command sudo tedge config set device.cert.path /etc/tedge/device-certs1/tedge-certificate.pem #Changing device.cert.path - ${set} Execute Command tedge config list - Should Contain ${set} device.cert.path=/etc/tedge/device-certs1/tedge-certificate.pem - - Execute Command sudo tedge config unset device.cert.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} device.cert.path=/etc/tedge/device-certs/tedge-certificate.pem - -set/unset c8y.root.cert.path - Execute Command sudo tedge config set c8y.root.cert.path /etc/ssl/certs1 #Changing c8y.root.cert.path - ${set} Execute Command tedge config list - Should Contain ${set} c8y.root.cert.path=/etc/ssl/certs1 - - Execute Command sudo tedge config unset c8y.root.cert.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} c8y.root.cert.path=/etc/ssl/certs +set/unset device.key_path + #Changing device.key_path + Execute Command + ... sudo tedge config set device.key_path /etc/tedge/device-certs1/tedge-private-key.pem + ${set} Execute Command tedge config list + Should Contain ${set} device.key_path=/etc/tedge/device-certs1/tedge-private-key.pem + + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset device.key_path + ${unset} Execute Command tedge config list + Should Contain ${unset} device.key_path=/etc/tedge/device-certs/tedge-private-key.pem + +set/unset device.cert_path + #Changing device.cert_path + Execute Command + ... sudo tedge config set device.cert_path /etc/tedge/device-certs1/tedge-certificate.pem + ${set} Execute Command tedge config list + Should Contain ${set} device.cert_path=/etc/tedge/device-certs1/tedge-certificate.pem + + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset device.cert_path + ${unset} Execute Command tedge config list + Should Contain ${unset} device.cert_path=/etc/tedge/device-certs/tedge-certificate.pem + +set/unset c8y.root_cert_path + Execute Command sudo tedge config set c8y.root_cert_path /etc/ssl/certs1 #Changing c8y.root_cert_path + ${set} Execute Command tedge config list + Should Contain ${set} c8y.root_cert_path=/etc/ssl/certs1 + + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset c8y.root_cert_path + ${unset} Execute Command tedge config list + Should Contain ${unset} c8y.root_cert_path=/etc/ssl/certs set/unset c8y.smartrest.templates - Execute Command sudo tedge config set c8y.smartrest.templates 1 #Changing c8y.smartrest.templates - ${set} Execute Command tedge config list + Execute Command sudo tedge config set c8y.smartrest.templates 1 #Changing c8y.smartrest.templates + ${set} Execute Command tedge config list Should Contain ${set} c8y.smartrest.templates=["1"] - Execute Command sudo tedge config unset c8y.smartrest.templates #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset c8y.smartrest.templates + ${unset} Execute Command tedge config list Should Contain ${unset} c8y.smartrest.templates=[] -set/unset az.root.cert.path - Execute Command sudo tedge config set az.root.cert.path /etc/ssl/certs1 #Changing az.root.cert.path - ${set} Execute Command tedge config list - Should Contain ${set} az.root.cert.path=/etc/ssl/certs1 +set/unset az.root_cert_path + Execute Command sudo tedge config set az.root_cert_path /etc/ssl/certs1 #Changing az.root_cert_path + ${set} Execute Command tedge config list + Should Contain ${set} az.root_cert_path=/etc/ssl/certs1 - Execute Command sudo tedge config unset az.root.cert.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} az.root.cert.path=/etc/ssl/certs + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset az.root_cert_path + ${unset} Execute Command tedge config list + Should Contain ${unset} az.root_cert_path=/etc/ssl/certs set/unset az.mapper.timestamp - Execute Command sudo tedge config set az.mapper.timestamp false #Changing az.mapper.timestamp - ${set} Execute Command tedge config list + Execute Command sudo tedge config set az.mapper.timestamp false #Changing az.mapper.timestamp + ${set} Execute Command tedge config list Should Contain ${set} az.mapper.timestamp=false - Execute Command sudo tedge config unset az.mapper.timestamp #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset az.mapper.timestamp + ${unset} Execute Command tedge config list Should Contain ${unset} az.mapper.timestamp=true -set/unset mqtt.bind_address - Execute Command sudo tedge config set mqtt.bind_address 127.1.1.1 #Changing mqtt.bind_address - ${set} Execute Command tedge config list - Should Contain ${set} mqtt.bind_address=127.1.1.1 +set/unset mqtt.bind.address + Execute Command sudo tedge config set mqtt.bind.address 127.1.1.1 #Changing mqtt.bind.address + ${set} Execute Command tedge config list + Should Contain ${set} mqtt.bind.address=127.1.1.1 - Execute Command sudo tedge config unset mqtt.bind_address #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} mqtt.bind_address=127.0.0.1 + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset mqtt.bind.address + ${unset} Execute Command tedge config list + Should Contain ${unset} mqtt.bind.address=127.0.0.1 -set/unset mqtt.port - Execute Command sudo tedge config set mqtt.port 8888 #Changing mqtt.port - ${set} Execute Command tedge config list - Should Contain ${set} mqtt.port=8888 +set/unset mqtt.bind.port + Execute Command sudo tedge config set mqtt.bind.port 8888 #Changing mqtt.bind.port + ${set} Execute Command tedge config list + Should Contain ${set} mqtt.bind.port=8888 - Execute Command sudo tedge config unset mqtt.port #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list - Should Contain ${unset} mqtt.port=1883 + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset mqtt.bind.port + ${unset} Execute Command tedge config list + Should Contain ${unset} mqtt.bind.port=1883 set/unset tmp.path - Execute Command sudo tedge config set tmp.path /tmp1 #Changing tmp.path - ${set} Execute Command tedge config list + Execute Command sudo tedge config set tmp.path /tmp1 #Changing tmp.path + ${set} Execute Command tedge config list Should Contain ${set} tmp.path=/tmp1 - Execute Command sudo tedge config unset tmp.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset tmp.path + ${unset} Execute Command tedge config list Should Contain ${unset} tmp.path=/tmp set/unset logs.path - Execute Command sudo tedge config set logs.path /var/log1 #Changing logs.path - ${set} Execute Command tedge config list + Execute Command sudo tedge config set logs.path /var/log1 #Changing logs.path + ${set} Execute Command tedge config list Should Contain ${set} logs.path=/var/log1 - Execute Command sudo tedge config unset logs.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset logs.path + ${unset} Execute Command tedge config list Should Contain ${unset} logs.path=/var/log set/unset run.path - Execute Command sudo tedge config set run.path /run1 #Changing run.path - ${set} Execute Command tedge config list + Execute Command sudo tedge config set run.path /run1 #Changing run.path + ${set} Execute Command tedge config list Should Contain ${set} run.path=/run1 - Execute Command sudo tedge config unset run.path #Undo the change by using the 'unset' command, value returns to default one - ${unset} Execute Command tedge config list + #Undo the change by using the 'unset' command, value returns to default one + Execute Command + ... sudo tedge config unset run.path + ${unset} Execute Command tedge config list Should Contain ${unset} run.path=/run diff --git a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot index d0545111cf5..064fa165d71 100644 --- a/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot +++ b/tests/RobotFramework/tests/tedge/http_file_transfer_api.robot @@ -1,23 +1,26 @@ +*** Comments *** #Command to execute: robot -d \results --timestampoutputs --log http_file_transfer_api.html --report NONE -v BUILD:840 -v HOST:192.168.1.130 thin-edge.io/tests/RobotFramework/tedge/http_file_transfer_api.robot + + *** Settings *** +Resource ../../resources/common.resource +Library ThinEdgeIO + +Suite Setup Custom Setup +Suite Teardown Custom Teardown -Resource ../../resources/common.resource -Library ThinEdgeIO +Force Tags theme:cli theme:configuration theme:childdevices -Test Tags theme:cli theme:configuration theme:childdevices -Suite Setup Custom Setup -Suite Teardown Custom Teardown *** Variables *** +${DEVICE_SN} # Parent device serial number +${DEVICE_IP} # Parent device host name which is reachable +${PORT}= 8000 -${DEVICE_SN} # Parent device serial number -${DEVICE_IP} # Parent device host name which is reachable -${PORT}= 8000 *** Test Cases *** - Get Put Delete - Setup skip_bootstrap=True # Setup child device + Setup skip_bootstrap=True # Setup child device Execute Command curl -X PUT -d "test of put" http://${DEVICE_IP}:${PORT}/tedge/file-transfer/file_a ${get}= Execute Command curl --silent http://${DEVICE_IP}:${PORT}/tedge/file-transfer/file_a @@ -26,7 +29,6 @@ Get Put Delete *** Keywords *** - Custom Setup ${DEVICE_SN}= Setup skip_bootstrap=False Set Suite Variable $DEVICE_SN ${DEVICE_SN} @@ -34,8 +36,8 @@ Custom Setup ${DEVICE_IP}= Get IP Address Set Suite Variable ${DEVICE_IP} - Execute Command sudo tedge config set mqtt.external.bind_address ${DEVICE_IP} - ${bind}= Execute Command tedge config get mqtt.external.bind_address strip=True + Execute Command sudo tedge config set mqtt.external.bind.address ${DEVICE_IP} + ${bind}= Execute Command tedge config get mqtt.external.bind.address strip=True Should Be Equal ${bind} ${DEVICE_IP} Execute Command sudo -u tedge mkdir -p /var/tedge Restart Service tedge-agent diff --git a/tests/RobotFramework/tests/tedge/tedge_config_get.robot b/tests/RobotFramework/tests/tedge/tedge_config_get.robot index ea63b629fda..580cc13d88f 100644 --- a/tests/RobotFramework/tests/tedge/tedge_config_get.robot +++ b/tests/RobotFramework/tests/tedge/tedge_config_get.robot @@ -1,10 +1,11 @@ *** Settings *** -Resource ../../resources/common.resource -Library ThinEdgeIO +Resource ../../resources/common.resource +Library ThinEdgeIO -Test Tags theme:cli theme:configuration -Suite Setup Custom Setup -Suite Teardown Get Logs +Suite Setup Custom Setup +Suite Teardown Get Logs + +Force Tags theme:cli theme:configuration *** Test Cases *** @@ -26,22 +27,25 @@ Invalid keys should not return anything on stdout and warnings on stderr ${stderr}= Execute Command tedge config get does.not.exist 2>&1 >/dev/null exp_exit_code=!0 Should Not Be Empty ${stderr} - Set configuration via environment variables [Template] Check known tedge environment settings - TEDGE_AZ_URL az.url az.example.com - TEDGE_C8Y_URL c8y.url example.com - TEDGE_DEVICE_KEY_PATH device.key.path /etc/example.key - TEDGE_DEVICE_CERT_PATH device.cert.path /etc/example.pem - TEDGE_MQTT_BIND_ADDRESS mqtt.bind_address 0.0.0.1 - TEDGE_MQTT_CLIENT_HOST mqtt.client.host custom_host_name - TEDGE_MQTT_CLIENT_PORT mqtt.client.port 8888 - + TEDGE_AZ_URL az.url az.example.com + TEDGE_C8Y_URL c8y.url example.com + TEDGE_DEVICE_KEY_PATH device.key_path /etc/example.key + TEDGE_DEVICE_CERT_PATH device.cert_path /etc/example.pem + TEDGE_MQTT_BIND_ADDRESS mqtt.bind.address 0.0.0.1 + TEDGE_MQTT_CLIENT_HOST mqtt.client.host custom_host_name + TEDGE_MQTT_CLIENT_PORT mqtt.client.port 8888 Set unknown configuration via environment variables - ${stdout} ${stderr}= Execute Command env TEDGE_C8Y_UNKNOWN_CONFIGURATION\=dummy TEDGE_C8Y_URL\=example.com tedge config get c8y.url stdout=${True} stderr=${True} + ${stdout} ${stderr}= Execute Command + ... env TEDGE_C8Y_UNKNOWN_CONFIGURATION\=dummy TEDGE_C8Y_URL\=example.com tedge config get c8y.url + ... stdout=${True} + ... stderr=${True} Should Be Equal ${stdout} example.com\n - Should Contain ${stderr} Unknown configuration field "c8y.unknown_configuration" from environment variable TEDGE_C8Y_UNKNOWN_CONFIGURATION + Should Contain + ... ${stderr} + ... Unknown configuration field "c8y_unknown_configuration" from environment variable TEDGE_C8Y_UNKNOWN_CONFIGURATION *** Keywords *** diff --git a/tests/images/debian-systemd/files/bootstrap.sh b/tests/images/debian-systemd/files/bootstrap.sh index 55ad43a591b..69db0961ed3 100755 --- a/tests/images/debian-systemd/files/bootstrap.sh +++ b/tests/images/debian-systemd/files/bootstrap.sh @@ -716,9 +716,9 @@ EOF chown tedge:tedge /setup/client.* tedge config set mqtt.client.port 8883 - tedge config set mqtt.client.auth.cafile /etc/mosquitto/ca_certificates/ca.crt - tedge config set mqtt.client.auth.certfile /setup/client.crt - tedge config set mqtt.client.auth.keyfile /setup/client.key + tedge config set mqtt.client.auth.ca_file /etc/mosquitto/ca_certificates/ca.crt + tedge config set mqtt.client.auth.cert_file /setup/client.crt + tedge config set mqtt.client.auth.key_file /setup/client.key systemctl restart mosquitto }