kubos_system/
config.rs

1//
2// Copyright (C) 2018 Kubos Corporation
3//
4// Licensed under the Apache License, Version 2.0 (the "License")
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15//
16use failure::{bail, Error};
17use serde_derive::Deserialize;
18use std::env;
19use std::fs::File;
20use std::io;
21use std::io::prelude::*;
22
23use toml::Value;
24
25/// The default configuration file path
26pub static DEFAULT_PATH: &str = "/etc/kubos-config.toml";
27
28#[derive(Clone, Debug, Deserialize)]
29/// A simple address consisting of an IP address and port number
30pub struct Address {
31    ip: String,
32    port: u16,
33}
34
35impl Address {
36    /// Returns the IP portion of this address
37    pub fn ip(&self) -> &str {
38        &self.ip
39    }
40
41    /// Returns the port of this address
42    pub fn port(&self) -> u16 {
43        self.port
44    }
45}
46
47/// KubOS config used by either Apps or Services. KubOS config files use the TOML format, and can
48/// may contain multiple named Categories. Typically each category corresponds to an App or Service
49/// name. This allows one config file to store configuration for multiple Apps / Services at a
50/// time.
51///
52/// Example KubOS config files for a Service called `my-service` with an IP/port binding
53/// ```toml
54/// [my-service]
55/// my-property = "value"
56///
57/// [my-service.addr]
58/// ip = 0.0.0.0
59/// port = 8181
60/// ```
61///
62#[derive(Clone, Debug)]
63pub struct Config {
64    addr: Option<Address>,
65    raw: Value,
66}
67
68impl Default for Config {
69    fn default() -> Self {
70        Config {
71            addr: None,
72            raw: Value::String("".to_string()),
73        }
74    }
75}
76
77impl Config {
78    /// Creates and parses configuration data from the system configuration
79    /// file or the path passed as the '-c' or '--config' option to this
80    /// executable.
81    ///
82    /// # Arguments
83    /// `name` - Category name used as a key in the config file
84    pub fn new(name: &str) -> Result<Self, Error> {
85        Self::new_from_path(name, get_config_path()?)
86    }
87
88    /// Creates and parses configuration data from the passed in configuration
89    /// path.
90    /// # Arguments
91    /// `name` - Category name used as a key in the config file
92    /// `path` - Path to configuration file
93    pub fn new_from_path(name: &str, path: String) -> Result<Self, Error> {
94        parse_config_file(name, path)
95    }
96
97    /// Creates and parses configuration data from the passed in configuration
98    /// string.
99    /// # Arguments
100    /// `name` - Category name used as a key in the config
101    /// `config` - Config data as a string
102    pub fn new_from_str(name: &str, config: &str) -> Result<Self, Error> {
103        parse_config_str(name, config)
104    }
105
106    /// Returns the configured hosturl string in the following
107    /// format (using IPv4 addresses) - 0.0.0.0:0000
108    pub fn hosturl(&self) -> Option<String> {
109        self.addr
110            .as_ref()
111            .map(|addr| format!("{}:{}", addr.ip(), addr.port()))
112    }
113
114    /// Returns the category's configuration information
115    /// in the `toml::Value` format.
116    /// This will contain the ip/port if provided, along with any other
117    /// configuration information found in the config file.
118    ///
119    /// ### Examples
120    ///
121    /// ```rust,no_run
122    /// use kubos_system::Config;
123    ///
124    /// let config = Config::new("example-service").unwrap();
125    /// let raw = config.raw();
126    /// let bus = raw["bus"].as_str();
127    /// ```
128    pub fn raw(&self) -> toml::Value {
129        self.raw.clone()
130    }
131
132    /// Performs a get on the raw config data
133    ///
134    /// # Arguments
135    /// `key` - Key of value to get from config
136    pub fn get(&self, key: &str) -> Option<toml::Value> {
137        self.raw.get(key).cloned()
138    }
139
140    //// Get config data encoded as a hexadecimal string
141    ///
142    /// * Arguments
143    /// `key` - Key of value to get from config
144    pub fn get_hex(&self, key: &str) -> Option<u8> {
145        let val = self.raw.get(key)?.as_str()?;
146        let val = val.strip_prefix("0x").unwrap_or(val);
147
148        u8::from_str_radix(val, 16).ok()
149    }
150}
151
152fn get_config_path() -> Result<String, Error> {
153    // Manually check for a "-c {config-path}" command line argument specifying a custom config
154    // file path to use.
155    // Doing it this way so that entities which use this module (apps, services) can have any
156    // number of additional command arguments
157    let mut args = env::args();
158
159    // Navigate to the "-c" option
160    let config_arg_pos = args.position(|arg| arg == "-c");
161
162    if let Some(_pos) = config_arg_pos {
163        // The config path will be the arg immediately after "-c"
164        match args.next() {
165            Some(path) => Ok(path),
166            None => bail!("The '-c' arg was specified, but no path value was provided"),
167        }
168    } else {
169        // The "-c" arg wasn't specified, so we can go ahead with the default
170        Ok(DEFAULT_PATH.to_string())
171    }
172}
173
174fn get_file_data(path: String) -> Result<String, io::Error> {
175    let mut contents = String::new();
176    let mut file = File::open(path)?;
177    file.read_to_string(&mut contents)?;
178    Ok(contents)
179}
180
181fn parse_config_file(name: &str, path: String) -> Result<Config, Error> {
182    let contents = get_file_data(path)?;
183    parse_config_str(name, &contents)
184}
185
186fn parse_config_str(name: &str, contents: &str) -> Result<Config, Error> {
187    let data: Value = toml::from_str(contents)?;
188    let mut config = Config::default();
189
190    if let Some(data) = data.get(name) {
191        if let Some(address) = data.get("addr") {
192            config.addr = Some(address.clone().try_into()?);
193        }
194        config.raw = data.clone();
195    } else {
196        bail!("Failed to find {} in config", name);
197    }
198
199    Ok(config)
200}