Merge pull request 'Merge dev in testing: Feat: Ersetzt program.rs, sudo.rs und config.rs durch entsprechende Rust-Crates. Lagert Unit-Tests in eigenen Ordner aus.' (#20) from dev into testing
Testing Build, Publish & Preview Release / Build, Publish Packages (Testing) & Create Preview Release (push) Successful in 2m31s

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
2026-08-26 18:35:12 +00:00
14 changed files with 180 additions and 468 deletions
+10
View File
@@ -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] [target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc" linker = "aarch64-linux-gnu-gcc"
+1
View File
@@ -3,6 +3,7 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/target" /> <excludeFolder url="file://$MODULE_DIR$/target" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
+12 -16
View File
@@ -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. 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, - **Root-Rechte**: Die Anwendung erfordert Root-Rechte zur Verwaltung von Docker und Compose-Services. Fehlen diese,
eskaliert sie via `sudo`. 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). Error, `[?]` Warn, `[i]` Info, `[d]` Debug).
- **Lizenz**: GNU General Public License v3.0 or later (`GPL-3.0-or-later`). - **Lizenz**: GNU General Public License v3.0 or later (`GPL-3.0-or-later`).
@@ -39,20 +39,22 @@ DockerUpdate/
├── LICENSE # GPL-3.0 Lizenztext ├── LICENSE # GPL-3.0 Lizenztext
├── README.md # Projektdokumentation für Endanwender und Administratoren ├── README.md # Projektdokumentation für Endanwender und Administratoren
├── AGENTS.md # Richtlinien und Dokumentation für Agenten ├── 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/ └── src/
├── main.rs # Einstiegspunkt, Root-Prüfung, Ausführung ├── main.rs # Einstiegspunkt, Root-Prüfung, Ausführung
├── program.rs # Hilfsfunktion zur Programmnamensermittlung ├── lib.rs # Bibliotheksexporte für Module und Tests
├── sudo.rs # Root-Prüfung (geteuid == 0) und Re-Exec via sudo ├── config.rs # Konfigurationsdatenstrukturen (config-ctdra / TOML)
├── config.rs # Konfigurationsverwaltung (confy / TOML)
├── log.rs # Threadsicheres Logging (Terminal + Datei im Temp-Verzeichnis)
└── updater.rs # Scan-, Pull-, Restart- und Bereinigungslogik für Compose-Apps └── updater.rs # Scan-, Pull-, Restart- und Bereinigungslogik für Compose-Apps
``` ```
### Modulverantwortlichkeiten ### Modulverantwortlichkeiten
- **`src/main.rs`**: - **`src/main.rs`**:
Initialer Startpunkt. Prüft mit `sudo::is_run_as_root()`, ob Root-Rechte vorliegen. Wenn nicht, wird mit Initialer Startpunkt. Prüft mit `sudo_ctdra::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. `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`**: - **`src/updater.rs`**:
- Sucht in Unterverzeichnissen von `apps_dir` nach Compose-Dateien (`docker-compose.yaml`, `docker-compose.yml`, - Sucht in Unterverzeichnissen von `apps_dir` nach Compose-Dateien (`docker-compose.yaml`, `docker-compose.yml`,
`compose.yaml`, `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`). - 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. - Führt nach Updates `docker image prune -f` aus.
- **`src/config.rs`**: - **`src/config.rs`**:
- Lädt und speichert die Konfiguration (`AppConfig`, `General` mit `apps_dir` und `log_level`). - Definiert die Konfigurationsdatenstrukturen (`AppConfig`, `General` mit `apps_dir` und `log_level`).
- Standardpfade: `/etc/docker-update/config.toml` (Root) bzw. `~/.config/docker-update/config.toml` (Benutzer). - Lädt und verwaltet die Konfiguration über `config-ctdra` (Standardpfade: `/etc/docker-update/config.toml` bzw.
- **`src/log.rs`**: `~/.config/docker-update/config.toml`).
- 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 <args>` per `exec` aus.
- **`.gitea/workflows/`**: - **`.gitea/workflows/`**:
- `testing.yaml`: Führt bei Push auf `testing` Tests und Builds durch, veröffentlicht Pakete in der Gitea Package - `testing.yaml`: Führt bei Push auf `testing` Tests und Builds durch, veröffentlicht Pakete in der Gitea Package
Registry Registry
Generated
+41 -4
View File
@@ -43,6 +43,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" 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]] [[package]]
name = "confy" name = "confy"
version = "2.0.0" version = "2.0.0"
@@ -64,13 +76,13 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]] [[package]]
name = "docker-update" name = "docker-update"
version = "1.1.0" version = "1.2.0"
dependencies = [ dependencies = [
"confy", "config-ctdra",
"libc 1.0.0-alpha.4", "logger-ctdra",
"serde", "serde",
"serde-saphyr", "serde-saphyr",
"time", "sudo-ctdra",
] ]
[[package]] [[package]]
@@ -161,6 +173,16 @@ version = "1.0.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.3" version = "2.8.3"
@@ -212,6 +234,12 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.47" version = "1.0.47"
@@ -283,6 +311,15 @@ version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" 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]] [[package]]
name = "syn" name = "syn"
version = "3.0.3" version = "3.0.3"
+4 -4
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "docker-update" name = "docker-update"
version = "1.1.0" version = "1.2.0"
edition = "2024" edition = "2024"
authors = ['DragonSlayer_14'] authors = ['DragonSlayer_14']
readme = "README.md" 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" description = "Automatisches Aktualisieren von Docker-Compose-Anwendungen in /var/apps mit :latest-Images"
[dependencies] [dependencies]
time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] }
serde = { version = "1.0.229", features = ["derive"] } serde = { version = "1.0.229", features = ["derive"] }
serde-saphyr = "1.1.0" serde-saphyr = "1.1.0"
confy = "2.0.0" logger-ctdra = { version = "1.0.3", registry = "gitea" }
libc = "1.0.0-alpha.4" sudo-ctdra = { version = "1.0.0", registry = "gitea" }
config-ctdra = { version = "1.0.4", registry = "gitea" }
[profile.release] [profile.release]
debug = "none" debug = "none"
+7 -63
View File
@@ -1,16 +1,13 @@
//! Modul für die Konfigurationsverwaltung der Anwendung. //! Modul für die Konfigurationsverwaltung der Anwendung.
//! //!
//! Dieses Modul stellt die Funktionalität zum Laden, Speichern und Verwalten //! Dieses Modul stellt die Datenstrukturen für die Anwendungskonfiguration bereit.
//! der Anwendungskonfiguration bereit. Die Konfiguration wird in einer Datei gespeichert //! Das Laden, Speichern und Cachen erfolgt über das Crate `config-ctdra`.
//! und beim Programmstart automatisch geladen.
use crate::program; use config_ctdra::get_config as get_cached_config;
use crate::sudo::is_run_as_root;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
/// Hauptkonfigurationsstruktur der Anwendung. /// Hauptkonfigurationsstruktur der Anwendung.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct AppConfig { pub struct AppConfig {
/// Allgemeine Einstellungen /// Allgemeine Einstellungen
pub general: General, pub general: General,
@@ -25,7 +22,7 @@ impl Default for AppConfig {
} }
/// Allgemeine Konfigurationseinstellungen. /// Allgemeine Konfigurationseinstellungen.
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct General { pub struct General {
/// Log-Level für die Anwendung (error, warn, info, debug) /// Log-Level für die Anwendung (error, warn, info, debug)
pub log_level: String, pub log_level: String,
@@ -42,60 +39,7 @@ impl Default for General {
} }
} }
static CONFIG_NAME: &str = "config"; /// Gibt die globale Konfiguration zurück.
static CONFIG: OnceLock<AppConfig> = 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.
pub fn get_config() -> &'static AppConfig { pub fn get_config() -> &'static AppConfig {
CONFIG.get_or_init(|| { get_cached_config::<AppConfig>()
load_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<F>(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");
}
} }
+4
View File
@@ -0,0 +1,4 @@
//! DockerUpdate Bibliotheksexporte
pub mod config;
pub mod updater;
-254
View File
@@ -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]: <Text>` 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<String> for LogLevel {
type Error = LogLevel;
fn try_from(value: String) -> Result<Self, Self> {
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<LogLevel> = OnceLock::new();
/// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug.
static LOG_FILE: OnceLock<Mutex<Option<File>>> = 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<Option<File>>`: 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<Option<File>> {
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: `<programmname>-<konstante-uuid>`
/// - 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<File> {
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"));
}
}
+23 -11
View File
@@ -1,16 +1,28 @@
mod program; use docker_update::config::get_config;
mod sudo; use docker_update::updater;
mod log; use logger_ctdra::{LogLevel, log};
mod config; use std::process::exit;
mod updater; use sudo_ctdra::{is_run_as_root, run_as_root};
use crate::log::{log, LogLevel};
fn main() { fn main() {
if !sudo::is_run_as_root() { if !is_run_as_root() {
log("auth", "Root-Rechte erforderlich. Starte mit sudo neu...", LogLevel::Info); log(
sudo::run_as_root(); "auth",
return; "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); log("main", "docker-update gestartet.", LogLevel::Info);
-14
View File
@@ -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())
}
-28
View File
@@ -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<String> = 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);
}
+1 -73
View File
@@ -1,5 +1,5 @@
use crate::config; use crate::config;
use crate::log::{LogLevel, log}; use logger_ctdra::{LogLevel, log};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
@@ -411,75 +411,3 @@ pub fn run_updates() {
LogLevel::Info, 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);
}
}
+8
View File
@@ -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");
}
+68
View File
@@ -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);
}