chiark / gitweb /
wip impl config reading
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 23 Jul 2021 10:01:31 +0000 (11:01 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Fri, 23 Jul 2021 10:01:31 +0000 (11:01 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
src/bin/client.rs
src/config.rs

index b8d2cf5a1edd586f017eb9679f04fa055466a9af..bf2d5969a25b93c4940c2bff96251a573757f21b 100644 (file)
@@ -4,7 +4,7 @@
 
 use hippotat::prelude::*;
 
+#[throws(AE)]
 fn main() {
-  let opts = config::Opts::from_args();
-  eprintln!("{:#?}", &opts);
+  config::read();
 }
index a04824572484772d074b90c609cb4f72fad58575..1d6caf29813d9356aa9955ec292a30d6de14a0a2 100644 (file)
@@ -4,9 +4,13 @@
 
 use crate::prelude::*;
 
+use configparser::ini::Ini;
+
+static MAIN_CONFIGS: [&str;_] = ["main.cfg", "config.d", "secrets.d"];
+
 #[derive(StructOpt,Debug)]
 pub struct Opts {
-  /// Default config file or directory
+  /// Top-level config file or directory
   ///
   /// Look for `main.cfg`, `config.d` and `secrets.d` here.
   ///
@@ -14,7 +18,7 @@ pub struct Opts {
   #[structopt(long, default_value="/etc/hippotat")]
   pub config: PathBuf,
   
-  /// Read this in addition, after the other config files
+  /// Additional config files or dirs, which can override the others
   #[structopt(long, multiple=true, number_of_values=1)]
   pub extra_config: Vec<PathBuf>,
 }
@@ -54,3 +58,146 @@ pub struct InstanceConfig {
   pub url:                          Uri,
   pub vroutes:                      Vec<CidrString>,
 }
+
+type SectionMap = HashMap<String, Option<String>>;
+
+pub struct Config {
+  opts: Opts,
+}
+
+static OUTSIDE_SECTION: &str = "[";
+
+struct Aggregate {
+  sections: HashMap<String, SectionMap>,
+}
+
+impl Aggregate {
+  #[throws(AE)] // AE does not include path
+  fn read_file(&mut self, path: Path,
+               ekok: &Fn(ErrorKind) -> Result<(),()>)
+               -> Result<(), io::ErrorKind>
+  {
+    let f = match File::open(path) {
+      Err(e) if ekok(e.kind()) => return Err(e.kind()),
+      r => r.context("open")?;
+    }
+
+    let mut s = String::new();
+    f.read_to_string(&mut s).context("read")?;
+
+    let mut ini = configparser::ini::Ini::new_cs();
+    ini.set_default_section(OUTSIDE_SECTION);
+    ini.read(s).context("parse as INI");
+    let map = mem::take(ini.get_mut_map);
+    if map.get(OUTSIDE_SECTION).is_some() {
+      throw!(anyhow!("INI file contains settings outside a section"));
+    }
+
+    // xxx parse section names here
+    // xxx save Arc<PathBuf> where we found each item
+
+    self.sections.extend(map.drain());
+    Ok(())
+  }
+
+  #[throws(AE)] // AE includes path
+  fn read_dir_d(&mut self, path: Path
+                ekok: &Fn(ErrorKind))
+                -> Result<(), io::ErrorKind>
+  {
+    let wc = || format("{:?}", path);
+    for ent in match fs::read_dir(path) {
+      Err(e) if ekok(e.kind()) => return Err(e.kind()),
+      r => r.context("open directory").with_context(wc)?;
+    } {
+      let ent = ent.context("read directory").with_context(wc)?;
+      let leaf = ent.file_name().as_str();
+      let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
+      if leaf.length() == 0 { continue }
+      if ! leaf.chars().all(
+        |c| c=='-' || c=='_' || c.is_ascii_alphenumeric()
+      ) { continue }
+
+      // OK we want this one
+      self.read_file(&ent.path, &|_| false)
+        .with_context(format!("{:?}", &ent.path))??;
+    }
+  }
+
+  #[throws(AE)] // AE includes everything
+  fn read_toplevel(&mut self, toplevel: &Path) {
+    match se;lf.read_file(
+      toplevel,
+      |k| matches!(k, EK::NotFound || EK::IsDirectory)
+    )
+      .with_context(format!("{:?}", toplevel))
+      .context("top-level config directory (or file)")?
+    {
+      Err(EK::NotFound) => { },
+
+      Err(EK::IsDirectory) => {
+        let mk = |leaf| [ toplevel, leaf ].iter().collect::<PathBuf>();
+
+        for &(try_main, desc) in &[
+          ("main.cfg", "main config file"),
+          ("master.cfg", "obsolete-named main config file"),
+        ] {
+          let main = mk(try_main);
+          match self.read_file(&main, |e| e == EK::NotFound)
+            .with_context(format!("{:?}", &main))
+            .context(desc)?
+          {
+            Ok(()) => break,
+            Err(EK::NotFound) => { },
+            x => panic!("huh? {:?}", &x),
+          }
+        }
+
+        for &(try_dir, desc) in &[
+          ("config.d", "per-link config directory"),
+          ("secrets.d", "per-link secrets directory"),
+        ] {
+          let dir = mk(try_dir);
+          match agg.read_dir(&dir, |k| k == EK::NotFound).context(desc)? {
+            Ok(()) => { },
+            Err(EK::NotFound) => { },
+            x => panic!("huh? {:?}", &x),
+          }
+        }
+      }
+    }
+  }
+
+  #[throws(AE)] // AE includes extra, but does that this is extra
+  fn read_extra(&mut self, extra: &Path) {
+
+    match self.read_file(extra, |k| k == EK::IsDirectory)
+      .with_context(format!("{:?}", extra))?
+    {
+      Ok(()) => return,
+      Err(EK::IsDirectory) => { }
+      x => panic!("huh? {:?}", &x),
+    }
+
+    self.read_dir(extra, |_| false)??;
+  }
+}
+
+
+#[throws(AE)]
+pub fn read() {
+  let opts = config::Opts::from_args();
+
+  (||{
+    let agg = Aggregate::default();
+
+    agg.read_toplevel(&opts.config)?;
+    for extra in &opts.extra_config {
+      agg.read_extra(extra).context("extra config")?;
+    }
+
+    eprintln!("GOT {:?}", agg);
+
+    Ok::<_,AE>();
+  }).context("read configuration");
+}