diff options
author | Michael Neumann <mneumann@ntecs.de> | 2021-04-07 15:19:06 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-04-07 15:19:06 +0200 |
commit | 6472cb57fe196b5e19bc7a5eceeb994843dfa095 (patch) | |
tree | 74b7036564396239c1f8946602b297d3505c3660 | |
parent | f9c30859d284145c25a1fd9121f968c7d011994a (diff) |
Refactor TEdge config (#155)
- Refactor TEdgeConfig
- Move configuration into `tedge_config` crate.
- Add `ConfigSetting` trait. Each "thing" that we want to make
configurable will have it's own struct-type. For example `struct
DeviceIdSetting` or `C8yUrlSetting`.
- The `ConfigSetting` trait does not provide any means of how to
read/write/unset a configuration setting. It only provides the key,
description and the value type of the setting, but no implementation.
- Use trait `ConfigSettingAccessor` to implemenent `query`, `update` and
`unset` methods.
- `TEdgeConfigRepository` implements functionality to read/write a
`TEdgeConfig`. The `TEdgeConfig` struct itself just holds data.
- Add `TEdgeConfigLocation` which holds all data that we need to resolve the defaults in
TEdgeConfig, including the name of the config file (TODO: we might want to split these two apart).
The defaults differ depending on the location of the `tedge.toml`, whether it's located in `/etc` or a users `$HOME/.tedge`
(or in tests).
- Only add the `tedge_config` crate. `tedge` is currently not modified
to use the new API.
- Remove remaining `unwrap`s, also from tests
- No longer create intermediate directories upon saving configuration
# FOLLOW-UP TASKS
- Refactor the `tedge` crate to use `tedge_config`.
- Compute the `device.id` from the certificate stored in `device.cert.path`, if any.
- Ensure that the config is updated only by `tedge config set/unset`.
- Extend tedge_config_location to also manage the paths to the bridge configuration files.
- Update `ConfigRepository::store()` to properly set file ownership.
-rw-r--r-- | Cargo.lock | 12 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | tedge/Cargo.toml | 1 | ||||
-rw-r--r-- | tedge_config/Cargo.toml | 14 | ||||
-rw-r--r-- | tedge_config/src/config_setting.rs | 42 | ||||
-rw-r--r-- | tedge_config/src/error.rs | 26 | ||||
-rw-r--r-- | tedge_config/src/lib.rs | 12 | ||||
-rw-r--r-- | tedge_config/src/models/connect_url.rs | 74 | ||||
-rw-r--r-- | tedge_config/src/models/mod.rs | 3 | ||||
-rw-r--r-- | tedge_config/src/settings.rs | 128 | ||||
-rw-r--r-- | tedge_config/src/tedge_config.rs | 176 | ||||
-rw-r--r-- | tedge_config/src/tedge_config_dto.rs | 60 | ||||
-rw-r--r-- | tedge_config/src/tedge_config_location.rs | 163 | ||||
-rw-r--r-- | tedge_config/src/tedge_config_repository.rs | 78 | ||||
-rw-r--r-- | tedge_config/tests/test_tedge_config.rs | 481 |
15 files changed, 1271 insertions, 0 deletions
@@ -1746,6 +1746,7 @@ dependencies = [ "serde", "sha-1", "structopt", + "tedge_config", "tempfile", "thiserror", "tokio", @@ -1758,6 +1759,17 @@ dependencies = [ ] [[package]] +name = "tedge_config" +version = "0.1.0" +dependencies = [ + "assert_matches", + "serde", + "tempfile", + "thiserror", + "toml", +] + +[[package]] name = "tedge_mapper" version = "0.1.0" dependencies = [ @@ -3,6 +3,7 @@ members = [ "common/mqtt_client", "tedge", + "tedge_config", "mapper/tedge_mapper", "mapper/cumulocity/c8y_translator_lib", ] diff --git a/tedge/Cargo.toml b/tedge/Cargo.toml index fa06d9de..af568516 100644 --- a/tedge/Cargo.toml +++ b/tedge/Cargo.toml @@ -30,6 +30,7 @@ which = "4.0" x509-parser = "0.9" zeroize = "1.2" sha-1 = "0.9" +tedge_config = { path = "../tedge_config" } [target.'cfg(unix)'.dependencies] users = "0.11.0" diff --git a/tedge_config/Cargo.toml b/tedge_config/Cargo.toml new file mode 100644 index 00000000..311c1f09 --- /dev/null +++ b/tedge_config/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tedge_config" +version = "0.1.0" +authors = ["Software AG <thin-edge-team@softwareag.com>"] +edition = "2018" + +[dependencies] +toml = "0.5" +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"] } +tempfile = "3.2" + +[dev-dependencies] +assert_matches = "1.4" diff --git a/tedge_config/src/config_setting.rs b/tedge_config/src/config_setting.rs new file mode 100644 index 00000000..c34d1fc9 --- /dev/null +++ b/tedge_config/src/config_setting.rs @@ -0,0 +1,42 @@ +pub trait ConfigSetting { + /// This is something like `device.id`. + const KEY: &'static str; + + const DESCRIPTION: &'static str; + + /// The underlying value type of the configuration setting. + type Value; +} + +pub trait ConfigSettingAccessor<T: ConfigSetting> { + /// Read a configuration setting + fn query(&self, setting: T) -> ConfigSettingResult<T::Value>; + + fn query_optional(&self, setting: T) -> ConfigSettingResult<Option<T::Value>> { + match self.query(setting) { + Ok(value) => Ok(Some(value)), + Err(ConfigSettingError::ConfigNotSet { .. }) => Ok(None), + Err(err) => Err(err), + } + } + + /// Update a configuration setting + fn update(&mut self, _setting: T, _value: T::Value) -> ConfigSettingResult<()>; + + /// Unset a configuration setting / reset to default + fn unset(&mut self, _setting: T) -> ConfigSettingResult<()>; +} + +pub type ConfigSettingResult<T> = Result<T, ConfigSettingError>; + +#[derive(thiserror::Error, Debug)] +pub enum ConfigSettingError { + #[error( + r#"A value for `{key}` is missing. + A value can be set with `tedge config set {key} <value>`"# + )] + ConfigNotSet { key: &'static str }, + + #[error("Readonly setting")] + ReadonlySetting, +} diff --git a/tedge_config/src/error.rs b/tedge_config/src/error.rs new file mode 100644 index 00000000..dd79eb04 --- /dev/null +++ b/tedge_config/src/error.rs @@ -0,0 +1,26 @@ +#[derive(thiserror::Error, Debug)] +pub enum TEdgeConfigError { + #[error("TOML parse error")] + TOMLParseError(#[from] toml::de::Error), + + #[error("TOML serialization error")] + InvalidTOMLError(#[from] toml::ser::Error), + + #[error("I/O error")] + IOError(#[from] std::io::Error), + + #[error("Home directory not found")] + HomeDirectoryNotFound, + + #[error("Invalid characters found in home directory path")] + InvalidCharacterInHomeDirectoryPath, + + #[error(transparent)] + ConfigSettingError(#[from] crate::ConfigSettingError), + + #[error(transparent)] + InvalidConfigUrl(#[from] crate::models::InvalidConnectUrl), + + #[error("Config file not found: {0}")] + ConfigFileNotFound(std::path::PathBuf), +} diff --git a/tedge_config/src/lib.rs b/tedge_config/src/lib.rs new file mode 100644 index 00000000..ce268495 --- /dev/null +++ b/tedge_config/src/lib.rs @@ -0,0 +1,12 @@ +mod config_setting; +mod error; +mod models; +mod settings; +mod tedge_config; +mod tedge_config_dto; +mod tedge_config_location; +mod tedge_config_repository; + +use self::tedge_config_dto::*; +pub use self::{config_setting::*, error::*, models::*, settings::*}; +pub use self::{tedge_config::*, tedge_config_location::*, tedge_config_repository::*}; diff --git a/tedge_config/src/models/connect_url.rs b/tedge_config/src/models/connect_url.rs new file mode 100644 index 00000000..ce7af030 --- /dev/null +++ b/tedge_config/src/models/connect_url.rs @@ -0,0 +1,74 @@ +use std::convert::TryFrom; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +#[serde(try_from = "String", into = "String")] +pub struct ConnectUrl(String); + +#[derive(thiserror::Error, Debug)] +#[error( + "Provided URL: '{0}' contains scheme or port. + Provided URL should contain only domain, eg: 'subdomain.cumulocity.com'." +)] +pub struct InvalidConnectUrl(pub String); + +impl TryFrom<String> for ConnectUrl { + type Error = InvalidConnectUrl; + + fn try_from(input: String) -> Result<Self, Self::Error> { + if input.contains(':') { + Err(InvalidConnectUrl(input)) + } else { + Ok(Self(input)) + } + } +} + +impl TryFrom<&str> for ConnectUrl { + type Error = InvalidConnectUrl; + + fn try_from(input: &str) -> Result<Self, Self::Error> { + ConnectUrl::try_from(input.to_string()) + } +} + +impl ConnectUrl { + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl Into<String> for ConnectUrl { + fn into(self) -> String { + self.0 + } +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_port() { + let input = "test.address.com:8883"; + + assert!(ConnectUrl::try_from(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_scheme_http() { + let input = "http://test.address.com"; + + assert!(ConnectUrl::try_from(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_err_provided_address_with_port_and_http() { + let input = "http://test.address.com:8883"; + + assert!(ConnectUrl::try_from(input).is_err()); +} + +#[test] +fn connect_url_from_string_should_return_string() -> Result<(), crate::TEdgeConfigError> { + let input = "test.address.com"; + let expected = "test.address.com"; + + assert_eq!(&ConnectUrl::try_from(input)?.as_str(), &expected); + Ok(()) +} diff --git a/tedge_config/src/models/mod.rs b/tedge_config/src/models/mod.rs new file mode 100644 index 00000000..ef226c0f --- /dev/null +++ b/tedge_config/src/models/mod.rs @@ -0,0 +1,3 @@ +pub mod connect_url; + +pub use self::connect_url::*; diff --git a/tedge_config/src/settings.rs b/tedge_config/src/settings.rs new file mode 100644 index 00000000..e85c5cf2 --- /dev/null +++ b/tedge_config/src/settings.rs @@ -0,0 +1,128 @@ +use crate::{config_setting::*, models::*}; +use std::path::PathBuf; + +/// +/// Identifier of the device within the fleet. It must be globally +/// unique and the same one used in the device certificate. +/// +/// Example: Raspberrypi-4d18303a-6d3a-11eb-b1a6-175f6bb72665") +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct DeviceIdSetting; + +impl ConfigSetting for DeviceIdSetting { + const KEY: &'static str = "device.id"; + + const DESCRIPTION: &'static str = concat!( + "Identifier of the device within the fleet. It must be ", + "globally unique and the same one used in the device certificate. ", + "Example: Raspberrypi-4d18303a-6d3a-11eb-b1a6-175f6bb72665" + ); + + type Value = String; +} + +/// +/// Path to the private key file. Example: /home/user/.tedge/tedge-private-key.pem +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct DeviceKeyPathSetting; + +impl ConfigSetting for DeviceKeyPathSetting { + const KEY: &'static str = "device.key.path"; + + const DESCRIPTION: &'static str = + "Path to the private key file. Example: /home/user/.tedge/tedge-private-key.pem"; + + type Value = PathBuf; +} + +/// +/// Path to the certificate file. +/// +/// Example: /home/user/.tedge/tedge-certificate.crt +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct DeviceCertPathSetting; + +impl ConfigSetting for DeviceCertPathSetting { + const KEY: &'static str = "device.cert.path"; + + const DESCRIPTION: &'static str = + "Path to the certificate file. Example: /home/user/.tedge/tedge-certificate.crt"; + + type Value = PathBuf; +} + +/// +/// Tenant endpoint URL of Cumulocity tenant. +/// +/// Example: your-tenant.cumulocity.com +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct C8yUrlSetting; + +impl ConfigSetting for C8yUrlSetting { + const KEY: &'static str = "c8y.url"; + + const DESCRIPTION: &'static str = + "Tenant endpoint URL of Cumulocity tenant. Example: your-tenant.cumulocity.com"; + type Value = ConnectUrl; +} + +/// +/// Path where Cumulocity root certificate(s) are located. +/// +/// Example: /home/user/.tedge/c8y-trusted-root-certificates.pem +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct C8yRootCertPathSetting; + +impl ConfigSetting for C8yRootCertPathSetting { + const KEY: &'static str = "c8y.root_cert_path"; + + const DESCRIPTION: &'static str = concat!( + "Path where Cumulocity root certificate(s) are located. ", + "Example: /home/user/.tedge/c8y-trusted-root-certificates.pem" + ); + + type Value = PathBuf; +} + +/// +/// Tenant endpoint URL of Azure IoT tenant. +/// +/// Example: MyAzure.azure-devices.net +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct AzureUrlSetting; + +impl ConfigSetting for AzureUrlSetting { + const KEY: &'static str = "device.id"; + + const DESCRIPTION: &'static str = concat!( + "Tenant endpoint URL of Azure IoT tenant. ", + "Example: MyAzure.azure-devices.net" + ); + + type Value = ConnectUrl; +} + +/// +/// Path where Azure IoT root certificate(s) are located. +/// +/// Example: /home/user/.tedge/azure-trusted-root-certificates.pem +/// +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct AzureRootCertPathSetting; + +impl ConfigSetting for AzureRootCertPathSetting { + const KEY: &'static str = "azure.root_cert_path"; + + const DESCRIPTION: &'static str = concat!( + "Path where Azure IoT root certificate(s) are located. ", + "Example: /home/user/.tedge/azure-trusted-root-certificates.pem" + ); + + type Value = PathBuf; +} diff --git a/tedge_config/src/tedge_config.rs b/tedge_config/src/tedge_config.rs new file mode 100644 index 00000000..89d7b86f --- /dev/null +++ b/tedge_config/src/tedge_config.rs @@ -0,0 +1,176 @@ +use crate::*; +use std::path::PathBuf; + +/// Represents the complete configuration of a thin edge device. +/// This configuration is a wrapper over the device specific configurations +/// as well as the IoT cloud provider specific configurations. +/// +#[derive(Debug)] +pub struct TEdgeConfig { + pub(crate) data: TEdgeConfigDto, + pub(crate) config_location: TEdgeConfigLocation, +} + +impl ConfigSettingAccessor<DeviceIdSetting> for TEdgeConfig { + fn query(&self, _setting: DeviceIdSetting) -> ConfigSettingResult<String> { + self.data + .device + .id + .clone() + .ok_or(ConfigSettingError::ConfigNotSet { + key: DeviceIdSetting::KEY, + }) + } + + fn update(&mut self, _setting: DeviceIdSetting, _value: String) -> ConfigSettingResult<()> { + Err(ConfigSettingError::ReadonlySetting) + } + + fn unset(&mut self, _setting: DeviceIdSetting) -> ConfigSettingResult<()> { + Err(ConfigSettingError::ReadonlySetting) + } +} + +impl ConfigSettingAccessor<AzureUrlSetting> for TEdgeConfig { + fn query(&self, _setting: AzureUrlSetting) -> ConfigSettingResult<ConnectUrl> { + self.data + .azure + .url + .clone() + .ok_or(ConfigSettingError::ConfigNotSet { + key: AzureUrlSetting::KEY, + }) + } + + fn update(&mut self, _setting: AzureUrlSetting, value: ConnectUrl) -> ConfigSettingResult<()> { + self.data.azure.url = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: AzureUrlSetting) -> ConfigSettingResult<()> { + self.data.azure.url = None; + Ok(()) + } +} + +impl ConfigSettingAccessor<C8yUrlSetting> for TEdgeConfig { + fn query(&self, _setting: C8yUrlSetting) -> ConfigSettingResult<ConnectUrl> { + self.data + .c8y + .url + .clone() + .ok_or(ConfigSettingError::ConfigNotSet { + key: C8yUrlSetting::KEY, + }) + } + + fn update(&mut self, _setting: C8yUrlSetting, value: ConnectUrl) -> ConfigSettingResult<()> { + self.data.c8y.url = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: C8yUrlSetting) -> ConfigSettingResult<()> { + self.data.c8y.url = None; + Ok(()) + } +} + +impl ConfigSettingAccessor<DeviceCertPathSetting> for TEdgeConfig { + fn query(&self, _setting: DeviceCertPathSetting) -> ConfigSettingResult<PathBuf> { + Ok(self + .data + .device + .cert_path + .clone() + .unwrap_or_else(|| self.config_location.default_device_cert_path.clone())) + } + + fn update( + &mut self, + _setting: DeviceCertPathSetting, + value: PathBuf, + ) -> ConfigSettingResult<()> { + self.data.device.cert_path = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: DeviceCertPathSetting) -> ConfigSettingResult<()> { + self.data.device.cert_path = None; + Ok(()) + } +} + +impl ConfigSettingAccessor<DeviceKeyPathSetting> for TEdgeConfig { + fn query(&self, _setting: DeviceKeyPathSetting) -> ConfigSettingResult<PathBuf> { + Ok(self + .data + .device + .key_path + .clone() + .unwrap_or_else(|| self.config_location.default_device_key_path.clone())) + } + + fn update( + &mut self, + _setting: DeviceKeyPathSetting, + value: PathBuf, + ) -> ConfigSettingResult<()> { + self.data.device.key_path = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: DeviceKeyPathSetting) -> ConfigSettingResult<()> { + self.data.device.key_path = None; + Ok(()) + } +} + +impl ConfigSettingAccessor<AzureRootCertPathSetting> for TEdgeConfig { + fn query(&self, _setting: AzureRootCertPathSetting) -> ConfigSettingResult<PathBuf> { + Ok(self + .data + .azure + .root_cert_path + .clone() + .unwrap_or_else(|| self.config_location.default_azure_root_cert_path.clone())) + } + + fn update( + &mut self, + _setting: AzureRootCertPathSetting, + value: PathBuf, + ) -> ConfigSettingResult<()> { + self.data.azure.root_cert_path = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: AzureRootCertPathSetting) -> ConfigSettingResult<()> { + self.data.azure.root_cert_path = None; + Ok(()) + } +} + +impl ConfigSettingAccessor<C8yRootCertPathSetting> for TEdgeConfig { + fn query(&self, _setting: C8yRootCertPathSetting) -> ConfigSettingResult<PathBuf> { + Ok(self + .data + .c8y + .root_cert_path + .clone() + .unwrap_or_else(|| self.config_location.default_c8y_root_cert_path.clone())) + } + + fn update( + &mut self, + _setting: C8yRootCertPathSetting, + value: PathBuf, + ) -> ConfigSettingResult<()> { + self.data.c8y.root_cert_path = Some(value); + Ok(()) + } + + fn unset(&mut self, _setting: C8yRootCertPathSetting) -> ConfigSettingResult<()> { + self.data.c8y.root_cert_path = None; + Ok(()) + } +} diff --git a/tedge_config/src/tedge_config_dto.rs b/tedge_config/src/tedge_config_dto.rs new file mode 100644 index 00000000..7f67770d --- /dev/null +++ b/tedge_config/src/tedge_config_dto.rs @@ -0,0 +1,60 @@ +//! Crate-private plain-old data-type used for serialization. + +use crate::*; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Deserialize, Serialize)] +pub(crate) struct TEdgeConfigDto { + /// Captures the device specific configurations + #[serde(default)] + pub(crate) device: DeviceConfigDto, + + /// Captures the configurations required to connect to Cumulocity + #[serde(default)] + pub(crate) c8y: CumulocityConfigDto, + #[serde(default)] + pub(crate) azure: AzureConfigDto, +} + +/// Represents the device specific configurations defined in the [device] section +/// of the thin edge configuration TOML file +#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Deserialize, Serialize)] +pub(crate) struct DeviceConfigDto { + /// The unique id of the device + pub(crate) id: Option<String>, + + /// Path where the device's private key is stored. + /// Defaults to $HOME/.tedge/tedge-private.pem + pub(crate) key_path: Option<PathBuf>, + + /// Path where the device's certificate is stored. + /// Defaults to $HOME/.tedge/tedge-certificate.crt + pub(crate) cert_path: Option<PathBuf>, +} + +/// Represents the Cumulocity specific configurations defined in the +/// [c8y] section of the thin edge configuration TOML file +#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Deserialize, Serialize)] +pub(crate) struct CumulocityConfigDto { + /// Preserves the current status of the connection + pub(crate) connect: Option<String>, + + /// Endpoint URL of the Cumulocity tenant + pub(crate) url: Option<ConnectUrl>, + + /// The path where Cumulocity root certificate(s) are stored. + /// The value can be a directory path as well as the path of the direct certificate file. + pub(crate) root_cert_path: Option<PathBuf>, +} + +#[serde(deny_unknown_fields)] +#[derive(Debug, Default, Deserialize, Serialize)] +pub(crate) struct AzureConfigDto { + pub(crate) connect: Option<String>, + pub(crate) url: Option<ConnectUrl>, + pub(crate) root_cert_path: Option<PathBuf>, +} diff --git a/tedge_config/src/tedge_config_location.rs b/tedge_config/src/tedge_config_location.rs new file mode 100644 index 00000000..fdf45f51 --- /dev/null +++ b/tedge_config/src/tedge_config_location.rs @@ -0,0 +1,163 @@ +use std::path::{Path, PathBuf}; + +const DEFAULT_ETC_PATH: &str = "/etc"; +const TEDGE_CONFIG_FILE: &str = "tedge.toml"; + +/// Information about where `tedge.toml` is located and the defaults that are based +/// on that location. +/// +/// Broadly speaking, we distinguish two different locations: +/// +/// - System-wide locations under `/etc/tedge` +/// - User-local locations under `$HOME/.tedge` +/// +/// We DO NOT base the defaults on the currently executing user. Instead, we base +/// the defaults on the location of the `tedge.toml` file. If it is located in +/// `/etc`, regardless of the executing user, we use defaults that use system-wide +/// locations (e.g. `/etc/ssl/certs`). Whereas if `tedge.toml` is located in a users +/// home directory, we base the defaults on locations within the users home directory. +/// +/// This allows run `sudo tedge -c '$HOME/.tedge/tedge.toml ...` where the defaults are picked up +/// correctly. +/// +/// The choice, where to find `tedge.toml` on the other hand is based on the executing user AND the +/// env `$HOME`. But once we have found `tedge.toml`, we never again have to care about the +/// executing user (except when `chown`ing files...). +/// +#[derive(Debug, Clone)] +pub struct TEdgeConfigLocation { + /// Full path to `tedge.toml`. + pub tedge_config_path: PathBuf, + + /// Default device cert path + pub default_device_cert_path: PathBuf, + + /// Default device key path + pub default_device_key_path: PathBuf, + + /// Default path for azure root certificates + pub default_azure_root_cert_path: PathBuf, + + /// Default path for c8y root certificates + pub default_c8y_root_cert_path: PathBuf, +} + +impl TEdgeConfigLocation { + /// `tedge.toml` is located in `/etc/tedge`. All defaults are based on system locations. + pub fn from_default_system_location() -> Self { + Self::from_system_location(DEFAULT_ETC_PATH) + } + + /// `tedge.toml` is located in `${etc_path}/tedge`. All defaults are based on system locations. + pub fn from_system_location(etc_path: impl AsRef<Path>) -> Self { + let etc_path = etc_path.as_ref(); + let tedge_path = etc_path.join("tedge"); + Self { + tedge_config_path: tedge_path.join(TEDGE_CONFIG_FILE), + default_device_cert_path: tedge_path + .join("device-certs") + .join("tedge-certificate.pem"), + default_device_key_path: tedge_path + .join("device-certs") + .join("tedge-private-key.pem"), + default_azure_root_cert_path: etc_path.join("ssl").join("certs"), + default_c8y_root_cert_path: etc_path.join("ssl").join("certs"), + } + } + + /// `tedge.toml` is located in `$HOME/.tedge/tedge.toml`. All defaults are relative to the + /// `$HOME/.tedge` directory. + pub fn from_users_home_location(home_path: impl AsRef<Path>) -> Self { + let etc_path = Path::new(DEFAULT_ETC_PATH); + let tedge_path = home_path.as_ref().join(".tedge"); + Self { + tedge_config_path: tedge_path.join(TEDGE_CONFIG_FILE), + default_device_cert_path: tedge_path + .join("device-certs") + .join("tedge-certificate.pem"), + default_device_key_path: tedge_path + .join("device-certs") + .join("tedge-private-key.pem"), + default_azure_root_cert_path: etc_path.join("ssl").join("certs"), + default_c8y_root_cert_path: etc_path.join("ssl").join("certs"), + } + } +} + +#[test] +fn test_from_default_system_location() { + let config_location = TEdgeConfigLocation::from_default_system_location(); + assert_eq!( + config_location.tedge_config_path, + PathBuf::from("/etc/tedge/tedge.toml") + ); + assert_eq!( + config_location.default_device_cert_path, + PathBuf::from("/etc/tedge/device-certs/tedge-certificate.pem") + ); + assert_eq!( + config_location.default_device_key_path, + PathBuf::from("/etc/tedge/device-certs/tedge-private-key.pem") + ); + assert_eq!( + config_location.default_azure_root_cert_path, + PathBuf::from("/etc/ssl/certs") + ); + assert_eq!( + config_location.default_c8y_root_cert_path, + PathBuf::from("/etc/ssl/certs") + ); +} + +#[test] +fn test_from_system_location() { + // "/usr/local/etc" is often used for installed services on FreeBSD + let config_location = TEdgeConfigLocation::from_system_location("/usr/local/etc"); + assert_eq!( + config_location.tedge_config_path, + PathBuf::from("/usr/local/etc/tedge/tedge.toml") + ); + assert_eq!( + config_location.default_device_cert_path, + PathBuf::from("/usr/local/etc/tedge/device-certs/tedge-certificate.pem") + ); + assert_eq!( + config_location.default_device_key_path, + PathBuf::from("/usr/local/etc/tedge/device-certs/tedge-private-key.pem") + ); + // XXX: This should actually be "/etc/ssl/certs". + assert_eq!( + config_location.default_azure_root_cert_path, + PathBuf::from("/usr/local/etc/ssl/certs") + ); + // XXX: This should actually be "/etc/ssl/certs". + assert_eq!( + config_location.default_c8y_root_cert_path, + PathBuf::from("/usr/local/etc/ssl/certs") + ); +} + +#[test] +fn test_from_users_home_location() { + let config_location = TEdgeConfigLocation::from_users_home_location("/home/user"); + assert_eq!( + config_location.tedge_config_path, + PathBuf::from("/home/user/.tedge/tedge.toml") + ); + assert_eq!( + config_location.default_device_cert_path, + PathBuf::from("/home/user/.tedge/device-certs/tedge-certificate.pem") + ); + assert_eq!( + config_location.default_device_key_path, + PathBuf::from("/home/user/.tedge/device-certs/tedge-private-key.pem") + ); + assert_eq!( + config_location.default_azure_root_cert_path, + PathBuf::from("/etc/ssl/certs") + ); + assert_eq!( + config_location.default_c8y_root_cert_path, + PathBuf::from("/etc/ssl/certs") + ); +} diff --git a/tedge_config/src/tedge_config_repository.rs b/tedge_config/src/tedge_config_repository.rs new file mode 100644 index 00000000..97e60ecd --- /dev/null +++ b/tedge_config/src/tedge_config_repository.rs @@ -0,0 +1,78 @@ +use crate::*; +use std::io::Write; +use std::path::PathBuf; +use tempfile::NamedTempFile; + +/// TEdgeConfigRepository is resposible for loading and storing TEdgeConfig entities. +/// +pub struct TEdgeConfigRepository { + config_location: TEdgeConfigLocation, +} + +pub trait ConfigRepository<T> { + type Error; + fn load(&self) -> Result<T, Self::Error>; + fn store(&self, config: T) -> Result<(), Self::Error>; +} + +impl ConfigRepository<TEdgeConfig> for TEdgeConfigRepository { + type Error = TEdgeConfigError; + + fn load(&self) -> Result<TEdgeConfig, TEdgeConfigError> { + let config = self.read_file_or_default(self.config_location.tedge_config_path.clone())?; + Ok(config) + } + + // XXX: Explicitly set the file per |