summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMichael Neumann <mneumann@ntecs.de>2021-04-07 15:19:06 +0200
committerGitHub <noreply@github.com>2021-04-07 15:19:06 +0200
commit6472cb57fe196b5e19bc7a5eceeb994843dfa095 (patch)
tree74b7036564396239c1f8946602b297d3505c3660
parentf9c30859d284145c25a1fd9121f968c7d011994a (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.lock12
-rw-r--r--Cargo.toml1
-rw-r--r--tedge/Cargo.toml1
-rw-r--r--tedge_config/Cargo.toml14
-rw-r--r--tedge_config/src/config_setting.rs42
-rw-r--r--tedge_config/src/error.rs26
-rw-r--r--tedge_config/src/lib.rs12
-rw-r--r--tedge_config/src/models/connect_url.rs74
-rw-r--r--tedge_config/src/models/mod.rs3
-rw-r--r--tedge_config/src/settings.rs128
-rw-r--r--tedge_config/src/tedge_config.rs176
-rw-r--r--tedge_config/src/tedge_config_dto.rs60
-rw-r--r--tedge_config/src/tedge_config_location.rs163
-rw-r--r--tedge_config/src/tedge_config_repository.rs78
-rw-r--r--tedge_config/tests/test_tedge_config.rs481
15 files changed, 1271 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index d45acba7..a604d262 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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 = [
diff --git a/Cargo.toml b/Cargo.toml
index f5e84120..c009a6bb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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