diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index f40d837..4b72e19 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -34,9 +34,9 @@ jobs: --name automation_rs \ --network mqtt \ -e RUST_LOG=automation=debug \ - -e MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \ - -e HUE_TOKEN=${{ secrets.HUE_TOKEN }} \ - -e NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \ + -e AUTOMATION__SECRETS__MQTT_PASSWORD=${{ secrets.MQTT_PASSWORD }} \ + -e AUTOMATION__SECRETS__HUE_TOKEN=${{ secrets.HUE_TOKEN }} \ + -e AUTOMATION__SECRETS__NTFY_TOPIC=${{ secrets.NTFY_TOPIC }} \ git.huizinga.dev/dreaded_x/automation_rs@${{ needs.build.outputs.digest }} docker network connect web automation_rs diff --git a/.gitignore b/.gitignore index fedaa2b..01a2117 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target .env +automation.toml diff --git a/Cargo.lock b/Cargo.lock index c8c3e6e..43af154 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,7 @@ dependencies = [ "automation_devices", "automation_lib", "axum", + "config", "dotenvy", "google_home", "hostname", @@ -327,6 +328,19 @@ dependencies = [ "windows-link", ] +[[package]] +name = "config" +version = "0.15.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa974509d38b33ff89282db9c3295707ccf031727c0de9772038ec526852ba" +dependencies = [ + "async-trait", + "pathdiff", + "serde", + "toml", + "winnow", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -466,7 +480,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1262,6 +1276,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1409,7 +1429,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1586,7 +1606,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1784,6 +1804,15 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "serde_spanned" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2035,6 +2064,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_parser" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -2491,6 +2551,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +dependencies = [ + "memchr", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/Cargo.toml b/Cargo.toml index 2028765..3924414 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,10 @@ async-trait = { workspace = true } automation_devices = { workspace = true } automation_lib = { workspace = true } axum = { workspace = true } +config = { version = "0.15.15", default-features = false, features = [ + "async", + "toml", +] } dotenvy = { workspace = true } google_home = { workspace = true } hostname = { workspace = true } diff --git a/Dockerfile b/Dockerfile index 8cc4b3b..0719f81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,6 @@ RUN cargo auditable build --release FROM gcr.io/distroless/cc-debian12:nonroot AS runtime COPY --from=builder /app/target/release/automation /app/automation -ENV AUTOMATION_CONFIG=/app/config.lua +ENV AUTOMATION__ENTRYPOINT=/app/config.lua COPY ./config.lua /app/config.lua CMD [ "/app/automation" ] diff --git a/config.lua b/config.lua index c12bf8f..419a6b7 100644 --- a/config.lua +++ b/config.lua @@ -1,16 +1,13 @@ local device_manager = require("device_manager") local utils = require("utils") +local secrets = require("secrets") +local debug = require("variables").debug or false print(_VERSION) local host = utils.get_hostname() print("Running @" .. host) -local debug, value = pcall(utils.get_env, "DEBUG") -if debug and value ~= "true" then - debug = false -end - local function mqtt_z2m(topic) return "zigbee2mqtt/" .. topic end @@ -28,12 +25,12 @@ local mqtt_client = require("mqtt").new({ port = 8883, client_name = "automation-" .. host, username = "mqtt", - password = utils.get_env("MQTT_PASSWORD"), + password = secrets.mqtt_password, tls = host == "zeus" or host == "hephaestus", }) local ntfy = Ntfy.new({ - topic = utils.get_env("NTFY_TOPIC"), + topic = secrets.ntfy_topic, }) device_manager:add(ntfy) @@ -147,7 +144,7 @@ on_light:add(function(light) end) local hue_ip = "10.0.0.102" -local hue_token = utils.get_env("HUE_TOKEN") +local hue_token = secrets.hue_token local hue_bridge = HueBridge.new({ identifier = "hue_bridge", diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..39e0931 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,17 @@ +use std::collections::HashMap; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Config { + #[serde(default = "default_entrypoint")] + pub entrypoint: String, + #[serde(default)] + pub variables: HashMap, + #[serde(default)] + pub secrets: HashMap, +} + +fn default_entrypoint() -> String { + "./config.lua".into() +} diff --git a/src/main.rs b/src/main.rs index 0fb6f1a..7f1a8ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ #![feature(iter_intersperse)] +mod config; +mod secret; mod web; use std::net::SocketAddr; @@ -6,6 +8,7 @@ use std::path::Path; use std::process; use std::time::{SystemTime, UNIX_EPOCH}; +use ::config::{Environment, File}; use automation_lib::config::{FulfillmentConfig, MqttConfig}; use automation_lib::device_manager::DeviceManager; use automation_lib::helpers; @@ -14,6 +17,7 @@ use axum::extract::{FromRef, State}; use axum::http::StatusCode; use axum::routing::post; use axum::{Json, Router}; +use config::Config; use dotenvy::dotenv; use google_home::{GoogleHome, Request, Response}; use mlua::LuaSerdeExt; @@ -22,6 +26,8 @@ use tokio::net::TcpListener; use tracing::{debug, error, info, warn}; use web::{ApiError, User}; +use crate::secret::EnvironmentSecretFile; + #[derive(Clone)] struct AppState { pub openid_url: String, @@ -69,7 +75,21 @@ async fn app() -> anyhow::Result<()> { dotenv().ok(); tracing_subscriber::fmt::init(); - // console_subscriber::init(); + + let config: Config = ::config::Config::builder() + .add_source( + File::with_name(&format!("{}.toml", std::env!("CARGO_PKG_NAME"))).required(false), + ) + .add_source( + Environment::default() + .prefix(std::env!("CARGO_PKG_NAME")) + .separator("__"), + ) + .add_source(EnvironmentSecretFile::default()) + .build() + .unwrap() + .try_deserialize() + .unwrap(); info!("Starting automation_rs..."); @@ -133,11 +153,10 @@ async fn app() -> anyhow::Result<()> { lua.register_module("device_manager", device_manager.clone())?; + lua.register_module("variables", lua.to_value(&config.variables)?)?; + lua.register_module("secrets", lua.to_value(&config.secrets)?)?; + let utils = lua.create_table()?; - let get_env = lua.create_function(|_lua, name: String| { - std::env::var(name).map_err(mlua::ExternalError::into_lua_err) - })?; - utils.set("get_env", get_env)?; let get_hostname = lua.create_function(|_lua, ()| { hostname::get() .map(|name| name.to_str().unwrap_or("unknown").to_owned()) @@ -151,17 +170,13 @@ async fn app() -> anyhow::Result<()> { .as_millis()) })?; utils.set("get_epoch", get_epoch)?; - lua.register_module("utils", utils)?; automation_devices::register_with_lua(&lua)?; helpers::register_with_lua(&lua)?; - // TODO: Make this not hardcoded - let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into()); - let config_path = Path::new(&config_filename); - - let fulfillment_config: mlua::Value = lua.load(config_path).eval_async().await?; + let entrypoint = Path::new(&config.entrypoint); + let fulfillment_config: mlua::Value = lua.load(entrypoint).eval_async().await?; let fulfillment_config: FulfillmentConfig = lua.from_value(fulfillment_config)?; // Create google home fulfillment route diff --git a/src/secret.rs b/src/secret.rs new file mode 100644 index 0000000..5428d93 --- /dev/null +++ b/src/secret.rs @@ -0,0 +1,43 @@ +use std::str::from_utf8; + +use config::{ConfigError, Source, Value, ValueKind}; + +#[derive(Debug, Clone, Default)] +pub struct EnvironmentSecretFile {} + +const SUFFIX: &str = "__file"; +const PREFIX: &str = concat!(std::env!("CARGO_PKG_NAME"), "__"); + +impl Source for EnvironmentSecretFile { + fn clone_into_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn collect(&self) -> Result, ConfigError> { + Ok(std::env::vars() + .flat_map(|(key, value): (String, String)| { + let key = key.to_lowercase(); + if !key.starts_with(PREFIX) { + return None; + } + + if !key.ends_with(SUFFIX) { + return None; + } + + let suffix_length = key.len() - SUFFIX.len(); + let key = key[PREFIX.len()..suffix_length].replace("__", "."); + + if key.is_empty() { + return None; + } + + let content = from_utf8(&std::fs::read(&value).unwrap()) + .unwrap() + .to_owned(); + + Some((key, Value::new(Some(&value), ValueKind::String(content)))) + }) + .collect()) + } +}