-
Notifications
You must be signed in to change notification settings - Fork 463
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Don't lower-case the keys when parsing config.yaml (#1038)
`struct DockerConfig` is set up so that its serde Deserializer expects a field named `createOptions`. However the config crate's YAML parser normalizes all keys to lower case. Even if the user sets `agent.config.createOptions` in `config.yaml`, the serde Deserializer will see a key named `createoptions` and discard it as unknown. The actual `create_options` field is then set to an empty default. This prevents users from having custom create options for the Edge Agent container. With this change, the code no longer use the `config::File` source to parse `config.yaml`. Instead, it uses a new `YamlFileSource` that also impls `config::Source`. This alternative source has similar code to `config::File` but does not lower-case the field names. Since the `config::Environment` source still lower-cases the keys that it parses from environment variables, it does mean that it will not be able to override keys in the config.yaml that are of mixed-case. For example, `IOTEDGE_FOO` will override `foo` and not `Foo`. In practice, all the top-level keys that could be easily overridden are completely lower-case, so it is not likely to break any customers. Fixes #1036
- Loading branch information
Showing
6 changed files
with
236 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
use std::borrow::Cow; | ||
use std::collections::HashMap; | ||
use std::fs::File; | ||
use std::io::Read; | ||
use std::path::PathBuf; | ||
|
||
use config::{ConfigError, Source, Value}; | ||
use yaml_rust::{Yaml, YamlLoader}; | ||
|
||
/// This is similar to [`config::File`] in that it parses a YAML string or file and implements `config::Source`. | ||
/// | ||
/// We use this to parse our config.yaml instead of `config::File` because `config::File` lower-cases all field names that it reads. | ||
/// This causes issues with fields like `agent.config.createOptions` since the config crate returns `agent.config.createoptions` | ||
/// which the serde deserializer ignores. | ||
#[derive(Clone, Debug)] | ||
pub(crate) enum YamlFileSource { | ||
File(PathBuf), | ||
String(&'static str), | ||
} | ||
|
||
impl Source for YamlFileSource { | ||
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> { | ||
Box::new(self.clone()) | ||
} | ||
|
||
fn collect(&self) -> Result<HashMap<String, Value>, ConfigError> { | ||
let origin = match self { | ||
YamlFileSource::File(path) => Some(path.to_string_lossy().into_owned()), | ||
YamlFileSource::String(_) => None, | ||
}; | ||
|
||
let contents = match self { | ||
YamlFileSource::File(path) => { | ||
let mut file = | ||
File::open(path).map_err(|err| ConfigError::Foreign(Box::new(err)))?; | ||
let mut contents = String::new(); | ||
let _ = file | ||
.read_to_string(&mut contents) | ||
.map_err(|err| ConfigError::Foreign(Box::new(err)))?; | ||
Cow::Owned(contents) | ||
} | ||
|
||
YamlFileSource::String(s) => Cow::Borrowed(*s), | ||
}; | ||
|
||
let docs = YamlLoader::load_from_str(&*contents) | ||
.map_err(|err| ConfigError::Foreign(Box::new(err)))?; | ||
|
||
let mut docs = docs.into_iter(); | ||
let doc = match docs.next() { | ||
Some(doc) => { | ||
if docs.next().is_some() { | ||
return Err(ConfigError::Foreign(Box::new( | ||
YamlFileSourceError::MoreThanOneDocument, | ||
))); | ||
} | ||
|
||
doc | ||
} | ||
|
||
None => Yaml::Hash(Default::default()), | ||
}; | ||
|
||
let mut result = HashMap::new(); | ||
|
||
if let Yaml::Hash(hash) = doc { | ||
for (key, value) in hash { | ||
if let Yaml::String(key) = key { | ||
result.insert(key, from_yaml_value(origin.as_ref(), value)?); | ||
} | ||
} | ||
} | ||
|
||
Ok(result) | ||
} | ||
} | ||
|
||
/// Identical to https://github.com/mehcode/config-rs/blob/0.8.0/src/file/format/yaml.rs#L32-L68 | ||
/// except that it does not lower-case hash keys. | ||
/// | ||
/// Unfortunately the `ValueKind` enum used by the `Value` constructor is not exported from the crate. | ||
/// It does however impl `From` for the various corresponding standard types, so this code uses those. | ||
/// The only difference is the fallback `_` case at the end. | ||
fn from_yaml_value(uri: Option<&String>, value: Yaml) -> Result<Value, ConfigError> { | ||
match value { | ||
Yaml::String(value) => Ok(Value::new(uri, value)), | ||
Yaml::Real(value) => { | ||
// TODO: Figure out in what cases this can fail? | ||
Ok(Value::new( | ||
uri, | ||
value | ||
.parse::<f64>() | ||
.map_err(|err| ConfigError::Foreign(Box::new(err)))?, | ||
)) | ||
} | ||
Yaml::Integer(value) => Ok(Value::new(uri, value)), | ||
Yaml::Boolean(value) => Ok(Value::new(uri, value)), | ||
Yaml::Hash(table) => { | ||
let mut m = HashMap::new(); | ||
for (key, value) in table { | ||
if let Yaml::String(key) = key { | ||
m.insert(key, from_yaml_value(uri, value)?); | ||
} | ||
// TODO: should we do anything for non-string keys? | ||
} | ||
Ok(Value::new(uri, m)) | ||
} | ||
Yaml::Array(array) => { | ||
let mut l = vec![]; | ||
|
||
for value in array { | ||
l.push(from_yaml_value(uri, value)?); | ||
} | ||
|
||
Ok(Value::new(uri, l)) | ||
} | ||
|
||
// 1. Yaml NULL | ||
// 2. BadValue – It shouldn't be possible to hit BadValue as this only happens when | ||
// using the index trait badly or on a type error but we send back nil. | ||
// 3. Alias – No idea what to do with this and there is a note in the lib that its | ||
// not fully supported yet anyway | ||
// | ||
// The original function returns `Value::new(uri, ValueKind::Nil)` here. | ||
// Since `ValueKind` is private, we have to return Err instead. It shouldn't be a problem for our use case | ||
// since we don't expect null / bad value / alias. | ||
value => Err(ConfigError::Foreign(Box::new( | ||
YamlFileSourceError::UnrecognizedYamlValue(value), | ||
))), | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
enum YamlFileSourceError { | ||
MoreThanOneDocument, | ||
UnrecognizedYamlValue(Yaml), | ||
} | ||
|
||
impl std::fmt::Display for YamlFileSourceError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
match self { | ||
YamlFileSourceError::MoreThanOneDocument => { | ||
write!(f, "more than one YAML document provided") | ||
} | ||
YamlFileSourceError::UnrecognizedYamlValue(value) => { | ||
write!(f, "unrecognized YAML value {:?}", value) | ||
} | ||
} | ||
} | ||
} | ||
|
||
impl std::error::Error for YamlFileSourceError {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
provisioning: | ||
source: "manual" | ||
device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" | ||
agent: | ||
name: "edgeAgent" | ||
type: "docker" | ||
env: | ||
AbC: "VAluE1" | ||
DeF: "VAluE2" | ||
config: | ||
image: "microsoft/azureiotedge-agent:1.0" | ||
auth: {} | ||
createOptions: | ||
Hostname: VAluE3 | ||
hostname: "localhost" | ||
|
||
connect: | ||
workload_uri: "http://localhost:8081" | ||
management_uri: "http://localhost:8080" | ||
|
||
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
provisioning: | ||
source: "manual" | ||
device_connection_string: "HostName=something.something.com;DeviceId=something;SharedAccessKey=QXp1cmUgSW9UIEVkZ2U=" | ||
agent: | ||
name: "edgeAgent" | ||
type: "docker" | ||
env: | ||
AbC: "VAluE1" | ||
DeF: "VAluE2" | ||
config: | ||
image: "microsoft/azureiotedge-agent:1.0" | ||
auth: {} | ||
createOptions: | ||
Hostname: VAluE3 | ||
hostname: "localhost" | ||
|
||
connect: | ||
workload_uri: "http://localhost:8081" | ||
management_uri: "http://localhost:8080" | ||
|
||
listen: | ||
workload_uri: "http://0.0.0.0:8081" | ||
management_uri: "http://0.0.0.0:8080" | ||
|
||
homedir: "C:\\Temp" | ||
|
||
moby_runtime: | ||
uri: "npipe://./pipe/iotedge_moby_engine" | ||
network: "azure-iot-edge" |