use toml::Value;
use failure::Error;
use errors::*;
use url::Url;
use std::fs::File;
use std::io::Read;
use std::env;
use std::path::PathBuf;
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
pub struct Config {
pub chef_server_url: String,
node_name: Option<String>,
client_name: Option<String>,
#[serde(skip)] profile: String,
client_key: String,
pub validator_key: Option<String>,
#[serde(default = "default_auth_string")]
pub sign_ver: String,
}
impl Config {
pub fn from_str(toml: &str, profile: &str) -> Result<Self, Error> {
let credentials = toml.parse::<Value>().unwrap();
let credentials = credentials[profile].clone();
let mut creds: Config = credentials
.try_into()
.map_err(ChefError::TomlDeserializeError)?;
creds.profile = profile.into();
Ok(creds)
}
pub fn from_credentials(profile: Option<&str>) -> Result<Self, Error> {
let credentials = get_chef_path("credentials")?;
let profile = select_profile_name(profile);
debug!(
"Opening credentials file: {:?} with profile: {:?}",
credentials, profile
);
match File::open(&credentials) {
Ok(mut fh) => {
let mut config = String::new();
fh.read_to_string(&mut config)?;
Self::from_str(&config, &profile)
}
Err(_) => Err(ChefError::UnparseableConfigError(String::from(
"Unable to read credentials file",
)).into()),
}
}
pub fn client_name(&self) -> Result<&str, Error> {
let profile = self.profile.clone();
if self.client_name.is_some() && self.node_name.is_some() {
Err(ChefError::DuplicateClientNameError(profile).into())
} else if self.node_name.is_some() {
self.node_name
.as_ref()
.ok_or_else(|| {
ChefError::UnparseableConfigError(format!(
"failed to read node name for profile: {}",
profile
)).into()
})
.and_then(|n| Ok(n.as_ref()))
} else if self.client_name.is_some() {
self.client_name
.as_ref()
.ok_or_else(|| {
ChefError::UnparseableConfigError(format!(
"failed to read client name for profile: {}",
profile
)).into()
})
.and_then(|n| Ok(n.as_ref()))
} else {
Err(ChefError::UnparseableConfigError(format!(
"No node_name or client_name found for profile: {}",
profile
)).into())
}
}
pub fn key(&self) -> Result<Vec<u8>, Error> {
if self.client_key
.starts_with("-----BEGIN RSA PRIVATE KEY-----")
{
Ok(self.client_key.as_bytes().into())
} else {
let key_path = get_chef_path(&self.client_key)?;
debug!("Reading key from {:?}", key_path);
let mut key: Vec<u8> = vec![];
File::open(&key_path).and_then(|mut fh| fh.read_to_end(&mut key))?;
debug!("Read private key: {:?}", key);
Ok(key)
}
}
fn endpoint(&self) -> Result<Url, Error> {
Url::parse(self.chef_server_url.as_ref()).map_err(|e| e.into())
}
pub fn organization_path(&self) -> Result<String, Error> {
let endpoint = self.endpoint()?;
Ok(endpoint.path().into())
}
pub fn url_base(&self) -> Result<String, Error> {
let endpoint = self.endpoint()?;
let host = &endpoint.host_str().unwrap();
let port = &endpoint.port_or_known_default().unwrap();
let scheme = &endpoint.scheme();
Ok(format!("{}://{}:{}", scheme, host, port))
}
}
fn select_profile_name(name: Option<&str>) -> String {
if name.is_some() {
debug!("Using explicit profile name");
return name.unwrap().into();
};
if let Ok(env) = env::var("CHEF_PROFILE") {
debug!("Using environment variable for profile name");
return env;
};
let path = match get_chef_path("context") {
Ok(p) => p,
Err(_) => {
debug!("Unable to figure out path to context file, using default profile");
return String::from("default");
}
};
if let Ok(mut fh) = File::open(path) {
debug!("Reading profile context from file");
let mut contents = String::new();
return match fh.read_to_string(&mut contents) {
Ok(_) => contents,
Err(_) => {
info!("Failed to read profile from file, using default");
String::from("default")
}
};
}
debug!("Using default profile name");
String::from("default")
}
fn get_chef_path(val: &str) -> Result<String, Error> {
let home_dir = match env::home_dir() {
Some(path) => path,
None => {
return Err(ChefError::PrivateKeyError(String::from(
"Could not identify user's home directory",
)).into())
}
};
let mut p = PathBuf::from(val);
if !p.is_absolute() {
p = home_dir;
p.push(".chef");
p.push(val)
}
match p.to_str() {
Some(path) => Ok(path.to_owned()),
None => Err(ChefError::PrivateKeyError(String::from(
"Could not construct a path to the user's .chef directory",
)).into()),
}
}
fn default_auth_string() -> String {
"1.3".into()
}
#[cfg(test)]
mod tests {
use super::*;
use std;
const CREDENTIALS: &'static str = r#"
[default]
node_name = 'barney'
client_key = 'barney_rubble.pem'
chef_server_url = 'https://api.chef.io/organizations/bedrock'
"#;
#[test]
fn test_from_str() {
let cfg = Config::from_str(CREDENTIALS, "default").unwrap();
assert_eq!(cfg.node_name.unwrap(), "barney")
}
#[test]
fn test_organization_path() {
let cfg = Config::from_str(CREDENTIALS, "default").unwrap();
assert_eq!(cfg.organization_path().unwrap(), "/organizations/bedrock")
}
#[test]
fn test_default_profile_name() {
std::env::remove_var("CHEF_PROFILE");
assert_eq!(select_profile_name(None), String::from("default"))
}
#[test]
fn test_environment_profile_name() {
std::env::set_var("CHEF_PROFILE", "environment");
assert_eq!(select_profile_name(None), String::from("environment"));
std::env::remove_var("CHEF_PROFILE");
}
#[test]
fn test_explicit_profile_name() {
assert_eq!(
select_profile_name(Some("explicit")),
String::from("explicit")
)
}
#[test]
fn test_get_chef_path() {
let home = std::env::home_dir().unwrap();
std::env::set_var("HOME", "/home/barney");
let path = get_chef_path("credentials").unwrap();
assert_eq!(path, "/home/barney/.chef/credentials");
std::env::set_var("HOME", home);
}
#[test]
fn test_absolute_get_chef_path() {
let home = std::env::home_dir().unwrap();
std::env::set_var("HOME", "/home/barney");
let path = get_chef_path("/home/fred/.chef/fred.pem").unwrap();
assert_eq!(path, "/home/fred/.chef/fred.pem");
std::env::set_var("HOME", home);
}
}