Feat: Ersetzt program.rs, sudo.rs und config.rs durch entsprechende Rust-Crates. Lagert Unit-Tests in eigenen Ordner aus.
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
Generated
+1
@@ -3,6 +3,7 @@
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
@@ -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 <args>` 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
|
||||
|
||||
Generated
+41
-4
@@ -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"
|
||||
|
||||
+4
-4
@@ -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"
|
||||
|
||||
+7
-63
@@ -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<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.
|
||||
/// Gibt die globale Konfiguration zurück.
|
||||
pub fn get_config() -> &'static AppConfig {
|
||||
CONFIG.get_or_init(|| {
|
||||
load_config()
|
||||
})
|
||||
get_cached_config::<AppConfig>()
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
//! DockerUpdate Bibliotheksexporte
|
||||
|
||||
pub mod config;
|
||||
pub mod updater;
|
||||
-254
@@ -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"));
|
||||
}
|
||||
}
|
||||
+24
-12
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user