diff --git a/edgelet/edgelet-core/src/module.rs b/edgelet/edgelet-core/src/module.rs index 12384563ce9..ef752fb93c2 100644 --- a/edgelet/edgelet-core/src/module.rs +++ b/edgelet/edgelet-core/src/module.rs @@ -14,7 +14,7 @@ use futures::{Future, Stream}; use pid::Pid; use serde_json; -use edgelet_utils::ensure_not_empty_with_context; +use edgelet_utils::{ensure_not_empty_with_context, serialize_ordered}; use error::{Error, ErrorKind, Result}; @@ -144,6 +144,7 @@ pub struct ModuleSpec { type_: String, config: T, #[serde(default = "HashMap::new")] + #[serde(serialize_with = "serialize_ordered")] env: HashMap, } diff --git a/edgelet/edgelet-utils/src/lib.rs b/edgelet/edgelet-utils/src/lib.rs index 462743329fb..41b42b7803d 100644 --- a/edgelet/edgelet-utils/src/lib.rs +++ b/edgelet/edgelet-utils/src/lib.rs @@ -40,7 +40,7 @@ use std::collections::HashMap; pub use error::{Error, ErrorKind}; pub use logging::log_failure; pub use macros::ensure_not_empty_with_context; -pub use ser_de::{serde_clone, string_or_struct}; +pub use ser_de::{serde_clone, serialize_ordered, string_or_struct}; pub fn parse_query(query: &str) -> HashMap<&str, &str> { query diff --git a/edgelet/edgelet-utils/src/ser_de.rs b/edgelet/edgelet-utils/src/ser_de.rs index 2179067b98d..e0dad0ea594 100644 --- a/edgelet/edgelet-utils/src/ser_de.rs +++ b/edgelet/edgelet-utils/src/ser_de.rs @@ -1,13 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. +use std::collections::{BTreeMap, HashMap}; use std::fmt; +use std::hash::BuildHasher; use std::marker::PhantomData; use std::result::Result as StdResult; use std::str::FromStr; use failure::ResultExt; use serde::de::{self, Deserialize, DeserializeOwned, Deserializer, MapAccess, Visitor}; -use serde::ser::Serialize; +use serde::ser::{Serialize, Serializer}; use serde_json; use error::{ErrorKind, Result}; @@ -67,6 +69,18 @@ where .context(ErrorKind::SerdeClone)?) } +pub fn serialize_ordered( + x: &HashMap, + serializer: S, +) -> StdResult +where + S: Serializer, + T: BuildHasher, +{ + let sorted_map: BTreeMap<_, _> = x.into_iter().collect(); + sorted_map.serialize(serializer) +} + #[cfg(test)] mod tests { use super::*; @@ -94,6 +108,12 @@ mod tests { options: Options, } + #[derive(Debug, Serialize)] + struct Setting { + #[serde(serialize_with = "serialize_ordered")] + map: HashMap, + } + #[test] fn deser_from_map() { let container_json = json!({ @@ -151,4 +171,26 @@ mod tests { assert_eq!(c1.name, c2.name); assert_eq!(c1.age, c2.age); } + + #[test] + fn serde_serialize_map() { + let setting_json = json!({ + "map": { + "a": "val1", + "b": "val2", + "c": "val3" + } + }) + .to_string(); + + let mut map = HashMap::new(); + map.insert("b".to_string(), "val2".to_string()); + map.insert("a".to_string(), "val1".to_string()); + map.insert("c".to_string(), "val3".to_string()); + + let map_container = Setting { map }; + + let s = serde_json::to_string(&map_container).unwrap(); + assert_eq!(s, setting_json); + } } diff --git a/edgelet/iotedged/src/settings.rs b/edgelet/iotedged/src/settings.rs index 008ed18966d..948d4b2eea5 100644 --- a/edgelet/iotedged/src/settings.rs +++ b/edgelet/iotedged/src/settings.rs @@ -268,6 +268,8 @@ mod tests { #[cfg(unix)] static GOOD_SETTINGS1: &str = "test/linux/sample_settings1.yaml"; #[cfg(unix)] + static GOOD_SETTINGS2: &str = "test/linux/sample_settings2.yaml"; + #[cfg(unix)] static BAD_SETTINGS: &str = "test/linux/bad_sample_settings.yaml"; #[cfg(unix)] static GOOD_SETTINGS_TG: &str = "test/linux/sample_settings.tg.yaml"; @@ -277,6 +279,8 @@ mod tests { #[cfg(windows)] static GOOD_SETTINGS1: &str = "test/windows/sample_settings1.yaml"; #[cfg(windows)] + static GOOD_SETTINGS2: &str = "test/windows/sample_settings2.yaml"; + #[cfg(windows)] static BAD_SETTINGS: &str = "test/windows/bad_sample_settings.yaml"; #[cfg(windows)] static GOOD_SETTINGS_TG: &str = "test/windows/sample_settings.tg.yaml"; @@ -364,6 +368,22 @@ mod tests { assert_eq!(settings.diff_with_cached(path).unwrap(), false); } + #[test] + fn diff_with_same_cached_env_var_unordered_returns_false() { + let tmp_dir = TempDir::new("blah").unwrap(); + let path = tmp_dir.path().join("cache"); + let settings1 = Settings::::new(Some(GOOD_SETTINGS2)).unwrap(); + let settings_to_write = serde_json::to_string(&settings1).unwrap(); + let sha_to_write = Sha256::digest_str(&settings_to_write); + let base64_to_write = base64::encode(&sha_to_write); + FsFile::create(path.clone()) + .unwrap() + .write_all(base64_to_write.as_bytes()) + .unwrap(); + let settings = Settings::::new(Some(GOOD_SETTINGS)).unwrap(); + assert_eq!(settings.diff_with_cached(path).unwrap(), false); + } + #[test] fn diff_with_different_cached_returns_true() { let tmp_dir = TempDir::new("blah").unwrap(); diff --git a/edgelet/iotedged/test/linux/sample_settings.yaml b/edgelet/iotedged/test/linux/sample_settings.yaml index 768a5624fd3..e2ae2d07f46 100644 --- a/edgelet/iotedged/test/linux/sample_settings.yaml +++ b/edgelet/iotedged/test/linux/sample_settings.yaml @@ -6,7 +6,9 @@ provisioning: agent: name: "edgeAgent" type: "docker" - env: {} + env: + abc: "value1" + acd: "value2" config: image: "microsoft/azureiotedge-agent:1.0" auth: {} diff --git a/edgelet/iotedged/test/linux/sample_settings2.yaml b/edgelet/iotedged/test/linux/sample_settings2.yaml new file mode 100644 index 00000000000..3172340b8bc --- /dev/null +++ b/edgelet/iotedged/test/linux/sample_settings2.yaml @@ -0,0 +1,31 @@ + +# Configures the provisioning mode +provisioning: + source: "manual" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" +agent: + name: "edgeAgent" + type: "docker" + env: + acd: "value2" + abc: "value1" + config: + image: "microsoft/azureiotedge-agent:1.0" + auth: {} +hostname: "localhost" + +# Sets the connection uris for clients +connect: + workload_uri: "http://localhost:8081" + management_uri: "http://localhost:8080" + +# Sets the uris to listen on +# These can be different than the connect uris. +# For instance, when using the fd:// scheme for systemd +listen: + workload_uri: "http://0.0.0.0:8081" + management_uri: "http://0.0.0.0:8080" +homedir: "/tmp" +moby_runtime: + uri: "http://localhost:2375" + network: "azure-iot-edge" diff --git a/edgelet/iotedged/test/windows/sample_settings.yaml b/edgelet/iotedged/test/windows/sample_settings.yaml index 93226254bb8..d748c107e61 100644 --- a/edgelet/iotedged/test/windows/sample_settings.yaml +++ b/edgelet/iotedged/test/windows/sample_settings.yaml @@ -6,7 +6,9 @@ provisioning: agent: name: "edgeAgent" type: "docker" - env: {} + env: + abc: "value1" + acd: "value2" config: image: "microsoft/azureiotedge-agent:1.0" auth: {} diff --git a/edgelet/iotedged/test/windows/sample_settings2.yaml b/edgelet/iotedged/test/windows/sample_settings2.yaml new file mode 100644 index 00000000000..7f20f746893 --- /dev/null +++ b/edgelet/iotedged/test/windows/sample_settings2.yaml @@ -0,0 +1,31 @@ + +# Configures the provisioning mode +provisioning: + source: "manual" + device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=something" +agent: + name: "edgeAgent" + type: "docker" + env: + acd: "value2" + abc: "value1" + config: + image: "microsoft/azureiotedge-agent:1.0" + auth: {} +hostname: "localhost" + +# Sets the connection uris for clients +connect: + workload_uri: "http://localhost:8081" + management_uri: "http://localhost:8080" + +# Sets the uris to listen on +# These can be different than the connect uris. +# For instance, when using the fd:// scheme for systemd +listen: + workload_uri: "http://0.0.0.0:8081" + management_uri: "http://0.0.0.0:8080" +homedir: "C:\\Temp" +moby_runtime: + uri: "http://localhost:2375" + network: "azure-iot-edge"