From e27202d63bb0ad2fc3f6c6374b1504558e0678a6 Mon Sep 17 00:00:00 2001
From: clerie <git@clerie.de>
Date: Tue, 9 May 2023 08:40:39 +0200
Subject: [PATCH] Split in multiple binaries

---
 Cargo.toml                 |   2 +-
 src/bin/nixos-validator.rs | 162 ++++++++++++++++++++++++++++++++++++
 src/lib.rs                 |   1 +
 src/main.rs                | 164 ++-----------------------------------
 src/nixos.rs               |  40 +++++++++
 5 files changed, 209 insertions(+), 160 deletions(-)
 create mode 100644 src/bin/nixos-validator.rs
 create mode 100644 src/lib.rs
 create mode 100644 src/nixos.rs

diff --git a/Cargo.toml b/Cargo.toml
index 9299d4e..e78f8de 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "nixos-exporter"
-version = "0.5.0"
+version = "0.6.0"
 edition = "2021"
 
 [dependencies]
diff --git a/src/bin/nixos-validator.rs b/src/bin/nixos-validator.rs
new file mode 100644
index 0000000..ae3435e
--- /dev/null
+++ b/src/bin/nixos-validator.rs
@@ -0,0 +1,162 @@
+use axum::{
+    extract::{State, Query},
+    http::StatusCode,
+    response::IntoResponse,
+    routing::get,
+    Router,
+};
+
+use std::collections::HashMap;
+use std::net::SocketAddr;
+use std::str::FromStr;
+
+use nixos_exporter::nixos::NixStorePath;
+
+#[derive(Clone)]
+struct AppState {
+    listen: String,
+    prometheus_url: String,
+    prometheus_query_tag_template: String,
+    hydra_url: String,
+    hydra_job_template: String,
+}
+
+impl AppState{
+    pub fn new() -> Self {
+        Self {
+            listen: String::from("[::]:9152"),
+            prometheus_url: String::from(""),
+            prometheus_query_tag_template: String::from("instance=\"{}\""),
+            hydra_url: String::from(""),
+            hydra_job_template: String::from("nixfiles/nixfiles/nixosConfigurations.{}"),
+        }
+    }
+
+    pub fn is_valid(self) -> bool {
+        let mut valid = true;
+        if self.prometheus_url == String::from("") {
+            println!("Prometheus url is not specified");
+            valid = false;
+        }
+        if self.hydra_url == String::from("") {
+            println!("Hydra url is not specified");
+            valid = false;
+        }
+
+        return valid;
+    }
+}
+
+#[tokio::main]
+async fn main() {
+    let mut app_state = AppState::new();
+
+    let mut args = std::env::args();
+    let _name = args.next().unwrap();
+    loop {
+        let arg = if let Some(arg) = args.next() {
+            arg
+        } else {
+            break;
+        };
+
+        match arg.as_str() {
+            "--help" | "-h" => {
+                println!("Prometheus exporter for NixOS systems");
+                println!("Use --listen <addr:port> bind the web service.");
+                println!("Output will be on /metrics endpoint. HTTP 500 if something broke while scraping.");
+                std::process::exit(0);
+            }
+            "--listen" => {
+                app_state.listen = args.next().unwrap();
+            }
+            "--prometheus-url" => {
+                app_state.prometheus_url = args.next().unwrap();
+            }
+            "--prometheus-query-tag-template" => {
+                app_state.prometheus_query_tag_template = args.next().unwrap();
+            }
+            "--hydra-url" => {
+                app_state.hydra_url = args.next().unwrap();
+            }
+            "--hydra-job-template" => {
+                app_state.hydra_job_template = args.next().unwrap();
+            }
+            unknown => {
+                println!("unknown option: {}", unknown);
+                std::process::exit(1)
+            }
+        }
+    }
+
+    if !app_state.clone().is_valid() {
+        std::process::exit(1);
+    }
+
+    let app = Router::new();
+    let app = app.route("/metrics", get(check));
+    let app = app.with_state(app_state.clone());
+
+    let addr = SocketAddr::from_str(&app_state.listen.clone()).unwrap();
+    println!("listening on http://{}", addr);
+    axum::Server::bind(&addr)
+        .serve(app.into_make_service())
+        .await
+        .unwrap();
+}
+
+async fn check(State(app_state): State<AppState>, Query(params): Query<HashMap<String, String>>) -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl IntoResponse)> {
+    let target = params.get("target")
+        .ok_or_else(|| (StatusCode::NOT_FOUND, "specify target"))?;
+
+    if target.contains("\"") {
+        return Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid target name"));
+    }
+
+    let client = reqwest::Client::new();
+
+    let prometheus_req = client.get(format!("{}/api/v1/query?query=nixos_current_system_hash{{{}}}", app_state.prometheus_url, app_state.prometheus_query_tag_template.clone().replace("{}", target)))
+        .header("Accept", "application/json")
+        .send().await
+        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Promehteus can't get reached"))?;
+
+    if prometheus_req.status() != reqwest::StatusCode::OK {
+       return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra"));
+    }
+
+    let prometheus_body = prometheus_req.json::<serde_json::Value>().await
+        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Prometheus"))?;
+
+    let current_system_hash = prometheus_body["data"]["result"][0]["metric"]["hash"].as_str()
+        .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No current metric found in Prometheus"))?;
+
+    let hydra_req = client.get(format!("{}/job/{}/latest", app_state.hydra_url, app_state.hydra_job_template.clone().replace("{}", target)))
+        .header("Accept", "application/json")
+        .send().await
+        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Hydra can't get reached"))?;
+
+    if hydra_req.status() != reqwest::StatusCode::OK {
+        return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra"));
+    }
+
+    let hydra_body = hydra_req.json::<serde_json::Value>().await
+        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?;
+
+    let nix_store_path = hydra_body["buildoutputs"]["out"]["path"].as_str()
+        .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?;
+
+    let hydra_system_hash = NixStorePath::from_path_buf(std::path::PathBuf::from(nix_store_path))
+        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid store path returned by Hydra"))?
+        .hash;
+
+    let mut status = "0";
+
+    if current_system_hash == hydra_system_hash {
+        status = "1";
+    }
+
+    return Ok((
+        StatusCode::OK,
+        format!("nixos_current_system_is_sync{{}} {}\n", status)
+    ));
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..4c990e7
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1 @@
+pub mod nixos;
diff --git a/src/main.rs b/src/main.rs
index c5cab96..1eb9791 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,4 @@
 use axum::{
-    extract::{State, Query},
     http::StatusCode,
     response::IntoResponse,
     routing::get,
@@ -9,92 +8,23 @@ use axum::{
 use std::collections::HashMap;
 use std::net::SocketAddr;
 use std::str::FromStr;
-use std::path::PathBuf;
 
-#[derive(Clone, PartialEq)]
-enum OperationMode {
-    None,
-    Exporter,
-    Validator,
-}
+use nixos_exporter::nixos::NixStorePath;
 
 #[derive(Clone)]
 struct AppState {
     listen: String,
-    operationmode: OperationMode,
-    prometheus_url: String,
-    prometheus_query_tag_template: String,
-    hydra_url: String,
-    hydra_job_template: String,
 }
 
 impl AppState{
     pub fn new() -> Self {
         Self {
             listen: String::from("[::]:9152"),
-            operationmode: OperationMode::None,
-            prometheus_url: String::from(""),
-            prometheus_query_tag_template: String::from("instance=\"{}\""),
-            hydra_url: String::from(""),
-            hydra_job_template: String::from("nixfiles/nixfiles/nixosConfigurations.{}"),
         }
     }
 
     pub fn is_valid(self) -> bool {
-        let mut valid = true;
-        if self.operationmode == OperationMode::None {
-            println!("operationmode is not set");
-            valid = false;
-        }
-        if self.prometheus_url == String::from("") && self.operationmode == OperationMode::Validator {
-            println!("Prometheus url is not specified");
-            valid = false;
-        }
-        if self.hydra_url == String::from("") && self.operationmode == OperationMode::Validator {
-            println!("Hydra url is not specified");
-            valid = false;
-        }
-
-        return valid;
-    }
-}
-
-#[derive(Clone)]
-struct NixStorePath {
-    hash: String,
-    name: String,
-}
-
-impl NixStorePath {
-    pub fn from_str_symlink(path: &str) -> Result<Self, String> {
-        Ok(Self::from_path_buf_symlink(PathBuf::from(path))?)
-    }
-
-    pub fn from_path_buf_symlink(path: PathBuf) -> Result<Self, String> {
-        Ok(Self::from_path_buf(path.read_link().map_err(|err| err.to_string())?)?)
-    }
-
-    pub fn from_path_buf(path: PathBuf) -> Result<Self, String> {
-        let store_path_name = path.iter().nth(3)
-            .ok_or_else(|| String::from("Can't read store path name"))?
-            .to_str()
-            .ok_or_else(|| String::from("Failed converting store path name to string"))?
-            .to_string();
-        Ok(Self::from_store_path_name(store_path_name)?)
-    }
-
-    pub fn from_store_path_name(store_path_name: String) -> Result<Self, String> {
-        let (hash, name) = store_path_name
-            .split_once("-")
-            .ok_or_else(|| String::from("Failed splitting store path name for hash and name"))?;
-        Ok(Self {
-            hash: hash.to_string(),
-            name: name.to_string(),
-        })
-    }
-
-    pub fn to_prometheus_metric(self, infix: String) -> Result<String, String> {
-        return Ok(format!("nixos_{}_hash{{hash=\"{}\"}} 1\nnixos_{}_name{{name=\"{}\"}} 1\n", infix, self.hash, infix, self.name));
+        return true;
     }
 }
 
@@ -103,7 +33,7 @@ async fn main() {
     let mut app_state = AppState::new();
 
     let mut args = std::env::args();
-    let name = args.next().unwrap();
+    let _name = args.next().unwrap();
     loop {
         let arg = if let Some(arg) = args.next() {
             arg
@@ -121,24 +51,6 @@ async fn main() {
             "--listen" => {
                 app_state.listen = args.next().unwrap();
             }
-            "--prometheus-url" => {
-                app_state.prometheus_url = args.next().unwrap();
-            }
-            "--prometheus-query-tag-template" => {
-                app_state.prometheus_query_tag_template = args.next().unwrap();
-            }
-            "--hydra-url" => {
-                app_state.hydra_url = args.next().unwrap();
-            }
-            "--hydra-job-template" => {
-                app_state.hydra_job_template = args.next().unwrap();
-            }
-            "exporter" => {
-                app_state.operationmode = OperationMode::Exporter;
-            }
-            "validator" => {
-                app_state.operationmode = OperationMode::Validator;
-            }
             unknown => {
                 println!("unknown option: {}", unknown);
                 std::process::exit(1)
@@ -150,18 +62,8 @@ async fn main() {
         std::process::exit(1);
     }
 
-    let mut app = Router::new();
-    if app_state.operationmode == OperationMode::Exporter {
-        println!("Running NixOS Exporter in Exporter mode");
-        app = app.route("/metrics", get(metrics));
-    } else if app_state.operationmode == OperationMode::Validator {
-        println!("Running NixOS Exporter in Validator mode");
-        app = app.route("/metrics", get(check));
-    } else {
-        println!("Run mode not specified, do {} --help", name);
-        std::process::exit(1);
-    };
-
+    let app = Router::new();
+    let app = app.route("/metrics", get(metrics));
     let app = app.with_state(app_state.clone());
 
     let addr = SocketAddr::from_str(&app_state.listen.clone()).unwrap();
@@ -201,59 +103,3 @@ async fn metrics() -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl
         out,
     ));
 }
-
-async fn check(State(app_state): State<AppState>, Query(params): Query<HashMap<String, String>>) -> Result<(StatusCode, impl IntoResponse), (StatusCode, impl IntoResponse)> {
-    let target = params.get("target")
-        .ok_or_else(|| (StatusCode::NOT_FOUND, "specify target"))?;
-
-    if target.contains("\"") {
-        return Err((StatusCode::INTERNAL_SERVER_ERROR, "Invalid target name"));
-    }
-
-    let client = reqwest::Client::new();
-
-    let prometheus_req = client.get(format!("{}/api/v1/query?query=nixos_current_system_hash{{{}}}", app_state.prometheus_url, app_state.prometheus_query_tag_template.clone().replace("{}", target)))
-        .header("Accept", "application/json")
-        .send().await
-        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Promehteus can't get reached"))?;
-
-    if prometheus_req.status() != reqwest::StatusCode::OK {
-       return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra"));
-    }
-
-    let prometheus_body = prometheus_req.json::<serde_json::Value>().await
-        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?;
-
-    let current_system_hash = prometheus_body["data"]["result"][0]["metric"]["hash"].as_str()
-        .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?;
-
-    let hydra_req = client.get(format!("{}/job/{}/latest", app_state.hydra_url, app_state.hydra_job_template.clone().replace("{}", target)))
-        .header("Accept", "application/json")
-        .send().await
-        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Hydra can't get reached"))?;
-
-    if hydra_req.status() != reqwest::StatusCode::OK {
-        return Err((StatusCode::NOT_FOUND, "Target does not exist in Hydra"));
-    }
-
-    let hydra_body = hydra_req.json::<serde_json::Value>().await
-        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid response from Hydra"))?;
-
-    let nix_store_path = hydra_body["buildoutputs"]["out"]["path"].as_str()
-        .ok_or_else(|| (StatusCode::INTERNAL_SERVER_ERROR, "No buildoutput found in Hydra"))?;
-
-    let hydra_system_hash = NixStorePath::from_path_buf(std::path::PathBuf::from(nix_store_path))
-        .map_err(|_err| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid store path returned by Hydra"))?
-        .hash;
-
-    let mut status = "0";
-
-    if current_system_hash == hydra_system_hash {
-        status = "1";
-    }
-
-    return Ok((
-        StatusCode::OK,
-        format!("nixos_current_system_is_sync{{}} {}\n", status)
-    ));
-}
diff --git a/src/nixos.rs b/src/nixos.rs
new file mode 100644
index 0000000..107e0b1
--- /dev/null
+++ b/src/nixos.rs
@@ -0,0 +1,40 @@
+use std::path::PathBuf;
+
+#[derive(Clone)]
+pub struct NixStorePath {
+    pub hash: String,
+    pub name: String,
+}
+
+impl NixStorePath {
+    pub fn from_str_symlink(path: &str) -> Result<Self, String> {
+        Ok(Self::from_path_buf_symlink(PathBuf::from(path))?)
+    }
+
+    pub fn from_path_buf_symlink(path: PathBuf) -> Result<Self, String> {
+        Ok(Self::from_path_buf(path.read_link().map_err(|err| err.to_string())?)?)
+    }
+
+    pub fn from_path_buf(path: PathBuf) -> Result<Self, String> {
+        let store_path_name = path.iter().nth(3)
+            .ok_or_else(|| String::from("Can't read store path name"))?
+            .to_str()
+            .ok_or_else(|| String::from("Failed converting store path name to string"))?
+            .to_string();
+        Ok(Self::from_store_path_name(store_path_name)?)
+    }
+
+    pub fn from_store_path_name(store_path_name: String) -> Result<Self, String> {
+        let (hash, name) = store_path_name
+            .split_once("-")
+            .ok_or_else(|| String::from("Failed splitting store path name for hash and name"))?;
+        Ok(Self {
+            hash: hash.to_string(),
+            name: name.to_string(),
+        })
+    }
+
+    pub fn to_prometheus_metric(self, infix: String) -> Result<String, String> {
+        return Ok(format!("nixos_{}_hash{{hash=\"{}\"}} 1\nnixos_{}_name{{name=\"{}\"}} 1\n", infix, self.hash, infix, self.name));
+    }
+}