From ac3ece2377674711582bf55b2f7343dc21d2882d Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Mon, 8 Dec 2025 16:36:20 +0000 Subject: [PATCH 01/10] refactor: Deserialise separate mapper config file using existing logic for tedge.toml Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 15 +- .../tedge_config/mapper_config/compat.rs | 188 ++- .../tedge_config/mapper_config/mod.rs | 1068 +++-------------- .../src/tedge_toml/tedge_config_location.rs | 6 +- .../tedge_config/tests/mapper_config.rs | 4 +- .../tedge_config_macros/impl/src/dto.rs | 13 +- .../common/tedge_config_macros/src/multi.rs | 8 +- crates/core/tedge/src/cli/config/cli.rs | 31 +- .../core/tedge/src/cli/config/commands/add.rs | 4 +- .../core/tedge/src/cli/config/commands/get.rs | 2 + .../tedge/src/cli/config/commands/remove.rs | 4 +- .../core/tedge/src/cli/config/commands/set.rs | 4 +- .../tedge/src/cli/config/commands/unset.rs | 4 +- crates/core/tedge/src/cli/connect/command.rs | 14 +- crates/core/tedge_mapper/src/aws/mapper.rs | 4 +- crates/core/tedge_mapper/src/az/mapper.rs | 4 +- 16 files changed, 337 insertions(+), 1036 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index 5a702bb7563..d0f6f98f5ff 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -90,11 +90,13 @@ impl OptionalConfigError for OptionalConfig { } } +type AnyMap = anymap3::Map; + pub struct TEdgeConfig { + dto: TEdgeConfigDto, reader: TEdgeConfigReader, location: TEdgeConfigLocation, - cached_mapper_configs: - tokio::sync::Mutex>, + cached_mapper_configs: Arc>, } impl std::ops::Deref for TEdgeConfig { @@ -121,9 +123,10 @@ pub enum ConfigDecision { } impl TEdgeConfig { - pub(crate) fn from_dto(dto: &TEdgeConfigDto, location: TEdgeConfigLocation) -> Self { + pub(crate) fn from_dto(dto: TEdgeConfigDto, location: TEdgeConfigLocation) -> Self { Self { - reader: TEdgeConfigReader::from_dto(dto, &location), + reader: TEdgeConfigReader::from_dto(&dto, &location), + dto, location, cached_mapper_configs: <_>::default(), } @@ -231,6 +234,7 @@ impl TEdgeConfig { let map = load_mapper_config::( &AbsolutePath::try_new(path.as_str()).unwrap(), self, + profile.as_deref(), ) .await?; @@ -2113,7 +2117,8 @@ mod tests { let error = tedge_config .mapper_config::(&profile_name(None)) .await - .unwrap_err(); + .err() + .unwrap(); assert_eq!( format!("{error:#}"), format!( diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs index 4b6da9b175e..fdc801c9677 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs @@ -1,17 +1,48 @@ use super::*; +use crate::tedge_toml::WritableKey; use crate::tedge_toml::tedge_config::TEdgeConfigReaderAws; use crate::tedge_toml::tedge_config::TEdgeConfigReaderAz; use crate::tedge_toml::tedge_config::TEdgeConfigReaderC8y; use crate::tedge_toml::ReadableKey; use crate::TEdgeConfig; +pub trait IsCloudConfig { + fn cloud_type_for(&self) -> Option<(CloudType, Option)>; +} + +impl IsCloudConfig for WritableKey { + fn cloud_type_for(&self) -> Option<(CloudType, Option)> { + cloud_type_for(self.to_cow_str()) + } +} + +impl IsCloudConfig for ReadableKey { + fn cloud_type_for(&self) -> Option<(CloudType, Option)> { + cloud_type_for(self.to_cow_str()) + } +} + +fn cloud_type_for(key: Cow<'static, str>) -> Option<(CloudType, Option)> { + match key.split_once(".") { + Some(("c8y", rest)) => Some((CloudType::C8y, extract_profile_name(rest))), + Some(("az", rest)) => Some((CloudType::Az, extract_profile_name(rest))), + Some(("aws", rest)) => Some((CloudType::Aws, extract_profile_name(rest))), + _ => None, + } +} + +fn extract_profile_name(partial_config_key: &str) -> Option { + let partial_config_key = partial_config_key.strip_prefix("profiles.")?; + let (profile, _rest) = partial_config_key.split_once(".")?; + Some(profile.parse().unwrap()) +} /// Trait for creating cloud-specific mapper configuration from tedge.toml cloud sections /// /// This trait enables backward compatibility by loading the new `MapperConfig` format /// from the legacy c8y/az/aws configuration sections in tedge.toml. pub trait FromCloudConfig: Sized { /// The corresponding TEdgeConfigReader type for this cloud - type CloudConfigReader; + type CloudConfigReader: CloudConfigAccessor + Send + Sync + 'static; /// Returns the cloud type for this configuration fn load_cloud_mapper_config( @@ -34,7 +65,7 @@ pub fn load_cloud_mapper_config( tedge_config: &TEdgeConfig, ) -> Result, MapperConfigError> where - T: FromCloudConfig + ApplyRuntimeDefaults + SpecialisedCloudConfig, + T: SpecialisedCloudConfig, { T::load_cloud_mapper_config(profile, tedge_config) } @@ -50,7 +81,7 @@ impl FromCloudConfig for C8yMapperSpecificConfig { MapperConfigError::ConfigRead(format!("C8y profile '{}' not found", profile.unwrap())) })?; - build_mapper_config(c8y_config, tedge_config, profile) + build_mapper_config(c8y_config.clone(), profile) } fn from_cloud_config(c8y: &Self::CloudConfigReader, profile: Option<&str>) -> Self { @@ -74,8 +105,7 @@ impl FromCloudConfig for C8yMapperSpecificConfig { address: c8y.proxy.bind.address, port: Keyed { value: c8y.proxy.bind.port, - key: ReadableKey::C8yProxyBindPort(profile.map(<_>::to_owned)).to_cow_str(), - accessed: Arc::new(AtomicBool::new(false)), + key: ReadableKey::C8yProxyBindPort(profile.map(<_>::to_owned)), }, }, client: ProxyClientConfig { @@ -112,6 +142,11 @@ impl FromCloudConfig for C8yMapperSpecificConfig { enabled: c8y.mqtt_service.enabled, topics: c8y.mqtt_service.topics.clone(), }, + bridge: C8yBridgeConfig { + include: C8yBridgeIncludeConfig { + local_cleansession: c8y.bridge.include.local_cleansession, + }, + }, } } } @@ -127,11 +162,16 @@ impl FromCloudConfig for AzMapperSpecificConfig { MapperConfigError::ConfigRead(format!("Azure profile '{}' not found", profile.unwrap())) })?; - build_mapper_config(az_config, tedge_config, profile) + build_mapper_config(az_config.clone(), profile) } - fn from_cloud_config(_az: &Self::CloudConfigReader, _profile: Option<&str>) -> Self { - AzMapperSpecificConfig {} + fn from_cloud_config(az: &Self::CloudConfigReader, _profile: Option<&str>) -> Self { + AzMapperSpecificConfig { + mapper: AzCloudMapperConfig { + timestamp: az.mapper.timestamp, + timestamp_format: az.mapper.timestamp_format, + }, + } } } @@ -146,30 +186,33 @@ impl FromCloudConfig for AwsMapperSpecificConfig { MapperConfigError::ConfigRead(format!("AWS profile '{}' not found", profile.unwrap())) })?; - build_mapper_config(aws_config, tedge_config, profile) + build_mapper_config(aws_config.clone(), profile) } - fn from_cloud_config(_aws: &Self::CloudConfigReader, _profile: Option<&str>) -> Self { - AwsMapperSpecificConfig {} + fn from_cloud_config(aws: &Self::CloudConfigReader, _profile: Option<&str>) -> Self { + AwsMapperSpecificConfig { + mapper: AwsCloudMapperConfig { + timestamp: aws.mapper.timestamp, + timestamp_format: aws.mapper.timestamp_format, + }, + } } } /// Generic helper to build MapperConfig from any cloud config reader -fn build_mapper_config( - cloud_config: &R, - tedge_config: &TEdgeConfig, +pub fn build_mapper_config( + cloud_config: T::CloudConfigReader, profile: Option<&str>, ) -> Result, MapperConfigError> where - T: FromCloudConfig + ApplyRuntimeDefaults + SpecialisedCloudConfig, - R: CloudConfigAccessor, + T: SpecialisedCloudConfig, { let url = cloud_config.url().clone(); let device = DeviceConfig { id: to_optional_config( cloud_config.device_id().ok().map(|s| s.to_string()), - cloud_config.device_id_key(profile), + &cloud_config.device_id_key(profile), ), key_path: cloud_config.device_key_path().to_owned(), cert_path: cloud_config.device_cert_path().to_owned(), @@ -181,7 +224,6 @@ where let bridge = BridgeConfig { topic_prefix: cloud_config.bridge_topic_prefix(profile), keepalive_interval: cloud_config.bridge_keepalive_interval().clone(), - include: cloud_config.bridge_include_config(), }; let topics = cloud_config.topics().clone(); @@ -190,16 +232,10 @@ where let max_payload_size = cloud_config.max_payload_size(); - let mut cloud_specific = T::from_cloud_config(cloud_config, profile); - - cloud_specific.apply_runtime_defaults( - &url, - tedge_config, - &AbsolutePath::try_new(tedge_config.location.tedge_config_root_path().as_str()) - .expect("valid absolute path"), - ); + let cloud_specific = T::from_cloud_config(&cloud_config, profile); Ok(MapperConfig { + tedge_config_reader: cloud_config, url, root_cert_path, device, @@ -207,7 +243,6 @@ where bridge, mapper: MapperMapperConfig { mqtt: MqttConfig { max_payload_size }, - cloud_specific: cloud_config.mapper_specific(), }, cloud_specific, }) @@ -217,13 +252,10 @@ where /// /// This trait provides a uniform interface for accessing common fields /// from different cloud configuration readers (C8y, Az, Aws). -trait CloudConfigAccessor { - /// The mapper-specific type for this cloud - type MapperSpecific; - +pub trait CloudConfigAccessor { fn url(&self) -> &OptionalConfig; fn device_id(&self) -> Result; - fn device_id_key(&self, profile: Option<&str>) -> Cow<'static, str>; + fn device_id_key(&self, profile: Option<&str>) -> ReadableKey; fn device_key_path(&self) -> &AbsolutePath; fn device_cert_path(&self) -> &AbsolutePath; fn device_csr_path(&self) -> &AbsolutePath; @@ -231,22 +263,18 @@ trait CloudConfigAccessor { fn device_key_pin(&self) -> Option>; fn bridge_topic_prefix(&self, profile: Option<&str>) -> Keyed; fn bridge_keepalive_interval(&self) -> &SecondsOrHumanTime; - fn bridge_include_config(&self) -> BridgeIncludeConfig; fn topics(&self) -> &TemplatesSet; fn root_cert_path(&self, profile: Option<&str>) -> Keyed; fn max_payload_size(&self) -> MqttPayloadLimit; - fn mapper_specific(&self) -> Self::MapperSpecific; } impl CloudConfigAccessor for TEdgeConfigReaderC8y { - type MapperSpecific = EmptyMapperSpecific; - fn url(&self) -> &OptionalConfig { &self.url } - fn device_id_key(&self, profile: Option<&str>) -> Cow<'static, str> { - ReadableKey::C8yDeviceId(profile.map(<_>::to_owned)).to_cow_str() + fn device_id_key(&self, profile: Option<&str>) -> ReadableKey { + ReadableKey::C8yDeviceId(profile.map(<_>::to_owned)) } fn device_id(&self) -> Result { @@ -276,7 +304,7 @@ impl CloudConfigAccessor for TEdgeConfigReaderC8y { fn bridge_topic_prefix(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.bridge.topic_prefix.clone(), - ReadableKey::C8yBridgeTopicPrefix(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::C8yBridgeTopicPrefix(profile.map(<_>::to_owned)), ) } @@ -284,12 +312,6 @@ impl CloudConfigAccessor for TEdgeConfigReaderC8y { &self.bridge.keepalive_interval } - fn bridge_include_config(&self) -> BridgeIncludeConfig { - BridgeIncludeConfig { - local_cleansession: self.bridge.include.local_cleansession, - } - } - fn topics(&self) -> &TemplatesSet { &self.topics } @@ -297,28 +319,22 @@ impl CloudConfigAccessor for TEdgeConfigReaderC8y { fn root_cert_path(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.root_cert_path.clone(), - ReadableKey::C8yRootCertPath(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::C8yRootCertPath(profile.map(<_>::to_owned)), ) } fn max_payload_size(&self) -> MqttPayloadLimit { self.mapper.mqtt.max_payload_size } - - fn mapper_specific(&self) -> Self::MapperSpecific { - EmptyMapperSpecific {} - } } impl CloudConfigAccessor for TEdgeConfigReaderAz { - type MapperSpecific = AzMapperSpecific; - fn url(&self) -> &OptionalConfig { &self.url } - fn device_id_key(&self, profile: Option<&str>) -> Cow<'static, str> { - ReadableKey::AzDeviceId(profile.map(<_>::to_owned)).to_cow_str() + fn device_id_key(&self, profile: Option<&str>) -> ReadableKey { + ReadableKey::AzDeviceId(profile.map(<_>::to_owned)) } fn device_id(&self) -> Result { @@ -348,7 +364,7 @@ impl CloudConfigAccessor for TEdgeConfigReaderAz { fn bridge_topic_prefix(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.bridge.topic_prefix.clone(), - ReadableKey::AzBridgeTopicPrefix(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::AzBridgeTopicPrefix(profile.map(<_>::to_owned)), ) } @@ -356,10 +372,6 @@ impl CloudConfigAccessor for TEdgeConfigReaderAz { &self.bridge.keepalive_interval } - fn bridge_include_config(&self) -> BridgeIncludeConfig { - <_>::default() - } - fn topics(&self) -> &TemplatesSet { &self.topics } @@ -367,31 +379,22 @@ impl CloudConfigAccessor for TEdgeConfigReaderAz { fn root_cert_path(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.root_cert_path.clone(), - ReadableKey::AzRootCertPath(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::AzRootCertPath(profile.map(<_>::to_owned)), ) } fn max_payload_size(&self) -> MqttPayloadLimit { self.mapper.mqtt.max_payload_size } - - fn mapper_specific(&self) -> Self::MapperSpecific { - AzMapperSpecific { - timestamp: self.mapper.timestamp, - timestamp_format: self.mapper.timestamp_format, - } - } } impl CloudConfigAccessor for TEdgeConfigReaderAws { - type MapperSpecific = AwsMapperSpecific; - fn url(&self) -> &OptionalConfig { &self.url } - fn device_id_key(&self, profile: Option<&str>) -> Cow<'static, str> { - ReadableKey::AwsDeviceId(profile.map(<_>::to_owned)).to_cow_str() + fn device_id_key(&self, profile: Option<&str>) -> ReadableKey { + ReadableKey::AwsDeviceId(profile.map(<_>::to_owned)) } fn device_id(&self) -> Result { @@ -421,7 +424,7 @@ impl CloudConfigAccessor for TEdgeConfigReaderAws { fn bridge_topic_prefix(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.bridge.topic_prefix.clone(), - ReadableKey::AwsBridgeTopicPrefix(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::AwsBridgeTopicPrefix(profile.map(<_>::to_owned)), ) } @@ -429,10 +432,6 @@ impl CloudConfigAccessor for TEdgeConfigReaderAws { &self.bridge.keepalive_interval } - fn bridge_include_config(&self) -> BridgeIncludeConfig { - <_>::default() - } - fn topics(&self) -> &TemplatesSet { &self.topics } @@ -440,20 +439,13 @@ impl CloudConfigAccessor for TEdgeConfigReaderAws { fn root_cert_path(&self, profile: Option<&str>) -> Keyed { Keyed::new( self.root_cert_path.clone(), - ReadableKey::AwsRootCertPath(profile.map(<_>::to_owned)).to_cow_str(), + ReadableKey::AwsRootCertPath(profile.map(<_>::to_owned)), ) } fn max_payload_size(&self) -> MqttPayloadLimit { self.mapper.mqtt.max_payload_size } - - fn mapper_specific(&self) -> Self::MapperSpecific { - AwsMapperSpecific { - timestamp: self.mapper.timestamp, - timestamp_format: self.mapper.timestamp_format, - } - } } #[cfg(test)] @@ -469,7 +461,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -497,7 +489,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -518,7 +510,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -528,9 +520,9 @@ mod tests { config.url().or_none().unwrap().as_str(), "mydevice.azure-devices.net" ); - assert!(config.mapper.cloud_specific.timestamp); + assert!(config.cloud_specific.mapper.timestamp); assert_eq!( - config.mapper.cloud_specific.timestamp_format, + config.cloud_specific.mapper.timestamp_format, TimeFormat::Unix ); } @@ -543,7 +535,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -553,9 +545,9 @@ mod tests { config.url().or_none().unwrap().as_str(), "mydevice.amazonaws.com" ); - assert!(config.mapper.cloud_specific.timestamp); + assert!(config.cloud_specific.mapper.timestamp); assert_eq!( - config.mapper.cloud_specific.timestamp_format, + config.cloud_specific.mapper.timestamp_format, TimeFormat::Unix ); } @@ -568,7 +560,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -585,7 +577,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -613,7 +605,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -636,7 +628,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -657,7 +649,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); @@ -686,7 +678,7 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/tmp/tedge"), ); diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index b9988063746..c57433cd4c5 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -2,9 +2,13 @@ pub mod compat; use crate::models::CloudType; use crate::tedge_toml::tedge_config::cert_error_into_config_error; -use crate::tedge_toml::tedge_config::default_credentials_path; use crate::tedge_toml::ReadableKey; +use crate::tedge_toml::TEdgeConfigDtoAws; +use crate::tedge_toml::TEdgeConfigDtoAz; +use crate::tedge_toml::TEdgeConfigDtoC8y; use crate::TEdgeConfig; +use crate::TEdgeConfigDto; +use crate::TEdgeConfigReader; use super::super::models::auth_method::AuthMethod; use super::super::models::AbsolutePath; @@ -25,22 +29,19 @@ use super::OptionalConfig; use super::ReadError; use camino::Utf8Path; use certificate::PemCertificate; -use doku::Document; use serde::de::DeserializeOwned; -use serde::Deserialize; use std::borrow::Cow; use std::fmt::Display; use std::net::IpAddr; -use std::net::Ipv4Addr; use std::ops::Deref; -use std::sync::atomic::AtomicBool; use std::sync::Arc; +use tedge_config_macros::MultiDto; +use tedge_config_macros::ProfileName; pub use compat::load_cloud_mapper_config; pub use compat::FromCloudConfig; /// Device-specific configuration fields shared across all cloud types -#[derive(Debug, Document)] pub struct DeviceConfig { /// Device identifier (optional, will be derived from certificate if not set) id: OptionalConfig, @@ -81,40 +82,35 @@ impl DeviceConfig { } /// Bridge configuration fields shared across all cloud types -#[derive(Debug, Document)] pub struct BridgeConfig { /// The topic prefix for the bridge MQTT topic pub topic_prefix: Keyed, /// The amount of time after which the bridge should send a ping pub keepalive_interval: SecondsOrHumanTime, +} - pub include: BridgeIncludeConfig, +pub struct C8yBridgeConfig { + pub include: C8yBridgeIncludeConfig, } -/// Trait linking cloud-specific config to its mapper-specific configuration +/// Trait linking cloud-specific config to its DTO and config reader pub trait SpecialisedCloudConfig: - Sized - + DeserializeOwned - + ApplyRuntimeDefaults - + ExpectedCloudType - + FromCloudConfig - + Send - + Sync - + 'static + Sized + ExpectedCloudType + FromCloudConfig + Send + Sync + 'static { - /// The mapper-specific configuration type for this cloud - type SpecialisedMapperConfig: DeserializeOwned - + std::fmt::Debug - + Document - + Default - + Send - + Sync; + type CloudDto: DeserializeOwned + Send + Sync + 'static; + + fn into_config_reader( + dto: Self::CloudDto, + base_config: &TEdgeConfig, + profile: Option<&str>, + ) -> Self::CloudConfigReader; } /// Base mapper configuration with common fields and cloud-specific fields via generics -#[derive(Debug, Document)] pub struct MapperConfig { + pub tedge_config_reader: T::CloudConfigReader, + /// Endpoint URL of the cloud tenant url: OptionalConfig, @@ -130,169 +126,97 @@ pub struct MapperConfig { /// Bridge configuration pub bridge: BridgeConfig, - pub mapper: MapperMapperConfig, + pub mapper: MapperMapperConfig, - /// Cloud-specific configuration fields (flattened into the same level) + /// Cloud-specific configuration fields pub cloud_specific: T, } -/// Empty mapper-specific configuration for C8y (no cloud-specific mapper fields) -#[derive(Debug, Deserialize, Document, Default)] -pub struct EmptyMapperSpecific {} - -/// AWS-specific mapper configuration fields -#[derive(Debug, Deserialize, Document)] -pub struct AwsMapperSpecific { +/// AWS cloud-specific mapper configuration +pub struct AwsCloudMapperConfig { /// Whether to add timestamps to messages - #[serde(default = "default_timestamp")] pub timestamp: bool, /// The timestamp format to use - #[serde(default = "default_timestamp_format")] pub timestamp_format: TimeFormat, } -/// Azure-specific mapper configuration fields -#[derive(Debug, Deserialize, Document)] -pub struct AzMapperSpecific { +/// Azure cloud-specific mapper configuration +pub struct AzCloudMapperConfig { /// Whether to add timestamps to messages - #[serde(default = "default_timestamp")] pub timestamp: bool, /// The timestamp format to use - #[serde(default = "default_timestamp_format")] pub timestamp_format: TimeFormat, } -#[derive(Debug, Deserialize, Document)] -pub struct PartialMapperMapperConfig { - #[serde(default)] - mqtt: PartialMqttConfig, - - /// Cloud-specific mapper configuration (e.g., timestamp settings for AWS/Azure) - #[serde(flatten)] - pub cloud_specific: M, -} - -#[derive(Debug, Deserialize, Document, Default)] -pub struct PartialMqttConfig { - #[serde(default)] - pub max_payload_size: Option, -} - -#[derive(Debug, Document)] -pub struct MapperMapperConfig { +pub struct MapperMapperConfig { pub mqtt: MqttConfig, - - /// Cloud-specific mapper configuration - pub cloud_specific: M, } -#[derive(Debug, Document)] pub struct MqttConfig { /// Maximum MQTT payload size pub max_payload_size: MqttPayloadLimit, } /// SmartREST configuration for Cumulocity -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct SmartrestConfig { /// Set of SmartREST template IDs the device should subscribe to - #[serde(default)] pub templates: TemplatesSet, /// Switch using 501-503 or 504-506 SmartREST messages for operation status update - #[serde(default = "default_smartrest_use_operation_id")] pub use_operation_id: bool, /// SmartREST child device configuration - #[serde(default)] pub child_device: SmartrestChildDeviceConfig, } -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct Smartrest1Config { /// Set of SmartREST 1 template IDs the device should subscribe to - #[serde(default)] pub templates: TemplatesSet, } /// Child device SmartREST configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct SmartrestChildDeviceConfig { /// Attach the c8y_IsDevice fragment to child devices on creation - #[serde(default = "default_smartrest_child_device_create_with_marker")] pub create_with_device_marker: bool, } /// Proxy bind configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct ProxyBindConfig { /// The IP address local proxy binds to - #[serde(default = "default_proxy_bind_address")] pub address: IpAddr, /// The port local proxy binds to - #[serde(default = "default_proxy_bind_port")] pub port: Keyed, } #[derive(Debug)] pub struct Keyed { value: T, - key: Cow<'static, str>, - accessed: Arc, + key: ReadableKey, } impl Keyed { - fn new(value: T, key: impl Into>) -> Self { - Self { - value, - key: key.into(), - accessed: Arc::new(AtomicBool::new(false)), - } + fn new(value: T, key: ReadableKey) -> Self { + Self { value, key } } pub fn value(&self) -> &T { - self.accessed - .store(true, std::sync::atomic::Ordering::SeqCst); &self.value } - pub fn key(&self) -> &Cow<'static, str> { + pub fn key(&self) -> &ReadableKey { &self.key } } -impl Drop for Keyed { - fn drop(&mut self) { - // The key should always be set, but this has to happen after deserialising - // If the value has been used - if self.accessed.load(std::sync::atomic::Ordering::SeqCst) { - debug_assert!( - !self.key.is_empty(), - "Must set a key for a `Keyed` value after deserialising" - ) - } - } -} - impl Display for Keyed { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.value().fmt(f) } } -impl Document for Keyed { - fn ty() -> doku::Type { - T::ty() - } -} - impl Deref for Keyed { type Target = T; @@ -307,276 +231,253 @@ impl PartialEq for Keyed { } } -impl<'de, T: Deserialize<'de>> Deserialize<'de> for Keyed { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - Ok(Self { - value: T::deserialize(deserializer)?, - key: "".into(), - accessed: Arc::new(AtomicBool::new(false)), - }) - } -} - -/// Proxy client configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct ProxyClientConfig { /// The address of the host on which the proxy is running - #[serde(default = "default_proxy_client_host")] pub host: Arc, /// The port number on which the proxy is running - #[serde(default = "default_proxy_client_port")] pub port: u16, } -/// Helper function to deserialize OptionalConfig from Option -fn deserialize_optional_config<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, - T: serde::Deserialize<'de>, -{ - Option::::deserialize(deserializer).map(|opt| { - opt.map(|v| OptionalConfig::present(v, "")) - .unwrap_or_else(|| OptionalConfig::empty("")) - }) -} - /// HTTP proxy configuration for Cumulocity -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct ProxyConfig { /// Proxy bind configuration - #[serde(default)] pub bind: ProxyBindConfig, /// Proxy client configuration - #[serde(default)] pub client: ProxyClientConfig, /// Server certificate path for the proxy - #[serde( - default = "default_optional_config", - deserialize_with = "deserialize_optional_config" - )] pub cert_path: OptionalConfig, /// Server private key path for the proxy - #[serde( - default = "default_optional_config", - deserialize_with = "deserialize_optional_config" - )] pub key_path: OptionalConfig, /// CA certificates path for the proxy - #[serde( - default = "default_optional_config", - deserialize_with = "deserialize_optional_config" - )] pub ca_path: OptionalConfig, } /// Entity store configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct EntityStoreConfig { /// Enable auto registration feature - #[serde(default = "default_entity_store_auto_register")] pub auto_register: bool, /// On a clean start, resend the whole device state to the cloud - #[serde(default = "default_entity_store_clean_start")] pub clean_start: bool, } /// Software management configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct SoftwareManagementConfig { /// Software management API to use (legacy or advanced) - #[serde(default = "default_software_management_api")] pub api: SoftwareManagementApiFlag, /// Enable publishing c8y_SupportedSoftwareTypes fragment - #[serde(default = "default_software_management_with_types")] pub with_types: bool, } /// Operations configuration -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct OperationsConfig { /// Auto-upload the operation log once it finishes - #[serde(default = "default_operations_auto_log_upload")] pub auto_log_upload: AutoLogUpload, } /// Availability/heartbeat configuration for Cumulocity -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct AvailabilityConfig { /// Enable sending heartbeat to Cumulocity periodically - #[serde(default = "default_availability_enable")] pub enable: bool, /// Heartbeat interval to be sent to Cumulocity - #[serde(default = "default_availability_interval")] pub interval: SecondsOrHumanTime, } /// Feature enable/disable flags -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct EnableConfig { /// Enable log_upload feature - #[serde(default = "default_enable_log_upload")] pub log_upload: bool, /// Enable config_snapshot feature - #[serde(default = "default_enable_config_snapshot")] pub config_snapshot: bool, /// Enable config_update feature - #[serde(default = "default_enable_config_update")] pub config_update: bool, /// Enable firmware_update feature - #[serde(default = "default_enable_firmware_update")] pub firmware_update: bool, /// Enable device_profile feature - #[serde(default = "default_enable_device_profile")] pub device_profile: bool, } /// Bridge include configuration -#[derive(Debug, Deserialize, Document)] -pub struct BridgeIncludeConfig { +pub struct C8yBridgeIncludeConfig { /// Set the bridge local clean session flag - #[serde(default = "default_bridge_include_local_cleansession")] pub local_cleansession: AutoFlag, } /// MQTT service configuration for Cumulocity -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct MqttServiceConfig { /// Whether to connect to the MQTT service endpoint or not - #[serde(default = "default_mqtt_service_enabled")] pub enabled: bool, /// Set of MQTT topics for the MQTT service endpoint - #[serde(default = "default_mqtt_service_topics")] pub topics: TemplatesSet, } -impl Default for AwsMapperSpecific { - fn default() -> Self { - Self { - timestamp: default_timestamp(), - timestamp_format: default_timestamp_format(), - } - } -} - -impl Default for AzMapperSpecific { - fn default() -> Self { - Self { - timestamp: default_timestamp(), - timestamp_format: default_timestamp_format(), - } - } -} - /// Cumulocity-specific mapper configuration fields -#[derive(Debug, Deserialize, Document)] -#[serde(default)] pub struct C8yMapperSpecificConfig { /// Authentication method (certificate, basic, or auto) - #[serde(default = "default_auth_method")] pub auth_method: AuthMethod, /// Path to credentials file for basic auth - #[serde(default = "serde_placeholder_credentials_path")] pub credentials_path: AbsolutePath, /// SmartREST configuration - #[serde(default = "default_smartrest_config")] pub smartrest: SmartrestConfig, /// SmartREST1 configuration - #[serde(default = "default_smartrest1_config")] pub smartrest1: Smartrest1Config, /// HTTP endpoint for Cumulocity - // Note: http will be derived from url at runtime, no serde default - #[serde( - default = "default_optional_config", - deserialize_with = "deserialize_optional_config" - )] pub http: OptionalConfig>, /// MQTT endpoint for Cumulocity - // Note: mqtt will be derived from url at runtime, no serde default - #[serde( - default = "default_optional_config", - deserialize_with = "deserialize_optional_config" - )] pub mqtt: OptionalConfig>, /// HTTP proxy configuration - #[serde(default)] pub proxy: ProxyConfig, /// Entity store configuration - #[serde(default = "default_entity_store_config")] pub entity_store: EntityStoreConfig, /// Software management configuration - #[serde(default = "default_software_management_config")] pub software_management: SoftwareManagementConfig, /// Operations configuration - #[serde(default = "default_operations_config")] pub operations: OperationsConfig, /// Availability/heartbeat configuration - #[serde(default)] pub availability: AvailabilityConfig, /// Feature enable/disable flags - #[serde(default = "default_enable_config")] pub enable: EnableConfig, /// MQTT service configuration - #[serde(default)] pub mqtt_service: MqttServiceConfig, + + pub bridge: C8yBridgeConfig, } /// Azure IoT-specific mapper configuration fields -#[derive(Debug, Deserialize, Document)] -pub struct AzMapperSpecificConfig {} +pub struct AzMapperSpecificConfig { + pub mapper: AzCloudMapperConfig, +} /// AWS IoT-specific mapper configuration fields -#[derive(Debug, Deserialize, Document)] -pub struct AwsMapperSpecificConfig {} +pub struct AwsMapperSpecificConfig { + pub mapper: AwsCloudMapperConfig, +} /// CloudConfig implementation for C8y impl SpecialisedCloudConfig for C8yMapperSpecificConfig { - type SpecialisedMapperConfig = EmptyMapperSpecific; + type CloudDto = TEdgeConfigDtoC8y; + + fn into_config_reader( + dto: Self::CloudDto, + base_config: &TEdgeConfig, + profile: Option<&str>, + ) -> Self::CloudConfigReader { + let mut multi_dto = MultiDto::default(); + match profile { + Some(profile) => { + multi_dto.profiles.insert(profile.parse().unwrap(), dto); + } + None => multi_dto.non_profile = dto, + }; + let mut reader = TEdgeConfigReader::from_dto( + &TEdgeConfigDto { + c8y: multi_dto, + ..base_config.dto.clone() + }, + &base_config.location, + ); + match profile { + None => reader.c8y.non_profile, + Some(profile) => reader + .c8y + .profiles + .remove(&profile.parse::().unwrap()) + .unwrap(), + } + } } /// CloudConfig implementation for Azure impl SpecialisedCloudConfig for AzMapperSpecificConfig { - type SpecialisedMapperConfig = AzMapperSpecific; + type CloudDto = TEdgeConfigDtoAz; + + fn into_config_reader( + dto: Self::CloudDto, + base_config: &TEdgeConfig, + profile: Option<&str>, + ) -> Self::CloudConfigReader { + let mut multi_dto = MultiDto::default(); + match profile { + Some(profile) => { + multi_dto.profiles.insert(profile.parse().unwrap(), dto); + } + None => multi_dto.non_profile = dto, + }; + let mut reader = TEdgeConfigReader::from_dto( + &TEdgeConfigDto { + az: multi_dto, + ..base_config.dto.clone() + }, + &base_config.location, + ); + match profile { + None => reader.az.non_profile, + Some(profile) => reader + .az + .profiles + .remove(&profile.parse::().unwrap()) + .unwrap(), + } + } } /// CloudConfig implementation for AWS impl SpecialisedCloudConfig for AwsMapperSpecificConfig { - type SpecialisedMapperConfig = AwsMapperSpecific; + type CloudDto = TEdgeConfigDtoAws; + + fn into_config_reader( + dto: Self::CloudDto, + base_config: &TEdgeConfig, + profile: Option<&str>, + ) -> Self::CloudConfigReader { + let mut multi_dto = MultiDto::default(); + match profile { + Some(profile) => { + multi_dto.profiles.insert(profile.parse().unwrap(), dto); + } + None => multi_dto.non_profile = dto, + }; + // TODO take TEdgeConfigLocation + let mut reader = TEdgeConfigReader::from_dto( + &TEdgeConfigDto { + aws: multi_dto, + ..base_config.dto.clone() + }, + &base_config.location, + ); + match profile { + None => reader.aws.non_profile, + Some(profile) => reader + .aws + .profiles + .remove(&profile.parse::().unwrap()) + .unwrap(), + } + } } /// Type alias for Cumulocity mapper configuration @@ -616,44 +517,6 @@ impl From for MapperConfigError { } } -/// Partial device configuration with all fields optional for deserialization -#[derive(Debug, Deserialize)] -struct PartialDeviceConfig { - id: Option, - key_path: Option, - cert_path: Option, - csr_path: Option, - key_uri: Option>, - key_pin: Option>, -} - -/// Partial bridge configuration with all fields optional for deserialization -#[derive(Debug, Deserialize)] -struct PartialBridgeConfig { - topic_prefix: Option, - keepalive_interval: Option, - include: BridgeIncludeConfig, -} - -/// Partial mapper configuration with optional common fields -#[derive(Debug, Deserialize)] -#[serde(bound( - deserialize = "T: DeserializeOwned, T::SpecialisedMapperConfig: Default + DeserializeOwned" -))] -struct PartialMapperConfig { - url: Option, - root_cert_path: Option, - device: Option, - topics: Option, - bridge: Option, - - #[serde(default)] - mapper: PartialMapperMapperConfig, - - #[serde(flatten)] - cloud_specific: T, -} - /// Load and populate a mapper configuration from an external TOML file /// /// This function reads a mapper configuration file and applies defaults from @@ -670,128 +533,26 @@ struct PartialMapperConfig { pub(crate) async fn load_mapper_config( config_path: &AbsolutePath, tedge_config: &TEdgeConfig, + profile: Option<&str>, ) -> Result, MapperConfigError> where - T: DeserializeOwned + ApplyRuntimeDefaults + SpecialisedCloudConfig, + T: SpecialisedCloudConfig, { let toml_content = tokio::fs::read_to_string(config_path.as_std_path()).await?; - load_mapper_config_from_string(&toml_content, tedge_config, config_path) + load_mapper_config_from_string(&toml_content, tedge_config, profile) } fn load_mapper_config_from_string( toml_content: &str, tedge_config: &TEdgeConfig, - config_path: &AbsolutePath, + profile: Option<&str>, ) -> Result, MapperConfigError> where - T: DeserializeOwned + ApplyRuntimeDefaults + SpecialisedCloudConfig, + T: SpecialisedCloudConfig, { - let partial: PartialMapperConfig = toml::from_str(toml_content)?; - - let device = if let Some(partial_device) = partial.device { - DeviceConfig { - // device.id is optional - will be derived from certificate if not set - id: to_optional_config( - partial_device.id, - format!("{config_path}: device.id").into(), - ), - key_path: partial_device - .key_path - .unwrap_or_else(|| tedge_config.device.key_path.clone()), - cert_path: partial_device - .cert_path - .unwrap_or_else(|| tedge_config.device.cert_path.clone()), - csr_path: partial_device - .csr_path - .unwrap_or_else(|| tedge_config.device.csr_path.clone()), - key_uri: partial_device - .key_uri - .or_else(|| tedge_config.device.key_uri.or_none().cloned()), - key_pin: partial_device - .key_pin - .or_else(|| tedge_config.device.key_pin.or_none().cloned()), - } - } else { - // No device section in file, use all defaults from tedge_config - DeviceConfig { - // device.id is optional - will be derived from certificate if not set - id: to_optional_config( - tedge_config.device.id().ok().map(|s| s.to_string()), - "device.id".into(), - ), - key_path: tedge_config.device.key_path.clone(), - cert_path: tedge_config.device.cert_path.clone(), - csr_path: tedge_config.device.csr_path.clone(), - key_uri: tedge_config.device.key_uri.or_none().cloned(), - key_pin: tedge_config.device.key_pin.or_none().cloned(), - } - }; - - // Apply defaults for bridge fields - let bridge = if let Some(partial_bridge) = partial.bridge { - BridgeConfig { - topic_prefix: Keyed::new( - partial_bridge - .topic_prefix - .unwrap_or_else(T::default_bridge_topic_prefix), - format!("{config_path}: bridge.topic_prefix"), - ), - keepalive_interval: partial_bridge - .keepalive_interval - .unwrap_or_else(default_keepalive_interval), - include: partial_bridge.include, - } - } else { - // No bridge section, use all defaults - BridgeConfig { - topic_prefix: Keyed::new( - T::default_bridge_topic_prefix(), - format!("{config_path}: bridge.topic_prefix"), - ), - keepalive_interval: default_keepalive_interval(), - include: default_bridge_include_config(), - } - }; - - // Apply default for root_cert_path - let root_cert_path = Keyed::new( - partial - .root_cert_path - .unwrap_or_else(default_root_cert_path), - format!("{config_path}: root_cert_path"), - ); - - let url = to_optional_config(partial.url, format!("{config_path}: url").into()); - - // Apply default topics - let topics = partial.topics.unwrap_or_else(T::default_topics); - - // Apply default max_payload_size - let max_payload_size = partial - .mapper - .mqtt - .max_payload_size - .unwrap_or_else(T::default_max_payload_size); - - // Get cloud-specific config (already has serde defaults applied) - let mut cloud_specific = partial.cloud_specific; - - // Apply runtime defaults to cloud_specific - cloud_specific.apply_runtime_defaults(&url, tedge_config, config_path); - - // Construct the final configuration - Ok(MapperConfig { - url, - root_cert_path, - device, - topics, - bridge, - mapper: MapperMapperConfig { - mqtt: MqttConfig { max_payload_size }, - cloud_specific: partial.mapper.cloud_specific, - }, - cloud_specific, - }) + let cloud_dto: T::CloudDto = toml::from_str(toml_content)?; + let cloud_reader = T::into_config_reader(cloud_dto, tedge_config, profile); + compat::build_mapper_config(cloud_reader, profile) } pub trait ExpectedCloudType { @@ -816,25 +577,6 @@ impl ExpectedCloudType for AwsMapperSpecificConfig { } } -/// Trait for applying runtime defaults to cloud-specific configurations -pub trait ApplyRuntimeDefaults { - fn apply_runtime_defaults( - &mut self, - url: &OptionalConfig, - tedge_config: &TEdgeConfig, - config_path: &AbsolutePath, - ); - - /// Returns the default bridge topic prefix for this cloud type - fn default_bridge_topic_prefix() -> TopicPrefix; - - /// Returns the default topics for this cloud type - fn default_topics() -> TemplatesSet; - - /// Returns the default max payload size for this cloud type - fn default_max_payload_size() -> MqttPayloadLimit; -} - pub trait HasUrl { // The configured URL field, used to check whether profiles are fn configured_url(&self) -> &OptionalConfig; @@ -846,447 +588,13 @@ impl HasUrl for MapperConfig { } } -fn default_keepalive_interval() -> SecondsOrHumanTime { - "60s".parse().expect("Valid duration") -} - -// Common MapperConfig defaults -fn default_root_cert_path() -> AbsolutePath { - "/etc/ssl/certs".parse().expect("Valid path") -} - -// C8y mapper specific defaults -fn default_auth_method() -> AuthMethod { - AuthMethod::Certificate -} - -fn serde_placeholder_credentials_path() -> AbsolutePath { - AbsolutePath::try_new("/").expect("valid path") -} - -fn default_smartrest_config() -> SmartrestConfig { - SmartrestConfig { - templates: TemplatesSet::default(), - use_operation_id: true, - child_device: SmartrestChildDeviceConfig::default(), - } -} - -fn default_smartrest1_config() -> Smartrest1Config { - Smartrest1Config { - templates: TemplatesSet::default(), - } -} - -fn default_smartrest_use_operation_id() -> bool { - true -} - -fn default_smartrest_child_device_create_with_marker() -> bool { - false -} - -fn default_proxy_bind_address() -> IpAddr { - IpAddr::V4(Ipv4Addr::LOCALHOST) -} - -fn default_proxy_bind_port() -> Keyed { - Keyed::new(8001, "") -} - -fn default_proxy_client_host() -> Arc { - Arc::from("127.0.0.1") -} - -fn default_proxy_client_port() -> u16 { - 8001 // Will be overridden at runtime if bind.port differs -} - -fn default_optional_config() -> OptionalConfig { - OptionalConfig::empty("") -} - -fn default_proxy_config() -> ProxyConfig { - ProxyConfig { - bind: ProxyBindConfig { - address: default_proxy_bind_address(), - port: default_proxy_bind_port(), - }, - client: ProxyClientConfig { - host: default_proxy_client_host(), - port: default_proxy_client_port(), - }, - cert_path: default_optional_config(), - key_path: default_optional_config(), - ca_path: default_optional_config(), - } -} - -fn default_bridge_include_local_cleansession() -> AutoFlag { - AutoFlag::Auto -} - -fn default_bridge_include_config() -> BridgeIncludeConfig { - BridgeIncludeConfig { - local_cleansession: default_bridge_include_local_cleansession(), - } -} - -fn default_entity_store_auto_register() -> bool { - true -} - -fn default_entity_store_clean_start() -> bool { - true -} - -fn default_entity_store_config() -> EntityStoreConfig { - EntityStoreConfig { - auto_register: default_entity_store_auto_register(), - clean_start: default_entity_store_clean_start(), - } -} - -fn default_software_management_api() -> SoftwareManagementApiFlag { - SoftwareManagementApiFlag::Legacy -} - -fn default_software_management_with_types() -> bool { - false -} - -fn default_software_management_config() -> SoftwareManagementConfig { - SoftwareManagementConfig { - api: default_software_management_api(), - with_types: default_software_management_with_types(), - } -} - -fn default_operations_auto_log_upload() -> AutoLogUpload { - AutoLogUpload::OnFailure -} - -fn default_operations_config() -> OperationsConfig { - OperationsConfig { - auto_log_upload: default_operations_auto_log_upload(), - } -} - -fn default_availability_enable() -> bool { - true -} - -fn default_availability_interval() -> SecondsOrHumanTime { - "60m".parse().expect("Valid duration") -} - -fn default_availability_config() -> AvailabilityConfig { - AvailabilityConfig { - enable: default_availability_enable(), - interval: default_availability_interval(), - } -} - -fn default_mqtt_service_enabled() -> bool { - false -} - -fn default_mqtt_service_topics() -> TemplatesSet { - "$demo,$error".parse().expect("Valid templates set") -} - -fn default_enable_log_upload() -> bool { - true -} - -fn default_enable_config_snapshot() -> bool { - true -} - -fn default_enable_config_update() -> bool { - true -} - -fn default_enable_firmware_update() -> bool { - true -} - -fn default_enable_device_profile() -> bool { - true -} - -fn default_enable_config() -> EnableConfig { - EnableConfig { - log_upload: default_enable_log_upload(), - config_snapshot: default_enable_config_snapshot(), - config_update: default_enable_config_update(), - firmware_update: default_enable_firmware_update(), - device_profile: default_enable_device_profile(), - } -} - -// Azure/AWS timestamp defaults -fn default_timestamp() -> bool { - true -} - -fn default_timestamp_format() -> TimeFormat { - TimeFormat::Unix -} - -impl Default for SmartrestConfig { - fn default() -> Self { - default_smartrest_config() - } -} - -impl Default for Smartrest1Config { - fn default() -> Self { - default_smartrest1_config() - } -} - -impl Default for SmartrestChildDeviceConfig { - fn default() -> Self { - Self { - create_with_device_marker: default_smartrest_child_device_create_with_marker(), - } - } -} - -impl Default for ProxyBindConfig { - fn default() -> Self { - Self { - address: default_proxy_bind_address(), - port: default_proxy_bind_port(), - } - } -} - -impl Default for ProxyClientConfig { - fn default() -> Self { - Self { - host: default_proxy_client_host(), - port: default_proxy_client_port(), - } - } -} - -impl Default for ProxyConfig { - fn default() -> Self { - default_proxy_config() - } -} - -impl Default for EntityStoreConfig { - fn default() -> Self { - default_entity_store_config() - } -} - -impl Default for SoftwareManagementConfig { - fn default() -> Self { - default_software_management_config() - } -} - -impl Default for OperationsConfig { - fn default() -> Self { - default_operations_config() - } -} - -impl Default for AvailabilityConfig { - fn default() -> Self { - default_availability_config() - } -} - -impl Default for EnableConfig { - fn default() -> Self { - default_enable_config() - } -} - -impl Default for BridgeIncludeConfig { - fn default() -> Self { - default_bridge_include_config() - } -} - -impl Default for MqttServiceConfig { - fn default() -> Self { - Self { - enabled: default_mqtt_service_enabled(), - topics: default_mqtt_service_topics(), - } - } -} - -impl Default for C8yMapperSpecificConfig { - fn default() -> Self { - Self { - auth_method: default_auth_method(), - credentials_path: AbsolutePath::try_new("/").expect("Valid path"), - smartrest: default_smartrest_config(), - smartrest1: default_smartrest1_config(), - http: OptionalConfig::Empty("".into()), // Will be derived from url at runtime - mqtt: OptionalConfig::Empty("".into()), // Will be derived from url at runtime - proxy: ProxyConfig::default(), - entity_store: default_entity_store_config(), - software_management: default_software_management_config(), - operations: default_operations_config(), - availability: default_availability_config(), - enable: default_enable_config(), - mqtt_service: MqttServiceConfig::default(), - } - } -} - -impl Default for PartialMapperMapperConfig { - fn default() -> Self { - Self { - mqtt: PartialMqttConfig::default(), - cloud_specific: T::default(), - } - } -} - -fn set_key_if_blank(field: &mut OptionalConfig, value: Cow<'static, str>) { - use OptionalConfig as OC; +fn to_optional_config(field: Option, key: &ReadableKey) -> OptionalConfig { match field { - OC::Present { ref mut key, .. } | OC::Empty(ref mut key) if key.is_empty() => *key = value, - _ => (), - } -} - -fn set_key_if_blank2(field: &mut Keyed, value: Cow<'static, str>) { - if field.key.is_empty() { - field.key = value - } -} - -fn convert_optional_value>(field: &OptionalConfig) -> OptionalConfig { - match field.clone() { - OptionalConfig::Present { value, key } => OptionalConfig::Present { - value: value.into(), - key, + Some(value) => OptionalConfig::Present { + value, + key: key.to_cow_str(), }, - OptionalConfig::Empty(key) => OptionalConfig::Empty(key), - } -} - -fn to_optional_config(field: Option, key: Cow<'static, str>) -> OptionalConfig { - match field { - Some(value) => OptionalConfig::Present { value, key }, - None => OptionalConfig::Empty(key), - } -} - -impl ApplyRuntimeDefaults for C8yMapperSpecificConfig { - fn apply_runtime_defaults( - &mut self, - url: &OptionalConfig, - tedge_config: &TEdgeConfig, - config_path: &AbsolutePath, - ) { - // Derive http endpoint from url if it's not been set - if self.http.or_none().is_none() { - self.http = convert_optional_value(url); - } - - // Derive mqtt endpoint from url if it's not been set - if self.mqtt.or_none().is_none() { - self.mqtt = convert_optional_value(url); - } - - // Apply proxy port inheritance: client.port defaults to bind.port - if self.proxy.client.port == 8001 && self.proxy.bind.port != 8001 { - self.proxy.client.port = *self.proxy.bind.port; - } - - if self.credentials_path == serde_placeholder_credentials_path() { - self.credentials_path = default_credentials_path(&tedge_config.location) - } - - // Don't need to set the key for http or mqtt as these are set from url - set_key_if_blank( - &mut self.proxy.cert_path, - format!("{}: proxy.cert_path", config_path).into(), - ); - set_key_if_blank( - &mut self.proxy.key_path, - format!("{}: proxy.key_path", config_path).into(), - ); - set_key_if_blank( - &mut self.proxy.ca_path, - format!("{}: proxy.ca_path", config_path).into(), - ); - set_key_if_blank2( - &mut self.proxy.bind.port, - format!("{}: proxy.bind.port", config_path).into(), - ); - } - - fn default_bridge_topic_prefix() -> TopicPrefix { - TopicPrefix::try_new("c8y").unwrap() - } - - fn default_topics() -> TemplatesSet { - "te/+/+/+/+,te/+/+/+/+/twin/+,te/+/+/+/+/m/+,te/+/+/+/+/m/+/meta,te/+/+/+/+/e/+,te/+/+/+/+/a/+,te/+/+/+/+/status/health".parse().expect("Valid templateset") - } - - fn default_max_payload_size() -> MqttPayloadLimit { - super::c8y_mqtt_payload_limit() - } -} - -impl ApplyRuntimeDefaults for AzMapperSpecificConfig { - fn apply_runtime_defaults( - &mut self, - _url: &OptionalConfig, - _tedge_config: &TEdgeConfig, - _config_path: &AbsolutePath, - ) { - // Azure config has no runtime defaults currently - } - - fn default_bridge_topic_prefix() -> TopicPrefix { - TopicPrefix::try_new("az").unwrap() - } - - fn default_topics() -> TemplatesSet { - "te/+/+/+/+/m/+,te/+/+/+/+/e/+,te/+/+/+/+/a/+,te/+/+/+/+/status/health" - .parse() - .expect("Valid templateset") - } - - fn default_max_payload_size() -> MqttPayloadLimit { - super::az_mqtt_payload_limit() - } -} - -impl ApplyRuntimeDefaults for AwsMapperSpecificConfig { - fn apply_runtime_defaults( - &mut self, - _url: &OptionalConfig, - _tedge_config: &TEdgeConfig, - _config_path: &AbsolutePath, - ) { - // AWS config has no runtime defaults currently - } - - fn default_bridge_topic_prefix() -> TopicPrefix { - TopicPrefix::try_new("aws").unwrap() - } - fn default_topics() -> TemplatesSet { - "te/+/+/+/+/m/+,te/+/+/+/+/e/+,te/+/+/+/+/a/+,te/+/+/+/+/status/health" - .parse() - .expect("Valid templateset") - } - - fn default_max_payload_size() -> MqttPayloadLimit { - super::aws_mqtt_payload_limit() + None => OptionalConfig::Empty(key.to_cow_str()), } } @@ -1341,25 +649,27 @@ mod tests { #[test] fn empty_file_deserializes_with_all_defaults() { - let toml = ""; - let config: C8yMapperSpecificConfig = toml::from_str(toml).unwrap(); + let config = deserialize_from_str::("").unwrap(); // Verify all defaults are applied - assert_eq!(config.auth_method, AuthMethod::Certificate); - assert!(config.smartrest.use_operation_id); - assert!(config.entity_store.auto_register); - assert!(config.entity_store.clean_start); + assert_eq!(config.cloud_specific.auth_method, AuthMethod::Certificate); + assert!(config.cloud_specific.smartrest.use_operation_id); + assert!(config.cloud_specific.entity_store.auto_register); + assert!(config.cloud_specific.entity_store.clean_start); assert_eq!( - config.software_management.api, + config.cloud_specific.software_management.api, SoftwareManagementApiFlag::Legacy ); - assert!(!config.software_management.with_types); - assert_eq!(config.operations.auto_log_upload, AutoLogUpload::OnFailure); - assert!(config.enable.log_upload); - assert!(config.enable.config_snapshot); - assert!(config.enable.config_update); - assert!(config.enable.firmware_update); - assert!(config.enable.device_profile); + assert!(!config.cloud_specific.software_management.with_types); + assert_eq!( + config.cloud_specific.operations.auto_log_upload, + AutoLogUpload::OnFailure + ); + assert!(config.cloud_specific.enable.log_upload); + assert!(config.cloud_specific.enable.config_snapshot); + assert!(config.cloud_specific.enable.config_update); + assert!(config.cloud_specific.enable.firmware_update); + assert!(config.cloud_specific.enable.device_profile); } #[test] @@ -1430,24 +740,25 @@ mod tests { device_profile = false "#; - let config: C8yMapperSpecificConfig = toml::from_str(toml).unwrap(); + let config = deserialize_from_str::(toml).unwrap(); + let c8y_config = &config.cloud_specific; // All explicit values preserved, no defaults applied - assert_eq!(config.auth_method, AuthMethod::Basic); - assert!(!config.smartrest.use_operation_id); - assert!(!config.entity_store.auto_register); - assert!(!config.entity_store.clean_start); + assert_eq!(c8y_config.auth_method, AuthMethod::Basic); + assert!(!c8y_config.smartrest.use_operation_id); + assert!(!c8y_config.entity_store.auto_register); + assert!(!c8y_config.entity_store.clean_start); assert_eq!( - config.software_management.api, + c8y_config.software_management.api, SoftwareManagementApiFlag::Advanced ); - assert!(config.software_management.with_types); - assert_eq!(config.operations.auto_log_upload, AutoLogUpload::Always); - assert!(!config.enable.log_upload); - assert!(!config.enable.config_snapshot); - assert!(!config.enable.config_update); - assert!(!config.enable.firmware_update); - assert!(!config.enable.device_profile); + assert!(c8y_config.software_management.with_types); + assert_eq!(c8y_config.operations.auto_log_upload, AutoLogUpload::Always); + assert!(!c8y_config.enable.log_upload); + assert!(!c8y_config.enable.config_snapshot); + assert!(!c8y_config.enable.config_update); + assert!(!c8y_config.enable.firmware_update); + assert!(!c8y_config.enable.device_profile); } #[test] @@ -1461,15 +772,12 @@ mod tests { "#; let tedge_config = TEdgeConfig::from_dto( - &toml::from_str(tedge_toml).unwrap(), + toml::from_str(tedge_toml).unwrap(), TEdgeConfigLocation::from_custom_root("/not/a/real/directory"), ); - let config: C8yMapperConfig = load_mapper_config_from_string( - mapper_toml, - &tedge_config, - &AbsolutePath::try_new("notondisk.toml").unwrap(), - ) - .unwrap(); + let config: C8yMapperConfig = + load_mapper_config_from_string(mapper_toml, &tedge_config, Some("random-profile")) + .unwrap(); // Device fields should come from tedge_config defaults // Call the id() method to get the device ID (which should be set from tedge_toml) @@ -1502,7 +810,7 @@ mod tests { let config = deserialize_from_str::(toml).unwrap(); // For C8y, we check mqtt key since url is private - assert_eq!(config.mqtt().key(), "/not/on/disk.toml: url"); + assert_eq!(config.mqtt().key(), "c8y.url"); } #[test] @@ -1701,11 +1009,11 @@ mod tests { let config = deserialize_from_str::(toml).unwrap(); assert_eq!(config.mapper.mqtt.max_payload_size, MqttPayloadLimit(12345)); - assert!(!config.mapper.cloud_specific.timestamp); + assert!(!config.cloud_specific.mapper.timestamp); } #[test] - fn empty_proxy_cert_path_has_file_in_empty_key_name() { + fn empty_proxy_cert_path_matches_legacy_c8y_key_name() { let toml = r#" url = "tenant.cumulocity.com" "#; @@ -1714,20 +1022,16 @@ mod tests { assert_eq!( config.cloud_specific.proxy.cert_path.key(), - "/not/on/disk.toml: proxy.cert_path" + "c8y.proxy.cert_path" ) } fn deserialize_from_str(toml: &str) -> Result, MapperConfigError> where - T: DeserializeOwned + ApplyRuntimeDefaults + SpecialisedCloudConfig, + T: SpecialisedCloudConfig, { let tedge_config = - TEdgeConfig::from_dto(&TEdgeConfigDto::default(), TEdgeConfigLocation::default()); - load_mapper_config_from_string( - toml, - &tedge_config, - &AbsolutePath::try_new("/not/on/disk.toml").unwrap(), - ) + TEdgeConfig::from_dto(TEdgeConfigDto::default(), TEdgeConfigLocation::default()); + load_mapper_config_from_string(toml, &tedge_config, None) } } diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 97498b30114..f097aba9f86 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -102,7 +102,7 @@ impl TEdgeConfigLocation { "Loading configuration from {:?}", self.tedge_config_file_path ); - Ok(TEdgeConfig::from_dto(&dto, self.clone())) + Ok(TEdgeConfig::from_dto(dto, self.clone())) } pub(crate) fn load_sync(self) -> Result { @@ -111,7 +111,7 @@ impl TEdgeConfigLocation { "Loading configuration from {:?}", self.tedge_config_file_path ); - Ok(TEdgeConfig::from_dto(&dto, self)) + Ok(TEdgeConfig::from_dto(dto, self)) } async fn load_dto_from_toml_and_env(&self) -> Result { @@ -155,7 +155,7 @@ impl TEdgeConfigLocation { .fold(toml_value, |toml, migration| migration.apply_to(toml)); (dto, warnings) = deserialize_toml(migrated_toml, toml_path).unwrap(); } - (TEdgeConfig::from_dto(&dto, location), warnings) + (TEdgeConfig::from_dto(dto, location), warnings) } async fn load_dto_with_warnings( diff --git a/crates/common/tedge_config/tests/mapper_config.rs b/crates/common/tedge_config/tests/mapper_config.rs index 944aa799152..18112a5629f 100644 --- a/crates/common/tedge_config/tests/mapper_config.rs +++ b/crates/common/tedge_config/tests/mapper_config.rs @@ -96,7 +96,7 @@ async fn partial_migration_default_new_profile_legacy_errors() { .mapper_config::(&Some(prod_profile)) .await; - let err = prod_result.unwrap_err(); + let err = prod_result.err().unwrap(); let expected_path = format!("{}/mappers/c8y.d/prod.toml", ttd.utf8_path()); assert!( err.to_string().contains(&expected_path), @@ -136,7 +136,7 @@ async fn partial_migration_default_legacy_profile_new_errors() { .mapper_config::(&None::) .await; - let err = default_result.unwrap_err(); + let err = default_result.err().unwrap(); let expected_path = format!("{}/mappers/c8y.toml", ttd.utf8_path()); assert!( err.to_string().contains(&expected_path), diff --git a/crates/common/tedge_config_macros/impl/src/dto.rs b/crates/common/tedge_config_macros/impl/src/dto.rs index 7c49e7a6095..73666575876 100644 --- a/crates/common/tedge_config_macros/impl/src/dto.rs +++ b/crates/common/tedge_config_macros/impl/src/dto.rs @@ -78,7 +78,7 @@ pub fn generate( let doc_comment_attr = (!doc_comment.is_empty()).then(|| quote_spanned!(name.span()=> #[doc = #doc_comment])); quote_spanned! {name.span()=> - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::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) @@ -166,7 +166,7 @@ mod tests { let generated = generate_test_dto(&input); let expected = parse_quote! { - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::serde::Deserialize, ::serde::Serialize, PartialEq)] #[non_exhaustive] pub struct TEdgeConfigDto { #[serde(default)] @@ -184,7 +184,7 @@ mod tests { } } - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::serde::Deserialize, ::serde::Serialize, PartialEq)] #[non_exhaustive] pub struct TEdgeConfigDtoC8y { pub url: Option, @@ -197,7 +197,7 @@ mod tests { } } - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::serde::Deserialize, ::serde::Serialize, PartialEq)] #[non_exhaustive] pub struct TEdgeConfigDtoSudo { pub enable: Option, @@ -229,7 +229,7 @@ mod tests { .retain(only_struct_named("TEdgeConfigDtoDevice")); let expected = parse_quote! { - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::serde::Deserialize, ::serde::Serialize, PartialEq)] #[non_exhaustive] pub struct TEdgeConfigDtoDevice { pub id: Option, @@ -254,7 +254,7 @@ mod tests { .retain(only_struct_named("TEdgeConfigDtoDevice")); let expected = parse_quote! { - #[derive(Debug, Default, ::serde::Deserialize, ::serde::Serialize, PartialEq)] + #[derive(Debug, Default, Clone, ::serde::Deserialize, ::serde::Serialize, PartialEq)] #[non_exhaustive] pub struct TEdgeConfigDtoDevice { pub id: Option, @@ -273,6 +273,7 @@ mod tests { syn::parse2(tokens).unwrap() } + #[track_caller] fn assert_eq(actual: &syn::File, expected: &syn::File) { pretty_assertions::assert_eq!( prettyplease::unparse(actual), diff --git a/crates/common/tedge_config_macros/src/multi.rs b/crates/common/tedge_config_macros/src/multi.rs index 1118fb10900..2c9de60a270 100644 --- a/crates/common/tedge_config_macros/src/multi.rs +++ b/crates/common/tedge_config_macros/src/multi.rs @@ -13,9 +13,9 @@ use std::str::FromStr; #[serde(bound(serialize = "T: Serialize + Default + PartialEq"), default)] pub struct MultiDto { #[serde(skip_serializing_if = "is_default")] - profiles: ::std::collections::HashMap, + pub profiles: ::std::collections::HashMap, #[serde(flatten)] - non_profile: T, + pub non_profile: T, } fn is_default(map: &HashMap) -> bool { @@ -77,8 +77,8 @@ impl FromStr for ProfileName { #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)] pub struct MultiReader { - profiles: ::std::collections::HashMap, - non_profile: T, + pub profiles: ::std::collections::HashMap, + pub non_profile: T, parent: &'static str, } diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index 6700c7dfba3..a7b56846bba 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -3,6 +3,7 @@ use crate::cli::config::commands::*; use crate::command::*; use crate::ConfigError; use clap_complete::ArgValueCandidates; +use tedge_config::models::CloudType; use tedge_config::tedge_toml::mapper_config::AwsMapperSpecificConfig; use tedge_config::tedge_toml::mapper_config::AzMapperSpecificConfig; use tedge_config::tedge_toml::mapper_config::C8yMapperSpecificConfig; @@ -10,6 +11,7 @@ use tedge_config::tedge_toml::ProfileName; use tedge_config::tedge_toml::ReadableKey; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; +use tedge_config::tedge_toml::mapper_config::compat::IsCloudConfig; #[derive(clap::Subcommand, Debug)] pub enum ConfigCmd { @@ -188,38 +190,35 @@ impl BuildCommand for ConfigCmd { } } -pub async fn restrict_cloud_config_update( +pub async fn restrict_cloud_config_access( cmd: &str, - key: &WritableKey, + key: &(impl IsCloudConfig + std::fmt::Display), tedge_config: &TEdgeConfig, ) -> anyhow::Result<()> { use tedge_config::tedge_toml::ConfigDecision as CD; - let key = key.to_cow_str(); - let (cloud, config_source) = match key.split_once(".") { - None => unreachable!("Configuration keys always contain ."), - Some((cloud @ "c8y", rest)) => { - let profile = extract_profile_name(rest); + let Some((cloud, profile)) = key.cloud_type_for() else { + // Not a cloud config, we don't need to worry about + return Ok(()); + }; + let (cloud, config_source) = match cloud { + CloudType::C8y => { let source = tedge_config .decide_config_source::(profile.as_ref()) .await; (cloud, source) } - Some((cloud @ "az", rest)) => { - let profile = extract_profile_name(rest); + CloudType::Az => { let source = tedge_config .decide_config_source::(profile.as_ref()) .await; (cloud, source) } - Some((cloud @ "aws", rest)) => { - let profile = extract_profile_name(rest); + CloudType::Aws => { let source = tedge_config .decide_config_source::(profile.as_ref()) .await; (cloud, source) } - // Not a cloud config, we don't need to worry about - _ => return Ok(()), }; match config_source { @@ -239,9 +238,3 @@ pub async fn restrict_cloud_config_update( } } } - -fn extract_profile_name(partial_config_key: &str) -> Option { - let partial_config_key = partial_config_key.strip_prefix("profiles.")?; - let (profile, _rest) = partial_config_key.split_once(".")?; - Some(profile.parse().unwrap()) -} diff --git a/crates/core/tedge/src/cli/config/commands/add.rs b/crates/core/tedge/src/cli/config/commands/add.rs index 34ec8bd51a3..2f9021d03a3 100644 --- a/crates/core/tedge/src/cli/config/commands/add.rs +++ b/crates/core/tedge/src/cli/config/commands/add.rs @@ -1,5 +1,5 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_update; +use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -20,7 +20,7 @@ impl Command for AddConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_update("add", &self.key, &tedge_config).await?; + restrict_cloud_config_access("add", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, reader| { dto.try_append_str(reader, &self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/get.rs b/crates/core/tedge/src/cli/config/commands/get.rs index b03e79bf9d0..f056bdfa64d 100644 --- a/crates/core/tedge/src/cli/config/commands/get.rs +++ b/crates/core/tedge/src/cli/config/commands/get.rs @@ -2,6 +2,7 @@ use tedge_config::tedge_toml::ReadableKey; use tedge_config::TEdgeConfig; use crate::command::Command; +use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; pub struct GetConfigCommand { @@ -15,6 +16,7 @@ impl Command for GetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { + restrict_cloud_config_access("get", &self.key, &tedge_config).await?; match tedge_config.read_string(&self.key) { Ok(value) => { println!("{}", value); diff --git a/crates/core/tedge/src/cli/config/commands/remove.rs b/crates/core/tedge/src/cli/config/commands/remove.rs index 8d80bf9a239..329ddb915a7 100644 --- a/crates/core/tedge/src/cli/config/commands/remove.rs +++ b/crates/core/tedge/src/cli/config/commands/remove.rs @@ -1,5 +1,5 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_update; +use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -16,7 +16,7 @@ impl Command for RemoveConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_update("remove", &self.key, &tedge_config).await?; + restrict_cloud_config_access("remove", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, reader| { dto.try_remove_str(reader, &self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/set.rs b/crates/core/tedge/src/cli/config/commands/set.rs index 7108bab1c09..75ba44cc3b4 100644 --- a/crates/core/tedge/src/cli/config/commands/set.rs +++ b/crates/core/tedge/src/cli/config/commands/set.rs @@ -1,5 +1,5 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_update; +use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -20,7 +20,7 @@ impl Command for SetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_update("set", &self.key, &tedge_config).await?; + restrict_cloud_config_access("set", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, _reader| { dto.try_update_str(&self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/unset.rs b/crates/core/tedge/src/cli/config/commands/unset.rs index ab337d72c4c..83f63aae3e1 100644 --- a/crates/core/tedge/src/cli/config/commands/unset.rs +++ b/crates/core/tedge/src/cli/config/commands/unset.rs @@ -1,5 +1,5 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_update; +use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -15,7 +15,7 @@ impl Command for UnsetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_update("unset", &self.key, &tedge_config).await?; + restrict_cloud_config_access("unset", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, _reader| Ok(dto.try_unset_key(&self.key)?)) .await diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index 193be50744f..fb09a0b9b51 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -794,7 +794,11 @@ pub async fn bridge_config( bridge_keyfile: c8y_config.device.key_path.clone().into(), smartrest_templates: c8y_config.cloud_specific.smartrest.templates.clone(), smartrest_one_templates: c8y_config.cloud_specific.smartrest1.templates.clone(), - include_local_clean_session: c8y_config.bridge.include.local_cleansession, + include_local_clean_session: c8y_config + .cloud_specific + .bridge + .include + .local_cleansession, bridge_location, topic_prefix: c8y_config.bridge.topic_prefix.clone(), profile_name: profile.clone().map(Cow::into_owned), @@ -1253,9 +1257,9 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre let err = validate_config(&config, &cloud).await.unwrap_err(); pretty_assertions::assert_eq!(err.to_string(), format!("You have matching URLs and device IDs for different profiles. -{path}/mappers/c8y.toml: url, {path}/mappers/c8y.d/new.toml: url are set to the same value, but so are device.id, device.id. +c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id. -Each cloud profile requires either a unique URL or unique device ID, so it corresponds to a unique device in the associated cloud.", path = ttd.utf8_path())) +Each cloud profile requires either a unique URL or unique device ID, so it corresponds to a unique device in the associated cloud.")) } #[tokio::test] @@ -1274,9 +1278,9 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre let err = validate_config(&config, &cloud).await.unwrap_err(); pretty_assertions::assert_eq!(err.to_string(), format!("You have matching URLs and device IDs for different profiles. -{path}/mappers/c8y.toml: url, {path}/mappers/c8y.d/new.toml: url are set to the same value, but so are {path}/mappers/c8y.toml: device.id, {path}/mappers/c8y.d/new.toml: device.id. +c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id. -Each cloud profile requires either a unique URL or unique device ID, so it corresponds to a unique device in the associated cloud.", path = ttd.utf8_path())) +Each cloud profile requires either a unique URL or unique device ID, so it corresponds to a unique device in the associated cloud.")) } #[tokio::test] diff --git a/crates/core/tedge_mapper/src/aws/mapper.rs b/crates/core/tedge_mapper/src/aws/mapper.rs index 455fd60187b..da31d146c81 100644 --- a/crates/core/tedge_mapper/src/aws/mapper.rs +++ b/crates/core/tedge_mapper/src/aws/mapper.rs @@ -82,10 +82,10 @@ impl TEdgeComponent for AwsMapper { } let clock = Box::new(WallClock); let aws_converter = AwsConverter::new( - aws_config.mapper.cloud_specific.timestamp, + aws_config.cloud_specific.mapper.timestamp, clock, mqtt_schema, - aws_config.mapper.cloud_specific.timestamp_format, + aws_config.cloud_specific.mapper.timestamp_format, prefix.value().clone(), aws_config.mapper.mqtt.max_payload_size.0, ); diff --git a/crates/core/tedge_mapper/src/az/mapper.rs b/crates/core/tedge_mapper/src/az/mapper.rs index 92bb7a0721c..834bbf7dfc1 100644 --- a/crates/core/tedge_mapper/src/az/mapper.rs +++ b/crates/core/tedge_mapper/src/az/mapper.rs @@ -91,10 +91,10 @@ impl TEdgeComponent for AzureMapper { } let mqtt_schema = MqttSchema::with_root(tedge_config.mqtt.topic_root.clone()); let az_converter = AzureConverter::new( - az_config.mapper.cloud_specific.timestamp, + az_config.cloud_specific.mapper.timestamp, Box::new(WallClock), mqtt_schema, - az_config.mapper.cloud_specific.timestamp_format, + az_config.cloud_specific.mapper.timestamp_format, prefix, az_config.mapper.mqtt.max_payload_size.0, ); From 7e3219d6990c24f7732b5fc995b664d50b5bae3f Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Wed, 10 Dec 2025 15:17:45 +0000 Subject: [PATCH 02/10] PoC: get `tedge config get` working with new mapper config Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 154 +++++++----------- .../src/tedge_toml/tedge_config_location.rs | 112 +++++++++++++ crates/core/tedge/src/cli/config/cli.rs | 2 +- .../core/tedge/src/cli/config/commands/get.rs | 2 - 4 files changed, 172 insertions(+), 98 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index d0f6f98f5ff..b282b9ae0f1 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -1,7 +1,6 @@ mod version; use futures::Stream; use reqwest::NoProxy; -use tokio::fs::DirEntry; use version::TEdgeTomlVersion; mod append_remove; @@ -27,6 +26,7 @@ use super::models::TopicPrefix; use super::models::HTTPS_PORT; use super::models::MQTT_TLS_PORT; use super::tedge_config_location::TEdgeConfigLocation; +use crate::ConfigDecision; use crate::models::AbsolutePath; use crate::tedge_toml::mapper_config::AwsMapperSpecificConfig; use crate::tedge_toml::mapper_config::AzMapperSpecificConfig; @@ -107,19 +107,30 @@ impl std::ops::Deref for TEdgeConfig { } } -/// Decision about which configuration source to use -pub enum ConfigDecision { - /// Load from new format file at the given path - LoadNew { path: Utf8PathBuf }, - /// Load from tedge.toml via compatibility layer - LoadLegacy, - /// Configuration file not found - NotFound { path: Utf8PathBuf }, - /// Permission error accessing mapper config directory - PermissionError { - mapper_config_dir: Utf8PathBuf, - error: std::io::Error, - }, +impl TEdgeConfigDto { + pub(crate) async fn populate_mapper_configs(&mut self, location: &TEdgeConfigLocation) -> anyhow::Result<()> { + use futures::StreamExt; + use futures::TryStreamExt; + let mappers_dir = location.tedge_config_root_path().join("mappers"); + let all_profiles = location.mapper_config_profiles::().await; + match all_profiles { + Some(profiles) => { + // TODO don't fail on non-existence of file + let default_profile_toml = tokio::fs::read_to_string(mappers_dir.join("c8y.toml")).await.unwrap(); + // TODO quote path in error messages + let c8y_config: TEdgeConfigDtoC8y = toml::from_str(&default_profile_toml).context("failed to deserialise mapper config")?; + self.c8y.non_profile = c8y_config; + + self.c8y.profiles = profiles.filter_map(|profile| futures::future::ready(profile)).then(|profile| async { + let profile_toml = tokio::fs::read_to_string(mappers_dir.join("c8y.d").join(format!("{profile}.toml"))).await?; + let c8y_config: TEdgeConfigDtoC8y = toml::from_str(&profile_toml).context("failed to deserialise mapper config")?; + Ok::<_, anyhow::Error>((profile, c8y_config)) + }).try_collect().await?; + } + None => () + } + Ok(()) + } } impl TEdgeConfig { @@ -132,6 +143,10 @@ impl TEdgeConfig { } } + pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision where T: ExpectedCloudType{ + self.location.decide_config_source::(profile).await + } + pub(crate) fn location(&self) -> &TEdgeConfigLocation { &self.location } @@ -140,49 +155,6 @@ impl TEdgeConfig { self.location.tedge_config_root_path() } - /// Decide which configuration source to use for a given cloud and profile - /// - /// This function centralizes the decision logic for mapper configuration precedence: - /// 1. New format (`mappers/[cloud].toml` or `mappers/[cloud].d/[profile].toml`) takes precedence - /// 2. If new format exists for some profiles but not the requested one, returns NotFound - /// 3. If the config directory is inaccessible due to a permissions error, return Error - /// 4. If no new format exists at all, fall back to legacy tedge.toml format - pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision - where - T: ExpectedCloudType, - { - use tokio::fs::try_exists; - - let mapper_config_dir = self.mappers_config_dir(); - let ty = T::expected_cloud_type().to_string(); - - let filename = profile.map_or_else(|| format!("{ty}.toml"), |p| format!("{ty}.d/{p}.toml")); - let path = mapper_config_dir.join(&filename); - - let default_profile_path = mapper_config_dir.join(format!("{ty}.toml")); - let profile_dir_path = mapper_config_dir.join(format!("{ty}.d")); - - match ( - try_exists(&default_profile_path).await, - try_exists(&profile_dir_path).await, - try_exists(&path).await, - ) { - // The specific config we're looking for exists - (_, _, Ok(true)) => ConfigDecision::LoadNew { path }, - - // New format configs exist for this cloud, but not the specific profile requested - (Ok(true), _, _) | (_, Ok(true), _) => ConfigDecision::NotFound { path }, - - // No new format configs exist for this cloud, use legacy - (Ok(false), Ok(false), _) => ConfigDecision::LoadLegacy, - - // Permission error accessing mapper config directory - (Err(err), _, _) | (_, Err(err), _) => ConfigDecision::PermissionError { - mapper_config_dir, - error: err, - }, - } - } pub async fn c8y_mapper_config( &self, profile: &Option>, @@ -202,6 +174,29 @@ impl TEdgeConfig { self.mapper_config(profile).await } + // pub async fn cloud_config_reader( + // &self, + // profile: &Option>, + // ) -> anyhow::Result { + // let profile = profile.as_ref().map(|p| p.borrow().to_owned()); + // let ty = T::expected_cloud_type().to_string(); + + // match self.decide_config_source::(profile.as_ref()).await { + // ConfigDecision::LoadNew { path } => { + // let toml = tokio::fs::read_to_string(&path).await.with_context(|| format!("failed to read mapper configuration at {path}"))?; + // T::read_tedge_config(&toml, self, profile.as_deref()).map(<_>::into) + // }, + // ConfigDecision::NotFound { path } => { + // Err(anyhow!("mapper configuration file {path} doesn't exist")) + // } + // ConfigDecision::LoadLegacy => match T::expected_cloud_type() { + // CloudType::Aws => self.aws.try_get(profile.as_ref()).map(Cow::Borrowed), + // CloudType::Az => self.az.try_get(profile.as_ref()).map(Cow::Borrowed), + // CloudType::C8y => self.c8y.try_get(profile.as_ref()).map(Cow::Borrowed), + // }, + // } + // } + pub async fn mapper_config( &self, profile: &Option>, @@ -209,7 +204,7 @@ impl TEdgeConfig { let profile = profile.as_ref().map(|p| p.borrow().to_owned()); let ty = T::expected_cloud_type().to_string(); - match self.decide_config_source::(profile.as_ref()).await { + match self.location.decide_config_source::(profile.as_ref()).await { ConfigDecision::LoadNew { path } => { // Check for config conflict: both new format and legacy exist if self.has_legacy_config::(profile.as_ref()) { @@ -256,10 +251,6 @@ impl TEdgeConfig { } } - pub fn mappers_config_dir(&self) -> Utf8PathBuf { - self.root_dir().join("mappers") - } - /// Check if a legacy tedge.toml configuration exists for the given cloud /// type and profile by checking if the URL is configured /// @@ -293,7 +284,7 @@ impl TEdgeConfig { } pub fn profiled_config_directories(&self) -> impl Iterator + use<'_> { - CloudType::iter().map(|ty| self.mappers_config_dir().join(format!("{ty}.d"))) + CloudType::iter().map(|ty| self.location.mappers_config_dir().join(format!("{ty}.d"))) } async fn all_profiles( @@ -302,37 +293,10 @@ impl TEdgeConfig { where T: ExpectedCloudType, { - use futures::future::ready; - use futures::StreamExt; - - fn file_name_string(entry: tokio::io::Result) -> Option { - entry.ok()?.file_name().into_string().ok() - } - - fn profile_name_from_filename(filename: &str) -> Option { - ProfileName::try_from(filename.strip_suffix(".toml")?.to_owned()).ok() - } - - let ty = T::expected_cloud_type(); - - match self.decide_config_source::(None).await { - ConfigDecision::LoadNew { .. } - | ConfigDecision::NotFound { .. } - | ConfigDecision::PermissionError { .. } => { - let default_profile = futures::stream::once(ready(None)); - match tokio::fs::read_dir(self.mappers_config_dir().join(format!("{ty}.d"))).await { - Ok(profile_dir) => Box::new( - default_profile.chain( - tokio_stream::wrappers::ReadDirStream::new(profile_dir) - .filter_map(|entry| ready(file_name_string(entry))) - .filter_map(|s| ready(profile_name_from_filename(&s))) - .map(Some), - ), - ), - Err(_) => Box::new(default_profile), - } - } - ConfigDecision::LoadLegacy => match ty { + if let Some(migrated_profiles) = self.location.mapper_config_profiles::().await { + migrated_profiles + } else { + match T::expected_cloud_type() { CloudType::C8y => Box::new(futures::stream::iter( self.c8y.keys().map(|p| p.map(<_>::to_owned)), )), @@ -342,7 +306,7 @@ impl TEdgeConfig { CloudType::Aws => Box::new(futures::stream::iter( self.aws.keys().map(|p| p.map(<_>::to_owned)), )), - }, + } } } diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index f097aba9f86..8e3e50b5fea 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -6,11 +6,14 @@ use crate::TEdgeConfig; use crate::TEdgeConfigDto; use crate::TEdgeConfigError; use crate::TEdgeConfigReader; +use crate::tedge_toml::mapper_config::ExpectedCloudType; use anyhow::Context; use camino::Utf8Path; use camino::Utf8PathBuf; use serde::Deserialize as _; use serde::Serialize; +use tedge_config_macros::ProfileName; +use tokio::fs::DirEntry; use std::path::PathBuf; use tedge_utils::file::change_mode; use tedge_utils::file::change_mode_sync; @@ -81,6 +84,54 @@ impl TEdgeConfigLocation { &self.tedge_config_root_path } + pub fn mappers_config_dir(&self) -> Utf8PathBuf { + self.tedge_config_root_path.join("mappers") + } + + /// Decide which configuration source to use for a given cloud and profile + /// + /// This function centralizes the decision logic for mapper configuration precedence: + /// 1. New format (`mappers/[cloud].toml` or `mappers/[cloud].d/[profile].toml`) takes precedence + /// 2. If new format exists for some profiles but not the requested one, returns NotFound + /// 3. If the config directory is inaccessible due to a permissions error, return Error + /// 4. If no new format exists at all, fall back to legacy tedge.toml format + pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision + where + T: ExpectedCloudType, + { + use tokio::fs::try_exists; + + let mapper_config_dir = self.mappers_config_dir(); + let ty = T::expected_cloud_type().to_string(); + + let filename = profile.map_or_else(|| format!("{ty}.toml"), |p| format!("{ty}.d/{p}.toml")); + let path = mapper_config_dir.join(&filename); + + let default_profile_path = mapper_config_dir.join(format!("{ty}.toml")); + let profile_dir_path = mapper_config_dir.join(format!("{ty}.d")); + + match ( + try_exists(&default_profile_path).await, + try_exists(&profile_dir_path).await, + try_exists(&path).await, + ) { + // The specific config we're looking for exists + (_, _, Ok(true)) => ConfigDecision::LoadNew { path }, + + // New format configs exist for this cloud, but not the specific profile requested + (Ok(true), _, _) | (_, Ok(true), _) => ConfigDecision::NotFound { path }, + + // No new format configs exist for this cloud, use legacy + (Ok(false), Ok(false), _) => ConfigDecision::LoadLegacy, + + // Permission error accessing mapper config directory + (Err(err), _, _) | (_, Err(err), _) => ConfigDecision::PermissionError { + mapper_config_dir, + error: err, + }, + } + } + pub async fn update_toml( &self, update: &impl Fn(&mut TEdgeConfigDto, &TEdgeConfigReader) -> ConfigSettingResult<()>, @@ -158,6 +209,47 @@ impl TEdgeConfigLocation { (TEdgeConfig::from_dto(dto, location), warnings) } + pub(crate) async fn mapper_config_profiles( + &self, + ) -> Option> + Unpin + Send + Sync + '_>> + where + T: ExpectedCloudType, + { + use futures::future::ready; + use futures::StreamExt; + + fn file_name_string(entry: tokio::io::Result) -> Option { + entry.ok()?.file_name().into_string().ok() + } + + fn profile_name_from_filename(filename: &str) -> Option { + ProfileName::try_from(filename.strip_suffix(".toml")?.to_owned()).ok() + } + + let ty = T::expected_cloud_type(); + + match self.decide_config_source::(None).await { + ConfigDecision::LoadNew { .. } + | ConfigDecision::NotFound { .. } + | ConfigDecision::PermissionError { .. } => { + let default_profile = futures::stream::once(ready(None)); + match tokio::fs::read_dir(self.mappers_config_dir().join(format!("{ty}.d"))).await { + Ok(profile_dir) => Some(Box::new( + default_profile.chain( + tokio_stream::wrappers::ReadDirStream::new(profile_dir) + .filter_map(|entry| ready(file_name_string(entry))) + .filter_map(|s| ready(profile_name_from_filename(&s))) + .map(Some), + )), + ), + Err(_) => Some(Box::new(default_profile)), + } + } + ConfigDecision::LoadLegacy => None, + } + } + + async fn load_dto_with_warnings( &self, ) -> Result<(TEdgeConfigDto, UnusedValueWarnings), TEdgeConfigError> { @@ -191,6 +283,8 @@ impl TEdgeConfigLocation { update_with_environment_variables(&mut dto, &mut warnings)?; } + dto.populate_mapper_configs(self).await?; + Ok((dto, warnings)) } @@ -225,6 +319,8 @@ impl TEdgeConfigLocation { if Sources::INCLUDE_ENVIRONMENT { update_with_environment_variables(&mut dto, &mut warnings)?; } + + todo!("populate_mapper_configs_sync"); Ok((dto, warnings)) } @@ -298,6 +394,22 @@ impl ConfigSources for FileOnly { const INCLUDE_ENVIRONMENT: bool = false; } +/// Decision about which configuration source to use +pub enum ConfigDecision { + /// Load from new format file at the given path + LoadNew { path: Utf8PathBuf }, + /// Load from tedge.toml via compatibility layer + LoadLegacy, + /// Configuration file not found + NotFound { path: Utf8PathBuf }, + /// Permission error accessing mapper config directory + PermissionError { + mapper_config_dir: Utf8PathBuf, + error: std::io::Error, + }, +} + + #[derive(Default, Debug, PartialEq, Eq)] #[must_use] pub struct UnusedValueWarnings(Vec); diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index a7b56846bba..0f4a6acde29 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -195,7 +195,7 @@ pub async fn restrict_cloud_config_access( key: &(impl IsCloudConfig + std::fmt::Display), tedge_config: &TEdgeConfig, ) -> anyhow::Result<()> { - use tedge_config::tedge_toml::ConfigDecision as CD; + use tedge_config::ConfigDecision as CD; let Some((cloud, profile)) = key.cloud_type_for() else { // Not a cloud config, we don't need to worry about return Ok(()); diff --git a/crates/core/tedge/src/cli/config/commands/get.rs b/crates/core/tedge/src/cli/config/commands/get.rs index f056bdfa64d..b03e79bf9d0 100644 --- a/crates/core/tedge/src/cli/config/commands/get.rs +++ b/crates/core/tedge/src/cli/config/commands/get.rs @@ -2,7 +2,6 @@ use tedge_config::tedge_toml::ReadableKey; use tedge_config::TEdgeConfig; use crate::command::Command; -use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; pub struct GetConfigCommand { @@ -16,7 +15,6 @@ impl Command for GetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_access("get", &self.key, &tedge_config).await?; match tedge_config.read_string(&self.key) { Ok(value) => { println!("{}", value); From af2a8d6279e2df3fe085dcfc71e8885327ced00b Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 11 Dec 2025 11:17:48 +0000 Subject: [PATCH 03/10] Support `tedge config get/list` with generalised mapper config for all clouds Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 74 +++++++++++++++---- .../tedge_config/mapper_config/mod.rs | 2 +- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index b282b9ae0f1..25c56e80055 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -26,7 +26,6 @@ use super::models::TopicPrefix; use super::models::HTTPS_PORT; use super::models::MQTT_TLS_PORT; use super::tedge_config_location::TEdgeConfigLocation; -use crate::ConfigDecision; use crate::models::AbsolutePath; use crate::tedge_toml::mapper_config::AwsMapperSpecificConfig; use crate::tedge_toml::mapper_config::AzMapperSpecificConfig; @@ -34,6 +33,7 @@ use crate::tedge_toml::mapper_config::C8yMapperSpecificConfig; use crate::tedge_toml::mapper_config::HasUrl; use crate::tedge_toml::mapper_config::MapperConfigError; use crate::tedge_toml::mapper_config::SpecialisedCloudConfig; +use crate::ConfigDecision; use anyhow::anyhow; use anyhow::Context; use camino::Utf8Path; @@ -55,6 +55,7 @@ use reqwest::Certificate; use std::borrow::Borrow; use std::borrow::Cow; use std::collections::HashMap; +use std::io::ErrorKind; use std::io::Read; use std::iter::Iterator; use std::net::IpAddr; @@ -107,30 +108,64 @@ impl std::ops::Deref for TEdgeConfig { } } +async fn read_file_if_exists(path: &Utf8Path) -> anyhow::Result> { + match tokio::fs::read_to_string(path).await { + Ok(contents) => Ok(Some(contents)), + Err(e) if e.kind() == ErrorKind::NotFound => Ok(None), + Err(e) => Err(e).context(format!("failed to read mapper configuration from {path}")), + } +} + impl TEdgeConfigDto { - pub(crate) async fn populate_mapper_configs(&mut self, location: &TEdgeConfigLocation) -> anyhow::Result<()> { + async fn populate_single_mapper( + dto: &mut MultiDto, + location: &TEdgeConfigLocation, + ) -> anyhow::Result<()> { use futures::StreamExt; use futures::TryStreamExt; + let mappers_dir = location.tedge_config_root_path().join("mappers"); - let all_profiles = location.mapper_config_profiles::().await; + let all_profiles = location.mapper_config_profiles::().await; + let ty = T::expected_cloud_type(); match all_profiles { Some(profiles) => { - // TODO don't fail on non-existence of file - let default_profile_toml = tokio::fs::read_to_string(mappers_dir.join("c8y.toml")).await.unwrap(); + let default_profile_toml = + read_file_if_exists(&mappers_dir.join(format!("{ty}.toml"))).await?; // TODO quote path in error messages - let c8y_config: TEdgeConfigDtoC8y = toml::from_str(&default_profile_toml).context("failed to deserialise mapper config")?; - self.c8y.non_profile = c8y_config; - - self.c8y.profiles = profiles.filter_map(|profile| futures::future::ready(profile)).then(|profile| async { - let profile_toml = tokio::fs::read_to_string(mappers_dir.join("c8y.d").join(format!("{profile}.toml"))).await?; - let c8y_config: TEdgeConfigDtoC8y = toml::from_str(&profile_toml).context("failed to deserialise mapper config")?; - Ok::<_, anyhow::Error>((profile, c8y_config)) - }).try_collect().await?; + let default_profile_config: T::CloudDto = default_profile_toml.map_or_else( + || Ok(<_>::default()), + |toml| toml::from_str(&toml).context("failed to deserialise mapper config"), + )?; + dto.non_profile = default_profile_config; + + dto.profiles = profiles + .filter_map(|profile| futures::future::ready(profile)) + .then(|profile| async { + let profile_toml = tokio::fs::read_to_string( + mappers_dir.join(format!("{ty}.d/{profile}.toml")), + ) + .await?; + let profiled_config: T::CloudDto = toml::from_str(&profile_toml) + .context("failed to deserialise mapper config")?; + Ok::<_, anyhow::Error>((profile, profiled_config)) + }) + .try_collect() + .await?; } - None => () + None => (), } Ok(()) } + + pub(crate) async fn populate_mapper_configs( + &mut self, + location: &TEdgeConfigLocation, + ) -> anyhow::Result<()> { + Self::populate_single_mapper::(&mut self.c8y, location).await?; + Self::populate_single_mapper::(&mut self.az, location).await?; + Self::populate_single_mapper::(&mut self.aws, location).await?; + Ok(()) + } } impl TEdgeConfig { @@ -143,7 +178,10 @@ impl TEdgeConfig { } } - pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision where T: ExpectedCloudType{ + pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision + where + T: ExpectedCloudType, + { self.location.decide_config_source::(profile).await } @@ -204,7 +242,11 @@ impl TEdgeConfig { let profile = profile.as_ref().map(|p| p.borrow().to_owned()); let ty = T::expected_cloud_type().to_string(); - match self.location.decide_config_source::(profile.as_ref()).await { + match self + .location + .decide_config_source::(profile.as_ref()) + .await + { ConfigDecision::LoadNew { path } => { // Check for config conflict: both new format and legacy exist if self.has_legacy_config::(profile.as_ref()) { diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index c57433cd4c5..a2619d9c174 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -98,7 +98,7 @@ pub struct C8yBridgeConfig { pub trait SpecialisedCloudConfig: Sized + ExpectedCloudType + FromCloudConfig + Send + Sync + 'static { - type CloudDto: DeserializeOwned + Send + Sync + 'static; + type CloudDto: Default + DeserializeOwned + Send + Sync + 'static; fn into_config_reader( dto: Self::CloudDto, From c9a323aa7968d2f6db54b298595b81d8bca04365 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 11 Dec 2025 11:17:57 +0000 Subject: [PATCH 04/10] Run formatter Signed-off-by: James Rhodes --- .../tedge_config/mapper_config/compat.rs | 2 +- .../src/tedge_toml/tedge_config_location.rs | 20 +++++++++---------- crates/core/tedge/src/cli/config/cli.rs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs index fdc801c9677..991085904b2 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs @@ -1,9 +1,9 @@ use super::*; -use crate::tedge_toml::WritableKey; use crate::tedge_toml::tedge_config::TEdgeConfigReaderAws; use crate::tedge_toml::tedge_config::TEdgeConfigReaderAz; use crate::tedge_toml::tedge_config::TEdgeConfigReaderC8y; use crate::tedge_toml::ReadableKey; +use crate::tedge_toml::WritableKey; use crate::TEdgeConfig; pub trait IsCloudConfig { diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 8e3e50b5fea..568e07b31d6 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -1,26 +1,26 @@ use std::path::Path; +use crate::tedge_toml::mapper_config::ExpectedCloudType; use crate::tedge_toml::DtoKey; use crate::ConfigSettingResult; use crate::TEdgeConfig; use crate::TEdgeConfigDto; use crate::TEdgeConfigError; use crate::TEdgeConfigReader; -use crate::tedge_toml::mapper_config::ExpectedCloudType; use anyhow::Context; use camino::Utf8Path; use camino::Utf8PathBuf; use serde::Deserialize as _; use serde::Serialize; -use tedge_config_macros::ProfileName; -use tokio::fs::DirEntry; use std::path::PathBuf; +use tedge_config_macros::ProfileName; use tedge_utils::file::change_mode; use tedge_utils::file::change_mode_sync; use tedge_utils::file::change_user_and_group; use tedge_utils::file::change_user_and_group_sync; use tedge_utils::fs::atomically_write_file_async; use tedge_utils::fs::atomically_write_file_sync; +use tokio::fs::DirEntry; use tracing::debug; use tracing::subscriber::NoSubscriber; use tracing::warn; @@ -211,7 +211,9 @@ impl TEdgeConfigLocation { pub(crate) async fn mapper_config_profiles( &self, - ) -> Option> + Unpin + Send + Sync + '_>> + ) -> Option< + Box> + Unpin + Send + Sync + '_>, + > where T: ExpectedCloudType, { @@ -240,8 +242,8 @@ impl TEdgeConfigLocation { .filter_map(|entry| ready(file_name_string(entry))) .filter_map(|s| ready(profile_name_from_filename(&s))) .map(Some), - )), - ), + ), + )), Err(_) => Some(Box::new(default_profile)), } } @@ -249,7 +251,6 @@ impl TEdgeConfigLocation { } } - async fn load_dto_with_warnings( &self, ) -> Result<(TEdgeConfigDto, UnusedValueWarnings), TEdgeConfigError> { @@ -319,8 +320,8 @@ impl TEdgeConfigLocation { if Sources::INCLUDE_ENVIRONMENT { update_with_environment_variables(&mut dto, &mut warnings)?; } - - todo!("populate_mapper_configs_sync"); + + todo!("populate_mapper_configs_sync"); Ok((dto, warnings)) } @@ -409,7 +410,6 @@ pub enum ConfigDecision { }, } - #[derive(Default, Debug, PartialEq, Eq)] #[must_use] pub struct UnusedValueWarnings(Vec); diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index 0f4a6acde29..673e90e162d 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -4,6 +4,7 @@ use crate::command::*; use crate::ConfigError; use clap_complete::ArgValueCandidates; use tedge_config::models::CloudType; +use tedge_config::tedge_toml::mapper_config::compat::IsCloudConfig; use tedge_config::tedge_toml::mapper_config::AwsMapperSpecificConfig; use tedge_config::tedge_toml::mapper_config::AzMapperSpecificConfig; use tedge_config::tedge_toml::mapper_config::C8yMapperSpecificConfig; @@ -11,7 +12,6 @@ use tedge_config::tedge_toml::ProfileName; use tedge_config::tedge_toml::ReadableKey; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; -use tedge_config::tedge_toml::mapper_config::compat::IsCloudConfig; #[derive(clap::Subcommand, Debug)] pub enum ConfigCmd { From a640a456064e2a3924bf1719c1e11368175d4477 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 11 Dec 2025 15:37:47 +0000 Subject: [PATCH 05/10] Get the full `tedge config` api working for all clouds with migrated configs --- .../src/tedge_toml/tedge_config.rs | 43 +++++++--- .../tedge_config/mapper_config/mod.rs | 39 ++++++++- .../src/tedge_toml/tedge_config_location.rs | 49 ++++++++--- .../examples/debug_skip.rs | 42 ++++++++++ .../tedge_config_macros/impl/src/reader.rs | 82 ++++++++++++++++++- crates/core/tedge/src/cli/config/cli.rs | 54 ------------ .../core/tedge/src/cli/config/commands/add.rs | 2 - .../tedge/src/cli/config/commands/remove.rs | 2 - .../core/tedge/src/cli/config/commands/set.rs | 2 - .../tedge/src/cli/config/commands/unset.rs | 2 - 10 files changed, 231 insertions(+), 86 deletions(-) create mode 100644 crates/common/tedge_config_macros/examples/debug_skip.rs diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index 25c56e80055..408dabac101 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -1,6 +1,7 @@ mod version; use futures::Stream; use reqwest::NoProxy; +use serde::Deserialize; use version::TEdgeTomlVersion; mod append_remove; @@ -30,6 +31,7 @@ use crate::models::AbsolutePath; use crate::tedge_toml::mapper_config::AwsMapperSpecificConfig; use crate::tedge_toml::mapper_config::AzMapperSpecificConfig; use crate::tedge_toml::mapper_config::C8yMapperSpecificConfig; +use crate::tedge_toml::mapper_config::HasPath as _; use crate::tedge_toml::mapper_config::HasUrl; use crate::tedge_toml::mapper_config::MapperConfigError; use crate::tedge_toml::mapper_config::SpecialisedCloudConfig; @@ -129,24 +131,27 @@ impl TEdgeConfigDto { let ty = T::expected_cloud_type(); match all_profiles { Some(profiles) => { - let default_profile_toml = - read_file_if_exists(&mappers_dir.join(format!("{ty}.toml"))).await?; - // TODO quote path in error messages - let default_profile_config: T::CloudDto = default_profile_toml.map_or_else( + let toml_path = mappers_dir.join(format!("{ty}.toml")); + let default_profile_toml = read_file_if_exists(&toml_path).await?; + let mut default_profile_config: T::CloudDto = default_profile_toml.map_or_else( || Ok(<_>::default()), - |toml| toml::from_str(&toml).context("failed to deserialise mapper config"), + |toml| { + toml::from_str(&toml).with_context(|| { + format!("failed to deserialise mapper config in {toml_path}") + }) + }, )?; + default_profile_config.set_path(toml_path); dto.non_profile = default_profile_config; dto.profiles = profiles .filter_map(|profile| futures::future::ready(profile)) .then(|profile| async { - let profile_toml = tokio::fs::read_to_string( - mappers_dir.join(format!("{ty}.d/{profile}.toml")), - ) - .await?; - let profiled_config: T::CloudDto = toml::from_str(&profile_toml) + let toml_path = mappers_dir.join(format!("{ty}.d/{profile}.toml")); + let profile_toml = tokio::fs::read_to_string(&toml_path).await?; + let mut profiled_config: T::CloudDto = toml::from_str(&profile_toml) .context("failed to deserialise mapper config")?; + profiled_config.set_path(toml_path); Ok::<_, anyhow::Error>((profile, profiled_config)) }) .try_collect() @@ -610,6 +615,12 @@ pub static READABLE_KEYS: Lazy, doku::Type)>> = Lazy::new struct_field_paths(None, &fields) }); +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, doku::Document, serde::Serialize)] +pub enum MapperConfigLocation { + TedgeToml, + SeparateFile(#[doku(as = "String")] camino::Utf8PathBuf), +} + define_tedge_config! { #[tedge_config(reader(skip))] config: { @@ -737,6 +748,10 @@ define_tedge_config! { #[tedge_config(multi, reader(private))] c8y: { + #[tedge_config(reader(skip))] + #[serde(skip)] + read_from: Utf8PathBuf, + /// Endpoint URL of Cumulocity tenant #[tedge_config(example = "your-tenant.cumulocity.com")] // Config consumers should use `c8y.http`/`c8y.mqtt` as appropriate, hence this field is private @@ -977,6 +992,10 @@ define_tedge_config! { #[tedge_config(multi)] #[tedge_config(reader(private))] az: { + #[tedge_config(reader(skip))] + #[serde(skip)] + read_from: Utf8PathBuf, + /// Endpoint URL of Azure IoT tenant #[tedge_config(example = "myazure.azure-devices.net")] url: ConnectUrl, @@ -1064,6 +1083,10 @@ define_tedge_config! { #[tedge_config(multi)] #[tedge_config(reader(private))] aws: { + #[tedge_config(reader(skip))] + #[serde(skip)] + read_from: Utf8PathBuf, + /// Endpoint URL of AWS IoT tenant #[tedge_config(example = "your-endpoint.amazonaws.com")] url: ConnectUrl, diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index a2619d9c174..24d43fa1215 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -2,6 +2,7 @@ pub mod compat; use crate::models::CloudType; use crate::tedge_toml::tedge_config::cert_error_into_config_error; +use crate::tedge_toml::MapperConfigLocation; use crate::tedge_toml::ReadableKey; use crate::tedge_toml::TEdgeConfigDtoAws; use crate::tedge_toml::TEdgeConfigDtoAz; @@ -28,6 +29,7 @@ use super::MultiError; use super::OptionalConfig; use super::ReadError; use camino::Utf8Path; +use camino::Utf8PathBuf; use certificate::PemCertificate; use serde::de::DeserializeOwned; use std::borrow::Cow; @@ -98,7 +100,7 @@ pub struct C8yBridgeConfig { pub trait SpecialisedCloudConfig: Sized + ExpectedCloudType + FromCloudConfig + Send + Sync + 'static { - type CloudDto: Default + DeserializeOwned + Send + Sync + 'static; + type CloudDto: HasPath + Default + DeserializeOwned + Send + Sync + 'static; fn into_config_reader( dto: Self::CloudDto, @@ -107,6 +109,41 @@ pub trait SpecialisedCloudConfig: ) -> Self::CloudConfigReader; } +pub trait HasPath { + fn set_path(&mut self, path: Utf8PathBuf); + fn get_path(&self) -> Option<&Utf8Path>; +} + +impl HasPath for TEdgeConfigDtoC8y { + fn set_path(&mut self, path: Utf8PathBuf) { + self.read_from = Some(path) + } + + fn get_path(&self) -> Option<&Utf8Path> { + self.read_from.as_deref() + } +} + +impl HasPath for TEdgeConfigDtoAz { + fn set_path(&mut self, path: Utf8PathBuf) { + self.read_from = Some(path) + } + + fn get_path(&self) -> Option<&Utf8Path> { + self.read_from.as_deref() + } +} + +impl HasPath for TEdgeConfigDtoAws { + fn set_path(&mut self, path: Utf8PathBuf) { + self.read_from = Some(path) + } + + fn get_path(&self) -> Option<&Utf8Path> { + self.read_from.as_deref() + } +} + /// Base mapper configuration with common fields and cloud-specific fields via generics pub struct MapperConfig { pub tedge_config_reader: T::CloudConfigReader, diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 568e07b31d6..95520a2af25 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -1,6 +1,7 @@ use std::path::Path; use crate::tedge_toml::mapper_config::ExpectedCloudType; +use crate::tedge_toml::mapper_config::HasPath; use crate::tedge_toml::DtoKey; use crate::ConfigSettingResult; use crate::TEdgeConfig; @@ -140,7 +141,7 @@ impl TEdgeConfigLocation { let reader = TEdgeConfigReader::from_dto(&config, self); update(&mut config, &reader)?; - self.store(&config).await + self.store(config).await } fn toml_path(&self) -> &Utf8Path { @@ -274,7 +275,7 @@ impl TEdgeConfigLocation { .into_iter() .fold(toml, |toml, migration| migration.apply_to(toml)); - self.store(&migrated_toml).await?; + self.store_in(self.toml_path(), &migrated_toml).await?; (dto, warnings) = deserialize_toml(migrated_toml, toml_path)?; } @@ -326,19 +327,35 @@ impl TEdgeConfigLocation { Ok((dto, warnings)) } - async fn store(&self, config: &S) -> Result<(), TEdgeConfigError> { + async fn store(&self, mut config: TEdgeConfigDto) -> Result<(), TEdgeConfigError> { + self.store_cloud(&mut config.c8y.non_profile).await?; + self.store_cloud(&mut config.az.non_profile).await?; + self.store_cloud(&mut config.aws.non_profile).await?; + for profile in config.c8y.profiles.values_mut() { + self.store_cloud(profile).await?; + } + for profile in &mut config.az.profiles.values_mut() { + self.store_cloud(profile).await?; + } + for profile in &mut config.aws.profiles.values_mut() { + self.store_cloud(profile).await?; + } + self.store_in(self.toml_path(), &config).await + } + + async fn store_in( + &self, + toml_path: &Utf8Path, + 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 !tokio::fs::try_exists(&self.tedge_config_root_path) - .await - .unwrap_or(false) - { - tokio::fs::create_dir(self.tedge_config_root_path()).await?; + if !tokio::fs::try_exists(toml_path).await.unwrap_or(false) { + tokio::fs::create_dir_all(toml_path.parent().expect("provided path must have parent")) + .await?; } - let toml_path = self.toml_path(); - atomically_write_file_async(toml_path, toml.as_bytes()).await?; if let Err(err) = @@ -354,6 +371,18 @@ impl TEdgeConfigLocation { Ok(()) } + async fn store_cloud( + &self, + config: &mut S, + ) -> Result<(), TEdgeConfigError> { + if let Some(toml_path) = config.get_path() { + self.store_in(toml_path, config).await?; + // Discard config that has been persisted to another location + let _ = std::mem::take(config); + } + Ok(()) + } + fn store_sync(&self, config: &S) -> Result<(), TEdgeConfigError> { let toml = toml::to_string_pretty(&config)?; diff --git a/crates/common/tedge_config_macros/examples/debug_skip.rs b/crates/common/tedge_config_macros/examples/debug_skip.rs new file mode 100644 index 00000000000..f25a81a5058 --- /dev/null +++ b/crates/common/tedge_config_macros/examples/debug_skip.rs @@ -0,0 +1,42 @@ +use tedge_config_macros::*; + +#[derive(thiserror::Error, Debug)] +pub enum ReadError { + #[error(transparent)] + ConfigNotSet(#[from] ConfigNotSet), + #[error("Something went wrong: {0}")] + GenericError(String), + #[error(transparent)] + Multi(#[from] tedge_config_macros::MultiError), +} + +pub trait AppendRemoveItem { + type Item; + + fn append(current_value: Option, new_value: Self::Item) -> Option; + + fn remove(current_value: Option, remove_value: Self::Item) -> Option; +} + +impl AppendRemoveItem for T { + type Item = T; + + fn append(_current_value: Option, _new_value: Self::Item) -> Option { + unimplemented!() + } + + fn remove(_current_value: Option, _remove_value: Self::Item) -> Option { + unimplemented!() + } +} + +define_tedge_config! { + #[tedge_config(multi)] + c8y: { + #[tedge_config(reader(skip))] + #[serde(skip)] + read_from: camino::Utf8PathBuf, + }, +} + +fn main() {} diff --git a/crates/common/tedge_config_macros/impl/src/reader.rs b/crates/common/tedge_config_macros/impl/src/reader.rs index 7676690c260..c404c457814 100644 --- a/crates/common/tedge_config_macros/impl/src/reader.rs +++ b/crates/common/tedge_config_macros/impl/src/reader.rs @@ -53,7 +53,7 @@ fn generate_structs( for item in items { match item { - FieldOrGroup::Field(field) => { + FieldOrGroup::Field(field) if !field.reader().skip => { let ty = field.ty(); attrs.push(field.attrs().to_vec()); idents.push(field.ident()); @@ -144,6 +144,9 @@ fn generate_structs( false => parse_quote!(pub), }); } + FieldOrGroup::Field(_) => { + // Explicitly skipped using `#[tedge_config(reader(skip))]` + } FieldOrGroup::Group(_) => { // Explicitly skipped using `#[tedge_config(reader(skip))]` } @@ -550,7 +553,7 @@ fn generate_conversions( for item in items { match item { - FieldOrGroup::Field(field) => { + FieldOrGroup::Field(field) if !field.reader().skip => { let name = field.ident(); let value = reader_value_for_field(field, &parents, root_fields, Vec::new())?; field_conversions.push(quote_spanned!(name.span()=> #name: #value)); @@ -615,7 +618,7 @@ fn generate_conversions( generate_conversions(&sub_reader_name, &group.contents, parents, root_fields)?; rest.push(sub_conversions); } - FieldOrGroup::Group(_) | FieldOrGroup::Multi(_) => { + FieldOrGroup::Field(_) | FieldOrGroup::Group(_) | FieldOrGroup::Multi(_) => { // Skipped } } @@ -867,6 +870,40 @@ mod tests { ) } + #[test] + fn generate_structs_ignores_reader_skipped_fields_for_reader_only() { + let input: crate::input::Configuration = parse_quote!( + c8y: { + #[tedge_config(reader(skip))] + read_from: Utf8PathBuf, + }, + ); + let actual = generate_structs( + &parse_quote!(TEdgeConfigReader), + &input.groups, + Vec::new(), + "", + ) + .unwrap(); + let mut file: syn::File = syn::parse2(actual.clone()).unwrap(); + let target: syn::Ident = parse_quote!(TEdgeConfigReaderC8y); + file.items.retain( + |i| matches!(i, Item::Struct(ItemStruct { ident: self_ty, .. }) if *self_ty == target), + ); + + // Should contain no fields as all have been skipped + let expected = parse_quote! { + #[derive(::doku::Document, ::serde::Serialize, Debug, Clone)] + #[non_exhaustive] + pub struct TEdgeConfigReaderC8y {} + }; + + pretty_assertions::assert_eq!( + prettyplease::unparse(&file), + prettyplease::unparse(&expected) + ); + } + #[test] fn fields_are_public_only_if_directly_readable() { let input: crate::input::Configuration = parse_quote!( @@ -1055,4 +1092,43 @@ mod tests { prettyplease::unparse(&expected) ) } + + #[test] + fn skipped_reader_values_are_excluded_from_from_dto() { + let input: crate::input::Configuration = parse_quote!( + c8y: { + #[tedge_config(reader(skip))] + #[serde(skip)] + read_from: Utf8PathBuf, + }, + ); + let actual = generate_conversions( + &parse_quote!(TEdgeConfigReader), + &input.groups, + Vec::new(), + &input.groups, + ) + .unwrap(); + let mut file: syn::File = syn::parse2(actual).unwrap(); + let target: syn::Type = parse_quote!(TEdgeConfigReaderC8y); + file.items + .retain(|i| matches!(i, Item::Impl(ItemImpl { self_ty, ..}) if **self_ty == target)); + + let expected = parse_quote! { + impl TEdgeConfigReaderC8y { + #[allow(unused, clippy::clone_on_copy, clippy::useless_conversion)] + #[automatically_derived] + /// Converts the provided [TEdgeConfigDto] into a reader + pub(crate) fn from_dto(dto: &TEdgeConfigDto, location: &TEdgeConfigLocation) -> Self { + Self { + } + } + } + }; + + pretty_assertions::assert_eq!( + prettyplease::unparse(&file), + prettyplease::unparse(&expected) + ) + } } diff --git a/crates/core/tedge/src/cli/config/cli.rs b/crates/core/tedge/src/cli/config/cli.rs index 673e90e162d..df151d77005 100644 --- a/crates/core/tedge/src/cli/config/cli.rs +++ b/crates/core/tedge/src/cli/config/cli.rs @@ -3,11 +3,6 @@ use crate::cli::config::commands::*; use crate::command::*; use crate::ConfigError; use clap_complete::ArgValueCandidates; -use tedge_config::models::CloudType; -use tedge_config::tedge_toml::mapper_config::compat::IsCloudConfig; -use tedge_config::tedge_toml::mapper_config::AwsMapperSpecificConfig; -use tedge_config::tedge_toml::mapper_config::AzMapperSpecificConfig; -use tedge_config::tedge_toml::mapper_config::C8yMapperSpecificConfig; use tedge_config::tedge_toml::ProfileName; use tedge_config::tedge_toml::ReadableKey; use tedge_config::tedge_toml::WritableKey; @@ -189,52 +184,3 @@ impl BuildCommand for ConfigCmd { } } } - -pub async fn restrict_cloud_config_access( - cmd: &str, - key: &(impl IsCloudConfig + std::fmt::Display), - tedge_config: &TEdgeConfig, -) -> anyhow::Result<()> { - use tedge_config::ConfigDecision as CD; - let Some((cloud, profile)) = key.cloud_type_for() else { - // Not a cloud config, we don't need to worry about - return Ok(()); - }; - let (cloud, config_source) = match cloud { - CloudType::C8y => { - let source = tedge_config - .decide_config_source::(profile.as_ref()) - .await; - (cloud, source) - } - CloudType::Az => { - let source = tedge_config - .decide_config_source::(profile.as_ref()) - .await; - (cloud, source) - } - CloudType::Aws => { - let source = tedge_config - .decide_config_source::(profile.as_ref()) - .await; - (cloud, source) - } - }; - - match config_source { - // Cloud config stored in tedge.toml - CD::LoadLegacy => Ok(()), - - // The cloud config has been migrated to new format - CD::LoadNew { path } | CD::NotFound { path } => { - Err(anyhow::anyhow!("`tedge config {cmd}` cannot be used to update {cloud} mapper config. Please directly edit {path} to update {key}")) - }, - - // Mappers directory exists, but we can't see inside it to check if the cloud is migrated - CD::PermissionError { mapper_config_dir, error } => { - let message = format!("Could not access {mapper_config_dir} to establish whether {cloud} mapper config is stored in `tedge.toml` or a separate file"); - let error = anyhow::Error::new(error); - Err(error.context(message)) - } - } -} diff --git a/crates/core/tedge/src/cli/config/commands/add.rs b/crates/core/tedge/src/cli/config/commands/add.rs index 2f9021d03a3..24d72fcb197 100644 --- a/crates/core/tedge/src/cli/config/commands/add.rs +++ b/crates/core/tedge/src/cli/config/commands/add.rs @@ -1,5 +1,4 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -20,7 +19,6 @@ impl Command for AddConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_access("add", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, reader| { dto.try_append_str(reader, &self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/remove.rs b/crates/core/tedge/src/cli/config/commands/remove.rs index 329ddb915a7..360510af03c 100644 --- a/crates/core/tedge/src/cli/config/commands/remove.rs +++ b/crates/core/tedge/src/cli/config/commands/remove.rs @@ -1,5 +1,4 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -16,7 +15,6 @@ impl Command for RemoveConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_access("remove", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, reader| { dto.try_remove_str(reader, &self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/set.rs b/crates/core/tedge/src/cli/config/commands/set.rs index 75ba44cc3b4..c1b81fe7a90 100644 --- a/crates/core/tedge/src/cli/config/commands/set.rs +++ b/crates/core/tedge/src/cli/config/commands/set.rs @@ -1,5 +1,4 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -20,7 +19,6 @@ impl Command for SetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_access("set", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, _reader| { dto.try_update_str(&self.key, &self.value) diff --git a/crates/core/tedge/src/cli/config/commands/unset.rs b/crates/core/tedge/src/cli/config/commands/unset.rs index 83f63aae3e1..0604e09ce27 100644 --- a/crates/core/tedge/src/cli/config/commands/unset.rs +++ b/crates/core/tedge/src/cli/config/commands/unset.rs @@ -1,5 +1,4 @@ use crate::command::Command; -use crate::config::restrict_cloud_config_access; use crate::log::MaybeFancy; use tedge_config::tedge_toml::WritableKey; use tedge_config::TEdgeConfig; @@ -15,7 +14,6 @@ impl Command for UnsetConfigCommand { } async fn execute(&self, tedge_config: TEdgeConfig) -> Result<(), MaybeFancy> { - restrict_cloud_config_access("unset", &self.key, &tedge_config).await?; tedge_config .update_toml(&|dto, _reader| Ok(dto.try_unset_key(&self.key)?)) .await From 4152237ba6808570c8454355f50636613d95f984 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Thu, 11 Dec 2025 19:22:38 +0000 Subject: [PATCH 06/10] Remove tedge config sync API and add some tests for migrated mapper config commands Signed-off-by: James Rhodes --- crates/common/tedge_config/src/lib.rs | 5 - .../src/tedge_toml/tedge_config.rs | 23 -- .../tedge_config/mapper_config/mod.rs | 1 - .../src/tedge_toml/tedge_config_location.rs | 116 ++------- .../examples/debug_skip.rs | 42 ---- .../tedge_config_macros/impl/src/dto.rs | 2 +- crates/core/tedge/src/cli/certificate/cli.rs | 4 +- crates/core/tedge/src/cli/common.rs | 56 ++++- crates/core/tedge/tests/main.rs | 222 +++++------------- plugins/tedge_file_log_plugin/src/main.rs | 5 +- 10 files changed, 134 insertions(+), 342 deletions(-) delete mode 100644 crates/common/tedge_config_macros/examples/debug_skip.rs diff --git a/crates/common/tedge_config/src/lib.rs b/crates/common/tedge_config/src/lib.rs index e6cb7c02667..a51a780e6d3 100644 --- a/crates/common/tedge_config/src/lib.rs +++ b/crates/common/tedge_config/src/lib.rs @@ -27,11 +27,6 @@ impl TEdgeConfig { config_location.load().await } - pub fn load_sync(config_dir: impl AsRef) -> Result { - let config_location = TEdgeConfigLocation::from_custom_root(config_dir.as_ref()); - config_location.load_sync() - } - pub async fn update_toml( self, update: &impl Fn(&mut TEdgeConfigDto, &TEdgeConfigReader) -> ConfigSettingResult<()>, diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index 408dabac101..0d1e9fa70e4 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -217,29 +217,6 @@ impl TEdgeConfig { self.mapper_config(profile).await } - // pub async fn cloud_config_reader( - // &self, - // profile: &Option>, - // ) -> anyhow::Result { - // let profile = profile.as_ref().map(|p| p.borrow().to_owned()); - // let ty = T::expected_cloud_type().to_string(); - - // match self.decide_config_source::(profile.as_ref()).await { - // ConfigDecision::LoadNew { path } => { - // let toml = tokio::fs::read_to_string(&path).await.with_context(|| format!("failed to read mapper configuration at {path}"))?; - // T::read_tedge_config(&toml, self, profile.as_deref()).map(<_>::into) - // }, - // ConfigDecision::NotFound { path } => { - // Err(anyhow!("mapper configuration file {path} doesn't exist")) - // } - // ConfigDecision::LoadLegacy => match T::expected_cloud_type() { - // CloudType::Aws => self.aws.try_get(profile.as_ref()).map(Cow::Borrowed), - // CloudType::Az => self.az.try_get(profile.as_ref()).map(Cow::Borrowed), - // CloudType::C8y => self.c8y.try_get(profile.as_ref()).map(Cow::Borrowed), - // }, - // } - // } - pub async fn mapper_config( &self, profile: &Option>, diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index 24d43fa1215..5c7d7340d4c 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -2,7 +2,6 @@ pub mod compat; use crate::models::CloudType; use crate::tedge_toml::tedge_config::cert_error_into_config_error; -use crate::tedge_toml::MapperConfigLocation; use crate::tedge_toml::ReadableKey; use crate::tedge_toml::TEdgeConfigDtoAws; use crate::tedge_toml::TEdgeConfigDtoAz; diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 95520a2af25..6b2e787f43e 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -14,13 +14,11 @@ use camino::Utf8PathBuf; use serde::Deserialize as _; use serde::Serialize; use std::path::PathBuf; +use tedge_config_macros::MultiDto; use tedge_config_macros::ProfileName; use tedge_utils::file::change_mode; -use tedge_utils::file::change_mode_sync; use tedge_utils::file::change_user_and_group; -use tedge_utils::file::change_user_and_group_sync; use tedge_utils::fs::atomically_write_file_async; -use tedge_utils::fs::atomically_write_file_sync; use tokio::fs::DirEntry; use tracing::debug; use tracing::subscriber::NoSubscriber; @@ -157,15 +155,6 @@ impl TEdgeConfigLocation { Ok(TEdgeConfig::from_dto(dto, self.clone())) } - pub(crate) fn load_sync(self) -> Result { - let dto = self.load_dto_sync::()?; - debug!( - "Loading configuration from {:?}", - self.tedge_config_file_path - ); - Ok(TEdgeConfig::from_dto(dto, self)) - } - async fn load_dto_from_toml_and_env(&self) -> Result { self.load_dto::().await } @@ -178,14 +167,6 @@ impl TEdgeConfigLocation { Ok(dto) } - fn load_dto_sync(&self) -> Result { - let (dto, warnings) = self.load_dto_with_warnings_sync::()?; - - warnings.emit(); - - Ok(dto) - } - #[cfg(feature = "test")] pub(crate) fn load_toml_str(toml: &str, location: TEdgeConfigLocation) -> TEdgeConfig { let (tedge_config, warnings) = Self::load_toml_str_with_warnings(toml, location); @@ -290,56 +271,10 @@ impl TEdgeConfigLocation { Ok((dto, warnings)) } - fn load_dto_with_warnings_sync( - &self, - ) -> Result<(TEdgeConfigDto, UnusedValueWarnings), TEdgeConfigError> { - let toml_path = self.toml_path(); - let mut tedge_toml_readable = true; - let config = std::fs::read_to_string(toml_path).unwrap_or_else(|_| { - tedge_toml_readable = false; - String::new() - }); - let toml: toml::Value = toml::de::from_str(&config)?; - let (mut dto, mut warnings) = deserialize_toml(toml, toml_path)?; - - if let Some(migrations) = dto.config.version.unwrap_or_default().migrations() { - if !tedge_toml_readable { - 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_sync(&migrated_toml)?; - - // Reload DTO to get the settings in the right place - (dto, warnings) = deserialize_toml(migrated_toml, toml_path)?; - } - } - - if Sources::INCLUDE_ENVIRONMENT { - update_with_environment_variables(&mut dto, &mut warnings)?; - } - - todo!("populate_mapper_configs_sync"); - - Ok((dto, warnings)) - } - async fn store(&self, mut config: TEdgeConfigDto) -> Result<(), TEdgeConfigError> { - self.store_cloud(&mut config.c8y.non_profile).await?; - self.store_cloud(&mut config.az.non_profile).await?; - self.store_cloud(&mut config.aws.non_profile).await?; - for profile in config.c8y.profiles.values_mut() { - self.store_cloud(profile).await?; - } - for profile in &mut config.az.profiles.values_mut() { - self.store_cloud(profile).await?; - } - for profile in &mut config.aws.profiles.values_mut() { - self.store_cloud(profile).await?; - } + self.store_cloud(&mut config.c8y).await?; + self.store_cloud(&mut config.az).await?; + self.store_cloud(&mut config.aws).await?; self.store_in(self.toml_path(), &config).await } @@ -371,38 +306,27 @@ impl TEdgeConfigLocation { Ok(()) } - async fn store_cloud( - &self, - config: &mut S, - ) -> Result<(), TEdgeConfigError> { - if let Some(toml_path) = config.get_path() { - self.store_in(toml_path, config).await?; - // Discard config that has been persisted to another location - let _ = std::mem::take(config); + async fn store_cloud(&self, cloud: &mut MultiDto) -> Result<(), TEdgeConfigError> + where + S: Serialize + HasPath + Default + PartialEq, + { + if cloud.non_profile.get_path().is_some() { + self.store_cloud_entry(&cloud.non_profile).await?; + for profile in cloud.profiles.values() { + self.store_cloud_entry(profile).await?; + } + std::mem::take(cloud); } Ok(()) } - fn store_sync(&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.tedge_config_root_path.exists() { - std::fs::create_dir(self.tedge_config_root_path())?; - } - - let toml_path = self.toml_path(); - - atomically_write_file_sync(toml_path, toml.as_bytes())?; - - if let Err(err) = change_user_and_group_sync(toml_path.as_ref(), "tedge", "tedge") { - warn!("failed to set file ownership for '{toml_path}': {err}"); - } - - if let Err(err) = change_mode_sync(toml_path.as_ref(), 0o644) { - warn!("failed to set file permissions for '{toml_path}': {err}"); + async fn store_cloud_entry( + &self, + config: &S, + ) -> Result<(), TEdgeConfigError> { + if let Some(toml_path) = config.get_path() { + self.store_in(toml_path, config).await?; } - Ok(()) } } diff --git a/crates/common/tedge_config_macros/examples/debug_skip.rs b/crates/common/tedge_config_macros/examples/debug_skip.rs deleted file mode 100644 index f25a81a5058..00000000000 --- a/crates/common/tedge_config_macros/examples/debug_skip.rs +++ /dev/null @@ -1,42 +0,0 @@ -use tedge_config_macros::*; - -#[derive(thiserror::Error, Debug)] -pub enum ReadError { - #[error(transparent)] - ConfigNotSet(#[from] ConfigNotSet), - #[error("Something went wrong: {0}")] - GenericError(String), - #[error(transparent)] - Multi(#[from] tedge_config_macros::MultiError), -} - -pub trait AppendRemoveItem { - type Item; - - fn append(current_value: Option, new_value: Self::Item) -> Option; - - fn remove(current_value: Option, remove_value: Self::Item) -> Option; -} - -impl AppendRemoveItem for T { - type Item = T; - - fn append(_current_value: Option, _new_value: Self::Item) -> Option { - unimplemented!() - } - - fn remove(_current_value: Option, _remove_value: Self::Item) -> Option { - unimplemented!() - } -} - -define_tedge_config! { - #[tedge_config(multi)] - c8y: { - #[tedge_config(reader(skip))] - #[serde(skip)] - read_from: camino::Utf8PathBuf, - }, -} - -fn main() {} diff --git a/crates/common/tedge_config_macros/impl/src/dto.rs b/crates/common/tedge_config_macros/impl/src/dto.rs index 73666575876..7855a5eeb48 100644 --- a/crates/common/tedge_config_macros/impl/src/dto.rs +++ b/crates/common/tedge_config_macros/impl/src/dto.rs @@ -97,7 +97,7 @@ pub fn generate( impl #name { // If #name is a "multi" field, we don't use this method, but it's a pain to conditionally generate it, so just ignore the warning #[allow(unused)] - fn is_default(&self) -> bool { + pub fn is_default(&self) -> bool { self == &Self::default() } } diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index ad074e002f9..38f0106e6de 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -804,7 +804,7 @@ mod tests { let cloud: Option = cloud_arg.map(<_>::try_into).transpose().unwrap(); let ttd = TempTedgeDir::new(); ttd.file("tedge.toml").with_toml_content(toml); - let config = TEdgeConfig::load_sync(ttd.path()).unwrap(); + let config = TEdgeConfig::load(ttd.path()).await.unwrap(); let id = input_id.map(|s| s.to_string()); let result = get_device_id(id, &config, &cloud).await; assert_eq!(result.unwrap().as_str(), expected); @@ -826,7 +826,7 @@ mod tests { let cloud: Option = cloud_arg.map(<_>::try_into).transpose().unwrap(); let ttd = TempTedgeDir::new(); ttd.file("tedge.toml").with_toml_content(toml); - let config = TEdgeConfig::load_sync(ttd.path()).unwrap(); + let config = TEdgeConfig::load(ttd.path()).await.unwrap(); let id = input_id.map(|s| s.to_string()); let result = get_device_id(id, &config, &cloud).await; assert!(result.is_err()); diff --git a/crates/core/tedge/src/cli/common.rs b/crates/core/tedge/src/cli/common.rs index ed10950cc86..0ac7646e317 100644 --- a/crates/core/tedge/src/cli/common.rs +++ b/crates/core/tedge/src/cli/common.rs @@ -4,6 +4,7 @@ use clap_complete::ArgValueCandidates; use clap_complete::CompletionCandidate; use std::borrow::Cow; use std::fmt; +use std::path::Path; use tedge_config::get_config_dir; use tedge_config::tedge_toml::ProfileName; use tedge_config::TEdgeConfig; @@ -214,7 +215,14 @@ impl MaybeBorrowedCloud<'_> { /// `TEDGE_CONFIGURATION_DIR` environment variable, or `/etc/tedge` if /// that is not set pub fn profile_completions() -> Vec { - let Ok(tc) = TEdgeConfig::load_sync(get_config_dir()) else { + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current() + .block_on(profile_completions_for_config_dir(get_config_dir())) + }) +} + +async fn profile_completions_for_config_dir(dir: impl AsRef) -> Vec { + let Ok(tc) = TEdgeConfig::load(dir).await else { return vec![]; }; tc.c8y_keys_str() @@ -222,12 +230,44 @@ pub fn profile_completions() -> Vec { .map(CompletionCandidate::new) .chain(tc.az_keys_str().flatten().map(CompletionCandidate::new)) .chain(tc.aws_keys_str().flatten().map(CompletionCandidate::new)) - .chain( - tc.profiled_config_directories() - .flat_map(|dir| std::fs::read_dir(dir).into_iter().flatten()) - .filter_map(|entry| entry.ok()?.file_name().into_string().ok()) - .filter_map(|s| Some(s.strip_suffix(".toml")?.to_owned())) - .map(CompletionCandidate::new), - ) .collect() } + +#[cfg(test)] +mod tests { + use tedge_test_utils::fs::TempTedgeDir; + + use crate::cli::common::profile_completions_for_config_dir; + + #[tokio::test] + async fn profile_completions_include_tedge_toml_profile_names() { + let ttd = TempTedgeDir::new(); + ttd.file("tedge.toml") + .with_raw_content("[c8y.profiles.something]\n[c8y.profiles.other]"); + let completions = completion_names(&ttd).await; + assert_eq!(completions, ["other", "something"]); + } + + #[tokio::test] + async fn profile_completions_include_separate_mapper_config_profile_names() { + let ttd = TempTedgeDir::new(); + let c8y_profiles = ttd.dir("mappers").dir("c8y.d"); + c8y_profiles.file("profile1.toml").with_raw_content(""); + c8y_profiles.file("profile2.toml").with_raw_content(""); + let completions = completion_names(&ttd).await; + assert_eq!(completions, ["profile1", "profile2"]); + } + + /// Generates profile completions for the provided config dir, extracts just + /// the completion text portion, and sorts them to ensure the order is + /// stable + async fn completion_names(ttd: &TempTedgeDir) -> Vec { + let completions = profile_completions_for_config_dir(ttd.path()).await; + let mut completions = completions + .iter() + .map(|candidate| candidate.get_value().to_str().unwrap().to_owned()) + .collect::>(); + completions.sort(); + completions + } +} diff --git a/crates/core/tedge/tests/main.rs b/crates/core/tedge/tests/main.rs index 78fca31bd72..7f49efce4e5 100644 --- a/crates/core/tedge/tests/main.rs +++ b/crates/core/tedge/tests/main.rs @@ -403,63 +403,6 @@ mod tests { assert!(output_str.contains("Example")); } - // Tests for restrict_cloud_config_update behavior - #[test_case("set", "c8y.url", "new.example.com", "c8y", None; "set default c8y")] - #[test_case("unset", "c8y.url", "", "c8y", None; "unset default c8y")] - #[test_case("add", "c8y.smartrest.templates", "template1", "c8y", None; "add default c8y")] - #[test_case("remove", "c8y.smartrest.templates", "template1", "c8y", None; "remove default c8y")] - #[test_case("set", "az.url", "new.example.com", "az", None; "set default az")] - #[test_case("unset", "az.url", "", "az", None; "unset default az")] - #[test_case("set", "aws.url", "new.example.com", "aws", None; "set default aws")] - #[test_case("unset", "aws.url", "", "aws", None; "unset default aws")] - #[test_case("set", "c8y.profiles.prod.url", "new.example.com", "c8y", Some("prod"); "set profile c8y")] - #[test_case("set", "az.profiles.prod.url", "new.example.com", "az", Some("prod"); "set profile az")] - #[test_case("set", "aws.profiles.prod.url", "new.example.com", "aws", Some("prod"); "set profile aws")] - fn migrated_config_blocks_mutation_commands( - cmd: &str, - config_key: &str, - value: &str, - cloud: &str, - profile: Option<&str>, - ) -> Result<(), Box> { - let temp_dir = tempfile::tempdir()?; - - // Setup migrated config for the specific cloud being tested - match profile { - None => setup_migrated_default(&temp_dir, cloud), - Some(p) => setup_migrated_profile(&temp_dir, cloud, p), - } - - let test_home = temp_dir.path().to_str().unwrap(); - - // Build expected path in error message - let expected_path = match profile { - None => format!("{}/mappers/{}.toml", test_home, cloud), - Some(p) => format!("{}/mappers/{}.d/{}.toml", test_home, cloud, p), - }; - - // Attempt the command - let mut args = vec!["--config-dir", test_home, "config", cmd, config_key]; - if !value.is_empty() { - args.push(value); - } - - let mut command = tedge_command_with_test_home(args)?; - - // Verify it fails with appropriate error - command - .assert() - .failure() - .stderr(predicate::str::contains(format!("tedge config {cmd}"))) - .stderr(predicate::str::contains(format!( - "cannot be used to update {cloud} mapper config" - ))) - .stderr(predicate::str::contains(&expected_path)) - .stderr(predicate::str::contains(config_key)); - - Ok(()) - } - #[test_case("set", "c8y.url", "new.example.com", "c8y"; "set c8y")] #[test_case("unset", "c8y.url", "", "c8y"; "unset c8y")] #[test_case("add", "c8y.smartrest.templates", "template1", "c8y"; "add c8y")] @@ -535,153 +478,108 @@ mod tests { Ok(()) } - #[test] - fn c8y_migration_does_not_affect_az_and_aws() -> Result<(), Box> { + #[test_case::test_case("aws")] + #[test_case::test_case("c8y")] + #[test_case::test_case("az")] + fn migrated_configs_are_written_to_by_tedge_config_set( + cloud: &str, + ) -> Result<(), Box> { let temp_dir = tempfile::tempdir()?; - // Setup: c8y migrated, az and aws in legacy - setup_migrated_default(&temp_dir, "c8y"); - let tedge_toml = temp_dir.path().join("tedge.toml"); - std::fs::write( - &tedge_toml, - "[az]\nurl = \"az.example.com\"\n[aws]\nurl = \"aws.example.com\"\n", - )?; - + setup_migrated_default(&temp_dir, cloud); let test_home = temp_dir.path().to_str().unwrap(); - // c8y set should fail - let mut set_c8y_cmd = tedge_command_with_test_home([ + let mut set_config_cmd = tedge_command_with_test_home([ "--config-dir", test_home, "config", "set", - "c8y.url", + &format!("{cloud}.url"), "new.example.com", ])?; - set_c8y_cmd.assert().failure(); + set_config_cmd.assert().success(); - // az set should succeed - let mut set_az_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "az.url", - "new.az.example.com", - ])?; - set_az_cmd.assert().success(); - - // aws set should succeed - let mut set_aws_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "aws.url", - "new.aws.example.com", - ])?; - set_aws_cmd.assert().success(); + let migrated_toml = + std::fs::read_to_string(temp_dir.path().join(format!("mappers/{cloud}.toml")))?; + assert_eq!(migrated_toml.trim(), "url = \"new.example.com\""); + // tedge.toml will be created when `tedge config set` runs + let tedge_toml = std::fs::read_to_string(temp_dir.path().join("tedge.toml"))?; + assert_eq!(tedge_toml.trim(), ""); Ok(()) } - #[test] - fn az_migration_does_not_affect_c8y_and_aws() -> Result<(), Box> { + #[test_case::test_case("aws")] + #[test_case::test_case("c8y")] + #[test_case::test_case("az")] + fn migrated_profiled_configs_can_be_created_by_tedge_config_set( + cloud: &str, + ) -> Result<(), Box> { let temp_dir = tempfile::tempdir()?; - // Setup: az migrated, c8y and aws in legacy - setup_migrated_default(&temp_dir, "az"); - let tedge_toml = temp_dir.path().join("tedge.toml"); - std::fs::write( - &tedge_toml, - "[c8y]\nurl = \"c8y.example.com\"\n[aws]\nurl = \"aws.example.com\"\n", - )?; - + setup_migrated_default(&temp_dir, cloud); let test_home = temp_dir.path().to_str().unwrap(); - // az set should fail - let mut set_az_cmd = tedge_command_with_test_home([ + let mut set_config_command = tedge_command_with_test_home([ "--config-dir", test_home, "config", "set", - "az.url", + &format!("{cloud}.url"), "new.example.com", + "--profile", + "profile", ])?; - set_az_cmd.assert().failure(); - - // c8y set should succeed - let mut set_c8y_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "c8y.url", - "new.c8y.example.com", - ])?; - set_c8y_cmd.assert().success(); + set_config_command.assert().success(); - // aws set should succeed - let mut set_aws_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "aws.url", - "new.aws.example.com", - ])?; - set_aws_cmd.assert().success(); + let migrated_toml = std::fs::read_to_string( + temp_dir + .path() + .join(format!("mappers/{cloud}.d/profile.toml")), + ) + .unwrap(); + assert_eq!(migrated_toml.trim(), "url = \"new.example.com\""); + // tedge.toml will be created when `tedge config set` runs + let tedge_toml = std::fs::read_to_string(temp_dir.path().join("tedge.toml"))?; + assert_eq!(tedge_toml.trim(), ""); Ok(()) } - #[test] - fn aws_migration_does_not_affect_c8y_and_az() -> Result<(), Box> { + #[test_case::test_case("aws")] + #[test_case::test_case("c8y")] + #[test_case::test_case("az")] + fn migrated_profiled_configs_can_be_updated_by_tedge_config_set( + cloud: &str, + ) -> Result<(), Box> { let temp_dir = tempfile::tempdir()?; - // Setup: aws migrated, c8y and az in legacy - setup_migrated_default(&temp_dir, "aws"); - let tedge_toml = temp_dir.path().join("tedge.toml"); - std::fs::write( - &tedge_toml, - "[c8y]\nurl = \"c8y.example.com\"\n[az]\nurl = \"az.example.com\"\n", - )?; - + setup_migrated_profile(&temp_dir, cloud, "profile"); let test_home = temp_dir.path().to_str().unwrap(); - // aws set should fail - let mut set_aws_cmd = tedge_command_with_test_home([ + let mut set_config_command = tedge_command_with_test_home([ "--config-dir", test_home, "config", "set", - "aws.url", + &format!("{cloud}.url"), "new.example.com", + "--profile", + "profile", ])?; - set_aws_cmd.assert().failure(); - - // c8y set should succeed - let mut set_c8y_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "c8y.url", - "new.c8y.example.com", - ])?; - set_c8y_cmd.assert().success(); + set_config_command.assert().success(); - // az set should succeed - let mut set_az_cmd = tedge_command_with_test_home([ - "--config-dir", - test_home, - "config", - "set", - "az.url", - "new.az.example.com", - ])?; - set_az_cmd.assert().success(); + let migrated_toml = std::fs::read_to_string( + temp_dir + .path() + .join(format!("mappers/{cloud}.d/profile.toml")), + ) + .unwrap(); + assert_eq!(migrated_toml.trim(), "url = \"new.example.com\""); + // tedge.toml will be created when `tedge config set` runs + let tedge_toml = std::fs::read_to_string(temp_dir.path().join("tedge.toml"))?; + assert_eq!(tedge_toml.trim(), ""); Ok(()) } diff --git a/plugins/tedge_file_log_plugin/src/main.rs b/plugins/tedge_file_log_plugin/src/main.rs index e5fc5fdeeaf..81fe30add71 100644 --- a/plugins/tedge_file_log_plugin/src/main.rs +++ b/plugins/tedge_file_log_plugin/src/main.rs @@ -3,10 +3,11 @@ use tedge_config::TEdgeConfig; use tedge_file_log_plugin::bin::FileLogCli; use tedge_file_log_plugin::bin::TEdgeConfigView; -fn main() -> anyhow::Result<()> { +#[tokio::main] +async fn main() -> anyhow::Result<()> { let cli = FileLogCli::parse(); - let tedge_config = TEdgeConfig::load_sync(&cli.common.config_dir)?; + let tedge_config = TEdgeConfig::load(&cli.common.config_dir).await?; let tmp_dir = tedge_config.tmp.path.as_path(); let view = TEdgeConfigView::new(tmp_dir); From df024af0a165985bd2d5447463b28f20c1c07db4 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 12 Dec 2025 10:58:49 +0000 Subject: [PATCH 07/10] Fix failing unit tests Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 20 +++++++++++-------- .../src/tedge_toml/tedge_config_location.rs | 11 ++++++++-- .../tedge_config_macros/impl/src/dto.rs | 2 +- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index 0d1e9fa70e4..306c2b07184 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -114,7 +114,16 @@ async fn read_file_if_exists(path: &Utf8Path) -> anyhow::Result> match tokio::fs::read_to_string(path).await { Ok(contents) => Ok(Some(contents)), Err(e) if e.kind() == ErrorKind::NotFound => Ok(None), - Err(e) => Err(e).context(format!("failed to read mapper configuration from {path}")), + Err(e) => { + let dir = path.parent().unwrap(); + // If the error is actually with the mappers directory as a whole, + // feed that back to the user + if let Err(dir_error) = tokio::fs::read_dir(dir).await { + Err(dir_error).context(format!("failed to read {dir}")) + } else { + Err(e).context(format!("failed to read mapper configuration from {path}")) + } + }, } } @@ -2119,16 +2128,11 @@ mod tests { c8y.url = "from.tedge.toml" }); - let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); - let error = tedge_config - .mapper_config::(&profile_name(None)) - .await - .err() - .unwrap(); + let error = TEdgeConfig::load(ttd.path()).await.err().unwrap(); assert_eq!( format!("{error:#}"), format!( - "reading {}/mappers: Permission denied (os error 13)", + "failed to read {}/mappers: Permission denied (os error 13)", ttd.path().display() ) ); diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 6b2e787f43e..1a25c3ab75d 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -310,9 +310,16 @@ impl TEdgeConfigLocation { where S: Serialize + HasPath + Default + PartialEq, { - if cloud.non_profile.get_path().is_some() { + if let Some(default_profile_path) = cloud.non_profile.get_path() { self.store_cloud_entry(&cloud.non_profile).await?; - for profile in cloud.profiles.values() { + for (name, profile) in &mut cloud.profiles { + if profile.get_path().is_none() { + let Some((base_path, "toml")) = default_profile_path.as_str().rsplit_once(".") else { + panic!("{default_profile_path} should end in .toml") + }; + // TODO this feels like a hacky way to achieve this + profile.set_path(format!("{base_path}.d/{name}.toml").into()); + } self.store_cloud_entry(profile).await?; } std::mem::take(cloud); diff --git a/crates/common/tedge_config_macros/impl/src/dto.rs b/crates/common/tedge_config_macros/impl/src/dto.rs index 7855a5eeb48..73666575876 100644 --- a/crates/common/tedge_config_macros/impl/src/dto.rs +++ b/crates/common/tedge_config_macros/impl/src/dto.rs @@ -97,7 +97,7 @@ pub fn generate( impl #name { // If #name is a "multi" field, we don't use this method, but it's a pain to conditionally generate it, so just ignore the warning #[allow(unused)] - pub fn is_default(&self) -> bool { + fn is_default(&self) -> bool { self == &Self::default() } } From 19f2ddc929f9af713faf7fd3c24cf47c059efb61 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 12 Dec 2025 11:33:41 +0000 Subject: [PATCH 08/10] Properly fix creation of new profiled configs with `tedge config set` Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 12 ++-- .../tedge_config/mapper_config/mod.rs | 55 ++++++++++++++----- .../src/tedge_toml/tedge_config_location.rs | 24 ++------ 3 files changed, 51 insertions(+), 40 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index 306c2b07184..a053d57379c 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -123,7 +123,7 @@ async fn read_file_if_exists(path: &Utf8Path) -> anyhow::Result> } else { Err(e).context(format!("failed to read mapper configuration from {path}")) } - }, + } } } @@ -150,7 +150,7 @@ impl TEdgeConfigDto { }) }, )?; - default_profile_config.set_path(toml_path); + default_profile_config.set_mapper_config_dir(mappers_dir.clone()); dto.non_profile = default_profile_config; dto.profiles = profiles @@ -160,7 +160,7 @@ impl TEdgeConfigDto { let profile_toml = tokio::fs::read_to_string(&toml_path).await?; let mut profiled_config: T::CloudDto = toml::from_str(&profile_toml) .context("failed to deserialise mapper config")?; - profiled_config.set_path(toml_path); + profiled_config.set_mapper_config_dir(mappers_dir.clone()); Ok::<_, anyhow::Error>((profile, profiled_config)) }) .try_collect() @@ -736,7 +736,7 @@ define_tedge_config! { c8y: { #[tedge_config(reader(skip))] #[serde(skip)] - read_from: Utf8PathBuf, + mapper_config_dir: Utf8PathBuf, /// Endpoint URL of Cumulocity tenant #[tedge_config(example = "your-tenant.cumulocity.com")] @@ -980,7 +980,7 @@ define_tedge_config! { az: { #[tedge_config(reader(skip))] #[serde(skip)] - read_from: Utf8PathBuf, + mapper_config_dir: Utf8PathBuf, /// Endpoint URL of Azure IoT tenant #[tedge_config(example = "myazure.azure-devices.net")] @@ -1071,7 +1071,7 @@ define_tedge_config! { aws: { #[tedge_config(reader(skip))] #[serde(skip)] - read_from: Utf8PathBuf, + mapper_config_dir: Utf8PathBuf, /// Endpoint URL of AWS IoT tenant #[tedge_config(example = "your-endpoint.amazonaws.com")] diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index 5c7d7340d4c..0115f3270c6 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -108,38 +108,65 @@ pub trait SpecialisedCloudConfig: ) -> Self::CloudConfigReader; } +#[derive(Clone, Copy, Debug)] +pub struct MapperConfigPath<'a> { + base_dir: &'a Utf8Path, + cloud_type: CloudType, +} + +impl MapperConfigPath<'_> { + pub fn path_for(&self, profile: Option<&ProfileName>) -> Utf8PathBuf { + let dir = self.base_dir; + let ty = self.cloud_type; + match profile { + None => dir.join(format!("{ty}.toml")), + Some(profile) => dir.join(format!("{ty}.d/{profile}.toml")), + } + .into() + } +} + pub trait HasPath { - fn set_path(&mut self, path: Utf8PathBuf); - fn get_path(&self) -> Option<&Utf8Path>; + fn set_mapper_config_dir(&mut self, path: Utf8PathBuf); + fn config_path(&self) -> Option>; } impl HasPath for TEdgeConfigDtoC8y { - fn set_path(&mut self, path: Utf8PathBuf) { - self.read_from = Some(path) + fn set_mapper_config_dir(&mut self, path: Utf8PathBuf) { + self.mapper_config_dir = Some(path) } - fn get_path(&self) -> Option<&Utf8Path> { - self.read_from.as_deref() + fn config_path(&self) -> Option> { + Some(MapperConfigPath { + base_dir: self.mapper_config_dir.as_deref()?, + cloud_type: CloudType::C8y, + }) } } impl HasPath for TEdgeConfigDtoAz { - fn set_path(&mut self, path: Utf8PathBuf) { - self.read_from = Some(path) + fn set_mapper_config_dir(&mut self, path: Utf8PathBuf) { + self.mapper_config_dir = Some(path) } - fn get_path(&self) -> Option<&Utf8Path> { - self.read_from.as_deref() + fn config_path(&self) -> Option> { + Some(MapperConfigPath { + base_dir: self.mapper_config_dir.as_deref()?, + cloud_type: CloudType::Az, + }) } } impl HasPath for TEdgeConfigDtoAws { - fn set_path(&mut self, path: Utf8PathBuf) { - self.read_from = Some(path) + fn set_mapper_config_dir(&mut self, path: Utf8PathBuf) { + self.mapper_config_dir = Some(path) } - fn get_path(&self) -> Option<&Utf8Path> { - self.read_from.as_deref() + fn config_path(&self) -> Option> { + Some(MapperConfigPath { + base_dir: self.mapper_config_dir.as_deref()?, + cloud_type: CloudType::Aws, + }) } } diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 1a25c3ab75d..639d8bb575e 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -310,32 +310,16 @@ impl TEdgeConfigLocation { where S: Serialize + HasPath + Default + PartialEq, { - if let Some(default_profile_path) = cloud.non_profile.get_path() { - self.store_cloud_entry(&cloud.non_profile).await?; + if let Some(paths) = cloud.non_profile.config_path() { + self.store_in(&paths.path_for(None), &cloud.non_profile) + .await?; for (name, profile) in &mut cloud.profiles { - if profile.get_path().is_none() { - let Some((base_path, "toml")) = default_profile_path.as_str().rsplit_once(".") else { - panic!("{default_profile_path} should end in .toml") - }; - // TODO this feels like a hacky way to achieve this - profile.set_path(format!("{base_path}.d/{name}.toml").into()); - } - self.store_cloud_entry(profile).await?; + self.store_in(&paths.path_for(Some(name)), profile).await?; } std::mem::take(cloud); } Ok(()) } - - async fn store_cloud_entry( - &self, - config: &S, - ) -> Result<(), TEdgeConfigError> { - if let Some(toml_path) = config.get_path() { - self.store_in(toml_path, config).await?; - } - Ok(()) - } } pub trait ConfigSources { From 499c20d0cb941dc2438f83d926c29ad7ae678769 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 12 Dec 2025 15:05:42 +0000 Subject: [PATCH 09/10] Simplify code and ensure mapper config is read only once Signed-off-by: James Rhodes --- .../src/tedge_toml/tedge_config.rs | 237 ++-------- .../tedge_config/mapper_config/compat.rs | 31 -- .../tedge_config/mapper_config/mod.rs | 434 ------------------ .../src/tedge_toml/tedge_config_location.rs | 9 +- .../tedge_config/tests/mapper_config.rs | 216 --------- crates/core/tedge/src/cli/certificate/cli.rs | 62 +-- crates/core/tedge/src/cli/connect/aws.rs | 4 +- crates/core/tedge/src/cli/connect/azure.rs | 4 +- crates/core/tedge/src/cli/connect/c8y.rs | 4 +- crates/core/tedge/src/cli/connect/command.rs | 43 +- crates/core/tedge/src/cli/http/cli.rs | 5 +- crates/core/tedge/src/cli/upload/mod.rs | 2 +- crates/core/tedge_mapper/src/aws/mapper.rs | 6 +- crates/core/tedge_mapper/src/az/mapper.rs | 6 +- crates/core/tedge_mapper/src/c8y/mapper.rs | 2 +- .../tedge_mqtt_bridge/src/config.rs | 3 +- plugins/c8y_firmware_plugin/src/lib.rs | 2 +- plugins/c8y_remote_access_plugin/src/lib.rs | 5 +- 18 files changed, 101 insertions(+), 974 deletions(-) diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index a053d57379c..d9bb69810a8 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -49,14 +49,12 @@ use certificate::CloudHttpConfig; use certificate::PemCertificate; use doku::Document; use futures::StreamExt; -use mapper_config::load_mapper_config; use mapper_config::ExpectedCloudType; use mapper_config::MapperConfig; use once_cell::sync::Lazy; use reqwest::Certificate; use std::borrow::Borrow; use std::borrow::Cow; -use std::collections::HashMap; use std::io::ErrorKind; use std::io::Read; use std::iter::Iterator; @@ -93,13 +91,10 @@ impl OptionalConfigError for OptionalConfig { } } -type AnyMap = anymap3::Map; - pub struct TEdgeConfig { dto: TEdgeConfigDto, reader: TEdgeConfigReader, location: TEdgeConfigLocation, - cached_mapper_configs: Arc>, } impl std::ops::Deref for TEdgeConfig { @@ -188,7 +183,6 @@ impl TEdgeConfig { reader: TEdgeConfigReader::from_dto(&dto, &location), dto, location, - cached_mapper_configs: <_>::default(), } } @@ -207,113 +201,35 @@ impl TEdgeConfig { self.location.tedge_config_root_path() } - pub async fn c8y_mapper_config( + pub fn c8y_mapper_config( &self, profile: &Option>, - ) -> anyhow::Result>> { - self.mapper_config(profile).await + ) -> anyhow::Result> { + self.mapper_config(profile) } - pub async fn az_mapper_config( + pub fn az_mapper_config( &self, profile: &Option>, - ) -> anyhow::Result>> { - self.mapper_config(profile).await + ) -> anyhow::Result> { + self.mapper_config(profile) } - pub async fn aws_mapper_config( + pub fn aws_mapper_config( &self, profile: &Option>, - ) -> anyhow::Result>> { - self.mapper_config(profile).await + ) -> anyhow::Result> { + self.mapper_config(profile) } - pub async fn mapper_config( + pub fn mapper_config( &self, profile: &Option>, - ) -> anyhow::Result>> { + ) -> anyhow::Result> { let profile = profile.as_ref().map(|p| p.borrow().to_owned()); - let ty = T::expected_cloud_type().to_string(); - - match self - .location - .decide_config_source::(profile.as_ref()) - .await - { - ConfigDecision::LoadNew { path } => { - // Check for config conflict: both new format and legacy exist - if self.has_legacy_config::(profile.as_ref()) { - let legacy_key = match profile.as_ref() { - None => ty, - Some(p) => format!("{ty}.profiles.{p}"), - }; - tracing::warn!( - "Both {path} and tedge.toml [{legacy_key}] exist. Using {path}. \ - Consider removing the [{legacy_key}] section from tedge.toml." - ); - } - - let mut cached_configs = self.cached_mapper_configs.lock().await; - let configs_for_cloud = cached_configs - .entry::, Arc>>>() - .or_default(); - - if let Some(cached_config) = configs_for_cloud.get(&profile) { - Ok(cached_config.clone()) - } else { - let map = load_mapper_config::( - &AbsolutePath::try_new(path.as_str()).unwrap(), - self, - profile.as_deref(), - ) - .await?; - - configs_for_cloud.insert(profile.clone(), Arc::new(map)); - - Ok(configs_for_cloud.get(&profile).unwrap().clone()) - } - } - ConfigDecision::NotFound { path } => { - Err(anyhow!("mapper configuration file {path} doesn't exist")) - } - ConfigDecision::LoadLegacy => Ok(Arc::new( - mapper_config::compat::load_cloud_mapper_config(profile.as_deref(), self)?, - )), - ConfigDecision::PermissionError { - mapper_config_dir, - error, - } => Err(error).with_context(|| format!("reading {mapper_config_dir}")), - } - } - /// Check if a legacy tedge.toml configuration exists for the given cloud - /// type and profile by checking if the URL is configured - /// - /// We have to check if URL is configured since the default profile (aka - /// `profile = None`) is always defined, regardless of whether `tedge.toml` - /// configures anything. - fn has_legacy_config(&self, profile: Option<&ProfileName>) -> bool - where - T: ExpectedCloudType, - { - match T::expected_cloud_type() { - CloudType::C8y => self - .c8y - .try_get(profile) - .ok() - .and_then(|c| c.url.or_none()) - .is_some(), - CloudType::Az => self - .az - .try_get(profile) - .ok() - .and_then(|c| c.url.or_none()) - .is_some(), - CloudType::Aws => self - .aws - .try_get(profile) - .ok() - .and_then(|c| c.url.or_none()) - .is_some(), - } + Ok(mapper_config::compat::load_cloud_mapper_config( + profile.as_deref(), + self, + )?) } pub fn profiled_config_directories(&self) -> impl Iterator + use<'_> { @@ -343,7 +259,8 @@ impl TEdgeConfig { } } - pub async fn all_mapper_configs(&self) -> Vec<(Arc>, Option)> + // TODO handle this properly with cli connect + pub async fn all_mapper_configs(&self) -> Vec<(MapperConfig, Option)> where T: SpecialisedCloudConfig, { @@ -351,7 +268,7 @@ impl TEdgeConfig { let mut generalised_profiles = self.all_profiles::().await; let mut configs = Vec::new(); while let Some(profile) = generalised_profiles.next().await { - if let Ok(config) = self.mapper_config(&profile).await { + if let Ok(config) = self.mapper_config(&profile) { if config.configured_url().or_none().is_some() { configs.push((config, profile)); } @@ -361,116 +278,59 @@ impl TEdgeConfig { configs } - pub async fn as_cloud_config( + pub fn as_cloud_config( &self, cloud: Cloud<'_>, - ) -> Result, MultiError> { + ) -> Result, MultiError> { Ok(match cloud { - Cloud::C8y(profile) => self - .c8y_mapper_config(&profile) - .await - .map(|config| DynCloudConfig::Arc(config as Arc<_>))?, - Cloud::Az(profile) => self - .az_mapper_config(&profile) - .await - .map(|config| DynCloudConfig::Arc(config as Arc<_>))?, - Cloud::Aws(profile) => self - .aws_mapper_config(&profile) - .await - .map(|config| DynCloudConfig::Arc(config as Arc<_>))?, + Cloud::C8y(profile) => self.c8y_mapper_config(&profile).map(Box::new)?, + Cloud::Az(profile) => self.az_mapper_config(&profile).map(Box::new)?, + Cloud::Aws(profile) => self.aws_mapper_config(&profile).map(Box::new)?, }) } - pub async fn device_id<'a>( - &self, - cloud: Option>>, - ) -> Result { + pub fn device_id<'a>(&self, cloud: Option>>) -> Result { Ok(match cloud.map(<_>::into) { None => self.device.id()?.to_owned(), - Some(Cloud::C8y(profile)) => self.c8y_mapper_config(&profile).await?.device.id()?, - Some(Cloud::Az(profile)) => self.az_mapper_config(&profile).await?.device.id()?, - Some(Cloud::Aws(profile)) => self.aws_mapper_config(&profile).await?.device.id()?, + Some(Cloud::C8y(profile)) => self.c8y_mapper_config(&profile)?.device.id()?, + Some(Cloud::Az(profile)) => self.az_mapper_config(&profile)?.device.id()?, + Some(Cloud::Aws(profile)) => self.aws_mapper_config(&profile)?.device.id()?, }) } - pub async fn device_key_path<'a>( + pub fn device_key_path<'a>( &self, cloud: Option>>, ) -> Result { Ok(match cloud.map(<_>::into) { None => self.device.key_path.clone(), - Some(Cloud::C8y(profile)) => self - .c8y_mapper_config(&profile) - .await? - .device - .key_path - .clone(), - Some(Cloud::Az(profile)) => self - .az_mapper_config(&profile) - .await? - .device - .key_path - .clone(), - Some(Cloud::Aws(profile)) => self - .aws_mapper_config(&profile) - .await? - .device - .key_path - .clone(), + Some(Cloud::C8y(profile)) => self.c8y_mapper_config(&profile)?.device.key_path.clone(), + Some(Cloud::Az(profile)) => self.az_mapper_config(&profile)?.device.key_path.clone(), + Some(Cloud::Aws(profile)) => self.aws_mapper_config(&profile)?.device.key_path.clone(), }) } - pub async fn device_cert_path<'a>( + pub fn device_cert_path<'a>( &self, cloud: Option>>, ) -> Result { Ok(match cloud.map(<_>::into) { None => self.device.cert_path.clone(), - Some(Cloud::C8y(profile)) => self - .c8y_mapper_config(&profile) - .await? - .device - .cert_path - .clone(), - Some(Cloud::Az(profile)) => self - .az_mapper_config(&profile) - .await? - .device - .cert_path - .clone(), - Some(Cloud::Aws(profile)) => self - .aws_mapper_config(&profile) - .await? - .device - .cert_path - .clone(), + Some(Cloud::C8y(profile)) => self.c8y_mapper_config(&profile)?.device.cert_path.clone(), + Some(Cloud::Az(profile)) => self.az_mapper_config(&profile)?.device.cert_path.clone(), + Some(Cloud::Aws(profile)) => self.aws_mapper_config(&profile)?.device.cert_path.clone(), }) } - pub async fn device_csr_path<'a>( + pub fn device_csr_path<'a>( &self, cloud: Option>>, ) -> Result { Ok(match cloud.map(<_>::into) { None => self.device.csr_path.clone(), - Some(Cloud::C8y(profile)) => self - .c8y_mapper_config(&profile) - .await? - .device - .csr_path - .clone(), - Some(Cloud::Az(profile)) => self - .az_mapper_config(&profile) - .await? - .device - .csr_path - .clone(), - Some(Cloud::Aws(profile)) => self - .aws_mapper_config(&profile) - .await? - .device - .csr_path - .clone(), + Some(Cloud::C8y(profile)) => self.c8y_mapper_config(&profile)?.device.csr_path.clone(), + Some(Cloud::Az(profile)) => self.az_mapper_config(&profile)?.device.csr_path.clone(), + Some(Cloud::Aws(profile)) => self.aws_mapper_config(&profile)?.device.csr_path.clone(), }) } @@ -530,7 +390,7 @@ impl TEdgeConfig { /// Note: This does not stream the certificates as they are read from disk. It /// simply reads all the certificates, then returns them as a stream. fn stream_trust_store( - mapper_config: Arc>, + mapper_config: MapperConfig, ) -> impl Stream + Send { futures::stream::once(async move { read_trust_store(&mapper_config.root_cert_path) @@ -1988,7 +1848,6 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let mapper_config = tedge_config .mapper_config::(&profile_name(None)) - .await .unwrap(); assert_eq!( mapper_config.http().or_none().unwrap().host().to_string(), @@ -2012,7 +1871,6 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let mapper_config = tedge_config .mapper_config::(&profile_name(Some("myprofile"))) - .await .unwrap(); assert_eq!( mapper_config.http().or_none().unwrap().host().to_string(), @@ -2038,10 +1896,8 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let res = tedge_config - .mapper_config::(&profile_name(Some("non-existent-profile"))) - .await; - assert_error_contains!(res, &ttd.dir("mappers/c8y.d").path().display().to_string()); - assert_error_contains!(res, "doesn't exist"); + .mapper_config::(&profile_name(Some("non-existent-profile"))); + assert_error_contains!(res, "C8y profile 'non-existent-profile' not found"); } #[tokio::test] @@ -2060,10 +1916,8 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let res = tedge_config - .mapper_config::(&profile_name(Some("non-existent-profile"))) - .await; - assert_error_contains!(res, &ttd.dir("mappers/c8y.d").path().display().to_string()); - assert_error_contains!(res, "doesn't exist"); + .mapper_config::(&profile_name(Some("non-existent-profile"))); + assert_error_contains!(res, "C8y profile 'non-existent-profile' not found"); } #[tokio::test] @@ -2076,7 +1930,6 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let config = tedge_config .mapper_config::(&profile_name(Some("myprofile"))) - .await .unwrap(); assert_eq!( config.http().or_none().unwrap().host().to_string(), @@ -2094,7 +1947,6 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let config = tedge_config .mapper_config::(&profile_name(None)) - .await .unwrap(); assert_eq!( config.http().or_none().unwrap().host().to_string(), @@ -2114,7 +1966,6 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let config = tedge_config .mapper_config::(&profile_name(None)) - .await .unwrap(); assert_eq!(config.url().or_none().unwrap().input, "az.url"); } diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs index 991085904b2..a2aea2bd09f 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/compat.rs @@ -3,39 +3,8 @@ use crate::tedge_toml::tedge_config::TEdgeConfigReaderAws; use crate::tedge_toml::tedge_config::TEdgeConfigReaderAz; use crate::tedge_toml::tedge_config::TEdgeConfigReaderC8y; use crate::tedge_toml::ReadableKey; -use crate::tedge_toml::WritableKey; use crate::TEdgeConfig; -pub trait IsCloudConfig { - fn cloud_type_for(&self) -> Option<(CloudType, Option)>; -} - -impl IsCloudConfig for WritableKey { - fn cloud_type_for(&self) -> Option<(CloudType, Option)> { - cloud_type_for(self.to_cow_str()) - } -} - -impl IsCloudConfig for ReadableKey { - fn cloud_type_for(&self) -> Option<(CloudType, Option)> { - cloud_type_for(self.to_cow_str()) - } -} - -fn cloud_type_for(key: Cow<'static, str>) -> Option<(CloudType, Option)> { - match key.split_once(".") { - Some(("c8y", rest)) => Some((CloudType::C8y, extract_profile_name(rest))), - Some(("az", rest)) => Some((CloudType::Az, extract_profile_name(rest))), - Some(("aws", rest)) => Some((CloudType::Aws, extract_profile_name(rest))), - _ => None, - } -} - -fn extract_profile_name(partial_config_key: &str) -> Option { - let partial_config_key = partial_config_key.strip_prefix("profiles.")?; - let (profile, _rest) = partial_config_key.split_once(".")?; - Some(profile.parse().unwrap()) -} /// Trait for creating cloud-specific mapper configuration from tedge.toml cloud sections /// /// This trait enables backward compatibility by loading the new `MapperConfig` format diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs index 0115f3270c6..2a5bd34decd 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config/mapper_config/mod.rs @@ -580,44 +580,6 @@ impl From for MapperConfigError { } } -/// Load and populate a mapper configuration from an external TOML file -/// -/// This function reads a mapper configuration file and applies defaults from -/// the root tedge configuration for any missing common fields (device, bridge, etc.). -/// -/// # Arguments -/// * `config_path` - Path to the external mapper configuration TOML file -/// * `tedge_config` - Root tedge configuration reader for default values -/// -/// # Returns -/// * `Ok(MapperConfig)` - Fully populated mapper configuration -/// * `Err(MapperConfigError)` - If file cannot be read, parsed, or required fields are missing -/// ``` -pub(crate) async fn load_mapper_config( - config_path: &AbsolutePath, - tedge_config: &TEdgeConfig, - profile: Option<&str>, -) -> Result, MapperConfigError> -where - T: SpecialisedCloudConfig, -{ - let toml_content = tokio::fs::read_to_string(config_path.as_std_path()).await?; - load_mapper_config_from_string(&toml_content, tedge_config, profile) -} - -fn load_mapper_config_from_string( - toml_content: &str, - tedge_config: &TEdgeConfig, - profile: Option<&str>, -) -> Result, MapperConfigError> -where - T: SpecialisedCloudConfig, -{ - let cloud_dto: T::CloudDto = toml::from_str(toml_content)?; - let cloud_reader = T::into_config_reader(cloud_dto, tedge_config, profile); - compat::build_mapper_config(cloud_reader, profile) -} - pub trait ExpectedCloudType { fn expected_cloud_type() -> CloudType; } @@ -702,399 +664,3 @@ impl MapperConfig { &self.cloud_specific.http } } - -#[cfg(test)] -mod tests { - use crate::TEdgeConfigDto; - use crate::TEdgeConfigLocation; - - use super::*; - - #[test] - fn empty_file_deserializes_with_all_defaults() { - let config = deserialize_from_str::("").unwrap(); - - // Verify all defaults are applied - assert_eq!(config.cloud_specific.auth_method, AuthMethod::Certificate); - assert!(config.cloud_specific.smartrest.use_operation_id); - assert!(config.cloud_specific.entity_store.auto_register); - assert!(config.cloud_specific.entity_store.clean_start); - assert_eq!( - config.cloud_specific.software_management.api, - SoftwareManagementApiFlag::Legacy - ); - assert!(!config.cloud_specific.software_management.with_types); - assert_eq!( - config.cloud_specific.operations.auto_log_upload, - AutoLogUpload::OnFailure - ); - assert!(config.cloud_specific.enable.log_upload); - assert!(config.cloud_specific.enable.config_snapshot); - assert!(config.cloud_specific.enable.config_update); - assert!(config.cloud_specific.enable.firmware_update); - assert!(config.cloud_specific.enable.device_profile); - } - - #[test] - fn partial_config_applies_missing_defaults() { - let toml = r#" - url = "tenant.example.com" - - [smartrest] - use_operation_id = false - - [enable] - log_upload = false - - [proxy.bind] - port = 4312 - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // Explicit values preserved - assert!(!config.cloud_specific.smartrest.use_operation_id); - assert!(!config.cloud_specific.enable.log_upload); - - // Defaults applied for missing fields - assert_eq!(config.cloud_specific.auth_method, AuthMethod::Certificate); - assert!(config.cloud_specific.entity_store.auto_register); - assert!(config.cloud_specific.enable.config_snapshot); - - // Runtime defaults: proxy port inheritance - assert_eq!(config.cloud_specific.proxy.bind.port, 4312); - assert_eq!(config.cloud_specific.proxy.client.port, 4312); - - // Runtime defaults: http/mqtt derived from url - assert_eq!( - config.cloud_specific.http.or_none().unwrap().to_string(), - "tenant.example.com:443" - ); - assert_eq!( - config.cloud_specific.mqtt.or_none().unwrap().to_string(), - "tenant.example.com:8883" - ); - } - - #[test] - fn explicit_values_override_all_defaults() { - let toml = r#" - auth_method = "basic" - - [smartrest] - use_operation_id = false - - [entity_store] - auto_register = false - clean_start = false - - [software_management] - api = "advanced" - with_types = true - - [operations] - auto_log_upload = "always" - - [enable] - log_upload = false - config_snapshot = false - config_update = false - firmware_update = false - device_profile = false - "#; - - let config = deserialize_from_str::(toml).unwrap(); - let c8y_config = &config.cloud_specific; - - // All explicit values preserved, no defaults applied - assert_eq!(c8y_config.auth_method, AuthMethod::Basic); - assert!(!c8y_config.smartrest.use_operation_id); - assert!(!c8y_config.entity_store.auto_register); - assert!(!c8y_config.entity_store.clean_start); - assert_eq!( - c8y_config.software_management.api, - SoftwareManagementApiFlag::Advanced - ); - assert!(c8y_config.software_management.with_types); - assert_eq!(c8y_config.operations.auto_log_upload, AutoLogUpload::Always); - assert!(!c8y_config.enable.log_upload); - assert!(!c8y_config.enable.config_snapshot); - assert!(!c8y_config.enable.config_update); - assert!(!c8y_config.enable.firmware_update); - assert!(!c8y_config.enable.device_profile); - } - - #[test] - fn device_fields_populate_from_tedge_config() { - let tedge_toml = r#" - device.id = "test-id" - "#; - - let mapper_toml = r#" - url = "tenant.example.com" - "#; - - let tedge_config = TEdgeConfig::from_dto( - toml::from_str(tedge_toml).unwrap(), - TEdgeConfigLocation::from_custom_root("/not/a/real/directory"), - ); - let config: C8yMapperConfig = - load_mapper_config_from_string(mapper_toml, &tedge_config, Some("random-profile")) - .unwrap(); - - // Device fields should come from tedge_config defaults - // Call the id() method to get the device ID (which should be set from tedge_toml) - assert_eq!(config.device.id().unwrap(), "test-id"); - // Other device fields have paths that come from the default tedge config - assert!(config.device.key_path.as_str().contains("tedge")); - assert!(config.device.cert_path.as_str().contains("tedge")); - assert!(config.device.csr_path.as_str().contains("tedge")); - } - - #[test] - fn http_endpoint_derives_from_url_when_missing() { - let toml = r#" - url = "my-tenant.cumulocity.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // http should be derived from url with HTTPS port - assert_eq!( - config.http().or_none().unwrap().to_string(), - "my-tenant.cumulocity.com:443" - ); - } - - #[test] - fn mqtt_key_contains_filename_if_missing() { - let toml = ""; - - let config = deserialize_from_str::(toml).unwrap(); - - // For C8y, we check mqtt key since url is private - assert_eq!(config.mqtt().key(), "c8y.url"); - } - - #[test] - fn mqtt_endpoint_derives_from_url_when_missing() { - let toml = r#" - url = "my-tenant.cumulocity.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // mqtt should be derived from url with MQTT TLS port - assert_eq!( - config.mqtt().or_none().unwrap().to_string(), - "my-tenant.cumulocity.com:8883" - ); - } - - #[test] - fn proxy_client_port_inherits_bind_port_when_unset() { - let toml = r#" - url = "tenant.example.com" - - [proxy.bind] - port = 9001 - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // Verify inheritance: client.port should match bind.port - assert_eq!(config.cloud_specific.proxy.bind.port, 9001); - assert_eq!(config.cloud_specific.proxy.client.port, 9001); - } - - #[test] - fn explicit_proxy_client_port_not_overridden() { - let toml = r#" - url = "tenant.example.com" - - [proxy.bind] - port = 9001 - - [proxy.client] - port = 7001 - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // Explicit client.port should be preserved, not inherited from bind.port - assert_eq!(config.cloud_specific.proxy.bind.port, 9001); - assert_eq!(config.cloud_specific.proxy.client.port, 7001); - } - - #[test] - fn root_cert_path_has_default() { - let toml = r#" - url = "tenant.example.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // root_cert_path should have default value - assert_eq!(config.root_cert_path.as_str(), "/etc/ssl/certs"); - } - - #[test] - fn bridge_config_has_defaults() { - let toml = r#" - url = "tenant.example.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // Bridge should have default values (currently c8y defaults) - assert_eq!(config.bridge.topic_prefix.as_str(), "c8y"); - assert_eq!(config.bridge.keepalive_interval.duration().as_secs(), 60); - } - - #[test] - fn max_payload_size_has_c8y_default() { - let toml = r#" - url = "tenant.example.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // max_payload_size should have C8Y default (16184 bytes) - assert_eq!(config.mapper.mqtt.max_payload_size.0, 16184); - } - - #[test] - fn az_max_payload_size_has_azure_default() { - let toml = r#" - url = "mydevice.azure-devices.net" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // max_payload_size should have Azure default (256 KB = 262144 bytes) - assert_eq!(config.mapper.mqtt.max_payload_size.0, 262144); - } - - #[test] - fn aws_max_payload_size_has_aws_default() { - let toml = r#" - url = "mydevice.amazonaws.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // max_payload_size should have AWS default (128 KB = 131072 bytes) - assert_eq!(config.mapper.mqtt.max_payload_size.0, 131072); - } - - #[test] - fn c8y_topics_include_twin_metadata() { - let toml = r#" - url = "tenant.cumulocity.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // C8y topics should include twin and metadata topics - let topics_str = config.topics.to_string(); - assert!(topics_str.contains("twin")); - assert!(topics_str.contains("meta")); - } - - #[test] - fn az_topics_exclude_twin_metadata() { - let toml = r#" - url = "mydevice.azure-devices.net" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // Azure topics should NOT include twin or metadata topics (simpler set) - let topics_str = config.topics.to_string(); - assert!(!topics_str.contains("twin")); - assert!(!topics_str.contains("meta")); - } - - #[test] - fn aws_topics_exclude_twin_metadata() { - let toml = r#" - url = "mydevice.amazonaws.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - // AWS topics should NOT include twin or metadata topics (simpler set) - let topics_str = config.topics.to_string(); - assert!(!topics_str.contains("twin")); - assert!(!topics_str.contains("meta")); - } - - #[test] - fn c8y_bridge_has_c8y_topic_prefix() { - let toml = r#" - url = "tenant.cumulocity.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - assert_eq!(config.bridge.topic_prefix.as_str(), "c8y"); - } - - #[test] - fn az_bridge_has_az_topic_prefix() { - let toml = r#" - url = "mydevice.azure-devices.net" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - assert_eq!(config.bridge.topic_prefix.as_str(), "az"); - } - - #[test] - fn aws_bridge_has_aws_topic_prefix() { - let toml = r#" - url = "mydevice.amazonaws.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - assert_eq!(config.bridge.topic_prefix.as_str(), "aws"); - } - - #[test] - fn aws_config_can_have_specialised_and_non_specialised_mapper_fields() { - let toml = r#" - mapper.timestamp = false - mapper.mqtt.max_payload_size = 12345 - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - assert_eq!(config.mapper.mqtt.max_payload_size, MqttPayloadLimit(12345)); - assert!(!config.cloud_specific.mapper.timestamp); - } - - #[test] - fn empty_proxy_cert_path_matches_legacy_c8y_key_name() { - let toml = r#" - url = "tenant.cumulocity.com" - "#; - - let config = deserialize_from_str::(toml).unwrap(); - - assert_eq!( - config.cloud_specific.proxy.cert_path.key(), - "c8y.proxy.cert_path" - ) - } - - fn deserialize_from_str(toml: &str) -> Result, MapperConfigError> - where - T: SpecialisedCloudConfig, - { - let tedge_config = - TEdgeConfig::from_dto(TEdgeConfigDto::default(), TEdgeConfigLocation::default()); - load_mapper_config_from_string(toml, &tedge_config, None) - } -} diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs index 639d8bb575e..7bd6e12ab31 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config_location.rs @@ -262,12 +262,12 @@ impl TEdgeConfigLocation { } } + dto.populate_mapper_configs(self).await?; + if Sources::INCLUDE_ENVIRONMENT { update_with_environment_variables(&mut dto, &mut warnings)?; } - dto.populate_mapper_configs(self).await?; - Ok((dto, warnings)) } @@ -576,11 +576,11 @@ type = "a-service-type""#; assert_eq!(warnings, UnusedValueWarnings::default()); assert_eq!( - tedge_config.device_cert_path(None::).await.unwrap(), + tedge_config.device_cert_path(None::).unwrap(), "/tedge/device-cert.pem".parse().unwrap() ); assert_eq!( - tedge_config.device_key_path(None::).await.unwrap(), + tedge_config.device_key_path(None::).unwrap(), "/tedge/device-key.pem".parse().unwrap() ); assert_eq!(tedge_config.device.ty, "a-device"); @@ -655,7 +655,6 @@ type = "a-service-type""#; .mapper_config::(&Some( ProfileName::try_from("test".to_owned()).unwrap(), )) - .await .unwrap(); assert_eq!( az_config.root_cert_path, diff --git a/crates/common/tedge_config/tests/mapper_config.rs b/crates/common/tedge_config/tests/mapper_config.rs index 18112a5629f..9a5023c70e9 100644 --- a/crates/common/tedge_config/tests/mapper_config.rs +++ b/crates/common/tedge_config/tests/mapper_config.rs @@ -1,153 +1,8 @@ -use std::io::Write as _; -use std::sync::Arc; -use std::sync::LazyLock; - use tedge_config::tedge_toml::mapper_config::C8yMapperSpecificConfig; use tedge_config::TEdgeConfig; use tedge_config_macros::ProfileName; use tedge_test_utils::fs::TempTedgeDir; -#[tokio::test] -async fn new_format_takes_precedence_over_legacy() { - std::env::set_var("NO_COLOR", "true"); - let log_capture = TestLogCapture::new().await; - - let subscriber = tracing_subscriber::fmt() - .with_max_level(tracing::Level::WARN) - .with_writer(log_capture.clone()) - .finish(); - - let _guard = tracing::subscriber::set_default(subscriber); - - let mapper_config = r#" - url = "from-mapper-config.example.com" - "#; - let tedge_toml = r#" - [c8y] - url = "from-tedge-toml.example.com" - "#; - - let ttd = TempTedgeDir::new(); - ttd.file("tedge.toml").with_raw_content(tedge_toml); - ttd.dir("mappers") - .file("c8y.toml") - .with_raw_content(mapper_config); - - let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); - let c8y_config = tedge_config - .mapper_config::(&None::) - .await - .unwrap(); - assert_eq!( - c8y_config.mqtt().or_none().unwrap().host().to_string(), - "from-mapper-config.example.com" - ); - - if log_capture.has_warnings() { - let warning = log_capture - .get_logs() - .into_iter() - .filter(|log| log.contains("WARN")) - .find(|log| log.contains("Both") && log.contains("exist")) - .expect("Should find conflict warning"); - - let message = warning.rsplit_once(':').unwrap().1.trim(); - assert!( - message.contains("mappers/c8y.toml"), - "Warning should mention new config path" - ); - assert!( - message.contains("tedge.toml [c8y]"), - "Warning should mention legacy config" - ); - assert!( - message.contains("Consider removing"), - "Warning should suggest removing legacy config" - ); - } else { - panic!("Expected conflict warning to be logged, but found none") - } -} - -#[tokio::test] -async fn partial_migration_default_new_profile_legacy_errors() { - let ttd = TempTedgeDir::new(); - - ttd.dir("mappers") - .file("c8y.toml") - .with_toml_content(toml::toml! { - url = "default.example.com" - }); - - ttd.file("tedge.toml").with_toml_content(toml::toml! { - [c8y.profiles.prod] - url = "prod-from-legacy.example.com" - }); - - let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); - - let default_result = tedge_config - .mapper_config::(&None::) - .await; - assert!(default_result.is_ok()); - - let prod_profile = ProfileName::try_from("prod".to_string()).unwrap(); - let prod_result = tedge_config - .mapper_config::(&Some(prod_profile)) - .await; - - let err = prod_result.err().unwrap(); - let expected_path = format!("{}/mappers/c8y.d/prod.toml", ttd.utf8_path()); - assert!( - err.to_string().contains(&expected_path), - "Error should mention the missing profile file path. Got: {err}", - ); - assert!( - err.to_string().contains("doesn't exist"), - "Error should indicate file doesn't exist. Got: {err}", - ); -} - -#[tokio::test] -async fn partial_migration_default_legacy_profile_new_errors() { - let ttd = TempTedgeDir::new(); - - ttd.dir("mappers") - .dir("c8y.d") - .file("prod.toml") - .with_toml_content(toml::toml! { - url = "prod.example.com" - }); - - ttd.file("tedge.toml").with_toml_content(toml::toml! { - [c8y] - url = "default-from-legacy.example.com" - }); - - let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); - - let prod_profile = ProfileName::try_from("prod".to_string()).unwrap(); - let prod_result = tedge_config - .mapper_config::(&Some(prod_profile)) - .await; - assert!(prod_result.is_ok()); - - let default_result = tedge_config - .mapper_config::(&None::) - .await; - - let err = default_result.err().unwrap(); - let expected_path = format!("{}/mappers/c8y.toml", ttd.utf8_path()); - assert!( - err.to_string().contains(&expected_path), - "Error should mention the missing default config file path. Got: {err}", - ); - assert!( - err.to_string().contains("doesn't exist"), - "Error should indicate file doesn't exist. Got: {err}", - ); -} - #[tokio::test] async fn empty_new_config_uses_tedge_toml_defaults() { let ttd = TempTedgeDir::new(); @@ -164,7 +19,6 @@ async fn empty_new_config_uses_tedge_toml_defaults() { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let c8y_config = tedge_config .mapper_config::(&None::) - .await .unwrap(); assert!( @@ -182,73 +36,3 @@ async fn empty_new_config_uses_tedge_toml_defaults() { "Device ID should come from tedge.toml defaults" ); } - -#[derive(Clone)] -struct TestLogCapture { - logs: Arc>>, - _lock: Arc>, -} - -static LOG_TEST_LOCK: LazyLock> = LazyLock::new(<_>::default); - -impl TestLogCapture { - async fn new() -> Self { - let lock = LOG_TEST_LOCK.lock().await; - - Self { - logs: Arc::new(std::sync::Mutex::new(Vec::new())), - _lock: Arc::new(lock), - } - } - - fn has_warnings(&self) -> bool { - self.logs - .lock() - .unwrap() - .iter() - .any(|log| log.contains("WARN")) - } - - fn get_logs(&self) -> Vec { - self.logs.lock().unwrap().clone() - } -} - -impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for TestLogCapture { - type Writer = TestWriter; - - fn make_writer(&'a self) -> Self::Writer { - TestWriter { - logs: self.logs.clone(), - buf: Vec::new(), - } - } -} - -struct TestWriter { - logs: Arc>>, - buf: Vec, -} - -impl std::io::Write for TestWriter { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - self.buf.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> std::io::Result<()> { - if !self.buf.is_empty() { - if let Ok(s) = String::from_utf8(self.buf.clone()) { - self.logs.lock().unwrap().push(s); - } - self.buf.clear() - } - Ok(()) - } -} - -impl Drop for TestWriter { - fn drop(&mut self) { - let _ = self.flush(); - } -} diff --git a/crates/core/tedge/src/cli/certificate/cli.rs b/crates/core/tedge/src/cli/certificate/cli.rs index 38f0106e6de..a40d8acbd2b 100644 --- a/crates/core/tedge/src/cli/certificate/cli.rs +++ b/crates/core/tedge/src/cli/certificate/cli.rs @@ -245,8 +245,8 @@ impl BuildCommand for TEdgeCertCli { let cmd = CreateCertCmd { id: get_device_id(id, config, &cloud).await?, - cert_path: config.device_cert_path(cloud.as_ref()).await?.into(), - key_path: config.device_key_path(cloud.as_ref()).await?.into(), + cert_path: config.device_cert_path(cloud.as_ref())?.into(), + key_path: config.device_key_path(cloud.as_ref())?.into(), user: user.to_owned(), group: group.to_owned(), csr_template, @@ -262,25 +262,20 @@ impl BuildCommand for TEdgeCertCli { let cloud: Option = cloud.map(<_>::try_into).transpose()?; debug!(?cloud); let cloud_config = match cloud.as_ref() { - Some(c) => Some(config.as_cloud_config(c.into()).await?), + Some(c) => Some(config.as_cloud_config(c.into())?), None => None, }; let cryptoki = config .device - .cryptoki_config(cloud_config.as_ref().map(|c| c as &dyn CloudConfig))?; + .cryptoki_config(cloud_config.as_ref().map(|c| &**c as &dyn CloudConfig))?; let key = cryptoki .map(super::create_csr::Key::Cryptoki) .unwrap_or(Key::Local( - config - .device_key_path(cloud.as_ref()) - .await? - .to_owned() - .into(), + config.device_key_path(cloud.as_ref())?.to_owned().into(), )); debug!(?key); let current_cert = config .device_cert_path(cloud.as_ref()) - .await .map(|c| c.into()) .ok(); debug!(?current_cert); @@ -292,7 +287,7 @@ impl BuildCommand for TEdgeCertCli { csr_path: if let Some(output_path) = output_path { output_path } else { - config.device_csr_path(cloud.as_ref()).await?.into() + config.device_csr_path(cloud.as_ref())?.into() }, current_cert, user: user.to_owned(), @@ -316,12 +311,12 @@ impl BuildCommand for TEdgeCertCli { } => { let cloud: Option = cloud.map(<_>::try_into).transpose()?; let cloud_config = match cloud.as_ref() { - Some(c) => Some(config.as_cloud_config(c.into()).await?), + Some(c) => Some(config.as_cloud_config(c.into())?), None => None, }; let cryptoki_config = config .device - .cryptoki_config(cloud_config.as_ref().map(|c| c as &dyn CloudConfig))? + .cryptoki_config(cloud_config.as_ref().map(|c| &**c as &dyn CloudConfig))? .context("Cryptoki config is not enabled")?; CreateKeyHsmCmd { @@ -344,7 +339,7 @@ impl BuildCommand for TEdgeCertCli { show_new, } => { let cloud: Option = cloud.map(<_>::try_into).transpose()?; - let device_cert_path = config.device_cert_path(cloud.as_ref()).await?.into(); + let device_cert_path = config.device_cert_path(cloud.as_ref())?.into(); let cert_path = cert_path.unwrap_or(device_cert_path); let cmd = ShowCertCmd { cert_path: if show_new { @@ -361,7 +356,7 @@ impl BuildCommand for TEdgeCertCli { TEdgeCertCli::NeedsRenewal { cloud, cert_path } => { let cloud: Option = cloud.map(<_>::try_into).transpose()?; - let device_cert_path = config.device_cert_path(cloud.as_ref()).await?.into(); + let device_cert_path = config.device_cert_path(cloud.as_ref())?.into(); let cmd = ShowCertCmd { cert_path: cert_path.unwrap_or(device_cert_path), minimum: config.certificate.validity.minimum_duration.duration(), @@ -373,8 +368,8 @@ impl BuildCommand for TEdgeCertCli { TEdgeCertCli::Remove { cloud } => { let cloud: Option = cloud.map(<_>::try_into).transpose()?; let cmd = RemoveCertCmd { - cert_path: config.device_cert_path(cloud.as_ref()).await?.into(), - key_path: config.device_key_path(cloud.as_ref()).await?.into(), + cert_path: config.device_cert_path(cloud.as_ref())?.into(), + key_path: config.device_key_path(cloud.as_ref())?.into(), }; cmd.into_boxed() } @@ -384,9 +379,7 @@ impl BuildCommand for TEdgeCertCli { password, profile, }) => { - let c8y = config - .mapper_config::(&profile) - .await?; + let c8y = config.mapper_config::(&profile)?; let cmd = c8y::UploadCertCmd { device_id: c8y.device.id()?.clone(), path: c8y.device.cert_path.clone().into(), @@ -407,9 +400,7 @@ impl BuildCommand for TEdgeCertCli { retry_every, max_timeout, }) => { - let c8y_config = config - .mapper_config::(&profile) - .await?; + let c8y_config = config.mapper_config::(&profile)?; let (csr_path, generate_csr) = match csr_path { None => (c8y_config.device.csr_path.clone().into(), true), @@ -425,15 +416,14 @@ impl BuildCommand for TEdgeCertCli { .to_owned(), }; - let cryptoki = config.device.cryptoki_config(Some(&*c8y_config))?; + let cryptoki = config.device.cryptoki_config(Some(&c8y_config))?; let key = cryptoki .map(super::create_csr::Key::Cryptoki) .unwrap_or(Key::Local( config .device_key_path(Some(tedge_config::tedge_toml::Cloud::C8y( profile.as_ref(), - ))) - .await? + )))? .into(), )); let cmd = c8y::DownloadCertCmd { @@ -458,8 +448,8 @@ impl BuildCommand for TEdgeCertCli { ca, } => { let cloud: Option = cloud.map(<_>::try_into).transpose()?; - let cert_path: Utf8PathBuf = config.device_cert_path(cloud.as_ref()).await?.into(); - let key_path = config.device_key_path(cloud.as_ref()).await?.into(); + let cert_path: Utf8PathBuf = config.device_cert_path(cloud.as_ref())?.into(); + let key_path = config.device_key_path(cloud.as_ref())?.into(); let new_cert_path = CertificateShift::new_certificate_path(&cert_path); // The CA to renew a certificate is determined from the certificate @@ -492,17 +482,17 @@ impl BuildCommand for TEdgeCertCli { ); } else { let (csr_path, generate_csr) = match csr_path { - None => (config.device_csr_path(cloud.as_ref()).await?.into(), true), + None => (config.device_csr_path(cloud.as_ref())?.into(), true), Some(csr_path) => (csr_path, false), }; let c8y = match &cloud { None => { - let c8y_config = config.mapper_config(&None::).await?; + let c8y_config = config.mapper_config(&None::)?; C8yEndPoint::local_proxy(&c8y_config)? } #[cfg(feature = "c8y")] Some(Cloud::C8y(profile)) => { - let c8y_config = config.mapper_config(profile).await?; + let c8y_config = config.mapper_config(profile)?; C8yEndPoint::local_proxy(&c8y_config)? } #[cfg(any(feature = "aws", feature = "azure"))] @@ -514,17 +504,15 @@ impl BuildCommand for TEdgeCertCli { }; let cloud_config = match cloud.as_ref() { - Some(c) => Some(config.as_cloud_config(c.into()).await?), + Some(c) => Some(config.as_cloud_config(c.into())?), None => None, }; let cryptoki = config .device - .cryptoki_config(cloud_config.as_ref().map(|c| c as &dyn CloudConfig))?; + .cryptoki_config(cloud_config.as_ref().map(|c| &**c as &dyn CloudConfig))?; let key = cryptoki .map(super::create_csr::Key::Cryptoki) - .unwrap_or(Key::Local( - config.device_key_path(cloud.as_ref()).await?.into(), - )); + .unwrap_or(Key::Local(config.device_key_path(cloud.as_ref())?.into())); let cmd = c8y::RenewCertCmd { c8y, http_config: config.cloud_root_certs().await?, @@ -582,7 +570,7 @@ async fn get_device_id( config: &TEdgeConfig, cloud: &Option, ) -> Result { - match (id, config.device_id(cloud.as_ref()).await.ok()) { + match (id, config.device_id(cloud.as_ref()).ok()) { (None, None) => Err(anyhow!( "No device ID is provided. Use `--device-id ` option to specify the device ID." )), diff --git a/crates/core/tedge/src/cli/connect/aws.rs b/crates/core/tedge/src/cli/connect/aws.rs index bf6c7bb4acb..d5e04e47bf6 100644 --- a/crates/core/tedge/src/cli/connect/aws.rs +++ b/crates/core/tedge/src/cli/connect/aws.rs @@ -17,9 +17,7 @@ pub async fn check_device_status_aws( tedge_config: &TEdgeConfig, profile: Option<&ProfileName>, ) -> Result { - let aws_config = tedge_config - .mapper_config::(&profile) - .await?; + let aws_config = tedge_config.mapper_config::(&profile)?; let topic_prefix = &aws_config.bridge.topic_prefix; let aws_topic_pub_check_connection = format!("{topic_prefix}/test-connection"); let aws_topic_sub_check_connection = format!("{topic_prefix}/connection-success"); diff --git a/crates/core/tedge/src/cli/connect/azure.rs b/crates/core/tedge/src/cli/connect/azure.rs index dd035d861cf..d3d71f406e6 100644 --- a/crates/core/tedge/src/cli/connect/azure.rs +++ b/crates/core/tedge/src/cli/connect/azure.rs @@ -22,9 +22,7 @@ pub(crate) async fn check_device_status_azure( tedge_config: &TEdgeConfig, profile: Option<&ProfileName>, ) -> Result { - let az_config = tedge_config - .mapper_config::(&profile) - .await?; + let az_config = tedge_config.mapper_config::(&profile)?; let topic_prefix = &az_config.bridge.topic_prefix; let built_in_bridge_health = bridge_health_topic(topic_prefix, tedge_config).name; let azure_topic_device_twin_downstream = format!(r##"{topic_prefix}/twin/res/#"##); diff --git a/crates/core/tedge/src/cli/connect/c8y.rs b/crates/core/tedge/src/cli/connect/c8y.rs index 7e31bbe08ca..cda0137e265 100644 --- a/crates/core/tedge/src/cli/connect/c8y.rs +++ b/crates/core/tedge/src/cli/connect/c8y.rs @@ -172,9 +172,7 @@ pub(crate) async fn check_device_status_c8y( tedge_config: &TEdgeConfig, c8y_profile: Option<&ProfileName>, ) -> Result { - let c8y_config = tedge_config - .mapper_config::(&c8y_profile) - .await?; + let c8y_config = tedge_config.mapper_config::(&c8y_profile)?; let prefix = &c8y_config.bridge.topic_prefix; let built_in_bridge = tedge_config.mqtt.bridge.built_in; let bridge_health_topic = bridge_health_topic(prefix, tedge_config).name; diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index fb09a0b9b51..fb60dad4eea 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -108,13 +108,12 @@ impl Command for ConnectCommand { let cloud = tedge_config .as_cloud_config((&self.cloud).into()) - .await .map_err(anyhow::Error::new)?; let cryptoki_key_uri = tedge_config .device - .cryptoki_config(Some(&cloud))? + .cryptoki_config(Some(&*cloud))? .map(|c| match c { CryptokiConfig::Direct(d) => d.uri, CryptokiConfig::SocketService { uri, .. } => uri, @@ -379,11 +378,10 @@ impl ConnectCommand { #[cfg(feature = "c8y")] Cloud::C8y(profile_name) => { let device_type = &tedge_config.device.ty; - let c8y_config = tedge_config - .mapper_config::(profile_name) - .await?; + let c8y_config = + tedge_config.mapper_config::(profile_name)?; let mut mqtt_auth_config = - tedge_config.mqtt_auth_config_cloud_broker(&*c8y_config)?; + tedge_config.mqtt_auth_config_cloud_broker(&c8y_config)?; if let Some(client_config) = mqtt_auth_config.client.as_mut() { _certificate_shift .new_cert_path @@ -408,9 +406,7 @@ async fn credentials_path_for( match cloud { #[cfg(feature = "c8y")] Cloud::C8y(profile) => { - let c8y_config = _config - .mapper_config::(profile) - .await?; + let c8y_config = _config.mapper_config::(profile)?; Ok(Some(c8y_config.cloud_specific.credentials_path.clone())) } #[cfg(feature = "aws")] @@ -429,9 +425,7 @@ impl ConnectCommand { match &self.cloud { #[cfg(feature = "c8y")] Cloud::C8y(profile) => { - let c8y_config = tedge_config - .mapper_config::(profile) - .await?; + let c8y_config = tedge_config.mapper_config::(profile)?; if bridge_config.auth_type == AuthType::Certificate && !self.offline_mode { tenant_matches_configured_url( @@ -558,7 +552,7 @@ async fn validate_config( Ok(()) } -type MapperConfigData = (Arc>, Option); +type MapperConfigData = (MapperConfig, Option); fn disallow_matching_url_device_id(mapper_configs: &[MapperConfigData]) -> anyhow::Result<()> where @@ -702,9 +696,7 @@ pub async fn bridge_config( match cloud { #[cfg(feature = "azure")] MaybeBorrowedCloud::Azure(profile) => { - let az_config = config - .mapper_config::(profile) - .await?; + let az_config = config.mapper_config::(profile)?; let params = BridgeConfigAzureParams { mqtt_host: HostPort::::try_from( @@ -728,9 +720,7 @@ pub async fn bridge_config( } #[cfg(feature = "aws")] MaybeBorrowedCloud::Aws(profile) => { - let aws_config = config - .mapper_config::(profile) - .await?; + let aws_config = config.mapper_config::(profile)?; let params = BridgeConfigAwsParams { mqtt_host: HostPort::::try_from( @@ -755,9 +745,7 @@ pub async fn bridge_config( #[cfg(feature = "c8y")] MaybeBorrowedCloud::C8y(profile) => { use tedge_config::models::MQTT_CORE_TLS_PORT; - let c8y_config = config - .mapper_config::(profile) - .await?; + let c8y_config = config.mapper_config::(profile)?; let (remote_username, remote_password) = match c8y_config .cloud_specific @@ -864,11 +852,10 @@ impl ConnectCommand { if self.offline_mode { eprintln!("Offline mode. Skipping device creation in Cumulocity cloud.") } else { - let c8y_config = tedge_config - .mapper_config::(profile_name) - .await?; + let c8y_config = + tedge_config.mapper_config::(profile_name)?; let mqtt_auth_config = - tedge_config.mqtt_auth_config_cloud_broker(&*c8y_config)?; + tedge_config.mqtt_auth_config_cloud_broker(&c8y_config)?; let spinner = Spinner::start("Creating device in Cumulocity cloud"); let res = create_device_with_direct_connection( bridge_config, @@ -1147,9 +1134,7 @@ async fn tenant_matches_configured_url( configured_http_url: &str, ) -> Result> { let spinner = Spinner::start("Checking Cumulocity is connected to intended tenant"); - let c8y_config = tedge_config - .mapper_config::(&profile_name) - .await?; + let c8y_config = tedge_config.mapper_config::(&profile_name)?; let res = get_connected_c8y_url(tedge_config, &c8y_config).await; match spinner.finish(res) { Ok(url) if url == configured_mqtt_url || url == configured_http_url => Ok(true), diff --git a/crates/core/tedge/src/cli/http/cli.rs b/crates/core/tedge/src/cli/http/cli.rs index 8914551c0fb..8cf3cc5aca6 100644 --- a/crates/core/tedge/src/cli/http/cli.rs +++ b/crates/core/tedge/src/cli/http/cli.rs @@ -200,9 +200,8 @@ impl BuildCommand for TEdgeHttpCli { let uri = self.uri(); let (protocol, host, port) = if uri.starts_with("/c8y") { - let c8y_config = config - .mapper_config::(&self.c8y_profile()) - .await?; + let c8y_config = + config.mapper_config::(&self.c8y_profile())?; let client = &c8y_config.cloud_specific.proxy.client; let protocol = https_if_some(&c8y_config.cloud_specific.proxy.cert_path); (protocol, client.host.clone(), client.port) diff --git a/crates/core/tedge/src/cli/upload/mod.rs b/crates/core/tedge/src/cli/upload/mod.rs index 60c21a7bb3b..f77f4a5ced5 100644 --- a/crates/core/tedge/src/cli/upload/mod.rs +++ b/crates/core/tedge/src/cli/upload/mod.rs @@ -77,7 +77,7 @@ impl BuildCommand for UploadCmd { } => { let identity = config.http.client.auth.identity()?; let cloud_root_certs = config.cloud_root_certs().await?; - let c8y_config = config.mapper_config(&profile).await?; + let c8y_config = config.mapper_config(&profile)?; let c8y = C8yEndPoint::local_proxy(&c8y_config)?; let device_id = match device_id { None => c8y_config.device.id()?.clone(), diff --git a/crates/core/tedge_mapper/src/aws/mapper.rs b/crates/core/tedge_mapper/src/aws/mapper.rs index da31d146c81..2c363e759ea 100644 --- a/crates/core/tedge_mapper/src/aws/mapper.rs +++ b/crates/core/tedge_mapper/src/aws/mapper.rs @@ -34,9 +34,7 @@ impl TEdgeComponent for AwsMapper { tedge_config: TEdgeConfig, _config_dir: &tedge_config::Path, ) -> Result<(), anyhow::Error> { - let aws_config = tedge_config - .mapper_config::(&self.profile) - .await?; + let aws_config = tedge_config.mapper_config::(&self.profile)?; let prefix = &aws_config.bridge.topic_prefix; let aws_mapper_name = format!("tedge-mapper-{prefix}"); let (mut runtime, mut mqtt_actor) = @@ -58,7 +56,7 @@ impl TEdgeComponent for AwsMapper { cloud_config.set_keep_alive(aws_config.bridge.keepalive_interval.duration()); let tls_config = tedge_config - .mqtt_client_config_rustls(&*aws_config) + .mqtt_client_config_rustls(&aws_config) .context("Failed to create MQTT TLS config")?; cloud_config.set_transport(Transport::tls_with_config(tls_config.into())); diff --git a/crates/core/tedge_mapper/src/az/mapper.rs b/crates/core/tedge_mapper/src/az/mapper.rs index 834bbf7dfc1..1e9bec22765 100644 --- a/crates/core/tedge_mapper/src/az/mapper.rs +++ b/crates/core/tedge_mapper/src/az/mapper.rs @@ -35,9 +35,7 @@ impl TEdgeComponent for AzureMapper { tedge_config: TEdgeConfig, _config_dir: &tedge_config::Path, ) -> Result<(), anyhow::Error> { - let az_config = tedge_config - .mapper_config::(&self.profile) - .await?; + let az_config = tedge_config.mapper_config::(&self.profile)?; let prefix = &az_config.bridge.topic_prefix; let az_mapper_name = format!("tedge-mapper-{prefix}"); let (mut runtime, mut mqtt_actor) = @@ -66,7 +64,7 @@ impl TEdgeComponent for AzureMapper { cloud_config.set_keep_alive(az_config.bridge.keepalive_interval.duration()); let tls_config = tedge_config - .mqtt_client_config_rustls(&*az_config) + .mqtt_client_config_rustls(&az_config) .context("Failed to create MQTT TLS config")?; cloud_config.set_transport(Transport::tls_with_config(tls_config.into())); diff --git a/crates/core/tedge_mapper/src/c8y/mapper.rs b/crates/core/tedge_mapper/src/c8y/mapper.rs index 0d5e4a05377..85c10af6186 100644 --- a/crates/core/tedge_mapper/src/c8y/mapper.rs +++ b/crates/core/tedge_mapper/src/c8y/mapper.rs @@ -48,7 +48,7 @@ impl TEdgeComponent for CumulocityMapper { tedge_config: TEdgeConfig, cfg_dir: &tedge_config::Path, ) -> Result<(), anyhow::Error> { - let c8y_config = tedge_config.mapper_config(&self.profile).await?; + let c8y_config = tedge_config.mapper_config(&self.profile)?; let prefix = &c8y_config.bridge.topic_prefix; let c8y_mapper_name = format!("tedge-mapper-{prefix}"); let (mut runtime, mut mqtt_actor) = diff --git a/crates/extensions/tedge_mqtt_bridge/src/config.rs b/crates/extensions/tedge_mqtt_bridge/src/config.rs index 3929a63fdc5..3d1d09f062b 100644 --- a/crates/extensions/tedge_mqtt_bridge/src/config.rs +++ b/crates/extensions/tedge_mqtt_bridge/src/config.rs @@ -264,10 +264,9 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let c8y_config = tedge_config .mapper_config::(&None::) - .await .unwrap(); - use_key_and_cert(&mut opts, &*c8y_config).unwrap(); + use_key_and_cert(&mut opts, &c8y_config).unwrap(); let Transport::Tls(tls) = opts.transport() else { panic!("Transport should be type TLS") diff --git a/plugins/c8y_firmware_plugin/src/lib.rs b/plugins/c8y_firmware_plugin/src/lib.rs index ad59c48d24b..ab8be2520ba 100644 --- a/plugins/c8y_firmware_plugin/src/lib.rs +++ b/plugins/c8y_firmware_plugin/src/lib.rs @@ -57,7 +57,7 @@ pub async fn run(firmware_plugin_opt: FirmwarePluginOpt) -> Result<(), anyhow::E let tedge_config = tedge_config::TEdgeConfig::load(config_dir).await?; let c8y_profile = firmware_plugin_opt.profile; - let c8y_config = tedge_config.mapper_config(&c8y_profile).await?; + let c8y_config = tedge_config.mapper_config(&c8y_profile)?; if firmware_plugin_opt.init { warn!("This --init option has been deprecated and will be removed in a future release"); diff --git a/plugins/c8y_remote_access_plugin/src/lib.rs b/plugins/c8y_remote_access_plugin/src/lib.rs index 6aca4c99079..f1a7b4cd025 100644 --- a/plugins/c8y_remote_access_plugin/src/lib.rs +++ b/plugins/c8y_remote_access_plugin/src/lib.rs @@ -66,10 +66,7 @@ pub async fn run(opt: C8yRemoteAccessPluginOpt) -> miette::Result<()> { Ok(()) } Command::Connect((command, p)) => { - let c8y_config = tedge_config - .mapper_config(&p) - .await - .map_err(|e| miette!("{e}"))?; + let c8y_config = tedge_config.mapper_config(&p).map_err(|e| miette!("{e}"))?; proxy(command, &tedge_config, &c8y_config).await } Command::SpawnChild(command) => { From 7a37aad4fd87acb40e1072549fbdaf19d6504913 Mon Sep 17 00:00:00 2001 From: James Rhodes Date: Fri, 12 Dec 2025 16:20:03 +0000 Subject: [PATCH 10/10] Tidy up some more things that no longer need to be async Signed-off-by: James Rhodes --- Cargo.lock | 7 --- Cargo.toml | 1 - crates/common/tedge_config/Cargo.toml | 1 - .../src/tedge_toml/tedge_config.rs | 61 +++++++------------ crates/core/tedge/src/cli/connect/command.rs | 38 ++++++------ 5 files changed, 42 insertions(+), 66 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 102bc8e1213..a98699bda22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,12 +103,6 @@ dependencies = [ "backtrace", ] -[[package]] -name = "anymap3" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "170433209e817da6aae2c51aa0dd443009a613425dd041ebfb2492d1c4c11a25" - [[package]] name = "arc-swap" version = "1.7.1" @@ -4922,7 +4916,6 @@ name = "tedge_config" version = "1.7.1" dependencies = [ "anyhow", - "anymap3", "assert_matches", "camino", "certificate", diff --git a/Cargo.toml b/Cargo.toml index 7ea78c1d204..38df081ad4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,7 +76,6 @@ upload = { path = "crates/common/upload" } anstyle = "1.0" anyhow = "1.0" -anymap3 = "1.0" asn1-rs = { version = "0.7.0", features = ["bigint"] } assert-json-diff = "2.0" assert_cmd = "2.0" diff --git a/crates/common/tedge_config/Cargo.toml b/crates/common/tedge_config/Cargo.toml index 220853082ec..90022c92837 100644 --- a/crates/common/tedge_config/Cargo.toml +++ b/crates/common/tedge_config/Cargo.toml @@ -14,7 +14,6 @@ test = [] [dependencies] anyhow = { workspace = true } -anymap3 = { workspace = true } camino = { workspace = true, features = ["serde", "serde1"] } certificate = { workspace = true, features = ["reqwest"] } clap = { workspace = true } diff --git a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs index d9bb69810a8..b80e0d7751a 100644 --- a/crates/common/tedge_config/src/tedge_toml/tedge_config.rs +++ b/crates/common/tedge_config/src/tedge_toml/tedge_config.rs @@ -35,7 +35,6 @@ use crate::tedge_toml::mapper_config::HasPath as _; use crate::tedge_toml::mapper_config::HasUrl; use crate::tedge_toml::mapper_config::MapperConfigError; use crate::tedge_toml::mapper_config::SpecialisedCloudConfig; -use crate::ConfigDecision; use anyhow::anyhow; use anyhow::Context; use camino::Utf8Path; @@ -126,7 +125,7 @@ impl TEdgeConfigDto { async fn populate_single_mapper( dto: &mut MultiDto, location: &TEdgeConfigLocation, - ) -> anyhow::Result<()> { + ) -> anyhow::Result<()> where T::CloudDto: PartialEq { use futures::StreamExt; use futures::TryStreamExt; @@ -135,6 +134,9 @@ impl TEdgeConfigDto { let ty = T::expected_cloud_type(); match all_profiles { Some(profiles) => { + if !dto.is_default() { + tracing::warn!("{ty} configuration found in `tedge.toml`, but this will be ignored in favour of configuration in {mappers_dir}/{ty}.toml and {mappers_dir}/{ty}.d") + } let toml_path = mappers_dir.join(format!("{ty}.toml")); let default_profile_toml = read_file_if_exists(&toml_path).await?; let mut default_profile_config: T::CloudDto = default_profile_toml.map_or_else( @@ -186,13 +188,6 @@ impl TEdgeConfig { } } - pub async fn decide_config_source(&self, profile: Option<&ProfileName>) -> ConfigDecision - where - T: ExpectedCloudType, - { - self.location.decide_config_source::(profile).await - } - pub(crate) fn location(&self) -> &TEdgeConfigLocation { &self.location } @@ -236,38 +231,32 @@ impl TEdgeConfig { CloudType::iter().map(|ty| self.location.mappers_config_dir().join(format!("{ty}.d"))) } - async fn all_profiles( - &self, - ) -> Box> + Unpin + Send + Sync + '_> + fn all_profiles<'a, T>( + &'a self, + ) -> Box> + 'a> where T: ExpectedCloudType, { - if let Some(migrated_profiles) = self.location.mapper_config_profiles::().await { - migrated_profiles - } else { match T::expected_cloud_type() { - CloudType::C8y => Box::new(futures::stream::iter( + CloudType::C8y => Box::new( self.c8y.keys().map(|p| p.map(<_>::to_owned)), - )), - CloudType::Az => Box::new(futures::stream::iter( + ), + CloudType::Az => Box::new( self.az.keys().map(|p| p.map(<_>::to_owned)), - )), - CloudType::Aws => Box::new(futures::stream::iter( + ), + CloudType::Aws => Box::new( self.aws.keys().map(|p| p.map(<_>::to_owned)), - )), + ), } - } } - // TODO handle this properly with cli connect - pub async fn all_mapper_configs(&self) -> Vec<(MapperConfig, Option)> + pub fn all_mapper_configs(&self) -> Vec<(MapperConfig, Option)> where T: SpecialisedCloudConfig, { - use futures::stream::StreamExt; - let mut generalised_profiles = self.all_profiles::().await; + let mut generalised_profiles = self.all_profiles::(); let mut configs = Vec::new(); - while let Some(profile) = generalised_profiles.next().await { + while let Some(profile) = generalised_profiles.next() { if let Ok(config) = self.mapper_config(&profile) { if config.configured_url().or_none().is_some() { configs.push((config, profile)); @@ -338,15 +327,15 @@ impl TEdgeConfig { let roots = CLOUD_ROOT_CERTIFICATES .get_or_init(|| async { let c8y_roots = futures::stream::iter( - self.all_mapper_configs::().await, + self.all_mapper_configs::(), ) .flat_map(|(mapper, _profile)| stream_trust_store(mapper)); let az_roots = futures::stream::iter( - self.all_mapper_configs::().await, + self.all_mapper_configs::(), ) .flat_map(|(mapper, _profile)| stream_trust_store(mapper)); let aws_roots = futures::stream::iter( - self.all_mapper_configs::().await, + self.all_mapper_configs::() ) .flat_map(|(mapper, _profile)| stream_trust_store(mapper)); @@ -2016,8 +2005,7 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let configs = tedge_config - .all_mapper_configs::() - .await; + .all_mapper_configs::(); assert_eq!(configs.len(), 1); assert_eq!(configs[0].1, None); // default profile @@ -2049,8 +2037,7 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let configs = tedge_config - .all_mapper_configs::() - .await; + .all_mapper_configs::(); assert_eq!(configs.len(), 3); @@ -2089,8 +2076,7 @@ mod tests { let tedge_config = TEdgeConfig::load(ttd.path()).await.unwrap(); let c8y_configs = tedge_config - .all_mapper_configs::() - .await; + .all_mapper_configs::(); assert_eq!(c8y_configs.len(), 1); assert_eq!( c8y_configs[0] @@ -2104,8 +2090,7 @@ mod tests { ); let az_configs = tedge_config - .all_mapper_configs::() - .await; + .all_mapper_configs::(); assert_eq!(az_configs.len(), 1); assert_eq!( az_configs[0].0.url().or_none().unwrap().input, diff --git a/crates/core/tedge/src/cli/connect/command.rs b/crates/core/tedge/src/cli/connect/command.rs index fb60dad4eea..1de2d916a6c 100644 --- a/crates/core/tedge/src/cli/connect/command.rs +++ b/crates/core/tedge/src/cli/connect/command.rs @@ -144,7 +144,7 @@ impl Command for ConnectCommand { tedge_config.proxy.username.or_none().map(|u| u.as_str()), ); - validate_config(&tedge_config, &self.cloud).await?; + validate_config(&tedge_config, &self.cloud)?; if self.is_test_connection { self.check_bridge(&tedge_config, &bridge_config) @@ -521,7 +521,7 @@ impl ConnectCommand { } } -async fn validate_config( +fn validate_config( config: &TEdgeConfig, cloud: &MaybeBorrowedCloud<'_>, ) -> anyhow::Result<()> { @@ -531,19 +531,19 @@ async fn validate_config( match cloud { #[cfg(feature = "aws")] MaybeBorrowedCloud::Aws(_) => { - let configs = config.all_mapper_configs::().await; + let configs = config.all_mapper_configs::(); disallow_matching_url_device_id(&configs)?; disallow_matching_bridge_topic_prefix(&configs)?; } #[cfg(feature = "azure")] MaybeBorrowedCloud::Azure(_) => { - let configs = config.all_mapper_configs::().await; + let configs = config.all_mapper_configs::(); disallow_matching_url_device_id(&configs)?; disallow_matching_bridge_topic_prefix(&configs)?; } #[cfg(feature = "c8y")] MaybeBorrowedCloud::C8y(_) => { - let configs = config.all_mapper_configs::().await; + let configs = config.all_mapper_configs::(); disallow_matching_url_device_id(&configs)?; disallow_matching_bridge_topic_prefix(&configs)?; disallow_matching_proxy_bind_port(&configs)?; @@ -1198,7 +1198,7 @@ mod tests { let cloud = Cloud::C8y(None); let config = TEdgeConfig::load_toml_str(""); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1206,7 +1206,7 @@ mod tests { let cloud = Cloud::c8y(Some("new".parse().unwrap())); let config = TEdgeConfig::load_toml_str("c8y.profiles.new.url = \"example.com\""); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1218,7 +1218,7 @@ mod tests { c8y.profiles.new.url = \"example.com\"", ); - let err = validate_config(&config, &cloud).await.unwrap_err(); + let err = validate_config(&config, &cloud).unwrap_err(); pretty_assertions::assert_eq!(err.to_string(), "You have matching URLs and device IDs for different profiles. c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id. @@ -1239,7 +1239,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre .with_raw_content("url = \"example.com\""); let config = TEdgeConfig::load(ttd.path()).await.unwrap(); - let err = validate_config(&config, &cloud).await.unwrap_err(); + let err = validate_config(&config, &cloud).unwrap_err(); pretty_assertions::assert_eq!(err.to_string(), format!("You have matching URLs and device IDs for different profiles. c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id. @@ -1260,7 +1260,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre .with_raw_content("url = \"example.com\"\ndevice.id = \"my-device\""); let config = TEdgeConfig::load(ttd.path()).await.unwrap(); - let err = validate_config(&config, &cloud).await.unwrap_err(); + let err = validate_config(&config, &cloud).unwrap_err(); pretty_assertions::assert_eq!(err.to_string(), format!("You have matching URLs and device IDs for different profiles. c8y.url, c8y.profiles.new.url are set to the same value, but so are c8y.device.id, c8y.profiles.new.device.id. @@ -1278,7 +1278,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre c8y.profiles.new.proxy.bind.port = 8002", ); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1305,7 +1305,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre ), ); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1333,7 +1333,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre ), ); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1341,7 +1341,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre let cloud = Cloud::az(Some("new".parse().unwrap())); let config = TEdgeConfig::load_toml_str("az.profiles.new.url = \"example.com\""); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1349,7 +1349,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre let cloud = Cloud::aws(Some("new".parse().unwrap())); let config = TEdgeConfig::load_toml_str("aws.profiles.new.url = \"example.com\""); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1361,7 +1361,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre c8y.profiles.new.proxy.bind.port = 8002", ); - let err = validate_config(&config, &cloud).await.unwrap_err(); + let err = validate_config(&config, &cloud).unwrap_err(); eprintln!("err={err}"); assert!(err.to_string().contains("c8y.bridge.topic_prefix")); assert!(err @@ -1378,7 +1378,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre c8y.profiles.new.bridge.topic_prefix = \"c8y-new\"", ); - let err = validate_config(&config, &cloud).await.unwrap_err(); + let err = validate_config(&config, &cloud).unwrap_err(); eprintln!("err={err}"); assert!(err.to_string().contains("c8y.proxy.bind.port")); assert!(err.to_string().contains("c8y.profiles.new.proxy.bind.port")); @@ -1392,7 +1392,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre c8y.profiles.new.url = \"example.com\"", ); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } #[tokio::test] @@ -1401,7 +1401,7 @@ Each cloud profile requires either a unique URL or unique device ID, so it corre let config = TEdgeConfig::load_toml_str("az.profiles.new.bridge.topic_prefix = \"az-new\""); - validate_config(&config, &cloud).await.unwrap(); + validate_config(&config, &cloud).unwrap(); } } }