From 51a800802d2036c9344fca7996630c50a3f7fc9f Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Wed, 26 Aug 2026 20:34:12 +0200 Subject: [PATCH] Feat: Ersetzt program.rs, sudo.rs und config.rs durch entsprechende Rust-Crates. Lagert Unit-Tests in eigenen Ordner aus. --- .cargo/config.toml | 10 ++ .idea/DockerUpdater.iml | 1 + AGENTS.md | 28 ++--- Cargo.lock | 45 ++++++- Cargo.toml | 8 +- src/config.rs | 70 ++--------- src/lib.rs | 4 + src/log.rs | 254 ---------------------------------------- src/main.rs | 36 ++++-- src/program.rs | 14 --- src/sudo.rs | 28 ----- src/updater.rs | 74 +----------- tests/config.rs | 8 ++ tests/updater.rs | 68 +++++++++++ 14 files changed, 180 insertions(+), 468 deletions(-) create mode 100644 src/lib.rs delete mode 100644 src/log.rs delete mode 100644 src/program.rs delete mode 100644 src/sudo.rs create mode 100644 tests/config.rs create mode 100644 tests/updater.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index 3e74e2e..c7356c5 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,13 @@ +[registry] +default = "gitea" + +[registries.gitea] +index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" # Sparse index +# index = "https://gitea.creative-dragonslayer.de/Rust-Crates/_cargo-index.git" # Git + +[net] +git-fetch-with-cli = true + [target.aarch64-unknown-linux-gnu] linker = "aarch64-linux-gnu-gcc" diff --git a/.idea/DockerUpdater.iml b/.idea/DockerUpdater.iml index cf84ae4..bbe0a70 100644 --- a/.idea/DockerUpdater.iml +++ b/.idea/DockerUpdater.iml @@ -3,6 +3,7 @@ + diff --git a/AGENTS.md b/AGENTS.md index 8ba12a0..a82075c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ lädt diese herunter, startet betroffene Services neu und bereinigt ungenutzte Z Konstrukte oder bedingten Nicht-Linux-Kompilierungen (`#[cfg(target_os = ...)]`) hinzugefügt werden. - **Root-Rechte**: Die Anwendung erfordert Root-Rechte zur Verwaltung von Docker und Compose-Services. Fehlen diese, eskaliert sie via `sudo`. -- **Keine Unicode-Emojis im Logging**: Das Logging-Modul (`src/log.rs`) verwendet einheitliche ASCII-Präfixe (`[!]` +- **Keine Unicode-Emojis im Logging**: Das Logging-Modul (`logger-ctdra`) verwendet einheitliche ASCII-Präfixe (`[!]` Error, `[?]` Warn, `[i]` Info, `[d]` Debug). - **Lizenz**: GNU General Public License v3.0 or later (`GPL-3.0-or-later`). @@ -39,20 +39,22 @@ DockerUpdate/ ├── LICENSE # GPL-3.0 Lizenztext ├── README.md # Projektdokumentation für Endanwender und Administratoren ├── AGENTS.md # Richtlinien und Dokumentation für Agenten +├── tests/ +│ ├── config.rs # Tests für Konfigurationsdatenstrukturen +│ └── updater.rs # Tests für Updater-Hilfsfunktionen und Compose-Parsing └── src/ ├── main.rs # Einstiegspunkt, Root-Prüfung, Ausführung - ├── program.rs # Hilfsfunktion zur Programmnamensermittlung - ├── sudo.rs # Root-Prüfung (geteuid == 0) und Re-Exec via sudo - ├── config.rs # Konfigurationsverwaltung (confy / TOML) - ├── log.rs # Threadsicheres Logging (Terminal + Datei im Temp-Verzeichnis) + ├── lib.rs # Bibliotheksexporte für Module und Tests + ├── config.rs # Konfigurationsdatenstrukturen (config-ctdra / TOML) └── updater.rs # Scan-, Pull-, Restart- und Bereinigungslogik für Compose-Apps ``` ### Modulverantwortlichkeiten - **`src/main.rs`**: - Initialer Startpunkt. Prüft mit `sudo::is_run_as_root()`, ob Root-Rechte vorliegen. Wenn nicht, wird mit - `sudo::run_as_root()` neu gestartet. Ruft anschließend `updater::run_updates()` auf. + Initialer Startpunkt. Prüft mit `sudo_ctdra::is_run_as_root()`, ob Root-Rechte vorliegen. Wenn nicht, wird mit + `sudo_ctdra::run_as_root()` neu gestartet. Initialisiert das Logging über `logger-ctdra` anhand der Konfiguration und + ruft anschließend `updater::run_updates()` auf. - **`src/updater.rs`**: - Sucht in Unterverzeichnissen von `apps_dir` nach Compose-Dateien (`docker-compose.yaml`, `docker-compose.yml`, `compose.yaml`, `compose.yml`). @@ -63,15 +65,9 @@ DockerUpdate/ - Startet bei Änderungen Services neu (`docker compose up -d` mit Fallback auf `docker-compose up -d`). - Führt nach Updates `docker image prune -f` aus. - **`src/config.rs`**: - - Lädt und speichert die Konfiguration (`AppConfig`, `General` mit `apps_dir` und `log_level`). - - Standardpfade: `/etc/docker-update/config.toml` (Root) bzw. `~/.config/docker-update/config.toml` (Benutzer). -- **`src/log.rs`**: - - Formatiert Logmeldungen nach Schema `[Präfix][Zeitstempel][LEVEL][Tag]: Nachricht`. - - Gibt auf Konsole aus und schreibt zusätzlich in tagesbasierte Logdateien im Temp-Verzeichnis - (`/tmp/docker-update-.../log-YYYY-MM-DD.log`). -- **`src/sudo.rs`**: - - `is_run_as_root()`: Nutzt `libc::geteuid() == 0`. - - `run_as_root()`: Führt `sudo ` per `exec` aus. + - Definiert die Konfigurationsdatenstrukturen (`AppConfig`, `General` mit `apps_dir` und `log_level`). + - Lädt und verwaltet die Konfiguration über `config-ctdra` (Standardpfade: `/etc/docker-update/config.toml` bzw. + `~/.config/docker-update/config.toml`). - **`.gitea/workflows/`**: - `testing.yaml`: Führt bei Push auf `testing` Tests und Builds durch, veröffentlicht Pakete in der Gitea Package Registry diff --git a/Cargo.lock b/Cargo.lock index 49bf89d..2408000 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,18 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "config-ctdra" +version = "1.0.4" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "f7b3e25b95d4cdb6fc856f2af0106d4558b882c2f10798db46181e51827749a4" +dependencies = [ + "confy", + "program-ctdra", + "serde", + "sudo-ctdra", +] + [[package]] name = "confy" version = "2.0.0" @@ -64,13 +76,13 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "docker-update" -version = "1.1.0" +version = "1.2.0" dependencies = [ - "confy", - "libc 1.0.0-alpha.4", + "config-ctdra", + "logger-ctdra", "serde", "serde-saphyr", - "time", + "sudo-ctdra", ] [[package]] @@ -161,6 +173,16 @@ version = "1.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5" +[[package]] +name = "logger-ctdra" +version = "1.0.3" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "af9ea239d80163c80b12cae5f74c0c2824199921dedafba74678e96a11a8b15e" +dependencies = [ + "program-ctdra", + "time", +] + [[package]] name = "memchr" version = "2.8.3" @@ -212,6 +234,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "program-ctdra" +version = "1.0.0" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "54f20af67f90bf6d10697dd77bb4beb41dafad7fe0fb46aacfc81ad45a8e48bd" + [[package]] name = "quote" version = "1.0.47" @@ -283,6 +311,15 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "sudo-ctdra" +version = "1.0.0" +source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" +checksum = "2c2746122bbe36f821de70db3ca4ad24edfb77118fc6a6bc7693082b50d23e5f" +dependencies = [ + "libc 1.0.0-alpha.4", +] + [[package]] name = "syn" version = "3.0.3" diff --git a/Cargo.toml b/Cargo.toml index 3a2b23b..8be26b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "docker-update" -version = "1.1.0" +version = "1.2.0" edition = "2024" authors = ['DragonSlayer_14'] readme = "README.md" @@ -9,11 +9,11 @@ repository = "https://gitea.creative-dragonslayer.de/Linuxapps/DockerUpdate" description = "Automatisches Aktualisieren von Docker-Compose-Anwendungen in /var/apps mit :latest-Images" [dependencies] -time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] } serde = { version = "1.0.229", features = ["derive"] } serde-saphyr = "1.1.0" -confy = "2.0.0" -libc = "1.0.0-alpha.4" +logger-ctdra = { version = "1.0.3", registry = "gitea" } +sudo-ctdra = { version = "1.0.0", registry = "gitea" } +config-ctdra = { version = "1.0.4", registry = "gitea" } [profile.release] debug = "none" diff --git a/src/config.rs b/src/config.rs index 84ae1bb..6fecfd1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,16 +1,13 @@ //! Modul für die Konfigurationsverwaltung der Anwendung. //! -//! Dieses Modul stellt die Funktionalität zum Laden, Speichern und Verwalten -//! der Anwendungskonfiguration bereit. Die Konfiguration wird in einer Datei gespeichert -//! und beim Programmstart automatisch geladen. +//! Dieses Modul stellt die Datenstrukturen für die Anwendungskonfiguration bereit. +//! Das Laden, Speichern und Cachen erfolgt über das Crate `config-ctdra`. -use crate::program; -use crate::sudo::is_run_as_root; +use config_ctdra::get_config as get_cached_config; use serde::{Deserialize, Serialize}; -use std::sync::OnceLock; /// Hauptkonfigurationsstruktur der Anwendung. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct AppConfig { /// Allgemeine Einstellungen pub general: General, @@ -25,7 +22,7 @@ impl Default for AppConfig { } /// Allgemeine Konfigurationseinstellungen. -#[derive(Serialize, Deserialize, Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct General { /// Log-Level für die Anwendung (error, warn, info, debug) pub log_level: String, @@ -42,60 +39,7 @@ impl Default for General { } } -static CONFIG_NAME: &str = "config"; -static CONFIG: OnceLock = OnceLock::new(); - -/// Gibt die aktuelle Konfiguration zurück. -/// -/// Lädt die Konfiguration beim ersten Aufruf und speichert sie zwischen. -/// Nachfolgende Aufrufe geben die gespeicherte Konfiguration zurück. +/// Gibt die globale Konfiguration zurück. pub fn get_config() -> &'static AppConfig { - CONFIG.get_or_init(|| { - load_config() - }) + get_cached_config::() } - -/// Modifiziert die aktuelle Konfiguration mit der übergebenen Mutator-Funktion. -/// -/// Die Funktion lädt die Konfiguration neu von der Festplatte, wendet die Mutator-Funktion an -/// und speichert die geänderte Konfiguration anschließend wieder. -#[allow(dead_code)] -pub fn modify_config(mutator: F) -where - F: FnOnce(&mut AppConfig), -{ - // Immer frisch von Disk laden, damit Änderungen konsistent sind - let mut cfg = load_config(); - mutator(&mut cfg); - save_config(cfg); -} - -/// Lädt die Konfiguration aus der Konfigurationsdatei. -fn load_config() -> AppConfig { - if is_run_as_root() { - return confy::load_path(format!("/etc/{}/{}.toml", program::program_name(), CONFIG_NAME)).unwrap_or_default(); - } - confy::load(&*program::program_name(), CONFIG_NAME).unwrap_or_default() -} - -/// Speichert die übergebene Konfiguration in der Konfigurationsdatei. -#[allow(dead_code)] -fn save_config(config: AppConfig) { - if is_run_as_root() { - let _ = confy::store_path(format!("/etc/{}/{}.toml", program::program_name(), CONFIG_NAME), config); - return; - } - let _ = confy::store(&*program::program_name(), CONFIG_NAME, config); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_default_config() { - let config = AppConfig::default(); - assert_eq!(config.general.apps_dir, "/var/apps"); - assert_eq!(config.general.log_level, "info"); - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ec62780 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +//! DockerUpdate Bibliotheksexporte + +pub mod config; +pub mod updater; diff --git a/src/log.rs b/src/log.rs deleted file mode 100644 index 7d4bba1..0000000 --- a/src/log.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! Einfaches, threadsicheres Logging-Modul. -//! -//! Merkmale: -//! - Ausgabe auf Standard-Streams (stdout/stderr) und zusätzlich in eine Logdatei im temporären Verzeichnis. -//! - Nachrichtenvorlage: `[DD.MM.YYYY HH:MM:SS.mmm][LEVEL][TAG]: ` mit Präfix ([!]/[?]/[i]/[d]). -//! - Der aktuell verwendete Schweregradfilter ist statisch (`LOG_LEVEL`) und wird zur Laufzeit nicht geändert. -//! - Dateihandle wird lazily initialisiert und zwischengespeichert (`OnceLock`). -//! -//! Hinweise: -//! - Bei einem leeren `tag` wird ein leerer Tag-Abschnitt erzeugt. -//! - Die Logdatei wird unterhalb eines prozessspezifischen Ordners im System-Temp-Verzeichnis abgelegt und nach Datum -//! benannt (z. B. `log-2025-08-18.log`). Es wird im Append-Modus geschrieben. -//! -//! Beispiel (ohne Ausführung in Doctests): -//! ```rust,no_run -//! use crate::log::{log, LogLevel}; -//! -//! log("startup", "Dienst wird initialisiert...", LogLevel::Info); -//! log("db", "Verbindung unterbrochen!", LogLevel::Warn); -//! log("core", "Unerwarteter Fehler", LogLevel::Error); -//! ``` - -use std::env; -use std::fmt::{Display, Formatter}; -use std::fs::{File, OpenOptions, create_dir_all}; -use std::io::Write; -use std::sync::{Mutex, OnceLock}; - -use crate::config; -use crate::program; -use time::{OffsetDateTime, macros::format_description}; - -/// Schweregrade für Logeinträge in aufsteigender Detailtiefe. -/// -/// Die Reihenfolge bestimmt den Filter: Nur Einträge mit `log_level <= LOG_LEVEL` werden ausgegeben. -/// -/// Anzeige (Display): -/// - `Error` -> `ERROR` -/// - `Warn` -> `WARN` -/// - `Info` -> `INFO` -/// - `Debug` -> `DEBUG` -#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)] -pub enum LogLevel { - /// Kritische Fehler, gehen zusätzlich auf `stderr`. - Error = 1, - /// Wichtige Warnungen über potenzielle Probleme. - Warn = 2, - /// Allgemeine Betriebsinformationen. - Info = 3, - /// Ausführliche Diagnoseausgaben für die Entwicklung. - Debug = 4, -} - -impl Display for LogLevel { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - LogLevel::Error => write!(f, "ERROR"), - LogLevel::Warn => write!(f, "WARN"), - LogLevel::Info => write!(f, "INFO"), - LogLevel::Debug => write!(f, "DEBUG"), - } - } -} - -impl TryFrom for LogLevel { - type Error = LogLevel; - - fn try_from(value: String) -> Result { - match value.to_lowercase().as_str() { - "error" => Ok(LogLevel::Error), - "warn" => Ok(LogLevel::Warn), - "info" => Ok(LogLevel::Info), - "debug" => Ok(LogLevel::Debug), - _ => Err(LogLevel::Info), - } - } -} - -/// Globaler Schweregradfilter für Ausgabe. -static LOG_LEVEL: OnceLock = OnceLock::new(); - -/// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug. -static LOG_FILE: OnceLock>> = OnceLock::new(); - -/// Protokolliert eine Nachricht abhängig vom angegebenen Schweregrad. -/// -/// Verhalten: -/// - Wenn `log_level` größer als der globale Filter ist, wird nichts ausgegeben. -/// - `Error`-Meldungen gehen nach `stderr`, alle anderen nach `stdout`. -/// - Zusätzlich wird in eine tägliche Logdatei im Temp-Verzeichnis geschrieben (wenn erfolgreich geöffnet). -/// -/// Parameter: -/// - `tag`: Kurzer Kontext (z. B. Modulname). -/// - `message`: Die eigentliche Nachricht (eine Zeile). -/// - `log_level`: Schweregrad der Nachricht. -/// -/// Thread-Sicherheit: -/// - Dateischreibzugriffe sind über `Mutex` serialisiert. -/// -/// Beispiel: -/// ```rust,no_run -/// # use crate::log::{log, LogLevel}; -/// log("http", "Server gestartet auf Port 8080", LogLevel::Info); -/// ``` -pub fn log(tag: &str, message: &str, log_level: LogLevel) { - if log_level <= get_log_level() { - let message: String = format_message(tag, message, &log_level); - - if log_level == LogLevel::Error { - eprintln!("{}", message); - } else { - println!("{}", message); - } - - let file_lock = get_or_init_log_file(); - if let Ok(mut guard) = file_lock.lock() { - if let Some(f) = guard.as_mut() { - let _ = writeln!(f, "{}", message); - let _ = f.flush(); - } - } - } -} - -/// Liefert das einmalig initialisierte Log-Level für die Filterung. -/// -/// Ermittelt das konfigurierte Log-Level aus der Config-Datei. -/// Wenn ungültig oder nicht vorhanden, wird LogLevel::Info als Fallback verwendet. -/// -/// Rückgabe: -/// - `LogLevel`: Das zu verwendende Log-Level als enum-Wert. -fn get_log_level() -> LogLevel { - *LOG_LEVEL.get_or_init(|| { - let log_level = &config::get_config().general.log_level; - LogLevel::try_from(log_level.to_string()).unwrap_or(LogLevel::Info) - }) -} - -/// Formatiert eine Lognachricht mit Zeitstempel, Level, Tag und Präfix. -/// -/// Format: -/// - Datum/Zeit lokal (Fallback: UTC) im Format `DD.MM.YYYY HH:MM:SS.mmm`. -/// - Level als Text (`ERROR`, `WARN`, `INFO`, `DEBUG`). -/// - Tag in eckigen Klammern; bei leerem Tag entsteht ein leeres `[]`. -/// - Präfix pro Level: `[!]` (Error), `[?]` (Warn), `[i]` (Info), `[d]` (Debug). -/// -/// Beispielausgabe: -/// - `[i][18.08.2025 14:23:45.012][INFO][init]: Fertig` -/// -/// Hinweis: -/// - Diese Funktion formatiert nur; sie führt keinen I/O aus. -fn format_message(tag: &str, message: &str, log_level: &LogLevel) -> String { - let mut prefix: String = String::new(); - - let now_local: OffsetDateTime = - OffsetDateTime::now_local().unwrap_or(OffsetDateTime::now_utc()); - let fmt = - format_description!("[day].[month].[year] [hour]:[minute]:[second].[subsecond digits:3]"); - - if tag != "" { - prefix = format!("[{}]", tag); - } - - let message = format!( - "[{}][{}]{}: {}", - now_local.format(fmt).unwrap(), - log_level.to_string(), - prefix, - message - ); - - match log_level { - &LogLevel::Error => format!("[!]{}", message), - &LogLevel::Warn => format!("[?]{}", message), - &LogLevel::Info => format!("[i]{}", message), - &LogLevel::Debug => format!("[d]{}", message), - } -} - -/// Initialisiert das Logdatei-Handle beim ersten Aufruf und liefert eine Referenz darauf. -/// -/// Rückgabe: -/// - `&'static Mutex>`: Das Mutex schützt den optionalen Dateihandler. -/// `None` bedeutet, dass das Öffnen fehlgeschlagen ist (z. B. fehlende Rechte). -fn get_or_init_log_file() -> &'static Mutex> { - LOG_FILE.get_or_init(|| { - let file = open_log_file().ok(); - Mutex::new(file) - }) -} - -/// Öffnet (und erstellt bei Bedarf) die tagesbasierte Logdatei im Temp-Verzeichnis. -/// -/// Pfadaufbau: -/// - Basis: `std::env::temp_dir()` -/// - Unterordner: `-` -/// - Datei: `log-YYYY-MM-DD.log` im Append-Modus -/// -/// Rückgabe: -/// - `Ok(File)`, wenn der Ordner erstellt/gefunden und die Datei geöffnet/angelegt werden konnte. -/// - `Err(std::io::Error)`, wenn ein I/O-Fehler auftrat. -/// -/// Fehler: -/// - Gibt I/O-Fehler unverändert weiter (z. B. beim Erstellen des Ordners oder Öffnen der Datei). -fn open_log_file() -> std::io::Result { - let program = program::program_name(); - let rand = "13692bbf-a93b-43e9-9cc6-f05f94a8cfb6"; - - let mut dir = env::temp_dir(); - dir.push(format!("{}-{}", program, rand)); - create_dir_all(&dir)?; - - let today_fmt = format_description!("[year]-[month]-[day]"); - let date_str = OffsetDateTime::now_local() - .unwrap_or(OffsetDateTime::now_utc()) - .format(today_fmt) - .unwrap_or_else(|_| "0000-00-00".to_string()); - - let file_name = format!("log-{}.log", date_str); - dir.push(file_name); - - OpenOptions::new().create(true).append(true).open(dir) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_message_prefixes() { - let msg_info = format_message("test", "Info message", &LogLevel::Info); - assert!(msg_info.starts_with("[i]")); - assert!(msg_info.contains("[INFO][test]: Info message")); - - let msg_err = format_message("test", "Error message", &LogLevel::Error); - assert!(msg_err.starts_with("[!]")); - assert!(msg_err.contains("[ERROR][test]: Error message")); - - let msg_warn = format_message("test", "Warn message", &LogLevel::Warn); - assert!(msg_warn.starts_with("[?]")); - assert!(msg_warn.contains("[WARN][test]: Warn message")); - - let msg_debug = format_message("test", "Debug message", &LogLevel::Debug); - assert!(msg_debug.starts_with("[d]")); - assert!(msg_debug.contains("[DEBUG][test]: Debug message")); - } - - #[test] - fn test_format_message_empty_tag() { - let msg = format_message("", "No tag message", &LogLevel::Info); - assert!(msg.starts_with("[i]")); - assert!(msg.contains("[INFO]: No tag message")); - } -} diff --git a/src/main.rs b/src/main.rs index debdcd3..9799ba5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,31 @@ -mod program; -mod sudo; -mod log; -mod config; -mod updater; - -use crate::log::{log, LogLevel}; +use docker_update::config::get_config; +use docker_update::updater; +use logger_ctdra::{LogLevel, log}; +use std::process::exit; +use sudo_ctdra::{is_run_as_root, run_as_root}; fn main() { - if !sudo::is_run_as_root() { - log("auth", "Root-Rechte erforderlich. Starte mit sudo neu...", LogLevel::Info); - sudo::run_as_root(); - return; + if !is_run_as_root() { + log( + "auth", + "Root-Rechte erforderlich. Starte mit sudo neu...", + LogLevel::Info, + ); + let err = run_as_root(); + log( + "auth", + &format!("Fehler beim Ausführen von 'sudo': {}", err), + LogLevel::Error, + ); + exit(1); + } + + let config = get_config(); + if let Ok(level) = LogLevel::try_from(config.general.log_level.as_str()) { + logger_ctdra::init(level); } log("main", "docker-update gestartet.", LogLevel::Info); updater::run_updates(); log("main", "docker-update beendet.", LogLevel::Info); -} \ No newline at end of file +} diff --git a/src/program.rs b/src/program.rs deleted file mode 100644 index 8847199..0000000 --- a/src/program.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::env; - -/// Liefert den Programmnamen (Dateistamm der aktuellen ausführbaren Datei). -/// -/// Rückgabe: -/// - Dateistamm der aktuellen Executable als `String`. -/// - Fallback `"app"`, wenn der Name nicht ermittelt werden kann. -pub(crate) fn program_name() -> String { - env::current_exe() - .ok() - .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string())) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| "app".to_string()) -} diff --git a/src/sudo.rs b/src/sudo.rs deleted file mode 100644 index e4c1564..0000000 --- a/src/sudo.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::log::{LogLevel, log}; -use std::env; -use std::os::unix::process::CommandExt; -use std::process::{Command, exit}; - -/// Prüft, ob das Programm mit Root-/Administrator-Rechten ausgeführt wird. -/// -/// # Returns -/// -/// * `true` - Das Programm läuft mit erhöhten Rechten -/// * `false` - Das Programm läuft mit normalen Benutzerrechten -pub fn is_run_as_root() -> bool { - unsafe { libc::geteuid() == 0 } -} - -/// Startet das Programm mit Root-Rechten über `sudo` neu. -/// -/// Diese Funktion versucht das Programm mit erhöhten Rechten neu zu starten. -pub fn run_as_root() { - let commandline_args: Vec = env::args().collect(); - let err = Command::new("sudo").args(&commandline_args).exec(); - log( - "auth", - &format!("Fehler beim Ausführen von 'sudo': {}", err), - LogLevel::Error, - ); - exit(1); -} diff --git a/src/updater.rs b/src/updater.rs index 03a5bea..b02dfa5 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,5 +1,5 @@ use crate::config; -use crate::log::{LogLevel, log}; +use logger_ctdra::{LogLevel, log}; use serde::Deserialize; use std::collections::HashMap; use std::fs; @@ -411,75 +411,3 @@ pub fn run_updates() { LogLevel::Info, ); } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs::File; - use std::io::Write; - - #[test] - fn test_is_latest_image() { - assert!(is_latest_image("nginx")); - assert!(is_latest_image("nginx:latest")); - assert!(is_latest_image("nginx:LATEST")); - assert!(is_latest_image("gitea/gitea")); - assert!(is_latest_image("gitea/gitea:latest")); - assert!(is_latest_image("localhost:5000/my-app")); - assert!(is_latest_image("localhost:5000/my-app:latest")); - - assert!(!is_latest_image("nginx:alpine")); - assert!(!is_latest_image("gitea/gitea:1.20")); - assert!(!is_latest_image("postgres:15-alpine")); - assert!(!is_latest_image("localhost:5000/my-app:v1.0")); - assert!(!is_latest_image("nginx@sha256:1234567890abcdef")); - assert!(!is_latest_image("")); - } - - #[test] - fn test_parse_compose_services() { - let temp_dir = std::env::temp_dir().join("docker_update_test_compose"); - let _ = fs::create_dir_all(&temp_dir); - let compose_path = temp_dir.join("docker-compose.yaml"); - - let yaml_content = r#" -services: - gitea: - image: gitea/gitea:latest - ports: - - "3000:3000" - db: - image: postgres:15 - environment: - POSTGRES_DB: gitea -"#; - let mut file = File::create(&compose_path).unwrap(); - file.write_all(yaml_content.as_bytes()).unwrap(); - - let services = parse_compose_services(&compose_path).unwrap(); - assert_eq!(services.len(), 2); - - let gitea_svc = services.iter().find(|(s, _)| s == "gitea").unwrap(); - assert_eq!(gitea_svc.1, "gitea/gitea:latest"); - - let db_svc = services.iter().find(|(s, _)| s == "db").unwrap(); - assert_eq!(db_svc.1, "postgres:15"); - - let _ = fs::remove_dir_all(&temp_dir); - } - - #[test] - fn test_find_compose_file() { - let temp_dir = std::env::temp_dir().join("docker_update_test_find"); - let _ = fs::create_dir_all(&temp_dir); - - assert_eq!(find_compose_file(&temp_dir), None); - - let compose_path = temp_dir.join("docker-compose.yaml"); - File::create(&compose_path).unwrap(); - - assert_eq!(find_compose_file(&temp_dir), Some(compose_path)); - - let _ = fs::remove_dir_all(&temp_dir); - } -} diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 0000000..4b56bc0 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,8 @@ +use docker_update::config::AppConfig; + +#[test] +fn test_default_config() { + let config = AppConfig::default(); + assert_eq!(config.general.apps_dir, "/var/apps"); + assert_eq!(config.general.log_level, "info"); +} diff --git a/tests/updater.rs b/tests/updater.rs new file mode 100644 index 0000000..4d26cc5 --- /dev/null +++ b/tests/updater.rs @@ -0,0 +1,68 @@ +use docker_update::updater::{find_compose_file, is_latest_image, parse_compose_services}; +use std::fs::{self, File}; +use std::io::Write; + +#[test] +fn test_is_latest_image() { + assert!(is_latest_image("nginx")); + assert!(is_latest_image("nginx:latest")); + assert!(is_latest_image("nginx:LATEST")); + assert!(is_latest_image("gitea/gitea")); + assert!(is_latest_image("gitea/gitea:latest")); + assert!(is_latest_image("localhost:5000/my-app")); + assert!(is_latest_image("localhost:5000/my-app:latest")); + + assert!(!is_latest_image("nginx:alpine")); + assert!(!is_latest_image("gitea/gitea:1.20")); + assert!(!is_latest_image("postgres:15-alpine")); + assert!(!is_latest_image("localhost:5000/my-app:v1.0")); + assert!(!is_latest_image("nginx@sha256:1234567890abcdef")); + assert!(!is_latest_image("")); +} + +#[test] +fn test_parse_compose_services() { + let temp_dir = std::env::temp_dir().join("docker_update_test_compose"); + let _ = fs::create_dir_all(&temp_dir); + let compose_path = temp_dir.join("docker-compose.yaml"); + + let yaml_content = r#" +services: + gitea: + image: gitea/gitea:latest + ports: + - "3000:3000" + db: + image: postgres:15 + environment: + POSTGRES_DB: gitea +"#; + let mut file = File::create(&compose_path).unwrap(); + file.write_all(yaml_content.as_bytes()).unwrap(); + + let services = parse_compose_services(&compose_path).unwrap(); + assert_eq!(services.len(), 2); + + let gitea_svc = services.iter().find(|(s, _)| s == "gitea").unwrap(); + assert_eq!(gitea_svc.1, "gitea/gitea:latest"); + + let db_svc = services.iter().find(|(s, _)| s == "db").unwrap(); + assert_eq!(db_svc.1, "postgres:15"); + + let _ = fs::remove_dir_all(&temp_dir); +} + +#[test] +fn test_find_compose_file() { + let temp_dir = std::env::temp_dir().join("docker_update_test_find"); + let _ = fs::create_dir_all(&temp_dir); + + assert_eq!(find_compose_file(&temp_dir), None); + + let compose_path = temp_dir.join("docker-compose.yaml"); + File::create(&compose_path).unwrap(); + + assert_eq!(find_compose_file(&temp_dir), Some(compose_path)); + + let _ = fs::remove_dir_all(&temp_dir); +}