From 83ae3ad3cc0e762324f286f383e63a9d203f64e5 Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Mon, 24 Aug 2026 21:19:14 +0200 Subject: [PATCH 1/5] =?UTF-8?q?Feat:=20Einf=C3=BChrung=20eines=20Skripts?= =?UTF-8?q?=20zur=20dynamischen=20Bestimmung=20der=20Build-Nummer=20und=20?= =?UTF-8?q?Integration=20in=20die=20Build-Prozesse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/main.yaml | 31 +++++-- .gitea/workflows/testing.yaml | 31 +++++-- AGENTS.md | 3 +- scripts/get-build-number.py | 164 ++++++++++++++++++++++++++++++++++ scripts/package-arch.py | 32 ++++++- 5 files changed, 239 insertions(+), 22 deletions(-) create mode 100755 scripts/get-build-number.py diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml index 42b0760..4f3e1cb 100644 --- a/.gitea/workflows/main.yaml +++ b/.gitea/workflows/main.yaml @@ -42,24 +42,37 @@ jobs: cargo build --release --target aarch64-unknown-linux-gnu cargo build --release --target i686-unknown-linux-gnu + - name: Determine Build Number + id: build_num + env: + GITEA_URL: ${{ gitea.server_url || github.server_url }} + REPO: ${{ gitea.repository || github.repository }} + REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }} + TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }} + run: | + BUILD_NUM=$(python3 scripts/get-build-number.py) + echo "build_number=${BUILD_NUM}" >> $GITHUB_OUTPUT + echo "BUILD_NUMBER=${BUILD_NUM}" >> $GITHUB_ENV + echo "Ermittelte Build-Nummer: ${BUILD_NUM}" + - name: Build Debian Packages (.deb) run: | - cargo deb --target x86_64-unknown-linux-gnu --no-build - cargo deb --target aarch64-unknown-linux-gnu --no-build - cargo deb --target i686-unknown-linux-gnu --no-build + cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build + cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build + cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build - name: Build Fedora / RPM Packages (.rpm) run: | mkdir -p target/generate-rpm - cargo generate-rpm --target x86_64-unknown-linux-gnu -o target/generate-rpm - cargo generate-rpm --target aarch64-unknown-linux-gnu -o target/generate-rpm - cargo generate-rpm --target i686-unknown-linux-gnu -o target/generate-rpm + cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm + cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm + cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm - name: Build Arch Linux Packages (.pkg.tar.zst) run: | - python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu - python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu - python3 scripts/package-arch.py --target i686-unknown-linux-gnu + python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" + python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" + python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" - name: Publish Packages to Gitea Package Registry env: diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 8571718..e0c7561 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -42,24 +42,37 @@ jobs: cargo build --release --target aarch64-unknown-linux-gnu cargo build --release --target i686-unknown-linux-gnu + - name: Determine Build Number + id: build_num + env: + GITEA_URL: ${{ gitea.server_url || github.server_url }} + REPO: ${{ gitea.repository || github.repository }} + REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }} + TOKEN: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }} + run: | + BUILD_NUM=$(python3 scripts/get-build-number.py) + echo "build_number=${BUILD_NUM}" >> $GITHUB_OUTPUT + echo "BUILD_NUMBER=${BUILD_NUM}" >> $GITHUB_ENV + echo "Ermittelte Build-Nummer: ${BUILD_NUM}" + - name: Build Debian Packages (.deb) run: | - cargo deb --target x86_64-unknown-linux-gnu --no-build - cargo deb --target aarch64-unknown-linux-gnu --no-build - cargo deb --target i686-unknown-linux-gnu --no-build + cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build + cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build + cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build - name: Build Fedora / RPM Packages (.rpm) run: | mkdir -p target/generate-rpm - cargo generate-rpm --target x86_64-unknown-linux-gnu -o target/generate-rpm - cargo generate-rpm --target aarch64-unknown-linux-gnu -o target/generate-rpm - cargo generate-rpm --target i686-unknown-linux-gnu -o target/generate-rpm + cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm + cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm + cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm - name: Build Arch Linux Packages (.pkg.tar.zst) run: | - python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu - python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu - python3 scripts/package-arch.py --target i686-unknown-linux-gnu + python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" + python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" + python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}" - name: Publish Packages to Gitea Package Registry env: diff --git a/AGENTS.md b/AGENTS.md index 3be3543..9552807 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -32,7 +32,8 @@ DockerUpdate/ │ ├── testing.yaml # CI/CD für testing-Branch (Tests, Testing-Packages & Preview-Release) │ └── main.yaml # CI/CD für main-Branch (Tests, Stable-Packages & Release) ├── scripts/ -│ └── package-arch.py # Hilfsskript zur Arch-Linux-Paketerstellung (.pkg.tar.zst) +│ ├── get-build-number.py # Ermittelt dynamisch die nächste Build-Nummer für Pakete +│ └── package-arch.py # Hilfsskript zur Arch-Linux-Paketerstellung (.pkg.tar.zst) ├── Cargo.toml # Rust-Manifest, Abhängigkeiten und Metadaten für deb / generate-rpm / arch ├── Cargo.lock ├── LICENSE # GPL-3.0 Lizenztext diff --git a/scripts/get-build-number.py b/scripts/get-build-number.py new file mode 100755 index 0000000..fc3d02d --- /dev/null +++ b/scripts/get-build-number.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Ermittelt automatisch die nächste Build-Nummer (Revision / Release / pkgrel) +für eine gegebene Paket-Version (z. B. 1.0.2 -> wenn 1.0.2-1 existiert, wird 2 zurückgegeben). +""" +import argparse +import json +import os +import re +import subprocess +import sys +import urllib.parse +import urllib.request + + +def get_cargo_pkg_info(): + try: + metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"])) + pkg = metadata["packages"][0] + name = pkg.get("name", "docker-update") + version = pkg.get("version", "1.0.0") + repository = pkg.get("repository", "") + return name, version, repository + except Exception: + # Fallback auf einfaches Parsen der Cargo.toml + name = "docker-update" + version = "1.0.0" + repository = "" + if os.path.exists("Cargo.toml"): + with open("Cargo.toml", "r") as f: + for line in f: + line = line.strip() + if line.startswith("name ="): + name = line.split("=", 1)[1].strip().strip('"\'') + elif line.startswith("version ="): + version = line.split("=", 1)[1].strip().strip('"\'') + elif line.startswith("repository ="): + repository = line.split("=", 1)[1].strip().strip('"\'') + return name, version, repository + + +def parse_repo_info(repo_url): + """Extrahiert Server-URL, Owner und Repo-Name aus einer Repository-URL.""" + if not repo_url: + return None, None, None + parsed = urllib.parse.urlparse(repo_url) + server_url = f"{parsed.scheme}://{parsed.netloc}" + path_parts = [p for p in parsed.path.strip("/").split("/") if p] + if len(path_parts) >= 2: + owner = path_parts[0] + repo_name = path_parts[1] + if repo_name.endswith(".git"): + repo_name = repo_name[:-4] + return server_url, f"{owner}/{repo_name}", owner + return server_url, None, None + + +def query_existing_build_numbers(name, version, gitea_url, repo, owner, token=None): + build_nums = set() + pattern = re.compile(rf'(?:{re.escape(name)}[_-]|^v?){re.escape(version)}-(\d+)') + + headers = {"User-Agent": "docker-update-build-resolver"} + if token: + headers["Authorization"] = f"token {token}" + + # 1. Gitea Releases API + if gitea_url and repo: + try: + url = f"{gitea_url.rstrip('/')}/api/v1/repos/{repo}/releases?limit=50" + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=10) as resp: + releases = json.loads(resp.read().decode()) + for rel in releases: + tag = rel.get("tag_name", "") + m = pattern.search(tag) + if m: + build_nums.add(int(m.group(1))) + for asset in rel.get("assets", []): + asset_name = asset.get("name", "") + m = pattern.search(asset_name) + if m: + build_nums.add(int(m.group(1))) + except Exception as e: + sys.stderr.write(f"[Hinweis] Konnte Gitea Releases nicht abfragen: {e}\n") + + # 2. Gitea Packages API (falls Token vorhanden) + if gitea_url and owner and token: + try: + url = f"{gitea_url.rstrip('/')}/api/v1/packages/{owner}?limit=50" + req = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(req, timeout=10) as resp: + packages = json.loads(resp.read().decode()) + for pkg in packages: + pkg_ver = pkg.get("version", "") + m = pattern.search(pkg_ver) + if m: + build_nums.add(int(m.group(1))) + except Exception as e: + sys.stderr.write(f"[Hinweis] Konnte Gitea Packages nicht abfragen: {e}\n") + + # 3. Lokales Target-Verzeichnis prüfen + target_dirs = ["target/debian", "target/generate-rpm", "target/arch"] + for t_dir in target_dirs: + if os.path.isdir(t_dir): + for file in os.listdir(t_dir): + m = pattern.search(file) + if m: + build_nums.add(int(m.group(1))) + + return build_nums + + +def get_next_build_number(version=None, gitea_url=None, repo=None, owner=None, token=None, explicit_build_num=None): + if explicit_build_num is not None: + return int(explicit_build_num) + + env_build_num = os.environ.get("BUILD_NUMBER") or os.environ.get("BUILD_NUM") or os.environ.get("PKGREL") + if env_build_num: + try: + return int(env_build_num) + except ValueError: + pass + + pkg_name, pkg_version, pkg_repo = get_cargo_pkg_info() + if not version: + version = pkg_version + + default_server, default_repo, default_owner = parse_repo_info(pkg_repo) + + gitea_url = gitea_url or os.environ.get("GITEA_URL") or os.environ.get("GITHUB_SERVER_URL") or default_server + repo = repo or os.environ.get("REPO") or os.environ.get("GITHUB_REPOSITORY") or default_repo + owner = owner or os.environ.get("REPO_OWNER") or os.environ.get("GITHUB_REPOSITORY_OWNER") or default_owner + token = token or os.environ.get("TOKEN") or os.environ.get("GITEA_TOKEN") or os.environ.get("PACKAGE_TOKEN") or os.environ.get("RELEASE_TOKEN") + + existing_nums = query_existing_build_numbers(pkg_name, version, gitea_url, repo, owner, token) + if existing_nums: + return max(existing_nums) + 1 + return 1 + + +def main(): + parser = argparse.ArgumentParser(description="Ermittelt die nächste Build-Nummer für die Paketierung.") + parser.add_argument("--version", help="Paket-Version (Standard: aus Cargo.toml)") + parser.add_argument("--gitea-url", help="Gitea Basis-URL") + parser.add_argument("--repo", help="Repository (z. B. Owner/Repo)") + parser.add_argument("--owner", help="Repository Owner / Organisation") + parser.add_argument("--token", help="API-Token") + parser.add_argument("--build-number", type=int, help="Explizite Build-Nummer erzwingen") + + args = parser.parse_args() + + build_num = get_next_build_number( + version=args.version, + gitea_url=args.gitea_url, + repo=args.repo, + owner=args.owner, + token=args.token, + explicit_build_num=args.build_number, + ) + print(build_num) + + +if __name__ == "__main__": + main() diff --git a/scripts/package-arch.py b/scripts/package-arch.py index d6996ad..3bc56ed 100755 --- a/scripts/package-arch.py +++ b/scripts/package-arch.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 import argparse +import importlib.util import json import os import subprocess +import sys import tempfile import time @@ -22,7 +24,28 @@ TARGET_ARCH_MAP = { } -def build_package(target_triple=None, target_arch=None): +def resolve_pkgrel(version=None, default="1"): + # 1. Environment Variable + env_pkgrel = os.environ.get("BUILD_NUMBER") or os.environ.get("BUILD_NUM") or os.environ.get("PKGREL") + if env_pkgrel: + return str(env_pkgrel) + + # 2. get-build-number.py falls vorhanden + script_dir = os.path.dirname(os.path.abspath(__file__)) + getter_path = os.path.join(script_dir, "get-build-number.py") + if os.path.exists(getter_path): + try: + spec = importlib.util.spec_from_file_location("get_build_number", getter_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return str(module.get_next_build_number(version=version)) + except Exception as e: + sys.stderr.write(f"[Hinweis] Konnte get-build-number nicht ausführen: {e}\n") + + return str(default) + + +def build_package(target_triple=None, target_arch=None, pkgrel=None): metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"])) pkg = metadata["packages"][0] name = pkg["name"] @@ -34,7 +57,9 @@ def build_package(target_triple=None, target_arch=None): author = authors[0] if authors else "Unknown" arch_meta = pkg.get("metadata", {}).get("arch", {}) - pkgrel = arch_meta.get("pkgrel", "1") + default_pkgrel = arch_meta.get("pkgrel", "1") + if not pkgrel: + pkgrel = resolve_pkgrel(version=version, default=default_pkgrel) default_arch = arch_meta.get("arch", "x86_64") if target_arch: @@ -111,9 +136,10 @@ def main(): parser = argparse.ArgumentParser(description="Erstellt Arch Linux-Pakete (.pkg.tar.zst)") parser.add_argument("--target", help="Rust Target-Triple (z.B. x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, i686-unknown-linux-gnu)") parser.add_argument("--arch", help="Architektur (z.B. x86_64, aarch64, i686)") + parser.add_argument("--pkgrel", help="Release-/Build-Nummer (z.B. 1, 2, ...)") args = parser.parse_args() - build_package(target_triple=args.target, target_arch=args.arch) + build_package(target_triple=args.target, target_arch=args.arch, pkgrel=args.pkgrel) if __name__ == "__main__": From fc798b39a32108c1f8c8c8ae39ad44c4c57fc8d5 Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Mon, 24 Aug 2026 21:31:37 +0200 Subject: [PATCH 2/5] Feat: Dependency-Updates mit Versionierung und Deprecation von serde_yaml Aktualisierung von time, serde und libc sowie Hinweis zur Deprecation von serde_yaml --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f818ce6..05fd76e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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.41", features = ["formatting", "macros", "local-offset"] } -serde = { version="1.0.219", features = ["derive"] } -serde_yaml = "0.9" +time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] } +serde = { version = "1.0.229", features = ["derive"] } +serde_yaml = "0.9.34+deprecated" confy = "2.0.0" -libc = "1.0.0-alpha.1" +libc = "1.0.0-alpha.4" [profile.release] debug = "none" From 9c4a1e1c95cb4ea537c59c173a68debadce9c99b Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Mon, 24 Aug 2026 21:40:28 +0200 Subject: [PATCH 3/5] =?UTF-8?q?Feat:=20Dependency=20auf=20serde-saphyr=20a?= =?UTF-8?q?ktualisiert=20und=20Version=20auf=201.1.0=20erh=C3=B6ht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- Cargo.lock | 142 +++++++++++++++++++++++++++++++++++++++---------- Cargo.toml | 4 +- src/updater.rs | 2 +- 4 files changed, 119 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9552807..8ba12a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ DockerUpdate/ - **`src/updater.rs`**: - Sucht in Unterverzeichnissen von `apps_dir` nach Compose-Dateien (`docker-compose.yaml`, `docker-compose.yml`, `compose.yaml`, `compose.yml`). - - Parst Services via `serde_yaml` und filtert mit `is_latest_image()` nach Services mit `:latest`-Images oder ohne + - Parst Services via `serde-saphyr` und filtert mit `is_latest_image()` nach Services mit `:latest`-Images oder ohne expliziten Tag. - Führt `docker pull` für jedes gefundene Image aus und vergleicht Vorher/Nachher-Image-IDs via `docker image inspect`. diff --git a/Cargo.lock b/Cargo.lock index 236e74d..68c3eb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "cfg-if" version = "1.0.4" @@ -29,15 +64,33 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" [[package]] name = "docker-update" -version = "1.0.2" +version = "1.1.0" dependencies = [ "confy", "libc 1.0.0-alpha.4", "serde", - "serde_yaml", + "serde-saphyr", "time", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" +dependencies = [ + "encoding_rs", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -55,6 +108,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "granit-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ccd1be9ebf2bd5520dbfcfb72b70ef492f654061299a097056f3e73410e38ec" +dependencies = [ + "arraydeque", + "smallvec", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -80,12 +143,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - [[package]] name = "lazy_static" version = "1.5.0" @@ -104,12 +161,33 @@ version = "1.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d0f24f33af482526a4e3f9b47f0abb2c6377a1713c8aa4a8106994689a4cfa5" +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + [[package]] name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -143,12 +221,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "serde" version = "1.0.229" @@ -159,6 +231,23 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-saphyr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ec1f5cac0eb96063c64b28705255a7ed6e7d77f95c1d25e9f8b8c928006ce1" +dependencies = [ + "annotate-snippets", + "base64", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + [[package]] name = "serde_core" version = "1.0.229" @@ -189,17 +278,10 @@ dependencies = [ ] [[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" +name = "smallvec" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "syn" @@ -310,10 +392,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "unsafe-libyaml" -version = "0.2.11" +name = "unicode-width" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "windows-link" @@ -414,3 +496,9 @@ name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index 05fd76e..3a2b23b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "docker-update" -version = "1.0.2" +version = "1.1.0" edition = "2024" authors = ['DragonSlayer_14'] readme = "README.md" @@ -11,7 +11,7 @@ description = "Automatisches Aktualisieren von Docker-Compose-Anwendungen in /va [dependencies] time = { version = "0.3.55", features = ["formatting", "macros", "local-offset"] } serde = { version = "1.0.229", features = ["derive"] } -serde_yaml = "0.9.34+deprecated" +serde-saphyr = "1.1.0" confy = "2.0.0" libc = "1.0.0-alpha.4" diff --git a/src/updater.rs b/src/updater.rs index 20422f2..03a5bea 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -66,7 +66,7 @@ pub fn parse_compose_services(compose_path: &Path) -> Result Date: Mon, 24 Aug 2026 21:41:13 +0200 Subject: [PATCH 4/5] Feat: Dependency-Updates mit Versionierung und Deprecation von serde_yaml --- Cargo.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 68c3eb1..49bf89d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,9 +8,9 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" dependencies = [ - "anstyle", - "memchr", - "unicode-width", + "anstyle", + "memchr", + "unicode-width", ] [[package]] @@ -69,7 +69,7 @@ dependencies = [ "confy", "libc 1.0.0-alpha.4", "serde", - "serde-saphyr", + "serde-saphyr", "time", ] @@ -79,7 +79,7 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if", + "cfg-if", ] [[package]] @@ -88,7 +88,7 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fba3fe847045ecff794b9c138293a80db914678c453ad63fbf0c6a9eb6e00b22" dependencies = [ - "encoding_rs", + "encoding_rs", ] [[package]] @@ -114,8 +114,8 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ccd1be9ebf2bd5520dbfcfb72b70ef492f654061299a097056f3e73410e38ec" dependencies = [ - "arraydeque", - "smallvec", + "arraydeque", + "smallvec", ] [[package]] @@ -185,7 +185,7 @@ version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "autocfg", + "autocfg", ] [[package]] @@ -237,15 +237,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ec1f5cac0eb96063c64b28705255a7ed6e7d77f95c1d25e9f8b8c928006ce1" dependencies = [ - "annotate-snippets", - "base64", - "encoding_rs_io", - "granit-parser", - "nohash-hasher", - "num-traits", - "serde_core", - "smallvec", - "zmij", + "annotate-snippets", + "base64", + "encoding_rs_io", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", ] [[package]] From 51a800802d2036c9344fca7996630c50a3f7fc9f Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Wed, 26 Aug 2026 20:34:12 +0200 Subject: [PATCH 5/5] 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); +}