Compare commits

...
23 Commits
Author SHA1 Message Date
Mateusz Gruszczyński 7168bc4683 cleanups 2026-11-03 10:38:23 +01:00
gru fb5f01d291 Update http/nginx.conf 2026-10-23 23:44:03 +02:00
Mateusz Gruszczyński f86a62b8b1 suse 2026-10-22 16:10:02 +02:00
Mateusz Gruszczyński 1f0daa0679 cleanup 2026-10-22 13:03:12 +02:00
Mateusz Gruszczyński feb9d91a3d en 2026-10-22 12:39:04 +02:00
Mateusz Gruszczyński cd64b9a14e en 2026-10-22 11:52:18 +02:00
Mateusz Gruszczyński ccccdc2031 fancy dash 2026-10-22 11:23:32 +02:00
Mateusz Gruszczyński 3b317a4f0c chages in systemd 2026-10-21 10:50:35 +02:00
Mateusz Gruszczyński a1389a6a61 build agent 2026-10-19 22:39:09 +02:00
Mateusz Gruszczyński a946c38194 new_version 2026-10-19 11:09:13 +02:00
Mateusz Gruszczyński 910bb563f0 cleaups 2026-10-14 13:13:32 +02:00
Mateusz Gruszczyński af78f6528c update rust version 2026-10-10 23:34:10 +02:00
Mateusz Gruszczyński 17f1de681e fixes in opnsesne addon 2026-10-10 11:30:05 +02:00
Mateusz Gruszczyński c507e9e12a chamge in opn service 2026-10-10 00:16:01 +02:00
Mateusz Gruszczyński 86aebcfc05 update 2026-10-08 22:22:11 +02:00
Mateusz Gruszczyński f9b8c43f5f sever 2026-10-07 15:56:28 +02:00
Mateusz Gruszczyński c48ddedad3 sever 2026-10-07 14:13:58 +02:00
Mateusz Gruszczyński f77f692001 dep 2026-10-07 13:59:40 +02:00
Mateusz Gruszczyński 5807d11550 dep 2026-10-07 13:41:32 +02:00
Mateusz Gruszczyński 743f70675b chmod 2026-10-07 11:17:41 +02:00
Mateusz Gruszczyński e046b56322 fix zaleznosci 2026-10-07 11:05:35 +02:00
Mateusz Gruszczyński 8a9d63fa95 fix tests 2026-10-07 10:45:54 +02:00
Mateusz Gruszczyński fd8a7dd629 opn 2026-10-07 10:08:35 +02:00
57 changed files with 3655 additions and 213 deletions

No files matched your search

+1
View File
@@ -2,3 +2,4 @@ SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git
SOURCE_BRANCH=master SOURCE_BRANCH=master
REPOSITORY_URL=https://packages.linuxiarz.pl REPOSITORY_URL=https://packages.linuxiarz.pl
POLL_INTERVAL=300 POLL_INTERVAL=300
RUN_LINUX_TESTS=0
+13 -4
View File
@@ -18,6 +18,8 @@ RUN set -eux; \
mkdir -p /freebsd-sysroot; \ mkdir -p /freebsd-sysroot; \
tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib
FROM golang:1.26-bookworm AS go-toolchain
FROM rockylinux:9 FROM rockylinux:9
ARG RUST_TOOLCHAIN=stable ARG RUST_TOOLCHAIN=stable
@@ -30,7 +32,7 @@ ARG APTLY_ZIP_SHA256=535ed24eb706bc25a316b59d709a7e74e10165f2ffab8552097ba7e4816
RUN dnf -y install --allowerasing \ RUN dnf -y install --allowerasing \
bash binutils ca-certificates clang cmake coreutils createrepo_c curl file findutils \ bash binutils ca-certificates clang cmake coreutils createrepo_c curl file findutils \
gcc gcc-c++ git gnupg2 gzip libarchive lld llvm make openssl pkgconf-pkg-config \ gcc gcc-c++ git gnupg2 gzip libarchive lld llvm make openssl pkgconf-pkg-config \
python3 python3.11 rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 zstd \ python3 python3.11 rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 zstd bsdtar \
&& dnf clean all \ && dnf clean all \
&& rm -rf /var/cache/dnf && rm -rf /var/cache/dnf
@@ -68,8 +70,12 @@ RUN set -eux; \
rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version
COPY --from=freebsd-sysroot /freebsd-sysroot /opt/freebsd-sysroot COPY --from=freebsd-sysroot /freebsd-sysroot /opt/freebsd-sysroot
COPY --from=go-toolchain /usr/local/go /usr/local/go
ENV FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \ ENV PATH=/usr/local/go/bin:${PATH} \
GOPATH=/var/lib/package-builder/go \
GOCACHE=/tmp/rust-package-builder/go-cache \
FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \ CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \
CC_x86_64_unknown_freebsd=clang \ CC_x86_64_unknown_freebsd=clang \
CXX_x86_64_unknown_freebsd=clang++ \ CXX_x86_64_unknown_freebsd=clang++ \
@@ -79,10 +85,13 @@ ENV FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \ CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd15.1 -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld" CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd15.1 -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld"
# Registry/git/target cache is persistent, while rustup/toolchains stay in the image. # Cargo registry/git cache stays persistent; build target is disposable and
ENV CARGO_HOME=/var/lib/package-builder/cargo # must not grow on the /data volume between package builds.
ENV CARGO_HOME=/var/lib/package-builder/cargo \
CARGO_TARGET_DIR=/tmp/rust-package-builder/cargo-target
COPY scripts/ /opt/rust-package-builder/scripts/ COPY scripts/ /opt/rust-package-builder/scripts/
COPY packaging/ /opt/rust-package-builder/packaging/
COPY opnsense/ /opt/rust-package-builder/opnsense/ COPY opnsense/ /opt/rust-package-builder/opnsense/
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py
+313 -22
View File
@@ -1,13 +1,59 @@
# RustProxy unified package builder # RustProxy unified package builder
Wydanie wdrożeniowe (odchudzone): zawiera panel, konfigurację Nginx oraz wszystkie skrypty budowania Linux, OPNsense i FreeBSD. Nie zawiera pustych szkieletów projektów; dołączono lekkie testy offline dla pakowania SUSE. Istniejące katalogi `data/`, `gpg/` i plik `.env` pozostają nienaruszone podczas rozpakowywania.
Jeden kontener buildera buduje z jednego commita RustProxy: Jeden kontener buildera buduje z jednego commita RustProxy:
- Linux x86_64: DEB + podpisane repo APT, - Linux x86_64: DEB + podpisane repo APT (`rustproxy` **oraz osobny** `rustproxy-docker-agent`),
- Linux x86_64: RPM + podpisane repo DNF/YUM, - Linux x86_64: RPM + podpisane repo DNF/YUM (`rustproxy` **oraz osobny** `rustproxy-docker-agent`),
- **SUSE / openSUSE / openSUSE Leap x86_64**: oddzielne paczki RPM dostosowane do `zypper`, podpisane repo `suse/x86_64` (`rustproxy` i `rustproxy-docker-agent`),
- FreeBSD x86_64: cross-build bez VM FreeBSD, - FreeBSD x86_64: cross-build bez VM FreeBSD,
- OPNsense: `os-rustproxy-<version>_<revision>.pkg` + statyczne repo `pkg`. - OPNsense: `os-rustproxy-<version>_<revision>.pkg` + statyczne repo `pkg`,
- **natywne FreeBSD** (domyślnie włączone): `rustproxy-<version>_<revision>.pkg` + osobny katalog repo `pkg` pod `/freebsd/` (bez zależności OPNsense).
Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux i gałąź OPNsense są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD. **Agent Docker jest pakowany wyłącznie na Linux (DEB/RPM); brak paczki FreeBSD pkg dla agenta.** Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux, gałąź OPNsense i gałąź natywnego FreeBSD są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD.
## Panel repozytorium (RustProxy Package Registry)
The dashboard interface defaults to **English (`en-GB`)**, including its dates, navigation, notifications, and installation instructions. The **Docker discovery agent** installation card is shown only for **Linux (DEB/RPM, including SUSE/Zypper)**, never for OPNsense or native FreeBSD. Package listings/API also omit any stray agent PKG files for the FreeBSD channels.
Strony startowe repozytoriów (Linux DNF/APT, SUSE, OPNsense, FreeBSD) działają jako **lekka aplikacja Python** (`dashboard/`) za istniejącym Nginx. Wygląd nawiązuje do panelu RustProxy 0.34.6 (ciemny, jasny i kremowy motyw). **APT, RPM i pkg są nadal obsługiwane bezpośrednio przez Nginx**, więc mechanizm aktualizacji paczek, ścieżki plików, podpisy GPG i atomowe przełączanie `current` nie zmieniają się.
Panel udostępnia:
- **Przegląd**: najnowsza wersja, czas buildu, commit, stan Linux / SUSE / OPNsense / FreeBSD, statystyki i ostatnie paczki.
- **Paczki i wersje**: pełny katalog z nazwą, wersją, architekturą, datą, rozmiarem i linkiem pobrania; wyszukiwanie, sortowanie oraz filtry formatów/kanałów.
- **Instalacja**: aktualne polecenia dla APT, DNF, Zypper, OPNsense i natywnego FreeBSD oraz przyciski kopiowania.
- **Repozytoria**: linki do metadanych, katalogów i klucza GPG. Automatyczne odświeżanie co **60 sekund** oraz ręczne odświeżenie.
Adresy paneli (bez zmian domen i portów):
```text
REPOSITORY_URL/ Linux DEB + RPM (DNF) + SUSE RPM (Zypper)
REPOSITORY_URL/suse/x86_64/ podpisane RPM i metadane dla SUSE/openSUSE
OPNSENSE_REPOSITORY_URL/ OPNsense pkg
OPNSENSE_REPOSITORY_URL/freebsd/ Native FreeBSD pkg
```
Publiczne API, np. `https://twoje-repo.example/api/v1/packages`, zwraca JSON z `builds`, `packages` i `summary`. API jest dostępne na obu hostach. Panel nie zapisuje nic w repo; odczytuje wyłącznie opublikowany snapshot `data/repository/current`. Kontener Python nie publikuje portu hosta i ma dostęp **tylko do odczytu**.
Nowe buildy zapisują `built_at` w `LINUX-BUILD-INFO.txt` oraz `BUILD-INFO.txt` dla OPNsense/FreeBSD (UTC, ISO 8601). **Dla wydań sprzed tej zmiany** panel pokazuje czas modyfikacji pliku, oznaczony w podpowiedzi; nie twierdzi, że to potwierdzony czas buildu. Przełączanie wersji działa automatycznie i nie wymaga ponownego uruchamiania panelu.
Uruchomienie po aktualizacji projektu:
```bash
./start.sh
# lub ręcznie (przy ustawionym .env):
docker compose --env-file .env build repository-dashboard
docker compose --env-file .env up -d repository-dashboard repository-http
# API i zdrowie nginx
curl -fsS http://127.0.0.1:18765/api/v1/packages
curl -fsS http://127.0.0.1:18765/healthz
```
Panel zależy wyłącznie od biblioteki standardowej Pythona, bez dodatkowych pakietów pip. Jeżeli nie ma jeszcze buildów, pokaże komunikat **brak paczek**, a nie fikcyjne wersje.
## Szybki start ## Szybki start
@@ -30,7 +76,7 @@ Dla `http://` bez portu `start.sh` dopisze domyślny port. Dla `https://` zakła
Domyślne lokalne porty: Domyślne lokalne porty:
```text ```text
APT/RPM: 18765 -> nginx :8080 APT/RPM/Zypper: 18765 -> nginx :8080
OPNsense: 18766 -> nginx :8081 OPNsense: 18766 -> nginx :8081
``` ```
@@ -49,17 +95,80 @@ opn-rustproxy.linuxiarz.pl -> http://builder:18766
wspólny checkout wspólny checkout
/ \ / \
/ \ / \
Linux branch OPNsense branch Linux branch FreeBSD cross-build
cargo test izolowana kopia źródła cargo test |
cargo build poprawki FreeBSD cargo build +---+---+
DEB + RPM cross-build FreeBSD go test/build agent | |
APT + DNF os-rustproxy.pkg 2x DEB + 2x RPM + 2x SUSE RPM | |
| | APT + DNF + Zypper OPNsense FreeBSD native*
v v | | |
publikacja Linux publikacja OPNsense v v v
publikacja Linux os-rustproxy rustproxy.pkg
pkg repo pkg repo
* FreeBSD native: enabled by default; FREEBSD_NATIVE_ENABLED=0 disables it
``` ```
`cargo test` należy tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit` i `last_opnsense_commit`, więc kolejny cykl ponawia tylko brakującą gałąź. `cargo test` i `go test ./...` (agenta) należą tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM/SUSE nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit`, `last_opnsense_commit` i (jeśli włączone) `last_freebsd_commit`; kolejny cykl ponawia tylko brakującą gałąź.
## Docker Agent: osobne pakiety Linux
Builder sprawdza `docker-agent/go.mod`, uruchamia `go test ./...`, kompiluje statyczny agent z `CGO_ENABLED=0` dla `linux/amd64` i pakuje **niezależnie od głównego RustProxy**. Wersja paczki oraz wydrukowana wersja Go agenta są zgodne z `Cargo.toml` danego commita; `PACKAGE_RELEASE` jest wspólne dla obu produktów w jednym wydaniu. Build gałęzi Linux kończy się błędem, jeśli budowanie/testy agenta nie przejdą, więc nie opublikuje repo z niekompletną parą. OPNsense i natywne FreeBSD pozostają niezależne i bez agenta.
Zawartość katalogu `data/packages/` po udanym cyklu:
```text
data/packages/deb/rustproxy_<version>-<release>_amd64.deb
data/packages/deb/rustproxy-docker-agent_<version>-<release>_amd64.deb
data/packages/rpm/x86_64/rustproxy-<version>-<release>.x86_64.rpm
data/packages/rpm/x86_64/rustproxy-docker-agent-<version>-<release>.x86_64.rpm
data/packages/suse/x86_64/rustproxy-<version>-<release>.x86_64.rpm
data/packages/suse/x86_64/rustproxy-docker-agent-<version>-<release>.x86_64.rpm
```
Wszystkie pakiety (DEB, DNF RPM i SUSE RPM) są podpisane istniejącym kluczem GPG repozytorium; metadane obu repozytoriów RPM również są podpisane. Każde repo zawiera `rustproxy` i `rustproxy-docker-agent`. Zmiana rewizji pakowania Linux (`PACKAGING_REVISION=6`) wymusza przebudowę także dla niezmienionego commita.
Instalacja na hoście Docker (uruchom Docker Engine wcześniej):
```bash
# Debian / Ubuntu, po dodaniu repo APT
sudo apt update
sudo apt install rustproxy-docker-agent
# RHEL / Rocky / Alma / Fedora, po dodaniu repo DNF
sudo dnf install rustproxy-docker-agent
# Konfiguracja w panelu RustProxy: Settings -> Docker agents
sudo rustproxy-docker-agent info
sudo rustproxy-docker-agent info --show-token
sudo rustproxy-docker-agent print-fingerprint
sudo systemctl status rustproxy-docker-agent
```
Instalacja z lokalnego pliku: `sudo apt install ./rustproxy-docker-agent_*.deb` lub `sudo dnf install ./rustproxy-docker-agent-*.rpm` (jedna pasująca wersja w katalogu). Dla pakietu nie trzeba mieć kompilatora Go. Skrypty `postinstall` zakładają konto systemowe, zabezpieczają `/etc/rustproxy/docker-agent.env`, inicjalizują długi losowy token oraz samopodpisany certyfikat i uruchamiają systemd, jeśli Docker socket jest dostępny. Tokenu **nie wypisują do logów dpkg/rpm**. `info --show-token` wywołuje administrator świadomie, lokalnie.
Konfiguracja i klucze pozostają po aktualizacji: `/etc/rustproxy/docker-agent.env` (`config|noreplace`, finalnie `0600`) oraz `/var/lib/rustproxy-docker-agent/agent.crt` i `agent.key` (klucz `0600`). Rotacja przez `sudo rustproxy-docker-agent rotate-token` wymaga `sudo systemctl restart rustproxy-docker-agent` i aktualizacji tokenu w panelu RustProxy. Do ręcznej instalacji, bez pakietu, pozostaje `cd docker-agent && sudo ./install.sh` po sklonowaniu projektu z Go 1.22+; instrukcje: `docker-agent/README.md` w repo RustProxy.
W przypadku przechodzenia z instalacji ręcznej na paczkę: jednostka w `/etc/systemd/system/rustproxy-docker-agent.service` ma pierwszeństwo przed jednostką paczkowaną w `/usr/lib/systemd/system`. Należy zachować token/certyfikat, zatrzymać usługę, przenieść ręczną jednostkę poza katalog systemd i wykonać `systemctl daemon-reload` przed zainstalowaniem paczki. Głównego `rustproxy` i jego konfiguracji to nie dotyczy.
## SUSE / openSUSE / openSUSE Leap — RPM / Zypper
Gałąź Linux buduje dodatkowo **dwie natywne paczki RPM dla SUSE** z tego samego commita: `rustproxy` i `rustproxy-docker-agent`. Mają inne zależności niż paczki dla RHEL (m.in. `shadow` zamiast `shadow-utils`) i oddzielne, podpisane repozytorium `suse/x86_64` z metadanymi `repomd.xml.asc` oraz informacją `SUSE-BUILD-INFO.txt`. DNF/YUM nie jest zmieniany. Paczki SUSE mają kompresję gzip dla zgodności ze starszymi klientami RPM.
Dla systemów SUSE z Pythonem systemowym starszym niż 3.10 paczka serwera dopuszcza doinstalowanie wersjonowanego Pythona (z `pip`), np. `python311` i `python311-pip`. Lokalny wrapper `/usr/libexec/rustproxy/python3` jest dodawany wyłącznie do ścieżki `PATH` usługi RustProxy przez drop-in systemd. **Nie podmienia** systemowego `/usr/bin/python3`. Zalecane środowiska: **SLES 15 SP6/SP7 i nowsze**, **SLES 16**, **openSUSE Leap 15.6 i 16.x**, **openSUSE Tumbleweed** z architekturą x86_64 i `glibc >= 2.34`. Dla starszych wydań z glibc 2.31 (np. SLES 15 SP5) paczka nie jest kompatybilna bez osobnego builda na starszym toolchainie. Zależności dla konkretnej wersji sprawdź przez `zypper` przed wdrożeniem.
Instalacja po pierwszej publikacji (zamień `REPOSITORY_URL` na adres serwera pakietów):
```sh
sudo rpm --import REPOSITORY_URL/repo-key.gpg
sudo zypper addrepo --refresh --gpgcheck REPOSITORY_URL/suse/x86_64/ rustproxy-suse
sudo zypper refresh rustproxy-suse
sudo zypper install rustproxy
# Opcjonalnie, na hoście z Docker Engine:
sudo zypper install rustproxy-docker-agent
```
Aktualizacja: `sudo zypper update rustproxy rustproxy-docker-agent` (agent tylko gdy zainstalowany). Pakiety i repozytorium są publikowane atomowo wraz z APT/DNF jako część gałęzi Linux. Jeśli builder miał już wydanie z tą samą wersją, `PACKAGING_REVISION=6` wymusi jednorazową przebudowę; gałęzie OPNsense/FreeBSD zachowują dotychczasowy stan. **Paczki RPM nie znajdują się w samym archiwum projektu** — powstają po uruchomieniu buildera na źródłach RustProxy.
## OPNsense repo ## OPNsense repo
@@ -110,6 +219,9 @@ POLL_INTERVAL=300
PACKAGE_RELEASE=auto PACKAGE_RELEASE=auto
MAX_GLIBC_VERSION=2.34 MAX_GLIBC_VERSION=2.34
RUN_LINUX_TESTS=1 RUN_LINUX_TESTS=1
RELEASE_RETENTION=10
PACKAGE_RETENTION=10
CLEAN_BUILD_CACHE=1
OPNSENSE_ABI=26.7 OPNSENSE_ABI=26.7
OPNSENSE_RETENTION=10 OPNSENSE_RETENTION=10
@@ -120,10 +232,43 @@ FREEBSD_RUST_TOOLCHAIN=1.88.0
APPLY_FREEBSD_PATCHES=1 APPLY_FREEBSD_PATCHES=1
``` ```
`RUN_LINUX_TESTS=1` jest domyślne. Testy są bramką tylko dla DEB/RPM i nie wpływają na gałąź OPNsense. Ustaw `0`, jeśli chcesz całkowicie pominąć testy Linux. `RUN_LINUX_TESTS=1` jest domyślne. Testy są bramką tylko dla DEB/RPM/SUSE i nie wpływają na gałąź OPNsense. Ustaw `0`, jeśli chcesz całkowicie pominąć testy Linux.
`PACKAGE_RETENTION=10` zachowuje ostatnie 10 wersji **osobno dla RustProxy i Docker Agenta** w każdym kanale Linux: DEB, RHEL/DNF RPM i SUSE RPM. Oznacza to do 10 kompletnych wersji obu produktów, a nie 10 plików łącznie.
`RELEASE_RETENTION=10` zachowuje 10 najnowszych atomowych snapshotów `data/repository/releases/`. Aktualnie serwowany snapshot jest zawsze chroniony przed usunięciem.
`CLEAN_BUILD_CACHE=1` powoduje usunięcie `cargo target` oraz cache kompilacji Go po każdym pełnym cyklu budowania. `CARGO_TARGET_DIR` i `GOCACHE` znajdują się pod `/tmp/rust-package-builder`, więc duże artefakty kompilacji nie trafiają już do trwałego `data/`. Ustaw `0`, jeśli świadomie chcesz zachować te cache pomiędzy buildami w działającym kontenerze.
`OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji. `OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji.
`FREEBSD_NATIVE_ENABLED=1` (domyślnie) włącza natywny pakiet FreeBSD i oddzielny kanał publikacji. `0` wyłącza budowanie natywnego FreeBSD. **Uwaga przy aktualizacji:** istniejący plik `.env` z `FREEBSD_NATIVE_ENABLED=0` ma pierwszeństwo przed nową wartością domyślną — zmień go ręcznie na `1`, inaczej pakiet FreeBSD nadal nie powstanie. `FREEBSD_NATIVE_RETENTION=10` ogranicza liczbę zachowanych wersji. Repo używa tego samego publicznego hosta/portu co OPNsense, ale pod oddzielnym prefiksem `OPNSENSE_REPOSITORY_URL/freebsd/`.
## Automatyczny Rust toolchain podczas budowania
Builder sprawdza `rust-version` w `Cargo.toml` **pobranego commita** i opcjonalny
`rust-toolchain.toml`/`rust-toolchain` przed kompilacją każdej gałęzi.
- **Linux DEB/RPM:** używa istniejącego `stable`; wywołuje `rustup` tylko,
gdy brakuje toolchainu albo wersja jest starsza od wymaganej przez projekt.
- **FreeBSD/OPNsense:** `FREEBSD_RUST_TOOLCHAIN` pozostaje bazowym toolchainem.
Gdy jest starszy od `rust-version`, builder automatycznie dobiera wersję
minimalną projektu oraz doinstalowuje target `x86_64-unknown-freebsd`.
- Jeśli projekt przypina konkretny toolchain, builder **respektuje pin**.
Sprzeczny pin i `rust-version` powodują czytelny błąd zamiast cichej zmiany.
- Testy i build danej gałęzi używają tego samego wybranego toolchainu.
- Aktualizacja nie zmienia globalnego `rustup default`, systemowych pakietów
ani wersji już opublikowanych pakietów RustProxy.
- Toolchainy pobrane w trakcie działania są w warstwie kontenera
(`/opt/rustup`); po **odtworzeniu kontenera** mogą wymagać ponownego pobrania.
Cache Cargo w `data/cargo` pozostaje trwały.
- Jeśli pobranie toolchainu lub kompilacja się nie powiedzie, dany kanał
nie zostaje opublikowany; pozostały kanał builder obsługuje niezależnie.
Działanie nie wymaga nowej opcji w `.env`. Uruchom `./start.sh`, żeby
przebudować obraz z nowym skryptem. Aktualizacje toolchainu na podstawie
źródeł będą potem automatyczne przy kolejnych buildach.
## FreeBSD cross-build ## FreeBSD cross-build
Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa: Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa:
@@ -148,12 +293,88 @@ Kod pluginu OPNsense znajduje się w:
opnsense/plugin/ opnsense/plugin/
``` ```
## OPNsense: autostart i sprawdzanie aktualizacji aplikacji
W **Services -> RustProxy** zaznacz **Enabled** i naciśnij **Apply**. Pakiet
instaluje wykonywalny syshook w
`/usr/local/etc/rc.syshook.d/start/50-rustproxy.start`. Po restarcie systemu,
gdy sieć już działa, hook odświeża konfigurację z OPNsense i uruchamia usługę
tylko wtedy, gdy jest włączona. Powtórny start nie tworzy drugiego procesu.
Pole **Update check interval (seconds)** steruje argumentem RustProxy
`--update-check-interval-secs`. Domyślnie `3600` (godzina); dopuszczalny zakres
to `60`–`604800` sekund. Wartość nie dotyczy `POLL_INTERVAL` buildera,
który nadal odpowiada wyłącznie za sprawdzanie repozytorium kodu.
Sprawdzenie po restarcie OPNsense:
```sh
ls -l /usr/local/etc/rc.syshook.d/start/50-rustproxy.start
configctl rustproxy status
cat /var/run/rustproxy/command.txt
```
W `command.txt` powinien wystąpić aktualnie ustawiony parametr
`--update-check-interval-secs <liczba>`. Jeśli usługa nie wystartuje, sprawdź
`/var/log/rustproxy/rustproxy.log` oraz uruchom ręcznie:
```sh
configctl template reload OPNsense/RustProxy
configctl rustproxy start
```
Obsługa miękkiego przeładowania zwiększa `OPNSENSE_PACKAGING_REVISION` do `4`.
Pakowanie Linux ma `PACKAGING_REVISION=6`, a natywne FreeBSD `FREEBSD_NATIVE_PACKAGING_REVISION=2`. Każdy kanał otrzymuje osobną aktualizację przy zmianie swojej rewizji.
## DEB/RPM: dostęp do plików strony i zewnętrznych certyfikatów
Od rewizji pakowania **5** jednostka `rustproxy.service` używa
`ProtectHome=read-only` zamiast `ProtectHome=true`. Dzięki temu katalogi
`/home/...`, `/var/www/...` i `/srv/...` mogą być czytane na podstawie
normalnych uprawnień grupowych lub ACL, bez lokalnych wyjątków systemd.
`ProtectSystem=strict` nadal chroni te lokalizacje przed zapisem. Builder
normalizuje również `ProtectHome` w starszym `debian/rustproxy.service`, jeśli
taki plik istnieje w pobranym źródle; sama rewizja pakowania wystarcza do
ponownego zbudowania DEB/RPM przy niezmienionym kodzie aplikacji.
Instrukcja trafia **do obu paczek** jako
`/usr/share/doc/rustproxy/FILESYSTEM_ACCESS.md` (źródło:
[`packaging/rustproxy-filesystem-access.md`](packaging/rustproxy-filesystem-access.md)).
Opisuje bezpieczne przyznanie grupy lub ACL, odświeżenie grup przez restart,
obsługę certyfikatów Certbot i ich symlinków oraz ograniczenie aplikacyjne:
zewnętrzne PEM wymagają zaufanych katalogów właściciela `root`/`rustproxy`.
Pakiet nie przypisuje automatycznie szerokich grup do użytkownika usługi,
nie zmienia uprawnień witryn ani nie nadpisuje istniejących drop-inów.
Test pakowania jest dostępny w pełnym repozytorium developerskim (nie jest dołączony do archiwum wdrożeniowego).
## Rotacja logów RustProxy
- **DEB/RPM:** builder zawsze umieszcza `packaging/rustproxy.logrotate` w `/etc/logrotate.d/rustproxy` (nFPM: `config|noreplace`). Logrotate jest zależnością obu pakietów. Reguła obejmuje `/var/log/rustproxy/*.jsonl`, `*.log` i `vhosts/*.jsonl`, z rotacją codzienną lub po przekroczeniu 100 MiB, 14 archiwami, gzip z opóźnioną kompresją, `copytruncate` oraz `su rustproxy rustproxy`.
- **OPNsense / natywne FreeBSD PKG:** pakiet dostarcza `/usr/local/etc/newsyslog.conf.d/rustproxy.conf` dla tych samych ścieżek; `newsyslog` wykonuje rotację codziennie lub przy rozmiarze 100 MiB, przechowuje 14 archiwów, pomija puste pliki i używa gzip z niekompresowanym najnowszym archiwum. Plik `*.log` jest ponownie otwierany przez `daemon(8)` po `SIGHUP` (opcja `-H` w skrypcie rc.d). OPNsense uruchamia usługę jako `root` (`root:wheel` w `newsyslog`); natywne FreeBSD działa jako `rustproxy` (`rustproxy:rustproxy` w `newsyslog`).
**Uwaga FreeBSD:** `newsyslog` działa przez zmianę nazwy pliku, nie ma `copytruncate`. Pliki JSONL i inne logi otwarte bezpośrednio przez aplikację muszą być przez nią ponownie otwierane po rotacji. W przeciwnym razie proces może pisać do poprzedniego (przeniesionego) pliku; zachowanie nie jest identyczne z Linux. Reguły są instalowane, ale na działającym OPNsense należy zweryfikować sposób otwierania plików przez RustProxy.
Bieżąca rewizja pakowania Linux wynosi `PACKAGING_REVISION=6` i `OPNSENSE_PACKAGING_REVISION=4`; nowy kanał używa `FREEBSD_NATIVE_PACKAGING_REVISION=2`. `check-and-build.sh` sprawdza osobno rewizje pakowania Linux, OPNsense i FreeBSD native; nowa wersja samej wtyczki przebudowuje **tylko OPNsense**, nawet przy niezmienionym SHA aplikacji. Rewizja pakietu PKG zwiększy się automatycznie. W przypadku zmiany rewizji Linux analogicznie odświeżane są DEB/RPM. Konfiguracja w `/etc/logrotate.d/rustproxy` jest oznaczona jako `noreplace`, więc lokalna wersja zmodyfikowana przez administratora może zostać zachowana (zależnie od menedżera pakietów).
Kontrola po zainstalowaniu:
```sh
# DEB / RPM (nie wykonuje rotacji)
logrotate -d /etc/logrotate.d/rustproxy
# OPNsense (nie wykonuje rotacji)
newsyslog -n -v -f /usr/local/etc/newsyslog.conf.d/rustproxy.conf
```
Testy integracyjne są dostępne w pełnym repozytorium developerskim.
## Dane ## Dane
```text ```text
data/ data/
├── aptly/ ├── aptly/
├── cargo/ ├── cargo/ # registry/git cache; bez target/
├── packages/ ├── packages/
│ ├── deb/ │ ├── deb/
│ └── rpm/x86_64/ │ └── rpm/x86_64/
@@ -162,16 +383,17 @@ data/
│ ├── apt -> current/apt │ ├── apt -> current/apt
│ ├── rpm -> current/rpm │ ├── rpm -> current/rpm
│ ├── opnsense -> current/opnsense │ ├── opnsense -> current/opnsense
│ ├── freebsd -> current/freebsd
│ └── releases/ │ └── releases/
├── state/ ├── state/
└── work/ └── work/
``` ```
Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji Linux poprzedni katalog OPNsense jest zachowywany, a przy publikacji OPNsense zachowywane są aktualne APT/RPM. Dzięki temu obie gałęzie są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu. Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji jednego kanału poprzednie katalogi pozostałych kanałów są zachowywane. Dzięki temu wszystkie aktywne kanały są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu.
## HTTP i logi ## HTTP i logi
Jeden kontener nginx obsługuje oba repozytoria na dwóch lokalnych portach: Jeden kontener nginx obsługuje repo Linux i repo OPNsense na dwóch lokalnych portach; natywne FreeBSD jest serwowane jako `/freebsd/` na porcie OPNsense:
```bash ```bash
docker compose logs -f rust-package-builder docker compose logs -f rust-package-builder
@@ -183,10 +405,7 @@ curl -f http://127.0.0.1:18766/healthz
## Testy statyczne ## Testy statyczne
```bash Skrypty diagnostyczne są dostępne w pełnym repozytorium developerskim; wydanie wdrożeniowe zawiera tylko pliki potrzebne do uruchomienia.
./tests/static.sh
./tests/check-rustproxy-source.sh /sciezka/do/RustProxy
```
## Klucz GPG ## Klucz GPG
@@ -206,3 +425,75 @@ Jeżeli istnieje starsze `last_commit` oraz poprawne APT/RPM dla tego SHA, zosta
## FreeBSD patch version detection ## FreeBSD patch version detection
The OPNsense branch reuses the Cargo package `VERSION` already detected by the main build pipeline. If `apply-freebsd-patches.sh` is run manually, it falls back to `RUST_PROXY_PYTHON` (`/usr/bin/python3.11`) so Rocky Linux 9's default Python 3.9 does not cause a missing `tomllib` error. The OPNsense branch reuses the Cargo package `VERSION` already detected by the main build pipeline. If `apply-freebsd-patches.sh` is run manually, it falls back to `RUST_PROXY_PYTHON` (`/usr/bin/python3.11`) so Rocky Linux 9's default Python 3.9 does not cause a missing `tomllib` error.
### Czyszczenie danych po kompilacji i retencja
Builder nie zapisuje już `cargo target` w `data/cargo/target` ani `GOCACHE` w `data/go/cache`. Rejestr Cargo/cache Git oraz pobrane moduły Go mogą pozostać w `data/`, ale duże artefakty kompilacji trafiają pod `/tmp/rust-package-builder/`. Przy `CLEAN_BUILD_CACHE=1` (domyślnie) są usuwane po każdym cyklu, także gdy jedna z gałęzi zakończy się błędem. Workspace źródła i pliki pośrednie również są tworzone pod `/tmp/rust-package-builder/work` i standardowo usuwane po cyklu.
Przy starcie builder automatycznie usuwa pozostałości po starszych wydaniach: `data/cargo/target`, `data/go/cache` oraz stare katalogi w `data/work/`. Dzięki temu aktualizacja buildera odzyskuje miejsce bez ręcznego czyszczenia.
Retencja Linux jest kontrolowana przez `PACKAGE_RETENTION=10`: osobno zachowywane jest 10 DEB RustProxy, 10 DEB Docker Agenta, 10 RHEL RPM każdego produktu i 10 SUSE RPM każdego produktu. Po zmianie zestawu DEB lokalne repo Aptly jest synchronizowane z zachowanymi paczkami, stare snapshoty Aptly są usuwane, a `aptly db cleanup` zwalnia nieużywane obiekty.
`RELEASE_RETENTION=10` ogranicza liczbę atomowych katalogów `repository/releases`. Kolejne snapshoty korzystają z hardlinków do niezmienionych plików poprzedniego wydania, więc te same RPM-y i dane repozytorium nie są fizycznie kopiowane przy każdym buildzie.
Retencja OPNsense (`OPNSENSE_RETENTION=10`) i natywnego FreeBSD (`FREEBSD_NATIVE_RETENTION=10`) pozostaje niezależna.
## RustProxy 0.34.x: plik `--config` i miękki reload
- **Linux DEB/RPM:** gdy usługa systemd nie ma własnego `ExecReload`, builder dodaje wywołanie `rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload`. `systemctl reload rustproxy` wykonuje przeładowanie przez administracyjne API, nie restartuje procesu. Usługi z `ProtectSystem=strict` otrzymują dodatkowo zapis do `-/etc/rustproxy`. Aby panel mógł zapisywać wskazany `--config`, plik i jego katalog muszą być własnością użytkownika usługi. Przykładowo można ustawić `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` w `/etc/rustproxy/environment`, po uprzednim utworzeniu poprawnego pliku JSON.
- **OPNsense:** `configctl rustproxy reload` wykonuje miękkie przeładowanie z CLI. Skrypt `service.sh` nie robi już stop/start dla `reload`. Listener GUI po zmianie adresu/portu wymaga nadal `restart`, ponieważ samo przeładowanie nie zmienia gniazd nasłuchujących. Opcjonalna ścieżka `rustproxy_config_path` w `rc.conf` (absolutna, bez spacji) jest przekazywana jako `--config`; panel OPNsense nie ma jeszcze osobnego pola do jej edycji. CLI wymaga osiągalnego adresu administratora na loopback.
- **Czystemu FreeBSD** służy osobna paczka `rustproxy`, bez modeli MVC, configd, syshooków i pluginu OPNsense. Jej natywne `rc.d` uruchamia usługę na koncie `rustproxy`, domyślnie nasłuchuje na HTTP `8080`, admin na `127.0.0.1:9090`, a TLS jest domyślnie wyłączony. Po `sysrc rustproxy_enable=YES` można wykonać `service rustproxy start`, `service rustproxy reload` i `service rustproxy restart`. Ścieżkę JSON można przekazać przez `sysrc rustproxy_config=/usr/local/etc/rustproxy/config.json` po przygotowaniu prawidłowego pliku z uprawnieniami użytkownika `rustproxy`.
**Ważne:** CLI `reload` wymaga uruchomionego RustProxy, zainicjalizowanego administratora i API osiągalnego przez adres loopback. Zmiana adresów/portów nasłuchujących wymaga `restart`, a nie `reload`.
## Natywne repo FreeBSD — włączone domyślnie
Dla nowej instalacji FreeBSD native włącza się automatycznie. Jeśli masz już `.env`, **sprawdź**, czy ma:
```env
FREEBSD_NATIVE_ENABLED=1
FREEBSD_NATIVE_RETENTION=10
```
Następnie `./start.sh` (przebudowa kontenerów buildera i panelu). Natywna paczka pojawi się pod:
```text
${OPNSENSE_REPOSITORY_URL}/freebsd/
RustProxy-FreeBSD.conf
meta.conf
packagesite.pkg
data.pkg
rustproxy-latest.pkg
FreeBSD:15:amd64/rustproxy-<wersja>_<revision>.pkg
```
Na **czystym FreeBSD 15 amd64**:
```sh
mkdir -p /usr/local/etc/pkg/repos
fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf \
https://YOUR-OPNSENSE-REPO-HOST/freebsd/RustProxy-FreeBSD.conf
pkg update -f
pkg install rustproxy
sysrc rustproxy_enable=YES
service rustproxy start
service rustproxy reload
```
To osobny pakiet od `os-rustproxy`: nie należy instalować obu pakietów naraz, bo oba używają nazwy usługi i ścieżek RustProxy.
**Bezpieczeństwo repo:** aktualnie katalog `pkg` jest **niesygnowany**, analogicznie do istniejącego repo OPNsense (`signature_type: none`). To nadaje się do testów DEV na zaufanej infrastrukturze. Przed produkcyjnym wdrożeniem należy dodać podpis katalogu `pkg`, bezpieczną dystrybucję klucza publicznego i wykonać instalację oraz aktualizację na rzeczywistym FreeBSD. Generator katalogu korzysta z formatu repozytorium `pkg` w wersji **2** dla FreeBSD 15 (OPNsense zachowuje dotychczasowy format), ale testy offline nie zastępują testu klienta `pkg` na prawdziwym FreeBSD.
Pakietowe testy offline są dostępne w pełnym repozytorium developerskim (nie w wydaniu wdrożeniowym).
## Troubleshooting: no native FreeBSD build
1. Ensure `.env` contains `FREEBSD_NATIVE_ENABLED=1` (older installations often set `0`).
2. Rebuild/start: `docker compose --env-file .env up -d --build rust-package-builder repository-dashboard`.
3. Follow the builder logs: `docker compose --env-file .env logs -f rust-package-builder`.
4. Look for `build plan: ... freebsd=1` and then `FreeBSD native publication completed:`. If compilation fails, inspect the **first** `error:` in the same log. The OPNsense package uses the same FreeBSD cross-compiled executable but native FreeBSD publishes independently.
5. Check `data/repository/current/freebsd/BUILD-INFO.txt` and `data/repository/current/freebsd/meta.conf` (`version: 2;`). The API `/api/v1/packages` then shows the published FreeBSD package/version/build time.
The development test `tests/freebsd-release.sh` uses a simulated ELF to verify packaging/catalog/atomic publication without Docker. It **does not prove Rust compilation or installation on real FreeBSD**. For end-to-end validation, run `pkg update -f` and `pkg install rustproxy` on FreeBSD 15.1 amd64 against the published URL. The catalog is **not signed**, so only use a trusted HTTPS source during testing.
+69
View File
@@ -0,0 +1,69 @@
efd2628f1ff160f030259b32d96cf16093ff5f02ec24854643082749b5f5e578 .dockerignore
f5b89608eb69ad40a5dbf8e4f4ab00f4d62b1f3cd6ab30dabab9f3862d59e27a .env.example
c6d0d850c2c2a7491c239228982844e3c583d1fd71f8c75c0be999c306795374 .gitignore
15125941350152641991ef172d0dd63375e8cb7dae55a36abe0e05c6fe3c9c34 Dockerfile
fe94b8e7fbbda93ecffffcb7621d39a299d48726c6c63249ee2c633072d1f6d4 README.md
862263fa1f46c20f0d1e4dac5ffcc75abd55c08211b2c3864c5f8764b9d87793 dashboard/.dockerignore
ced0339cb93478c2faf7b7b74215fe03375086e11ca957bd8892b52802fbbd3a dashboard/Dockerfile
bc13583e4dc80d4914c7a82a06c1390b5118d4821c7d081f99d68a38e1156bac dashboard/app.py
3b03fe39336fdfb9cd4abc2fa9a47c335afd72fcc4a868375b6fd93ab4e98a71 dashboard/web/app.js
f1ac0fb5c4da97db09ff328cb86f4bc763fbdd84d7310d6e8e713be1c8707bb9 dashboard/web/favicon.svg
05821c98841c783014496d88192639609408e7907c3ac15687e6aafd966c5687 dashboard/web/index.html
94010e2be028d8153d1ffe9142e0a5aeb6d484c1edf5cb7318d70c5e32b5c985 dashboard/web/style.css
f0bb19d1668e8779072500ff722074a6b5cc31e04ab712a6084e27c117480f72 docker-compose.yml
88206715d29efce6b2b377d4f04e6ca9dfccf3aa4ee89668a1317358307e40b6 http/nginx.conf
d8459024f04ca514bd8e9d6bd3af872fb942fd85a7cdd5583f4a9d28aab6faba make_zip.py
77e898aebb54f8acf39d3eb1fb348e7f41532da1b0a9e1820a717fdcde406f71 opnsense/patches/0.20.0/0001-opnsense-build.patch
80bb419b980793f3a57b4fda0dc132a872186b3a07e0e6db2585c6057ab4f882 opnsense/plugin/+POST_INSTALL.pre
af4bc0ab259df3a0681fea0fcc7d84e08c8b1a1a1eb34300c4a01eccb49fa537 opnsense/plugin/+PRE_DEINSTALL.pre
07b99ecfd2437ee1e9e337c10ad31280c7c1d8a2c9254e3e2dc9eb5fbeba1a36 opnsense/plugin/Makefile
3acf504f1e6a4414f43f051cd1b28f595add3ed7301da091a1379813bef72059 opnsense/plugin/pkg-descr
ca979a78c1eef6ebdba9ec89504f5dc9c11d4977b3af999298a0c1a1eb3aba15 opnsense/plugin/src/etc/newsyslog.conf.d/rustproxy.conf
3d00d4319af64803f4620f8d5b837a33857f9682ed480b4fa8edf4988e88d014 opnsense/plugin/src/etc/rc.d/rustproxy
69a508ddefd342f125d1235b7b590c2d233a1d50adc0372dddee36ef8d65aede opnsense/plugin/src/etc/rc.syshook.d/10-rustproxy.start
9bfd871eebb3e499587cf19ddb604f50001b3097f3ae20ecc511f3a1067654e4 opnsense/plugin/src/etc/rc.syshook.d/start/50-rustproxy.start
41d5c3d555d57c17d756f80ca0c30bbd256a68e2dcf51dc2d4dc2674a614a3dd opnsense/plugin/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/Api/ServiceController.php
85833df45b327bb5d403db252ff25734b9d442f54cee51a9004ee1f889e9a74d opnsense/plugin/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/Api/SettingsController.php
747ee1fa625b0862c0de248d9cd181a600310fb893bb965d70ebaf617203c081 opnsense/plugin/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/IndexController.php
f25006f6743c5f7119ff1f56335c42f2f2b73c832d555c8ae0494857a4bdad9d opnsense/plugin/src/opnsense/mvc/app/controllers/OPNsense/RustProxy/forms/general.xml
8c1232f2b32158abab5e6fa465064b5e7d155faadfa8192aa2a7397f13d8b5c5 opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/ACL/ACL.xml
e8f29912261155366d3ce06dd640786cd37d4860de3b57481e9e2573ec6ef174 opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/Menu/Menu.xml
8682e5bbaadc49a66c9e99044ec649b89c1d79cf5a44ec595d162f2f18f6f5f9 opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/Migrations/M1_4_0.php
cb0b51313d0e5c22c8a192a359c94b3dec1d1e00ec0db6b3d8e46c8c3c03f2ea opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/Migrations/M1_5_0.php
01fbc85f40663dff4309f34c3c03dee293f36a0b2d79bcf89d6318a2be473cee opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.php
4f3c4f92972b27f2bc07721d67be331320985d3563c9da1df8e9bb0cfe143636 opnsense/plugin/src/opnsense/mvc/app/models/OPNsense/RustProxy/RustProxy.xml
3d2581da5c9ceafef9b6348e06beb78165eea9f06f0dd71fbcd7467142be8b41 opnsense/plugin/src/opnsense/mvc/app/views/OPNsense/RustProxy/index.volt
96a6cdcfe5150fb8687d47fe9f8a63f664ff4e3006a8441d3f065b61b4922df9 opnsense/plugin/src/opnsense/scripts/rustproxy/service.sh
d4fc4b261c8252afbc6f403f828656cb7a49174de8cd239a233f5475d4cfeacd opnsense/plugin/src/opnsense/service/conf/actions.d/actions_rustproxy.conf
32c1c3ed725c59d7850770e86bb3eab6bb2edc045bfa3e3be567d6468645e3bd opnsense/plugin/src/opnsense/service/templates/OPNsense/RustProxy/+TARGETS
5faeb5e371bfbf38497b2c49ee128120b99c371c19c3cc520a92a3f0b1d797e3 opnsense/plugin/src/opnsense/service/templates/OPNsense/RustProxy/rustproxy.conf
6a5b546163c5a57e8df0aa270bcaf5c08c7d786d427e24eea9bba88de83dedd9 packaging/freebsd/+POST_INSTALL
43004e4dd882bb89b35cfd5ee7c0a844de2ed4f5833bd5c4e57b61f4fd124119 packaging/freebsd/+PRE_DEINSTALL
173c204b5a3071cef2563881ac9f52437141a3303bc5c3f3a50fd637a045b4cf packaging/freebsd/rustproxy.newsyslog
c8dde46217c008bfc31420c19ecbd31e3db6d71ee48f99be856ad40ca3c7f490 packaging/freebsd/rustproxy.rc
79122afbd5964a87c0d933abe8b34745d4189e2a586d5918a82efbb9b2f50864 packaging/rustproxy-filesystem-access.md
0f7970888117b4bc810ad3f6abc6796f189d34e4863875b2501785e689471be6 packaging/rustproxy.logrotate
b5845f5510adcc3a63ccc710d8e893b33336051651917fa533e8b70df2e2991d packaging/suse-python3
eee3cb8959cb90cd1b5b320ec4674892e5d1a987049d1028dae39c3bdc9f3dc7 scripts/apply-freebsd-patches.sh
d1f2ac24d6742afed8d46fe827373cd4b3febebfcccf15f7265bed0cf8ec4c9c scripts/build-freebsd-release.sh
4e56d41be31d29d387e4441cfb2a771382e700d90abb3980055127a77636102c scripts/build-linux-release.sh
43a51a2734b5f2629598c3d84ca656480a46c2d09bc5b0b858e92aa6c89151ed scripts/build-opnsense-release.sh
16bafbba1eb2000b42c754bae92fee43301e57e0859e2cef17be2550006ab111 scripts/build-opnsense.sh
662037310a19cea7ca11d1f59141e40ba6ca1aa64710c117c6ff7bee3c8f7a2f scripts/build-release.sh
f5e2be2a0128e0d768c3ed4b151b21dfd39d35a688a4c3f6fc35877c3e2ee434 scripts/check-and-build.sh
cba164bde8967045bfb76de7eff46d4407dfb7ed2bc861fbc600782551d1629e scripts/ensure-rust-toolchain.sh
82656b00eb17798f35df61d84f338e22e6b0603ca9b48c011f9d398f6ef50d0d scripts/entrypoint.sh
c8402314cbcf87c30eb521a273a13628eccb78112acb85e0e85c9a7679550cca scripts/generate-freebsd-index.sh
d3193e4bbfe3d853db003610318815fdfc295fa501affffd3e18db4c7074a5a2 scripts/generate-index.sh
80f49932346268f2cd63cbfe4814c666cd665661e82c25f2e99883929ecae0c0 scripts/generate-opnsense-index.sh
094c603f30f6a41bddeedfefa9e2c260dc97f849f0a690475ebe128ce4b49475 scripts/generate-opnsense-repo.py
5aae7f35cc09576c07f8c57ce59cb6153840be15f187e0cf538d6987780f670a scripts/init-storage.sh
22593936f05004a299bdc589b5e8553c8537244acf9640f1c745e367d9940537 scripts/lib.sh
de4fbbb973d9cddb170ef1e9f4f6f7896018ed5e6184594c9636c0215adf5233 scripts/normalize-systemd-unit.py
0eca8ef60f15dce0c5651fc8a55de10e0d9a11f3816f04153608abcbe2e2e1d8 scripts/package-freebsd.sh
b098fa629f9fcc1a54baab3d46b28721eb582c314e084b3210259bf157c124a6 scripts/package-opnsense.sh
37233bdc0e14cdf32a18095fffc5fb219795ec7e838a112eb38fa29195a5d25c scripts/prepare-agent-package.sh
fc8c6ae8e35fb68f4851344ddf014759b7bac4f2d07b6b8bb383b3289e3ca23b scripts/prepare-package.sh
4bd555061e9a50faa05e2f8c095da2a6581e19b8d4c7fe01d791e59f5dd2f597 start.sh
bd765bbbf852f5d6e13aff9dbe8e743691b4c3e49168f8f32e77304191ff035c tests/test_cleanup_retention.py
bc54e4b93202454c298d1e67aa68f126e25b8c2b20e62db46a89932fdacd2ba9 tests/test_suse.py
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.12-alpine
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
REPOSITORY_ROOT=/srv/repository \
DASHBOARD_PORT=8000
WORKDIR /app
COPY --chown=10001:10001 app.py /app/app.py
COPY --chown=10001:10001 web/ /app/web/
USER 10001:10001
EXPOSE 8000
CMD ["python", "/app/app.py"]
+320
View File
@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""Read-only, live RustProxy package registry UI.
The existing Nginx continues to serve APT, DNF and pkg artifacts unchanged.
This service only generates the dashboard and a public JSON catalog.
"""
from __future__ import annotations
import json
import logging
import os
import re
import threading
import time
from datetime import datetime, timezone
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import quote, urlsplit
LOG = logging.getLogger("rustproxy.repository")
REPOSITORY = Path(os.environ.get("REPOSITORY_ROOT", "/srv/repository"))
WEB = Path(__file__).resolve().parent / "web"
PORT = int(os.environ.get("DASHBOARD_PORT", "8000"))
CACHE_SECONDS = max(1, int(os.environ.get("DASHBOARD_CACHE_SECONDS", "15")))
MAX_PACKAGES = 5000
SITES = ("linux", "suse", "opnsense", "freebsd")
META_PATHS = {
"linux": Path("LINUX-BUILD-INFO.txt"),
"suse": Path("SUSE-BUILD-INFO.txt"),
"opnsense": Path("opnsense/BUILD-INFO.txt"),
"freebsd": Path("freebsd/BUILD-INFO.txt"),
}
REPO_URLS = {
"linux": os.environ.get("REPOSITORY_URL", "http://localhost:18765").rstrip("/"),
"opnsense": os.environ.get("OPNSENSE_REPOSITORY_URL", "http://localhost:18766").rstrip("/"),
}
REPO_URLS["freebsd"] = REPO_URLS["opnsense"] + "/freebsd"
REPO_URLS["suse"] = REPO_URLS["linux"] + "/suse"
DEB_FILENAME = re.compile(r"^(?P<name>.+?)_(?P<version>[^_]+)_(?P<arch>[^_]+)\.deb$")
RPM_FILENAME = re.compile(
r"^(?P<name>.+)-(?P<version>\d[^-]*)-(?P<release>[^/]+)\."
r"(?P<arch>x86_64|amd64|aarch64|noarch|arm64)\.rpm$"
)
PKG_FILENAME = re.compile(r"^(?P<name>.+)-(?P<version>\d[^/]*)\.pkg$")
def iso_time(stamp: float) -> str:
return datetime.fromtimestamp(stamp, tz=timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
def parse_metadata(path: Path) -> dict[str, str]:
try:
# Metadata is emitted by our builder, not submitted through the dashboard.
text = path.read_text(encoding="utf-8")
except (FileNotFoundError, PermissionError, OSError):
return {}
info: dict[str, str] = {}
for line in text.splitlines():
key, separator, value = line.partition("=")
if separator and re.fullmatch(r"[a-z][a-z0-9_]*", key):
info[key] = value.strip()[:1024]
return info
def parse_filename(filename: str, kind: str) -> tuple[str, str, str] | None:
if kind == "deb":
m = DEB_FILENAME.fullmatch(filename)
return (m["name"], m["version"], m["arch"]) if m else None
if kind == "rpm":
m = RPM_FILENAME.fullmatch(filename)
if m:
return (m["name"], f"{m['version']}-{m['release']}", m["arch"])
return None
if filename in ("packagesite.pkg", "data.pkg") or filename.endswith("-latest.pkg"):
return None
m = PKG_FILENAME.fullmatch(filename)
return (m["name"], m["version"], "amd64") if m else None
def publication_info(current: Path, channel: str) -> dict[str, object]:
location = current / META_PATHS[channel]
raw = parse_metadata(location)
info: dict[str, object] = {
"channel": channel,
"published": bool(raw),
"app_version": raw.get("app_version", ""),
"package_version": raw.get("package_version", ""),
"package_release": raw.get("package_release", ""),
"source_commit": raw.get("source_commit", ""),
"source_ref": raw.get("source_ref", ""),
"source_url": raw.get("source_url", ""),
"build_reason": raw.get("build_reason", ""),
"packaging_revision": raw.get("packaging_revision", ""),
"opnsense_series": raw.get("opnsense_series", ""),
"freebsd_abi": raw.get("freebsd_abi", ""),
"built_at": None,
"date_source": "unknown",
}
if channel in ("linux", "suse") and info["app_version"]:
info["package_version"] = f"{info['app_version']}-{info['package_release']}" if info["package_release"] else info["app_version"]
stamp = raw.get("built_at", "")
if stamp:
try:
candidate = datetime.fromisoformat(stamp.replace("Z", "+00:00"))
if candidate.tzinfo is not None:
info["built_at"] = candidate.astimezone(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
info["date_source"] = "build"
except ValueError:
pass
if info["built_at"] is None and raw:
# Backward compatibility with builds made before 'built_at' was saved.
try:
info["built_at"] = iso_time(location.stat().st_mtime)
info["date_source"] = "file"
except OSError:
pass
return info
def get_current(repository: Path) -> Path | None:
try:
# Never follow a manipulated symlink outside the repository tree.
root = repository.resolve(strict=True)
current = (root / "current").resolve(strict=True)
if current.is_dir() and current.is_relative_to(root):
return current
except (OSError, RuntimeError):
pass
return None
def package_entry(path: Path, current: Path, site: str, kind: str, channel: str,
builds: dict[str, dict[str, object]]) -> dict[str, object] | None:
parsed = parse_filename(path.name, kind)
if not parsed or not path.is_file():
return None
name, version, arch = parsed
# Discovery agent is packaged for Linux DEB/RPM only; never advertise it as FreeBSD pkg.
if channel in ('opnsense', 'freebsd') and name == 'rustproxy-docker-agent':
return None
try:
stat = path.stat()
except OSError:
return None
relative = path.relative_to(current)
if site == "linux":
public_path = relative
else:
public_path = relative.relative_to(site)
# Each URL segment is encoded independently; separators remain separators.
encoded_path = "/".join(quote(p, safe="") for p in public_path.parts)
base = REPO_URLS[site]
built_at = iso_time(stat.st_mtime)
source = "file"
build = builds[channel]
if build.get("date_source") == "build" and version == build.get("package_version"):
built_at = str(build["built_at"])
source = "build"
return {
"name": name,
"version": version,
"architecture": arch,
"format": kind.upper() if kind != "pkg" else "PKG",
"channel": channel,
"size_bytes": stat.st_size,
"built_at": built_at,
"date_source": source,
"download_url": f"{base}/{encoded_path}",
"relative_path": relative.as_posix(),
}
def generate_catalog(repository: Path = REPOSITORY) -> dict[str, object]:
current = get_current(repository)
builds: dict[str, dict[str, object]] = {}
packages: list[dict[str, object]] = []
for channel in SITES:
builds[channel] = publication_info(current, channel) if current else {
"channel": channel, "published": False, "app_version": "", "package_version": "",
"package_release": "", "source_commit": "", "source_ref": "",
"source_url": "", "build_reason": "", "packaging_revision": "",
"opnsense_series": "", "freebsd_abi": "", "built_at": None, "date_source": "unknown",
}
if current is not None:
folders = (
("linux", "deb", "linux", current / "apt" / "pool", "*.deb"),
("linux", "rpm", "linux", current / "rpm" / "x86_64", "*.rpm"),
("suse", "rpm", "suse", current / "suse" / "x86_64", "*.rpm"),
("opnsense", "pkg", "opnsense", current / "opnsense", "*.pkg"),
("freebsd", "pkg", "freebsd", current / "freebsd", "*.pkg"),
)
for site, kind, channel, directory, pattern in folders:
if not directory.is_dir():
continue
try:
for file in directory.rglob(pattern):
result = package_entry(file, current, site, kind, channel, builds)
if result:
packages.append(result)
except OSError as e:
LOG.warning("Cannot scan %s: %s", directory, e)
packages.sort(key=lambda item: (str(item["built_at"]), str(item["version"])), reverse=True)
total_packages = len(packages)
packages = packages[:MAX_PACKAGES]
channel_counts = {site: sum(p["channel"] == site for p in packages) for site in SITES}
build_times = [str(b["built_at"]) for b in builds.values() if b["built_at"]]
return {
"generated_at": iso_time(time.time()),
"ready": any(b["published"] for b in builds.values()) or bool(packages),
"repository_urls": REPO_URLS,
"apt_distribution": os.environ.get("REPO_DISTRIBUTION", "stable"),
"apt_component": os.environ.get("REPO_COMPONENT", "main"),
"builds": builds,
"packages": packages,
"summary": {
"packages_count": total_packages,
"packages_visible": len(packages),
"channel_counts": channel_counts,
"channels_published": sum(bool(b["published"]) for b in builds.values()),
"latest_build_at": max(build_times, default=None),
},
}
class CatalogCache:
def __init__(self, repository: Path = REPOSITORY, ttl: int = CACHE_SECONDS):
self.repository = repository
self.ttl = ttl
self._lock = threading.Lock()
self._expires = 0.0
self._snapshot: dict[str, object] | None = None
def get(self) -> dict[str, object]:
with self._lock:
if self._snapshot is None or time.monotonic() >= self._expires:
self._snapshot = generate_catalog(self.repository)
self._expires = time.monotonic() + self.ttl
return self._snapshot
CACHE = CatalogCache()
class DashboardHandler(BaseHTTPRequestHandler):
server_version = "RustProxyRepository/1.0"
sys_version = ""
def do_GET(self) -> None:
path = urlsplit(self.path).path
if path == "/healthz":
self.send_bytes(b"ok\n", "text/plain; charset=utf-8", cache="no-store")
elif path == "/api/v1/packages":
try:
payload = json.dumps(CACHE.get(), ensure_ascii=False, separators=(",", ":"))
except Exception:
LOG.exception("Failed to build package catalog")
self.send_error(HTTPStatus.INTERNAL_SERVER_ERROR)
return
self.send_bytes(payload.encode("utf-8"), "application/json; charset=utf-8", cache="no-store")
elif path in ("/", "/index.html", "/freebsd/", "/freebsd/index.html"):
site = self.headers.get("X-Repo-Site", "linux")
if path.startswith("/freebsd/"):
site = "freebsd"
if site not in SITES:
site = "linux"
page = (WEB / "index.html").read_text(encoding="utf-8")
page = page.replace("__REPOSITORY_SITE__", site)
self.send_bytes(page.encode("utf-8"), "text/html; charset=utf-8", cache="no-store", html=True)
elif path in ("/ui/style.css", "/ui/app.js", "/ui/favicon.svg"):
name = path.rsplit("/", 1)[-1]
mime = {"style.css": "text/css", "app.js": "application/javascript", "favicon.svg": "image/svg+xml"}[name]
self.send_bytes((WEB / name).read_bytes(), f"{mime}; charset=utf-8", cache="public, max-age=600")
else:
self.send_error(HTTPStatus.NOT_FOUND, "Unknown dashboard route")
def do_HEAD(self) -> None:
# Nginx allows HEAD and GET; suppress response content for HEAD.
self.do_GET()
def send_bytes(self, payload: bytes, mime: str, *, cache: str, html: bool = False) -> None:
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", mime)
self.send_header("Content-Length", str(len(payload)))
self.send_header("Cache-Control", cache)
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
if html:
self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; "
"connect-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; "
"frame-ancestors 'none'; form-action 'none'")
self.end_headers()
if self.command != "HEAD":
self.wfile.write(payload)
def log_message(self, fmt: str, *args: object) -> None:
LOG.info("%s - %s", self.address_string(), fmt % args)
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
httpd = ThreadingHTTPServer(("0.0.0.0", PORT), DashboardHandler)
httpd.daemon_threads = True
LOG.info("Dashboard ready on port %s (repository: %s)", PORT, REPOSITORY)
try:
httpd.serve_forever(poll_interval=0.5)
except KeyboardInterrupt:
pass
finally:
httpd.server_close()
if __name__ == "__main__":
main()
+410
View File
@@ -0,0 +1,410 @@
/* RustProxy Package Registry — dependency-free interactive client */
(() => {
'use strict';
const el = (id) => document.getElementById(id);
const CHANNEL_LABELS = {linux:'Linux',suse:'SUSE / openSUSE',opnsense:'OPNsense',freebsd:'FreeBSD'};
const INSTALL_NAMES = {deb:'Debian / Ubuntu',rpm:'RHEL / Rocky / Alma / Fedora',suse:'SUSE / openSUSE / Leap',opnsense:'OPNsense',freebsd:'FreeBSD native'};
const SECTION_LABELS = {overview:'Overview',packages:'Packages & versions',install:'Installation',repository:'Repositories'};
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[char]));
const collator = new Intl.Collator('en', {numeric:true,sensitivity:'base'});
const formatNumber = new Intl.NumberFormat('en-GB');
const site = ['linux','opnsense','freebsd'].includes(document.body.dataset.site) ? document.body.dataset.site : 'linux';
const state = {
site, catalog:null, section:'overview', installing:site === 'linux' ? 'deb' : site,
channel:site === 'linux' ? 'all' : site,
format:'all', search:'', sort:'date', rows:25, refreshing:false,
};
const formatDate = (value) => {
if (!value) return '—';
const time = new Date(value);
if (Number.isNaN(time.valueOf())) return '—';
return new Intl.DateTimeFormat('en-GB', {year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}).format(time);
};
const formatShortDate = (value) => {
if (!value) return '—';
const time = new Date(value);
if (Number.isNaN(time.valueOf())) return '—';
return new Intl.DateTimeFormat('en-GB', {day:'2-digit',month:'short',year:'numeric'}).format(time);
};
const formatSize = (value) => {
const size = Number(value);
if (!Number.isFinite(size) || size < 0) return '—';
if (size < 1024) return `${formatNumber.format(size)} B`;
if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / 1024 ** 2).toFixed(1)} MB`;
};
const safeLink = (url) => {
try {
const resolved = new URL(url, window.location.href);
if (['http:','https:'].includes(resolved.protocol)) return resolved.href;
} catch (_) { /* invalid URL */ }
return '#';
};
const origin = (url) => typeof url === 'string' ? url.replace(/\/$/, '') : '';
const dateLabel = (kind) => kind === 'build' ? 'Build timestamp recorded by the builder' : 'File modification time (older builds without a build timestamp)';
const setText = (id, value) => {const node = el(id);if (node) node.textContent = value;};
const versionText = (b) => b?.app_version ? `v${b.app_version}` : '—';
let toastTimer;
function toast(message) {
const node = el('toast');
node.textContent = message;
node.hidden = false;
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(() => {node.hidden = true;}, 2700);
}
function setTheme(theme) {
const target = ['dark','light','cream'].includes(theme) ? theme : 'dark';
document.documentElement.dataset.theme = target;
setText('theme-name', {dark:'Dark',light:'Light',cream:'Cream'}[target]);
try {localStorage.setItem('rustproxy-repo-theme', target);} catch (_) { /* optional */ }
}
function navigate(section) {
const next = SECTION_LABELS[section] ? section : 'overview';
state.section = next;
document.querySelectorAll('.view').forEach((view) => {view.hidden = view.id !== `view-${next}`;});
document.querySelectorAll('[data-section]').forEach((btn) => {
const active = btn.dataset.section === next;
btn.classList.toggle('active', active);
if (active) btn.setAttribute('aria-current','page'); else btn.removeAttribute('aria-current');
});
setText('crumb-section', SECTION_LABELS[next]);
if (window.location.hash !== `#${next}`) history.replaceState(null, '', `${location.pathname}${location.search}#${next}`);
window.scrollTo({top:0,behavior:'instant'});
}
function buildCard(build, channel) {
const live = !!build?.published;
const commit = build?.source_commit ? build.source_commit.slice(0,9) : '—';
const subtitle = channel === 'linux' ? 'APT / DNF · amd64 + x86_64' : channel === 'suse'
? 'RPM / Zypper · x86_64' : channel === 'opnsense'
? `PKG · ${build?.opnsense_series || 'FreeBSD'}` : `PKG · ${build?.freebsd_abi || 'Native'}`;
const version = build?.package_version || (live ? build?.app_version : '') || '—';
return `<article class="build-card${live ? '' : ' off'}">
<div class="build-card-top"><span>${esc(CHANNEL_LABELS[channel].toUpperCase())} / ${['linux','suse'].includes(channel) ? 'SIGNED' : 'PKG'}</span><span class="pill ${live ? 'live' : 'missing'}">${live ? 'PUBLISHED' : 'NOT BUILT'}</span></div>
<div class="build-card-version">${esc(version)}</div><div class="build-card-sub">${esc(subtitle)}</div>
<div class="build-card-details"><div><small>BUILD DATE</small><strong title="${esc(dateLabel(build?.date_source))}">${esc(formatDate(build?.built_at))}</strong></div><div><small>COMMIT</small><strong class="commit">${esc(commit)}</strong></div></div>
</article>`;
}
function formatBadge(pkg) {
return `<span class="format-label${pkg.format === 'PKG' ? ' pkg' : ''}">${esc(pkg.format)}</span>`;
}
function tableRow(pkg, compact = false) {
const download = `<a class="download-link" href="${esc(safeLink(pkg.download_url))}" target="_blank" rel="noopener noreferrer" download title="Download ${esc(pkg.name)} ${esc(pkg.version)}">${compact ? 'Download' : 'Download file'} <span>↗</span></a>`;
const first = `<span class="artifact-name" title="${esc(pkg.name)}">${esc(pkg.name)}</span><span class="artifact-arch">${esc(pkg.architecture)}</span>`;
const format = `${formatBadge(pkg)}<span class="channel-tag ${pkg.format === 'PKG' ? 'pkg' : ''}">${esc(CHANNEL_LABELS[pkg.channel] || pkg.channel)}</span>`;
const date = `<span class="date-cell" title="${esc(dateLabel(pkg.date_source))}">${esc(formatDate(pkg.built_at))}</span>`;
if (compact) return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td>${download}</td></tr>`;
return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td><span class="date-cell">${esc(formatSize(pkg.size_bytes))}</span></td><td>${download}</td></tr>`;
}
function renderOverview(data) {
const main = data.builds?.[state.site];
const heroVersion = versionText(main);
setText('hero-version', heroVersion);
setText('hero-channel', state.site.toUpperCase());
setText('hero-date', formatDate(main?.built_at));
setText('hero-commit', main?.source_commit ? main.source_commit.slice(0,12) : '—');
el('hero-date').title = dateLabel(main?.date_source);
setText('stat-packages', formatNumber.format(data.summary?.packages_count || 0));
setText('stat-channels', `${data.summary?.channels_published || 0} / 4`);
setText('stat-date', formatShortDate(data.summary?.latest_build_at));
setText('stat-version', heroVersion);
setText('stat-release', main?.package_version ? `Package: ${main.package_version}` : 'No release published yet');
setText('stat-date-sub', 'Latest publication date');
setText('pipeline-updated', formatDate(data.generated_at));
const ready = !!data.ready;
el('hero-status').innerHTML = `<span class="pulse-dot"></span> ${ready ? 'REPOSITORY ONLINE' : 'WAITING FOR BUILD'}`;
el('hero-status').classList.toggle('waiting', !ready);
el('sidebar-status').classList.toggle('waiting', !ready);
el('sidebar-status').querySelector('span').textContent = ready ? 'REPOSITORY ONLINE' : 'WAITING FOR DATA';
const side = el('side-channels');
side.innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => `<span class="side-channel${data.builds[channel]?.published ? '' : ' disabled'}"><i class="tiny-dot"></i>${esc(CHANNEL_LABELS[channel])}<small>${channel === 'linux' ? 'APT / DNF' : channel === 'suse' ? 'RPM / Zypper' : 'PKG'}</small></span>`).join('');
el('build-grid').innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => buildCard(data.builds[channel],channel)).join('');
const recent = data.packages.filter((pkg) => state.site === 'linux' || pkg.channel === state.site).slice(0,6);
el('recent-rows').innerHTML = recent.length ? recent.map((pkg) => tableRow(pkg,true)).join('') : '<tr><td colspan="5" class="empty-table">No packages published yet. The catalog will populate after the first build.</td></tr>';
}
function filteredPackages() {
const arr = state.catalog?.packages || [];
const search = state.search.trim().toLocaleLowerCase('en-GB');
const filtered = arr.filter((pkg) => {
if (state.channel !== 'all' && pkg.channel !== state.channel) return false;
if (state.format !== 'all' && pkg.format !== state.format) return false;
if (!search) return true;
return [pkg.name,pkg.version,pkg.channel,pkg.format,pkg.architecture].some((field) => String(field ?? '').toLocaleLowerCase('en-GB').includes(search));
});
if (state.sort === 'name') filtered.sort((a,b) => collator.compare(a.name,b.name) || collator.compare(b.version,a.version));
if (state.sort === 'version') filtered.sort((a,b) => collator.compare(b.version,a.version) || collator.compare(a.name,b.name));
if (state.sort === 'size') filtered.sort((a,b) => b.size_bytes - a.size_bytes);
if (state.sort === 'date') filtered.sort((a,b) => String(b.built_at).localeCompare(String(a.built_at)) || collator.compare(b.version,a.version));
return filtered;
}
function renderPackages() {
const data = state.catalog;
if (!data) return;
const all = filteredPackages();
const shown = all.slice(0,state.rows);
setText('packages-total', `${formatNumber.format(data.summary.packages_count)} packages`);
setText('filter-count', `${formatNumber.format(all.length)} ${all.length === 1 ? 'result' : 'results'}`);
setText('result-range', all.length ? `Showing ${shown.length} of ${all.length}` : 'No matching packages');
el('catalog-rows').innerHTML = shown.length ? shown.map((pkg) => tableRow(pkg)).join('') : '<tr><td colspan="6" class="empty-table">No packages match these filters. Adjust your search or check back after a new release.</td></tr>';
el('show-more').hidden = shown.length >= all.length;
}
function instructions(kind, data) {
const urls = data.repository_urls || {};
const linux = origin(urls.linux);
const suse = origin(urls.suse || `${linux}/suse`);
const opnsense = origin(urls.opnsense);
const freebsd = origin(urls.freebsd);
const distribution = data.apt_distribution || 'stable';
const component = data.apt_component || 'main';
if (kind === 'deb') {
return {
description:'Add the signed APT repository, update the package list, and install RustProxy.',
signature:'GPG SIGNED',
note:'Supported: Debian / Ubuntu · amd64 architecture.',
reference:`${linux}/apt/dists/${encodeURIComponent(distribution)}/InRelease`,
agent:'sudo apt install rustproxy-docker-agent',
code:[
`curl -fsSL ${linux}/repo-key.gpg \\`,
' | sudo gpg --dearmor -o /usr/share/keyrings/rustproxy.gpg',
'',
`echo "deb [signed-by=/usr/share/keyrings/rustproxy.gpg] ${linux}/apt ${distribution} ${component}" \\`,
' | sudo tee /etc/apt/sources.list.d/rustproxy.list',
'',
'sudo apt update',
'sudo apt install rustproxy',
].join('\n'),
};
}
if (kind === 'rpm') {
return {
description:'Add the signed DNF/YUM repository and install the RustProxy server.',
signature:'GPG SIGNED',
note:'Supported: RHEL / Rocky / Alma / Fedora · x86_64 architecture.',
reference:`${linux}/rpm/x86_64/repodata/repomd.xml`,
agent:'sudo dnf install rustproxy-docker-agent',
code:[
`sudo rpm --import ${linux}/repo-key.gpg`,
'',
"sudo tee /etc/yum.repos.d/rustproxy.repo > /dev/null <<'REPO'",
'[rustproxy]',
'name=RustProxy Repository',
`baseurl=${linux}/rpm/$basearch`,
'enabled=1',
'gpgcheck=1',
'repo_gpgcheck=1',
`gpgkey=${linux}/repo-key.gpg`,
'REPO',
'',
'sudo dnf install rustproxy',
].join('\n'),
};
}
if (kind === 'suse') {
return {
description:'Add the signed RPM repository with Zypper on SUSE Linux Enterprise, openSUSE Leap or Tumbleweed.',
signature:'GPG SIGNED',
note:'x86_64. Minimum glibc: 2.34; intended for SLES 15 SP6+, SLES 16+, Leap 15.6+, and Tumbleweed. Check Python >= 3.10 dependencies.',
reference:`${suse}/x86_64/repodata/repomd.xml`,
agent:'sudo zypper install rustproxy-docker-agent',
code:[
`sudo rpm --import ${linux}/repo-key.gpg`,
`sudo zypper addrepo --refresh --gpgcheck ${suse}/x86_64/ rustproxy-suse`,
'sudo zypper refresh rustproxy-suse',
'sudo zypper install rustproxy',
].join('\n'),
};
}
if (kind === 'opnsense') {
return {
description:'Fetch the pkg repository configuration and install the os-rustproxy plugin.',
signature:'UNSIGNED PKG',
note:'The pkg repository index is unsigned. Only use a trusted HTTPS host.',
reference:`${opnsense}/RustProxy.conf`,
agent:null,
code:[
'mkdir -p /usr/local/etc/pkg/repos',
`fetch -o /usr/local/etc/pkg/repos/RustProxy.conf ${opnsense}/RustProxy.conf`,
'pkg update -f',
'pkg install os-rustproxy',
].join('\n'),
};
}
return {
description:'Install the native rustproxy package and enable its rc.d service.',
signature:'UNSIGNED PKG',
note:'The FreeBSD pkg index is unsigned. Only use a trusted HTTPS source.',
reference:`${freebsd}/RustProxy-FreeBSD.conf`,
agent:null,
code:[
'mkdir -p /usr/local/etc/pkg/repos',
`fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf ${freebsd}/RustProxy-FreeBSD.conf`,
'pkg update -f',
'pkg install rustproxy',
'sysrc rustproxy_enable=YES',
'service rustproxy start',
].join('\n'),
};
}
function renderInstall() {
if (!state.catalog) return;
const kind = state.installing;
const instructionsData = instructions(kind,state.catalog);
document.querySelectorAll('[data-install]').forEach((node) => {
const active = node.dataset.install === kind;
node.classList.toggle('active',active);
node.setAttribute('aria-selected', String(active));
});
setText('install-tab-title', INSTALL_NAMES[kind]);
setText('install-description', instructionsData.description);
setText('install-signature', instructionsData.signature);
el('install-signature').classList.toggle('live', instructionsData.signature === 'GPG SIGNED');
setText('install-command', instructionsData.code);
setText('install-note', instructionsData.note);
// Docker discovery agent is only built for Linux DEB/RPM.
const hasDockerAgent = (kind === 'deb' || kind === 'rpm' || kind === 'suse') && !!instructionsData.agent;
el('agent-card').hidden = !hasDockerAgent;
el('install-extra-grid').classList.toggle('single-card', !hasDockerAgent);
setText('trust-index', hasDockerAgent ? '02' : '01');
setText('agent-command', hasDockerAgent ? instructionsData.agent : '');
el('install-reference').href = safeLink(instructionsData.reference);
}
function endpointsCard(name, label, url, links, signed) {
return `<div class="endpoint-card">
<div class="endpoint-card-top"><h2>${esc(name)}</h2><span class="pill${signed ? ' live' : ''}">${esc(label)}</span></div>
<a href="${esc(safeLink(url))}" target="_blank" rel="noopener noreferrer">${esc(url)} ↗</a>
<ul>${links.map((link) => `<li><a href="${esc(safeLink(link.url))}" target="_blank" rel="noopener noreferrer"><span>${esc(link.name)}</span><span>↗</span></a></li>`).join('')}</ul>
</div>`;
}
function renderRepositories() {
const data = state.catalog;
if (!data) return;
const url = data.repository_urls;
const distribution = encodeURIComponent(data.apt_distribution || 'stable');
el('endpoints-grid').innerHTML = [
endpointsCard('Linux','GPG SIGNED',url.linux,[
{name:'GPG public key',url:`${url.linux}/repo-key.gpg`},
{name:'APT InRelease',url:`${url.linux}/apt/dists/${distribution}/InRelease`},
{name:'RPM repomd.xml',url:`${url.linux}/rpm/x86_64/repodata/repomd.xml`},
{name:'Build info',url:`${url.linux}/LINUX-BUILD-INFO.txt`},
],true),
endpointsCard('SUSE / openSUSE','GPG SIGNED',url.suse,[
{name:'GPG public key',url:`${url.linux}/repo-key.gpg`},
{name:'RPM repomd.xml',url:`${url.suse}/x86_64/repodata/repomd.xml`},
{name:'Metadata signature',url:`${url.suse}/x86_64/repodata/repomd.xml.asc`},
{name:'Build info',url:`${url.linux}/SUSE-BUILD-INFO.txt`},
],true),
endpointsCard('OPNsense','UNSIGNED PKG',url.opnsense,[
{name:'RustProxy.conf',url:`${url.opnsense}/RustProxy.conf`},
{name:'meta.conf',url:`${url.opnsense}/meta.conf`},
{name:'packagesite.pkg',url:`${url.opnsense}/packagesite.pkg`},
{name:'Build info',url:`${url.opnsense}/BUILD-INFO.txt`},
],false),
endpointsCard('FreeBSD','UNSIGNED PKG',url.freebsd,[
{name:'RustProxy-FreeBSD.conf',url:`${url.freebsd}/RustProxy-FreeBSD.conf`},
{name:'meta.conf',url:`${url.freebsd}/meta.conf`},
{name:'packagesite.pkg',url:`${url.freebsd}/packagesite.pkg`},
{name:'Build info',url:`${url.freebsd}/BUILD-INFO.txt`},
],false),
].join('');
el('metadata-table').innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => {
const b = data.builds[channel] || {};
const rows = [
['APPLICATION VERSION',b.app_version || '—','latest-value'],
['PACKAGE VERSION',b.package_version || '—',''],
['BUILD DATE',formatDate(b.built_at),''],
['SOURCE COMMIT',b.source_commit || '—',''],
['PACKAGING REVISION',b.packaging_revision || '—',''],
];
return `<div class="meta-block"><h3>${esc(CHANNEL_LABELS[channel])}</h3>${rows.map(([label,value,klass]) => `<div class="meta-row"><small>${esc(label)}</small><strong class="${klass}"${label === 'BUILD DATE' ? ` title="${esc(dateLabel(b.date_source))}"` : ''}>${esc(value)}</strong></div>`).join('')}</div>`;
}).join('');
}
function renderAll(data) {
state.catalog = data;
renderOverview(data);
renderPackages();
renderInstall();
renderRepositories();
setText('last-sync', formatDate(data.generated_at));
el('last-sync').innerHTML = `<i></i> ${esc(formatDate(data.generated_at))}`;
}
async function refresh() {
if (state.refreshing) return;
state.refreshing = true;
el('refresh').classList.add('busy');
el('refresh').disabled = true;
try {
const response = await fetch('/api/v1/packages', {cache:'no-store',headers:{'Accept':'application/json'}});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (!Array.isArray(data.packages) || !data.builds || !data.summary) throw new Error('Invalid catalog');
renderAll(data);
el('error-banner').hidden = true;
} catch (err) {
el('error-banner').hidden = false;
setText('last-sync','Load failed');
console.error('RustProxy repository:',err);
} finally {
state.refreshing = false;
el('refresh').classList.remove('busy');
el('refresh').disabled = false;
}
}
async function copyText(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
} else {
const input = document.createElement('textarea');
input.value = text;
input.setAttribute('readonly','');
input.className = 'clipboard-helper';
document.body.appendChild(input);
input.focus();input.select();
const copied = document.execCommand('copy');
input.remove();
if (!copied) throw new Error('Clipboard unsupported');
}
toast('Copied to clipboard');
} catch (_) { toast('Copy failed. Select and copy the command manually.'); }
}
function init() {
setText('year',new Date().getFullYear());
try {setTheme(localStorage.getItem('rustproxy-repo-theme') || 'dark');} catch (_) {setTheme('dark');}
document.querySelectorAll('[data-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.section)));
document.querySelectorAll('[data-go-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.goSection)));
document.querySelectorAll('[data-install]').forEach((button) => button.addEventListener('click',() => {state.installing = button.dataset.install;renderInstall();}));
document.querySelectorAll('[data-copy-target]').forEach((button) => button.addEventListener('click',() => copyText(el(button.dataset.copyTarget)?.textContent || '')));
el('theme-toggle').addEventListener('click',() => {
const themes = ['dark','light','cream'];
const i = themes.indexOf(document.documentElement.dataset.theme);
setTheme(themes[(i+1)%themes.length]);
});
el('refresh').addEventListener('click',() => {refresh();});
el('retry-inline').addEventListener('click',() => {refresh();});
el('search-input').addEventListener('input',(event) => {state.search = event.target.value;state.rows = 25;renderPackages();});
el('filter-channel').value = state.channel;
el('filter-channel').addEventListener('change',(event) => {state.channel = event.target.value;state.rows = 25;renderPackages();});
el('filter-format').addEventListener('change',(event) => {state.format = event.target.value;state.rows = 25;renderPackages();});
el('filter-sort').addEventListener('change',(event) => {state.sort = event.target.value;state.rows = 25;renderPackages();});
el('show-more').addEventListener('click',() => {state.rows += 25;renderPackages();});
window.addEventListener('hashchange',() => navigate(window.location.hash.slice(1)));
navigate(window.location.hash.slice(1) || 'overview');
refresh();
window.setInterval(() => {if (!document.hidden) refresh();}, 60000);
document.addEventListener('visibilitychange',() => {if (!document.hidden) refresh();});
}
init();
})();
+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<rect width="64" height="64" rx="11" fill="#131615"/>
<rect x="5" y="5" width="54" height="54" rx="8" fill="none" stroke="#f69b74" stroke-width="2"/>
<text x="11" y="42" fill="#f69b74" font-size="27" font-family="monospace" font-weight="bold">r</text>
<text x="26" y="42" fill="#bdd293" font-size="26" font-family="monospace" font-weight="bold">/</text>
<text x="39" y="42" fill="#f69b74" font-size="27" font-family="monospace" font-weight="bold">p</text>
</svg>

After

Width:  |  Height:  |  Size: 547 B

+132
View File
@@ -0,0 +1,132 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta name="theme-color" content="#131615">
<title>RustProxy · Package Registry</title>
<link rel="icon" type="image/svg+xml" href="/ui/favicon.svg">
<link rel="stylesheet" href="/ui/style.css">
<script defer src="/ui/app.js"></script>
</head>
<body data-site="__REPOSITORY_SITE__">
<div class="shell">
<aside class="sidebar" id="sidebar">
<a href="#overview" class="brand" aria-label="RustProxy Package Registry">
<span class="brand-mark" aria-hidden="true">r<span>/</span>p</span>
<span class="brand-text">RustProxy <small>PACKAGE REGISTRY</small></span>
</a>
<div class="edition">INDEPENDENT BY DESIGN <span>BUILD DISTRIBUTION</span></div>
<nav class="main-navigation" aria-label="Dashboard navigation">
<button class="nav active" data-section="overview" aria-current="page"><span>01</span><b>Overview</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="packages"><span>02</span><b>Packages &amp; versions</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="install"><span>03</span><b>Installation</b><span class="nav-arrow">↗</span></button>
<button class="nav" data-section="repository"><span>04</span><b>Repositories</b><span class="nav-arrow">↗</span></button>
</nav>
<div class="sidebar-label sidebar-label--second">AVAILABLE CHANNELS</div>
<div class="side-channels" id="side-channels">
<span class="side-channel"><i class="tiny-dot"></i> Linux <small>APT / DNF</small></span>
<span class="side-channel"><i class="tiny-dot"></i> SUSE / openSUSE <small>RPM / Zypper</small></span>
<span class="side-channel"><i class="tiny-dot"></i> OPNsense <small>PKG</small></span>
<span class="side-channel"><i class="tiny-dot"></i> FreeBSD <small>PKG</small></span>
</div>
<div class="sidebar-bottom">
<div class="small-label">REPOSITORY ENGINE</div>
<p>Python / HTTP API<br>Nginx / Signed APT + RPM + SUSE<br>FreeBSD pkg catalog</p>
<div class="local-tag" id="sidebar-status"><i></i><span>WAITING FOR DATA</span></div>
<div class="copyright">© RustProxy <span id="year"></span></div>
</div>
</aside>
<div class="workspace">
<header class="topbar">
<div class="crumb"><span>RUSTPROXY</span><b>/</b><span>REPOSITORY</span><b>/</b><strong id="crumb-section">Overview</strong></div>
<div class="top-actions">
<span class="live-sync" id="last-sync"><i></i> Loading data…</span>
<button class="quiet icon-button" id="refresh" title="Refresh package list" aria-label="Refresh data">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 7v5h-5M4 17v-5h5"/><path d="M5 9a8 8 0 0 1 13.4-2.2L20 12M4 12l1.6 5.2A8 8 0 0 0 19 15"/></svg>
</button>
<button class="quiet theme-switch" id="theme-toggle" title="Change theme" aria-label="Change theme"><span class="theme-glyph">◐</span><span id="theme-name">Dark</span></button>
</div>
</header>
<main class="main-content">
<div class="error-banner" id="error-banner" hidden role="status">Could not load the catalog. Retrying automatically in one minute. <button id="retry-inline">Retry now</button></div>
<section class="view" id="view-overview" aria-label="Overview">
<div class="overview-statusbar">
<span class="active-status" id="hero-status"><span class="pulse-dot"></span> LOADING</span>
<span class="muted-detail">Refreshes every 60 seconds</span>
</div>
<div class="hero-grid">
<div class="hero-release">
<div class="hero-header"><span><span class="dot-amber"></span> LATEST RELEASE <span id="hero-channel">LINUX</span></span><span class="hero-corner">R / P — 01</span></div>
<div class="hero-version-label">RUSTPROXY / APPLICATION VERSION</div>
<div class="hero-version" id="hero-version">—</div>
<div class="hero-foot">
<div><small>BUILT / PUBLISHED</small><strong id="hero-date">Not available</strong></div>
<div><small>SOURCE COMMIT</small><strong class="mono" id="hero-commit">—</strong></div>
<button class="hero-link" data-go-section="packages">View releases <span aria-hidden="true">↗</span></button>
</div>
<div class="hero-background-rings" aria-hidden="true"></div>
</div>
<div class="pipeline-panel">
<div class="pipeline-head"><span class="small-label">BUILD PIPELINE</span><span class="pipeline-live"><i class="tiny-dot"></i> READ ONLY</span></div>
<p>From source code to installable packages.</p>
<div class="pipeline-list">
<div class="pipeline-step"><span class="step-index">01</span><span class="step-info"><b>Git / source</b><small>RustProxy upstream</small></span><span class="step-right">GIT <span>↗</span></span></div>
<div class="pipeline-connector"></div>
<div class="pipeline-step"><span class="step-index">02</span><span class="step-info"><b>Build / package</b><small>Rust + Go + FreeBSD</small></span><span class="step-right">CI <span>↗</span></span></div>
<div class="pipeline-connector"></div>
<div class="pipeline-step"><span class="step-index">03</span><span class="step-info"><b>Repository publish</b><small>APT / RPM / Zypper / PKG</small></span><span class="step-right step-success">LIVE <span>●</span></span></div>
</div>
<div class="pipeline-footer">Last sync <span id="pipeline-updated">—</span></div>
</div>
</div>
<div class="stats-grid" aria-label="Repository statistics">
<div class="stat"><span>01 / CATALOG PACKAGES</span><strong id="stat-packages">—</strong><small>All published artifacts</small></div>
<div class="stat"><span>02 / ACTIVE CHANNELS</span><strong id="stat-channels">—</strong><small>Linux · SUSE · OPNsense · FreeBSD</small></div>
<div class="stat"><span>03 / LATEST BUILD</span><strong class="stat-date" id="stat-date">—</strong><small id="stat-date-sub">Latest publication time</small></div>
<div class="stat"><span>04 / CURRENT VERSION</span><strong id="stat-version">—</strong><small id="stat-release">Application version</small></div>
</div>
<div class="section-heading"><div><span class="section-index">01 / RELEASE CHANNELS</span><h2>Latest builds</h2></div><button class="text-link" data-go-section="packages">View catalog <span>↗</span></button></div>
<div class="build-grid" id="build-grid"><div class="skeleton-card">Loading channels…</div></div>
<div class="section-heading section-heading--second"><div><span class="section-index">02 / RECENT ARTIFACTS</span><h2>Recent packages</h2></div><button class="text-link" data-go-section="packages">Browse packages <span>↗</span></button></div>
<div class="card recent-card">
<div class="table-scroller"><table class="data-table"><thead><tr><th>PACKAGE</th><th>FORMAT / CHANNEL</th><th>PACKAGE VERSION</th><th>BUILD DATE</th><th class="table-action-col">FILE</th></tr></thead><tbody id="recent-rows"><tr><td colspan="5" class="empty-table">Loading data…</td></tr></tbody></table></div>
</div>
</section>
<section class="view" id="view-packages" aria-labelledby="packages-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> ARTIFACT EXPLORER <span class="eyebrow-separator">/</span> VERSIONS</div><h1 id="packages-title">Package <em>catalog.</em></h1><p class="lead">Search, filter, sort, and download published files. All details are read directly from the current repository.</p></div><div class="count-pill" id="packages-total">0 packages</div></div>
<div class="filter-panel"><div class="search-field"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m16.5 16.5 4 4"/></svg><input id="search-input" type="search" placeholder="Search by name, version, architecture..." aria-label="Search packages" autocomplete="off"></div><label class="filter-input">CHANNEL<select id="filter-channel"><option value="all">All</option><option value="linux">Linux</option><option value="suse">SUSE / openSUSE</option><option value="opnsense">OPNsense</option><option value="freebsd">FreeBSD</option></select></label><label class="filter-input">FORMAT<select id="filter-format"><option value="all">All</option><option value="DEB">DEB</option><option value="RPM">RPM</option><option value="PKG">PKG</option></select></label><label class="filter-input">SORT BY<select id="filter-sort"><option value="date">Newest first</option><option value="version">Version ↓</option><option value="name">Name A–Z</option><option value="size">Size ↓</option></select></label></div>
<div class="result-topline"><span id="filter-count">Loading…</span><span>Versions and dates are read from packages and build metadata</span></div>
<div class="card catalog-card"><div class="table-scroller"><table class="data-table data-table--catalog"><thead><tr><th>PACKAGE</th><th>CHANNEL</th><th>VERSION</th><th>BUILD DATE</th><th>SIZE</th><th>DOWNLOAD</th></tr></thead><tbody id="catalog-rows"><tr><td colspan="6" class="empty-table">Loading data…</td></tr></tbody></table></div><div class="catalog-bottom"><span id="result-range">—</span><button class="quiet show-more" id="show-more" hidden>Load 25 more ↓</button></div></div>
</section>
<section class="view" id="view-install" aria-labelledby="install-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> GET STARTED <span class="eyebrow-separator">/</span> INSTALLATION</div><h1 id="install-title">Install <em>RustProxy.</em></h1><p class="lead">Ready-to-run commands to add the repository and install RustProxy. Select a platform and copy the commands to your terminal.</p></div></div>
<div class="install-tabs" role="tablist" aria-label="Choose operating system"><button role="tab" aria-selected="true" data-install="deb" class="install-tab active">Debian / Ubuntu <small>APT · amd64</small></button><button role="tab" aria-selected="false" data-install="rpm" class="install-tab">RHEL / Rocky <small>DNF · x86_64</small></button><button role="tab" aria-selected="false" data-install="suse" class="install-tab">SUSE / openSUSE <small>Zypper · x86_64</small></button><button role="tab" aria-selected="false" data-install="opnsense" class="install-tab">OPNsense <small>FreeBSD pkg</small></button><button role="tab" aria-selected="false" data-install="freebsd" class="install-tab">FreeBSD <small>Native pkg</small></button></div>
<div class="install-card card"><div class="install-card-head"><div><span class="section-index">TERMINAL / INSTALL</span><h2 id="install-tab-title">Debian / Ubuntu</h2><p id="install-description">Add the signed APT repository and install the server.</p></div><span class="pill" id="install-signature">GPG SIGNED</span></div><div class="code-window"><div class="code-topbar"><span><i class="tiny-dot"></i><i class="tiny-dot"></i><i class="tiny-dot"></i> <b>install.sh</b></span><button class="copy-code" id="copy-install" data-copy-target="install-command">Copy <span>↗</span></button></div><pre><code id="install-command">Preparing commands…</code></pre></div><div class="install-foot"><span id="install-note">Requires sudo privileges.</span><a id="install-reference" class="text-link" href="#">Repository files ↗</a></div></div>
<div class="install-extra-grid single-card" id="install-extra-grid"><div class="card hint-card" id="agent-card" hidden><span class="hint-icon">01</span><h3>Docker discovery agent</h3><p>Linux also provides a separate <code>rustproxy-docker-agent</code> package. Install it separately from the main server.</p><div class="inline-command"><code id="agent-command">sudo apt install rustproxy-docker-agent</code><button data-copy-target="agent-command" class="quiet">Copy</button></div></div><div class="card hint-card"><span class="hint-icon" id="trust-index">01</span><h3>Signatures and trust</h3><p>APT, DNF and SUSE/Zypper repositories are GPG-signed. OPNsense and native FreeBSD currently use <code>signature_type: none</code> catalogs; use a trusted HTTPS source.</p></div></div>
</section>
<section class="view" id="view-repository" aria-labelledby="repository-title" hidden>
<div class="page-head compact-head"><div><div class="eyebrow"><span class="eyebrow-line"></span> REPOSITORY ENDPOINTS <span class="eyebrow-separator">/</span> METADATA</div><h1 id="repository-title">Package <em>sources.</em></h1><p class="lead">Public repository URLs, signatures, and package indexes. Current metadata comes from published builds.</p></div></div>
<div class="endpoints-grid" id="endpoints-grid"></div>
<div class="section-heading section-heading--second"><div><span class="section-index">SOURCE TRACE</span><h2>Release metadata</h2></div></div>
<div class="card metadata-card"><div class="meta-table" id="metadata-table"><span>Loading…</span></div></div>
</section>
<footer class="page-footer"><span>RUSTPROXY <b>/</b> PACKAGE REGISTRY</span></footer>
</main>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
</body>
</html>
+396
View File
@@ -0,0 +1,396 @@
/* RustProxy Package Registry · palette adapted from RustProxy 0.34.6 */
:root {
color-scheme: dark;
--bg:#131615; --panel:#1b1f1d; --panel2:#202622; --line:#323a34;
--muted:#a0aaa0; --text:#edf0e9; --accent:#f69b74; --green:#bdd293;
--red:#ffaaa2; --sidebar-bg:#101310; --brand-secondary:#cfe0ac;
--nav-active-bg:#242b23; --nav-hover-bg:#20251f; --text-subtle:#879082;
--separator:#596454; --border-hover:#89917c; --field-bg:#131813;
--field-line:#47523f; --placeholder:#778070; --row-line:#2b332c;
--outline-line:#717b67; --code-bg:#101810; --notice-text:#c9b99c;
--notice-line:#554833; --notice-bg:#29251c; --pill-line:#6d5a45;
--pill-live-line:#576847; --shadow:rgba(0,0,0,.22); --accent-ink:#221810;
--ring1:rgba(246,155,116,.08); --ring2:rgba(189,210,147,.04);
--mono:ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace;
--sans:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
}
html[data-theme="light"] {
color-scheme: light;
--bg:#f4f6f2; --panel:#fff; --panel2:#edf1eb; --line:#cbd3c7;
--muted:#5e685f; --text:#1d241f; --accent:#b85b38; --green:#5f783f;
--red:#b64a43; --sidebar-bg:#eef2ec; --brand-secondary:#6b8249;
--nav-active-bg:#e1e8dc; --nav-hover-bg:#e8ede6; --text-subtle:#687369;
--separator:#9ba69b; --border-hover:#839083; --field-bg:#fff;
--field-line:#b7c1b5; --placeholder:#7a867b; --row-line:#e0e5dd;
--outline-line:#98a497; --code-bg:#edf3e9; --notice-text:#685b45;
--notice-line:#d4c2a5; --notice-bg:#fff9ef; --pill-line:#cbb28f;
--pill-live-line:#a8ba8d; --shadow:rgba(31,39,33,.1); --accent-ink:#fff;
--ring1:rgba(184,91,56,.055); --ring2:rgba(95,120,63,.045);
}
html[data-theme="cream"] {
color-scheme: light;
--bg:#f2ede3; --panel:#fcf9f2; --panel2:#eae3d6; --line:#d5cabb;
--muted:#6b6257; --text:#302c27; --accent:#8c5942; --green:#5e7655;
--red:#a54645; --sidebar-bg:#eae2d5; --brand-secondary:#668367;
--nav-active-bg:#e0d3c0; --nav-hover-bg:#e6dccc; --text-subtle:#786e61;
--separator:#afa494; --border-hover:#968776; --field-bg:#fffcf7;
--field-line:#baad9a; --placeholder:#807466; --row-line:#e2d9cb;
--outline-line:#9c8d7c; --code-bg:#f0e8da; --notice-text:#765e40;
--notice-line:#d9c3a1; --notice-bg:#f8efdf; --pill-line:#c2a78d;
--pill-live-line:#b0bd9d; --shadow:rgba(64,49,34,.08); --accent-ink:#fff;
--ring1:rgba(140,89,66,.075); --ring2:rgba(94,118,85,.045);
}
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;color:var(--text);background:var(--bg);font:13px/1.6 var(--sans);min-width:320px}
button,input,select{font:inherit}
button{cursor:pointer}
button:disabled{opacity:.5;cursor:wait}
a{color:inherit;text-decoration:none}
a:hover{text-decoration:none}
h1,h2,h3,p{margin:0}
h1,h2,h3{line-height:1.18}
[hidden]{display:none!important}
button:focus-visible,a:focus-visible,input:focus-visible,select:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
::selection{background:var(--accent);color:var(--accent-ink)}
.shell{display:grid;grid-template-columns:250px minmax(0,1fr);min-height:100vh}
.sidebar{position:sticky;top:0;height:100vh;min-height:600px;display:flex;flex-direction:column;padding:30px 19px 24px;background:var(--sidebar-bg);border-right:1px solid var(--line)}
.brand{display:flex;align-items:center;gap:11px;padding:1px 3px;font-size:19px;font-weight:770;letter-spacing:-.8px;line-height:1.12}
.brand-mark{width:49px;height:49px;flex:0 0 49px;display:flex;align-items:center;justify-content:center;border:1px solid var(--accent);border-radius:3px;color:var(--accent);font:700 29px var(--mono);letter-spacing:-3px;padding-right:4px}
.brand-mark span{color:var(--brand-secondary);font-size:25px}
.brand-text{display:block}
.brand-text small{display:block;font:9px/1.4 var(--mono);letter-spacing:1.6px;color:var(--green);margin-top:5px}
.edition{padding:19px 4px 28px;color:var(--muted);font:9px/1.6 var(--mono);letter-spacing:1.15px;border-bottom:1px solid var(--line)}
.edition span{display:block;color:var(--accent);margin-top:5px}
.sidebar-label{margin:29px 0 11px 11px;color:var(--text-subtle);font:10px var(--mono);letter-spacing:1px}
.main-navigation{display:flex;flex-direction:column;gap:5px}
.nav{display:flex;gap:12px;align-items:center;padding:12px 12px 12px 14px;min-height:43px;border:0;border-left:2px solid transparent;color:var(--muted);background:transparent;text-align:left;transition:background .14s ease,color .14s ease;border-radius:2px}
.nav>span:first-child{font:10px var(--mono);opacity:.75}
.nav b{font-size:12.5px;font-weight:540;flex:1}
.nav-arrow{font-size:12px;opacity:0;transition:opacity .14s}
.nav:hover{background:var(--nav-hover-bg);color:var(--text)}
.nav:hover .nav-arrow{opacity:.9}
.nav.active{background:var(--nav-active-bg);color:var(--brand-secondary);border-left-color:var(--brand-secondary)}
.nav.active .nav-arrow{opacity:.9}
.sidebar-label--second{margin-top:36px}
.side-channels{display:flex;flex-direction:column;gap:5px;padding:0 7px}
.side-channel{display:flex;align-items:center;gap:11px;color:var(--muted);font-size:12px;padding:7px 8px}
.side-channel.disabled .tiny-dot{opacity:.25}
.side-channel small{font:10px var(--mono);color:var(--text-subtle);margin-left:auto}
.tiny-dot{width:6px;height:6px;display:inline-block;flex-shrink:0;background:var(--green);border-radius:50%}
.sidebar-bottom{margin-top:auto;padding:24px 10px 0;border-top:1px solid var(--line)}
.small-label{color:var(--muted);font:10px/1.3 var(--mono);letter-spacing:.7px}
.sidebar-bottom p{color:var(--text-subtle);font-size:11px;line-height:1.85;margin:12px 0 23px}
.local-tag{display:flex;gap:9px;align-items:center;color:var(--green);font:10px var(--mono);letter-spacing:.4px}
.local-tag i,.live-sync i{width:6px;height:6px;border-radius:50%;background:var(--green);display:inline-block;flex:none}
.local-tag.waiting{color:var(--muted)}
.local-tag.waiting i{background:var(--muted)}
.copyright{margin-top:30px;color:var(--text-subtle);font:10px var(--mono)}
.workspace{min-width:0}
.topbar{height:70px;padding:0 clamp(25px,4.3vw,72px);border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:14px;background:var(--bg);position:sticky;top:0;z-index:10}
.crumb{display:flex;gap:13px;align-items:center;color:var(--muted);font:10px var(--mono);letter-spacing:1px;white-space:nowrap}
.crumb b{color:var(--separator);font-weight:400}
.crumb strong{color:var(--text);font-weight:500}
.top-actions{display:flex;gap:9px;align-items:center}
.live-sync{margin-right:11px;color:var(--muted);display:flex;align-items:center;gap:10px;font:10px var(--mono);white-space:nowrap}
.quiet{color:var(--muted);background:transparent;border:1px solid var(--line);border-radius:3px;padding:8px 12px;white-space:nowrap;transition:color .14s,border-color .14s}
.quiet:hover{color:var(--text);border-color:var(--border-hover)}
.icon-button{display:inline-flex;align-items:center;justify-content:center;width:36px;height:35px;padding:0}
.icon-button.busy svg{animation:spin .9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.theme-switch{height:35px;display:flex;align-items:center;gap:8px;padding:0 12px}
.theme-glyph{font-size:18px;line-height:1;color:var(--accent)}
.main-content{padding:47px clamp(25px,4.3vw,72px) 25px;max-width:1660px;margin:0 auto}
.view{min-height:55vh}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;margin-bottom:29px}
.page-head h1{font-size:clamp(34px,3.8vw,54px);font-weight:550;letter-spacing:-2.3px;line-height:1.1}
.page-head h1 em{font-style:normal;color:var(--accent)}
.eyebrow{display:flex;align-items:center;gap:9px;font:10px var(--mono);color:var(--green);letter-spacing:1.9px;margin-bottom:21px}
.eyebrow-line{height:1px;width:20px;background:var(--green)}
.eyebrow-separator{color:var(--separator);margin:0 1px}
.lead{max-width:680px;color:var(--muted);font-size:13px;line-height:1.82;margin-top:17px}
.page-head-side{align-self:flex-end;text-align:right;padding-bottom:4px;flex-shrink:0}
.subtle-label{color:var(--text-subtle);font:9px var(--mono);letter-spacing:1px;margin-bottom:8px}
.active-status{display:inline-flex;gap:9px;align-items:center;font:10px var(--mono);letter-spacing:.8px;color:var(--green);border:1px solid var(--pill-live-line);padding:7px 10px}
.pulse-dot{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 0 4px color-mix(in srgb,var(--green) 12%,transparent)}
.active-status.waiting{color:var(--muted);border-color:var(--line)}
.active-status.waiting .pulse-dot{background:var(--muted);box-shadow:none}
.muted-detail{font:10px var(--mono);color:var(--text-subtle);margin-top:10px}
.hero-grid{display:grid;grid-template-columns:minmax(0,1.62fr) minmax(270px,.85fr);gap:16px;align-items:stretch}
.hero-release{position:relative;isolation:isolate;overflow:hidden;min-height:316px;padding:26px 29px 22px;border:1px solid var(--line);border-top:2px solid var(--accent);background:radial-gradient(ellipse at 93% 30%,var(--ring1),transparent 50%),radial-gradient(ellipse at 30% 110%,var(--ring2),transparent 60%),var(--panel)}
.hero-header{display:flex;justify-content:space-between;align-items:center;gap:10px;color:var(--green);font:10px var(--mono);letter-spacing:1px;position:relative;z-index:1}
.hero-header>span:first-child{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.dot-amber{width:6px;height:6px;border-radius:50%;background:var(--accent)}
.hero-header #hero-channel{color:var(--accent)}
.hero-corner{color:var(--text-subtle)}
.hero-version-label{color:var(--text-subtle);font:10px var(--mono);letter-spacing:1.6px;margin-top:47px;position:relative;z-index:1}
.hero-version{font:600 clamp(64px,7vw,108px)/1.1 var(--mono);letter-spacing:-6px;color:var(--text);margin-top:3px;position:relative;z-index:1;overflow-wrap:anywhere}
.hero-foot{border-top:1px solid var(--line);padding-top:20px;margin-top:21px;display:flex;align-items:flex-end;justify-content:space-between;gap:12px;position:relative;z-index:1}
.hero-foot>div{display:flex;flex-direction:column;gap:5px;min-width:0}
.hero-foot small{font:9px var(--mono);letter-spacing:1px;color:var(--text-subtle)}
.hero-foot strong{font:12px var(--sans);font-weight:550;color:var(--text);white-space:nowrap}
.hero-foot strong.mono{font:12px var(--mono);color:var(--green)}
.hero-link{border:1px solid var(--accent);background:var(--accent);color:var(--accent-ink);font-weight:650;padding:11px 15px;white-space:nowrap;border-radius:3px;transition:filter .15s}
.hero-link:hover{filter:brightness(1.09)}
.hero-link span{margin-left:8px}
.hero-background-rings{pointer-events:none;position:absolute;right:-150px;top:22px;width:420px;height:420px;border:1px solid color-mix(in srgb,var(--green) 10%,transparent);border-radius:50%;z-index:0}
.hero-background-rings::after{content:"";position:absolute;inset:44px;border:1px solid color-mix(in srgb,var(--accent) 10%,transparent);border-radius:50%}
.pipeline-panel{background:var(--panel);border:1px solid var(--line);padding:24px 24px 15px;min-height:316px;display:flex;flex-direction:column}
.pipeline-head{display:flex;justify-content:space-between;align-items:center;gap:10px}
.pipeline-live{font:9px var(--mono);color:var(--green);display:flex;align-items:center;gap:8px}
.pipeline-panel>p{font-size:12px;color:var(--muted);margin:17px 0 20px}
.pipeline-list{display:flex;flex-direction:column}
.pipeline-step{display:flex;align-items:center;gap:14px;min-width:0}
.step-index{width:35px;height:35px;flex-shrink:0;background:var(--panel2);border:1px solid var(--line);display:grid;place-items:center;color:var(--accent);font:11px var(--mono)}
.step-info{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.step-info b{font-weight:560;font-size:12px}
.step-info small{color:var(--text-subtle);font:10px var(--mono)}
.step-right{font:10px var(--mono);color:var(--muted)}
.step-right span{color:var(--separator);margin-left:3px}
.step-right.step-success{color:var(--green)}
.step-right.step-success span{font-size:8px;color:var(--green)}
.pipeline-connector{width:1px;height:14px;background:var(--line);margin:3px 0 3px 17px}
.pipeline-footer{display:flex;justify-content:space-between;gap:10px;border-top:1px solid var(--line);padding-top:13px;margin-top:auto;color:var(--text-subtle);font:9px var(--mono)}
.pipeline-footer span{color:var(--muted)}
.stats-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;margin-top:16px;border:1px solid var(--line);background:var(--panel)}
.stat{padding:22px 22px 21px;min-width:0}
.stat+.stat{border-left:1px solid var(--line)}
.stat>span{font:10px var(--mono);letter-spacing:.6px;color:var(--muted)}
.stat strong{display:block;font:500 clamp(21px,2.5vw,35px)/1.35 var(--mono);letter-spacing:-1px;margin:9px 0 3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.stat strong.stat-date{font-size:clamp(15px,2vw,24px)}
.stat:first-child strong{color:var(--accent)}
.stat small{color:var(--text-subtle);font-size:10px}
.section-heading{display:flex;align-items:end;justify-content:space-between;gap:15px;margin:42px 0 16px}
.section-heading--second{margin-top:39px}
.section-index{font:10px var(--mono);letter-spacing:1.2px;color:var(--green)}
h2{font-size:22px;letter-spacing:-.75px;font-weight:565;margin-top:7px}
h3{font-size:16px;font-weight:575;letter-spacing:-.3px}
.text-link{background:none;border:0;color:var(--accent);font:11px var(--mono);padding:4px 0;white-space:nowrap}
.text-link:hover{text-decoration:underline}
.text-link span{margin-left:5px}
.build-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}
.build-card,.skeleton-card{background:var(--panel);border:1px solid var(--line);padding:19px 22px 20px;min-width:0;position:relative}
.build-card:before{content:"";position:absolute;inset:0 auto 0 0;width:2px;background:var(--green);opacity:.75}
.build-card.off:before{background:var(--separator)}
.build-card-top{display:flex;justify-content:space-between;gap:8px;align-items:center;color:var(--muted)}
.build-card-top>span:first-child{font:10px var(--mono);letter-spacing:.7px}
.pill{display:inline-block;border:1px solid var(--pill-line);color:var(--accent);padding:3px 8px;font:9px var(--mono);letter-spacing:.4px;white-space:nowrap}
.pill.live{border-color:var(--pill-live-line);color:var(--green)}
.pill.missing{border-color:var(--line);color:var(--muted)}
.build-card-version{font:550 clamp(22px,2.3vw,31px)/1.4 var(--mono);letter-spacing:-1.5px;margin:15px 0 2px;overflow-wrap:anywhere}
.build-card-sub{color:var(--muted);font:11px var(--mono);margin-bottom:16px}
.build-card-details{border-top:1px solid var(--row-line);padding-top:14px;display:flex;justify-content:space-between;align-items:center;gap:6px}
.build-card-details>div{display:flex;flex-direction:column;gap:4px;min-width:0}
.build-card-details small{font:9px var(--mono);color:var(--text-subtle)}
.build-card-details strong{font:11px var(--mono);color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.build-card-details .commit{color:var(--green)}
.skeleton-card{grid-column:1/-1;color:var(--muted)}
.card{border:1px solid var(--line);background:var(--panel);min-width:0}
.recent-card{overflow:hidden}
.table-scroller{overflow-x:auto;scrollbar-color:var(--field-line) transparent;scrollbar-width:thin}
.data-table{border-collapse:collapse;width:100%;min-width:680px;text-align:left}
.data-table thead{background:var(--panel2)}
.data-table th{font:10px var(--mono);letter-spacing:.7px;color:var(--muted);padding:16px 19px;white-space:nowrap;border-bottom:1px solid var(--line)}
.data-table td{padding:15px 19px;border-bottom:1px solid var(--row-line);font-size:12px;color:var(--text);white-space:nowrap}
.data-table tbody tr:last-child td{border-bottom:0}
.data-table tbody tr:hover{background:color-mix(in srgb,var(--nav-hover-bg) 70%,transparent)}
.artifact-name{font-weight:570;display:block;max-width:270px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.artifact-arch{display:block;margin-top:3px;color:var(--text-subtle);font:10px var(--mono)}
.version-code{font:12px var(--mono);color:var(--green)}
.date-cell{font:11px var(--mono);color:var(--text)}
.date-cell small{display:block;color:var(--text-subtle);margin-top:2px}
.channel-tag{display:inline-flex;align-items:center;gap:7px;font:10px var(--mono);color:var(--muted)}
.channel-tag:before{content:"";width:5px;height:5px;background:var(--accent);border-radius:50%}
.channel-tag.pkg:before{background:var(--green)}
.format-label{display:inline-block;font:9px var(--mono);color:var(--accent);background:color-mix(in srgb,var(--accent) 7%,transparent);border:1px solid color-mix(in srgb,var(--accent) 28%,transparent);padding:3px 6px;letter-spacing:.5px;margin-right:7px}
.format-label.pkg{color:var(--green);border-color:var(--pill-live-line);background:color-mix(in srgb,var(--green) 7%,transparent)}
.download-link{font:11px var(--mono);color:var(--accent);border:1px solid var(--line);padding:7px 10px;display:inline-flex;gap:8px;align-items:center;white-space:nowrap}
.download-link:hover{border-color:var(--accent);background:var(--nav-hover-bg)}
.download-link span{font-size:13px}
.empty-table{color:var(--muted)!important;text-align:center;font-size:13px!important;padding:34px!important}
.compact-head{margin-bottom:34px}
.compact-head .lead{max-width:710px}
.count-pill{color:var(--green);border:1px solid var(--pill-live-line);font:10px var(--mono);padding:9px 14px;white-space:nowrap}
.filter-panel{border:1px solid var(--line);background:var(--panel);padding:15px;display:grid;grid-template-columns:minmax(220px,2.3fr) repeat(3,minmax(120px,1fr));gap:11px;align-items:end}
.search-field{position:relative;display:flex;align-items:center;height:41px;background:var(--field-bg);border:1px solid var(--field-line)}
.search-field svg{margin:0 12px;color:var(--muted);flex-shrink:0}
.search-field input{background:none;border:0;outline:none;width:100%;height:100%;color:var(--text);padding:0 11px 0 0;min-width:0;font-size:12px}
.search-field input::placeholder{color:var(--placeholder)}
.search-field:focus-within{border-color:var(--accent)}
.filter-input{display:flex;flex-direction:column;gap:8px;font:9px var(--mono);color:var(--muted);letter-spacing:.7px}
.filter-input select{appearance:none;width:100%;height:41px;padding:0 31px 0 11px;border:1px solid var(--field-line);background:var(--field-bg);color:var(--text);border-radius:0;font:12px var(--sans);background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 16px) 18px,calc(100% - 11px) 18px;background-size:5px 5px;background-repeat:no-repeat}
.filter-input select option{background:var(--panel);color:var(--text)}
.result-topline{display:flex;gap:12px;justify-content:space-between;margin:19px 0 13px;color:var(--muted);font:10px var(--mono)}
.result-topline span:first-child{color:var(--green)}
.catalog-bottom{display:flex;gap:15px;align-items:center;justify-content:space-between;padding:14px 19px;border-top:1px solid var(--line);color:var(--text-subtle);font:10px var(--mono)}
.show-more{font-size:11px;padding:8px 13px}
.install-tabs{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin-bottom:16px}
.install-tab{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding:16px 18px;border:1px solid var(--line);border-bottom:2px solid var(--line);background:var(--panel);color:var(--muted);font-size:13px;font-weight:600;text-align:left;transition:border-color .14s,color .14s}
.install-tab small{font:10px var(--mono);font-weight:400;color:var(--text-subtle)}
.install-tab:hover{border-color:var(--border-hover);color:var(--text)}
.install-tab.active{color:var(--text);border-bottom-color:var(--accent);background:var(--panel2)}
.install-tab.active small{color:var(--green)}
.install-card{padding:27px}
.install-card-head{display:flex;justify-content:space-between;align-items:flex-start;gap:15px;margin-bottom:22px}
.install-card-head h2{font-size:24px;margin:9px 0}
.install-card-head p{color:var(--muted);font-size:12px}
.code-window{border:1px solid var(--line);background:var(--code-bg);overflow:hidden}
.code-topbar{display:flex;justify-content:space-between;align-items:center;padding:10px 17px;border-bottom:1px solid var(--line);color:var(--text-subtle);font:11px var(--mono)}
.code-topbar>span{display:flex;align-items:center;gap:7px}
.code-topbar .tiny-dot:nth-child(1){background:var(--accent)}
.code-topbar .tiny-dot:nth-child(2){background:var(--notice-text)}
.code-topbar .tiny-dot:nth-child(3){background:var(--green)}
.code-topbar b{font-weight:400;margin-left:13px}
.copy-code{color:var(--accent);background:transparent;border:0;font:11px var(--mono);padding:4px 2px}
.copy-code:hover{text-decoration:underline}
.code-window pre{color:var(--text);font:12px/1.9 var(--mono);overflow:auto;margin:0;padding:21px 24px;min-height:170px;tab-size:2;white-space:pre}
.install-foot{display:flex;justify-content:space-between;gap:15px;margin-top:17px;color:var(--muted);font-size:11px}
.install-extra-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
.install-extra-grid.single-card{grid-template-columns:1fr}
.hint-card{padding:24px 26px}
.hint-icon{display:grid;place-items:center;width:34px;height:34px;border:1px solid var(--pill-live-line);color:var(--green);font:11px var(--mono);margin-bottom:18px}
.hint-card h3{margin-bottom:10px}
.hint-card p{color:var(--muted);font-size:12px;line-height:1.8;min-height:67px}
.hint-card code{font:11px var(--mono);color:var(--green)}
.inline-command{display:flex;align-items:center;justify-content:space-between;gap:8px;border:1px solid var(--line);background:var(--code-bg);padding:9px 12px;margin-top:15px}
.inline-command code{font-size:10px;overflow:auto;white-space:nowrap}
.inline-command button{font-size:10px;padding:5px 8px}
.endpoints-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}
.endpoint-card{border:1px solid var(--line);background:var(--panel);padding:23px 22px}
.endpoint-card-top{display:flex;align-items:center;justify-content:space-between;gap:9px;margin-bottom:19px}
.endpoint-card h2{font-size:20px;letter-spacing:-.4px;margin:0}
.endpoint-card>a{color:var(--green);font:11px var(--mono);word-break:break-all;display:block;min-height:54px;line-height:1.7}
.endpoint-card>a:hover{text-decoration:underline}
.endpoint-card ul{list-style:none;padding:15px 0 0;border-top:1px solid var(--line);margin:12px 0 0}
.endpoint-card li{margin:12px 0 0;font-size:11px}
.endpoint-card li a{display:flex;justify-content:space-between;gap:7px;align-items:center}
.endpoint-card li a span:first-child{color:var(--muted)}
.endpoint-card li a span:last-child{color:var(--accent)}
.endpoint-card li a:hover span:first-child{color:var(--text)}
.metadata-card{padding:3px 25px}
.meta-table{display:grid;grid-template-columns:repeat(3,minmax(0,1fr))}
.meta-block{padding:19px 0}
.meta-block+.meta-block{padding-left:23px;border-left:1px solid var(--line)}
.meta-block h3{font-size:13px;margin-bottom:12px}
.meta-row{display:flex;flex-direction:column;gap:4px;margin-top:11px}
.meta-row small{font:9px var(--mono);letter-spacing:.6px;color:var(--text-subtle)}
.meta-row strong{font:11px var(--mono);font-weight:400;color:var(--text);overflow-wrap:anywhere}
.meta-row strong.latest-value{color:var(--green)}
.page-footer{border-top:1px solid var(--line);color:var(--text-subtle);font:10px var(--mono);display:flex;justify-content:space-between;gap:14px;margin-top:54px;padding:19px 0}
.page-footer b{color:var(--accent);font-weight:400}
.error-banner{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 21px;padding:14px 16px;color:var(--notice-text);border:1px solid var(--notice-line);background:var(--notice-bg);font-size:12px}
.error-banner button{color:var(--accent);background:none;border:0;font-weight:600;white-space:nowrap}
.clipboard-helper{position:fixed;opacity:0;pointer-events:none}
.toast{position:fixed;right:24px;bottom:25px;background:var(--brand-secondary);color:var(--sidebar-bg);border:1px solid var(--pill-live-line);box-shadow:0 10px 35px var(--shadow);padding:13px 20px;font-size:12px;font-weight:600;z-index:100;max-width:min(400px,calc(100vw - 32px))}
@media(max-width:1390px){
.shell{grid-template-columns:226px minmax(0,1fr)}
.sidebar{padding-left:13px;padding-right:13px}
.topbar{padding-left:30px;padding-right:30px}
.main-content{padding-left:30px;padding-right:30px}
.hero-grid{grid-template-columns:minmax(0,1.35fr) minmax(245px,.85fr)}
.hero-foot{flex-wrap:wrap}
.hero-link{margin-left:auto}
.stat{padding:20px 15px}
.filter-panel{grid-template-columns:minmax(200px,1.8fr) repeat(3,minmax(110px,1fr))}
}
@media(max-width:1070px){
.hero-grid{grid-template-columns:minmax(0,1fr)}
.hero-release{min-height:295px}
.pipeline-panel{min-height:auto}
.pipeline-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.pipeline-step{gap:10px}
.pipeline-connector{display:none}
.step-right{display:none}
.pipeline-footer{margin-top:17px}
.build-grid{gap:10px}
.build-card{padding:16px}
.build-card-details{flex-wrap:wrap}
.build-card-details>div{width:100%}
.filter-panel{grid-template-columns:repeat(3,minmax(0,1fr))}
.search-field{grid-column:1/-1}
.endpoints-grid{grid-template-columns:1fr 1fr}
}
@media(max-width:780px){
.shell{display:block}
.sidebar{position:relative;width:auto;height:auto;min-height:auto;padding:13px 21px 10px;border-right:0;border-bottom:1px solid var(--line)}
.brand{font-size:17px}
.brand-mark{width:38px;height:38px;flex-basis:38px;font-size:23px}
.brand-mark span{font-size:19px}
.edition,.sidebar-label,.side-channels,.sidebar-bottom{display:none}
.main-navigation{flex-direction:row;overflow-x:auto;gap:3px;margin-top:13px;padding-bottom:2px}
.nav{flex:1;min-width:max-content;gap:8px;padding:10px 12px;font-size:12px;min-height:auto}
.nav-arrow{display:none}
.nav b{font-size:11px}
.topbar{position:relative;padding:0 20px;height:57px}
.main-content{padding:35px 20px 22px}
.crumb{font-size:9px;gap:7px;letter-spacing:.5px}
.page-head{align-items:flex-start}
.page-head-side{display:none}
.page-head h1{font-size:39px}
.live-sync{margin-right:1px;font-size:9px}
.stats-grid{grid-template-columns:1fr 1fr}
.stat:nth-child(3){border-left:0;border-top:1px solid var(--line)}
.stat:nth-child(4){border-top:1px solid var(--line)}
.build-grid{grid-template-columns:1fr 1fr}
.install-tabs{grid-template-columns:1fr 1fr}
.install-extra-grid{grid-template-columns:1fr}
.hint-card p{min-height:auto}
.meta-table{grid-template-columns:1fr}
.meta-block+.meta-block{border-left:0;border-top:1px solid var(--line);padding-left:0}
.endpoints-grid{grid-template-columns:1fr}
}
@media(max-width:540px){
.sidebar{padding:12px 13px 9px}
.main-navigation{gap:2px}
.nav{gap:5px;padding:9px 7px;flex:1;justify-content:center}
.nav>span:first-child{font-size:9px}
.nav b{font-size:10px}
.topbar{padding:0 15px}
.top-actions{gap:5px}
.live-sync{display:none}
.theme-switch{padding:0 9px}
.main-content{padding:28px 15px 18px}
.page-head h1{font-size:35px;letter-spacing:-1.5px}
.eyebrow{font-size:8px;letter-spacing:1.1px;gap:6px}
.eyebrow-line{width:13px}
.lead{font-size:12px}
.hero-release{padding:19px 17px 20px;min-height:270px}
.hero-version-label{margin-top:39px;font-size:9px}
.hero-version{font-size:clamp(46px,11vw,72px);letter-spacing:-3px}
.hero-corner{display:none}
.hero-foot{gap:10px}
.hero-foot>div{min-width:calc(50% - 10px)}
.hero-foot strong{font-size:11px}
.hero-link{margin-top:6px;margin-left:0;width:100%}
.pipeline-panel{padding:19px}
.pipeline-list{display:flex;flex-direction:column;gap:13px}
.step-right{display:block}
.stats-grid .stat{padding:17px 14px}
.stat>span{font-size:9px}
.stat strong{font-size:23px}
.stat strong.stat-date{font-size:16px}
.stat small{font-size:9px}
.build-grid{grid-template-columns:1fr}
.build-card-details{flex-wrap:nowrap}
.build-card-details>div{width:auto}
.section-heading h2{font-size:21px}
.filter-panel{grid-template-columns:1fr 1fr}
.search-field{grid-column:1/-1}
.filter-input:last-child{grid-column:1/-1}
.result-topline{flex-direction:column}
.install-tabs{gap:7px}
.install-tab{padding:11px 9px;font-size:12px}
.install-tab small{font-size:9px}
.install-card{padding:17px}
.install-card-head{flex-wrap:wrap}
.code-window pre{font-size:11px;padding:15px}
.install-foot{flex-direction:column;gap:8px}
.hint-card{padding:19px}
.page-footer{flex-direction:column;gap:7px}
}
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms!important;transition-duration:.01ms!important}}
/* Compact overview: no decorative marketing hero. */
.overview-statusbar{display:flex;gap:15px;align-items:center;justify-content:flex-end;flex-wrap:wrap;margin:0 0 18px}
.overview-statusbar .muted-detail{margin-top:0}
.main-navigation{margin-top:26px}
+27
View File
@@ -17,6 +17,10 @@ services:
PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}" PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}"
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}" MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
KEEP_WORKSPACE: "${KEEP_WORKSPACE:-0}" KEEP_WORKSPACE: "${KEEP_WORKSPACE:-0}"
RELEASE_RETENTION: "${RELEASE_RETENTION:-10}"
PACKAGE_RETENTION: "${PACKAGE_RETENTION:-10}"
CLEAN_BUILD_CACHE: "${CLEAN_BUILD_CACHE:-1}"
BUILD_TMP_DIR: "${BUILD_TMP_DIR:-/tmp/rust-package-builder}"
OPNSENSE_ABI: "${OPNSENSE_ABI:-26.7}" OPNSENSE_ABI: "${OPNSENSE_ABI:-26.7}"
OPNSENSE_RETENTION: "${OPNSENSE_RETENTION:-10}" OPNSENSE_RETENTION: "${OPNSENSE_RETENTION:-10}"
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}" FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
@@ -25,12 +29,35 @@ services:
FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}" FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}"
APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}" APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}"
RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}" RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"
FREEBSD_NATIVE_ENABLED: "${FREEBSD_NATIVE_ENABLED:-1}"
FREEBSD_NATIVE_RETENTION: "${FREEBSD_NATIVE_RETENTION:-10}"
volumes: volumes:
- ./data:/var/lib/package-builder - ./data:/var/lib/package-builder
- ./gpg:/run/secrets/gpg:ro - ./gpg:/run/secrets/gpg:ro
restart: unless-stopped restart: unless-stopped
repository-dashboard:
build:
context: ./dashboard
image: rustproxy/repository-dashboard:local
environment:
REPOSITORY_ROOT: /srv/repository
REPOSITORY_URL: "${REPOSITORY_URL:?REPOSITORY_URL is required}"
OPNSENSE_REPOSITORY_URL: "${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}"
REPO_DISTRIBUTION: "${REPO_DISTRIBUTION:-stable}"
REPO_COMPONENT: "${REPO_COMPONENT:-main}"
volumes:
- ./data/repository:/srv/repository:ro
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
restart: unless-stopped
repository-http: repository-http:
depends_on:
- repository-dashboard
image: nginxinc/nginx-unprivileged:stable-alpine-slim image: nginxinc/nginx-unprivileged:stable-alpine-slim
ports: ports:
- "0.0.0.0:${HTTP_PORT:-18765}:8080" - "0.0.0.0:${HTTP_PORT:-18765}:8080"
+65 -21
View File
@@ -23,7 +23,11 @@ http {
uwsgi_temp_path /tmp/uwsgi_temp; uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp; scgi_temp_path /tmp/scgi_temp;
# Linux APT/RPM repository. # Redirect all 404 responses, including upstream errors.
proxy_intercept_errors on;
error_page 404 = @redirect_home;
# Linux repository
server { server {
listen 8080 default_server; listen 8080 default_server;
server_name _; server_name _;
@@ -32,11 +36,6 @@ http {
index index.html; index index.html;
autoindex off; autoindex off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Cache-Control "max-age=600" always;
if ($request_method !~ ^(GET|HEAD)$) { if ($request_method !~ ^(GET|HEAD)$) {
return 405; return 405;
} }
@@ -47,12 +46,21 @@ http {
} }
location = / { location = / {
try_files /index.html @linux_initializing; proxy_set_header X-Repo-Site linux;
proxy_pass http://repository-dashboard:8000/;
} }
location @linux_initializing { location = /index.html {
default_type text/html; proxy_set_header X-Repo-Site linux;
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy repository</title></head><body><h1>RustProxy Linux repository</h1><p>Repository is being initialized.</p></body></html>'; proxy_pass http://repository-dashboard:8000/index.html;
}
location ^~ /ui/ {
proxy_pass http://repository-dashboard:8000;
}
location = /api/v1/packages {
proxy_pass http://repository-dashboard:8000;
} }
location ~ (^|/)\. { location ~ (^|/)\. {
@@ -60,12 +68,17 @@ http {
} }
location / { location / {
expires 10m;
try_files $uri =404; try_files $uri =404;
} }
# Redirect 404 to homepage
location @redirect_home {
return 302 /;
}
} }
# OPNsense/FreeBSD pkg repository. It has a separate public URL and local port, # OPNsense / FreeBSD repository
# but follows the same atomic repository/current switch as the Linux repo.
server { server {
listen 8081; listen 8081;
server_name _; server_name _;
@@ -74,11 +87,6 @@ http {
index index.html; index index.html;
autoindex off; autoindex off;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "no-referrer" always;
add_header Cache-Control "max-age=600" always;
if ($request_method !~ ^(GET|HEAD)$) { if ($request_method !~ ^(GET|HEAD)$) {
return 405; return 405;
} }
@@ -89,12 +97,42 @@ http {
} }
location = / { location = / {
try_files /index.html @opnsense_initializing; proxy_set_header X-Repo-Site opnsense;
proxy_pass http://repository-dashboard:8000/;
} }
location @opnsense_initializing { location = /index.html {
default_type text/html; proxy_set_header X-Repo-Site opnsense;
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy OPNsense repository</title></head><body><h1>RustProxy OPNsense repository</h1><p>Repository is being initialized.</p></body></html>'; proxy_pass http://repository-dashboard:8000/index.html;
}
location = /freebsd {
return 301 /freebsd/;
}
location = /freebsd/ {
proxy_set_header X-Repo-Site freebsd;
proxy_pass http://repository-dashboard:8000/freebsd/;
}
location = /freebsd/index.html {
proxy_set_header X-Repo-Site freebsd;
proxy_pass http://repository-dashboard:8000/freebsd/index.html;
}
location ^~ /ui/ {
proxy_pass http://repository-dashboard:8000;
}
location = /api/v1/packages {
proxy_pass http://repository-dashboard:8000;
}
# Serve FreeBSD package catalogs directly
location /freebsd/ {
alias /srv/repository/freebsd/;
expires 10m;
autoindex off;
} }
location ~ (^|/)\. { location ~ (^|/)\. {
@@ -102,7 +140,13 @@ http {
} }
location / { location / {
expires 10m;
try_files $uri =404; try_files $uri =404;
} }
# Redirect 404 to homepage
location @redirect_home {
return 302 /;
}
} }
} }
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
import os
import sys
import zipfile
import subprocess
from pathlib import Path
def run_git_command(args, repo_path: Path) -> bytes:
result = subprocess.run(
["git", *args],
cwd=repo_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
check=True,
)
return result.stdout
def get_files_to_archive(repo_path: Path) -> list[str]:
output = run_git_command(
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
repo_path,
)
files = output.decode("utf-8", errors="surrogateescape").split("\0")
return [f for f in files if f]
def make_zip(repo_path: Path, output_zip: Path) -> None:
files = get_files_to_archive(repo_path)
output_zip = output_zip.resolve()
if output_zip.exists():
output_zip.unlink()
with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for rel_path in files:
abs_path = repo_path / rel_path
if not abs_path.exists():
continue
if abs_path.resolve() == output_zip:
continue
zf.write(abs_path, arcname=rel_path)
print(f"Created: {output_zip}")
print(f"Added files: {len(files)}")
def main():
repo_path = Path.cwd()
if len(sys.argv) > 1:
output_zip = Path(sys.argv[1])
else:
output_zip = repo_path / f"{repo_path.name}.zip"
try:
run_git_command(["rev-parse", "--show-toplevel"], repo_path)
except subprocess.CalledProcessError:
print("Error: this directory is not a Git repository.", file=sys.stderr)
sys.exit(1)
make_zip(repo_path, output_zip)
if __name__ == "__main__":
main()
View File
File mode changed.
View File
File mode changed.
+1 -1
View File
@@ -1,6 +1,6 @@
PLUGIN_NAME= rustproxy PLUGIN_NAME= rustproxy
PLUGIN_VERSION?= 0.23.3 PLUGIN_VERSION?= 0.23.3
PLUGIN_REVISION?= 1 PLUGIN_REVISION?= 2
PLUGIN_COMMENT= RustProxy reverse proxy service PLUGIN_COMMENT= RustProxy reverse proxy service
PLUGIN_MAINTAINER?= local@localhost PLUGIN_MAINTAINER?= local@localhost
PLUGIN_WWW?= https://git.linuxiarz.pl/gru/RustProxy PLUGIN_WWW?= https://git.linuxiarz.pl/gru/RustProxy
@@ -0,0 +1,10 @@
# FreeBSD/OPNsense newsyslog equivalent for RustProxy log retention.
# Fields: path owner:group mode count size when flags [pidfile signal]
# 14 archives; rotate daily or above 100 MiB; gzip, keep newest uncompressed.
# G=glob, B=no injected text (important for JSONL), E=skip empty, P=delay gzip.
# The supervised *.log output is reopened on SIGHUP via daemon(8) -H.
# JSONL files and other application-owned logs cannot use Linux copytruncate:
# their writers must close/reopen the files to avoid writing to old inodes.
/var/log/rustproxy/*.jsonl root:wheel 0640 14 100M $D0 GBEZPN
/var/log/rustproxy/*.log root:wheel 0640 14 100M $D0 GBEZP /var/run/rustproxy/daemon.pid SIGHUP
/var/log/rustproxy/vhosts/*.jsonl root:wheel 0640 14 100M $D0 GBEZPN
+56 -2
View File
@@ -18,12 +18,15 @@ load_rc_config "$name"
: ${rustproxy_user:="root"} : ${rustproxy_user:="root"}
: ${rustproxy_group:="wheel"} : ${rustproxy_group:="wheel"}
: ${rustproxy_data_dir:="/var/db/rustproxy"} : ${rustproxy_data_dir:="/var/db/rustproxy"}
# Optional external canonical JSON (absolute path without spaces).
: ${rustproxy_config_path:=""}
: ${rustproxy_log_dir:="/var/log/rustproxy"} : ${rustproxy_log_dir:="/var/log/rustproxy"}
: ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"} : ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"}
: ${rustproxy_run_dir:="/var/run/rustproxy"} : ${rustproxy_run_dir:="/var/run/rustproxy"}
: ${rustproxy_command:="/usr/local/sbin/rust-proxy"} : ${rustproxy_command:="/usr/local/sbin/rust-proxy"}
: ${rustproxy_restart_delay:="5"} : ${rustproxy_restart_delay:="5"}
: ${rustproxy_allow_remote_admin:="1"} : ${rustproxy_allow_remote_admin:="1"}
: ${rustproxy_update_check_interval_secs:="3600"}
# FreeBSD uses jemalloc rather than glibc. Keep the same low-arena intent as # FreeBSD uses jemalloc rather than glibc. Keep the same low-arena intent as
# Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob. rc.conf can override # Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob. rc.conf can override
# rustproxy_malloc_conf or set it to an empty value to disable the tuning. # rustproxy_malloc_conf or set it to an empty value to disable the tuning.
@@ -147,7 +150,35 @@ rustproxy_admin_listen_resolved="$(rustproxy_resolve_listener \
"$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \ "$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \
"0.0.0.0" "9090" "admin")" || exit 1 "0.0.0.0" "9090" "admin")" || exit 1
rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs 0 serve --proxy-listen ${rustproxy_proxy_listen_resolved}" # The CLI expects a positive interval (seconds), not a value hardcoded in rc.d.
# Validate again at runtime for hand-written or stale configurations.
rustproxy_valid_update_interval()
{
case "$1" in
''|*[!0-9]*) return 1 ;;
esac
[ "$1" -ge 60 ] 2>/dev/null && [ "$1" -le 604800 ] 2>/dev/null
}
if ! rustproxy_valid_update_interval "$rustproxy_update_check_interval_secs"; then
echo "Invalid RustProxy update check interval: ${rustproxy_update_check_interval_secs} (60-604800 seconds)" >&2
exit 1
fi
# Restrict to shell-safe absolute paths because rc.subr expands command_args.
rustproxy_config_arg=""
if [ -n "${rustproxy_config_path}" ]; then
case "${rustproxy_config_path}" in
/*) ;;
*) echo "rustproxy_config_path must be an absolute path" >&2; exit 1 ;;
esac
case "${rustproxy_config_path}" in
*[!a-zA-Z0-9_./-]*) echo "rustproxy_config_path contains unsupported characters" >&2; exit 1 ;;
esac
rustproxy_config_arg="--config ${rustproxy_config_path}"
fi
rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen_resolved}"
if [ "$rustproxy_tls_enabled" = "1" ]; then if [ "$rustproxy_tls_enabled" = "1" ]; then
rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}" rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}"
fi fi
@@ -163,12 +194,15 @@ rustproxy_daemon_user_arg=""
if [ "$rustproxy_user" != "root" ]; then if [ "$rustproxy_user" != "root" ]; then
rustproxy_daemon_user_arg="-u ${rustproxy_user}" rustproxy_daemon_user_arg="-u ${rustproxy_user}"
fi fi
command_args="-r -R ${rustproxy_restart_delay} -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}" # -H allows daemon(8) to reopen the stdout/stderr log when newsyslog sends SIGHUP.
command_args="-r -R ${rustproxy_restart_delay} -H -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}"
required_files="${rustproxy_command}" required_files="${rustproxy_command}"
start_precmd="rustproxy_prestart" start_precmd="rustproxy_prestart"
start_postcmd="rustproxy_poststart" start_postcmd="rustproxy_poststart"
stop_postcmd="rustproxy_poststop" stop_postcmd="rustproxy_poststop"
status_cmd="rustproxy_status" status_cmd="rustproxy_status"
extra_commands="reload"
reload_cmd="rustproxy_reload"
rustproxy_prestart() rustproxy_prestart()
{ {
@@ -200,6 +234,7 @@ rustproxy_prestart()
if [ -n "${rustproxy_malloc_conf}" ]; then if [ -n "${rustproxy_malloc_conf}" ]; then
echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}" echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
fi fi
echo "RustProxy update check interval: ${rustproxy_update_check_interval_secs}s"
echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}" echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt" printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
/bin/chmod 0640 "${rustproxy_run_dir}/command.txt" /bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
@@ -268,6 +303,25 @@ rustproxy_status()
return 1 return 1
} }
rustproxy_reload()
{
# rust-proxy reload authenticates locally via SQLite; it must run as the
# same account as the daemon and address the listener through loopback.
rustproxy_child_pid >/dev/null || {
echo "RustProxy is not running; nothing to reload" >&2
return 1
}
_rp_port="${rustproxy_admin_listen_resolved##*:}"
case "${rustproxy_admin_listen_resolved}" in
\[*\]:*) _rp_local_admin="[::1]:${_rp_port}" ;;
*:*) _rp_local_admin="127.0.0.1:${_rp_port}" ;;
*) echo "Invalid RustProxy administration listen address" >&2; return 1 ;;
esac
# The optional path is already checked for safe characters above.
${rustproxy_command} --data-dir "${rustproxy_data_dir}" ${rustproxy_config_arg} \
--admin-listen "${_rp_local_admin}" reload
}
rustproxy_poststop() rustproxy_poststop()
{ {
/bin/rm -f "${child_pidfile}" "${pidfile}" /bin/rm -f "${child_pidfile}" "${pidfile}"
@@ -0,0 +1,19 @@
#!/bin/sh
# OPNsense invokes this hook after networking and configd are available.
# Regenerate from config.xml on every boot: /var and /usr/local/etc may have
# been restored, or the administrator may have changed the GUI settings.
# Retry briefly if configd is still finishing startup on slower firewalls.
attempt=1
while ! /usr/local/sbin/configctl template reload OPNsense/RustProxy; do
if [ "$attempt" -ge 3 ]; then
echo "RustProxy: failed to render configuration during boot (3 attempts)" >&2
exit 1
fi
attempt=$((attempt + 1))
/bin/sleep 2
done
# Use the helper directly: its enabled check is based on the freshly rendered
# configuration and it does not rely on configd reloading our action registry.
exec /usr/local/opnsense/scripts/rustproxy/service.sh boot
@@ -76,4 +76,16 @@
<type>checkbox</type> <type>checkbox</type>
<help>Pass --allow-remote-admin so the RustProxy panel accepts non-loopback clients. Keep firewall access restricted to trusted networks; do not expose the admin port to WAN unless this is intentional.</help> <help>Pass --allow-remote-admin so the RustProxy panel accepts non-loopback clients. Keep firewall access restricted to trusted networks; do not expose the admin port to WAN unless this is intentional.</help>
</field> </field>
<field>
<label>Updates</label>
<type>header</type>
<static>true</static>
</field>
<field>
<id>rustproxy.general.updateCheckIntervalSecs</id>
<label>Update check interval (seconds)</label>
<type>text</type>
<help>How often RustProxy checks for new application versions. Default: 3600 seconds (1 hour). Allowed range: 60-604800 seconds. Changes take effect after Apply.</help>
</field>
</form> </form>
@@ -0,0 +1,17 @@
<?php
namespace OPNsense\RustProxy\Migrations;
use OPNsense\Base\BaseModelMigration;
/** Fill the new update-check interval for existing installations. */
class M1_5_0 extends BaseModelMigration
{
public function run($model)
{
parent::run($model);
if (trim((string)$model->general->updateCheckIntervalSecs) === '') {
$model->general->updateCheckIntervalSecs = '3600';
}
}
}
@@ -1,6 +1,6 @@
<model> <model>
<mount>//OPNsense/rustproxy</mount> <mount>//OPNsense/rustproxy</mount>
<version>1.4.0</version> <version>1.5.0</version>
<description>RustProxy service settings</description> <description>RustProxy service settings</description>
<items> <items>
<general> <general>
@@ -57,6 +57,13 @@
<Default>1</Default> <Default>1</Default>
<Required>Y</Required> <Required>Y</Required>
</allowRemoteAdmin> </allowRemoteAdmin>
<updateCheckIntervalSecs type="IntegerField">
<Default>3600</Default>
<MinimumValue>60</MinimumValue>
<MaximumValue>604800</MaximumValue>
<Required>Y</Required>
<ValidationMessage>Enter the number of seconds between update checks (60-604800).</ValidationMessage>
</updateCheckIntervalSecs>
<!-- Legacy combined listener values retained for seamless upgrades. --> <!-- Legacy combined listener values retained for seamless upgrades. -->
<proxyListen type="IPPortField"/> <proxyListen type="IPPortField"/>
@@ -16,6 +16,11 @@ start_service()
echo "RustProxy is disabled in OPNsense configuration" echo "RustProxy is disabled in OPNsense configuration"
return 0 return 0
fi fi
# OPNsense may invoke rc.d in addition to this boot hook. Avoid a second
# daemon supervisor when the service is already running.
if "$RC_SCRIPT" status >/dev/null 2>&1; then
return 0
fi
"$RC_SCRIPT" onestart "$RC_SCRIPT" onestart
} }
@@ -31,10 +36,19 @@ case "${1:-}" in
stop) stop)
stop_service stop_service
;; ;;
restart|reload) restart)
stop_service stop_service
start_service start_service
;; ;;
reload)
if [ "$enabled" != "1" ]; then
echo "RustProxy is disabled in OPNsense configuration" >&2
exit 1
fi
# Soft reload is performed by rust-proxy CLI through the live admin API.
# Listener changes from the GUI still require the explicit restart action.
"$RC_SCRIPT" reload
;;
status) status)
"$RC_SCRIPT" status "$RC_SCRIPT" status
;; ;;
@@ -20,7 +20,7 @@ message:restarting RustProxy
allowed_groups:wheel,wwwonly allowed_groups:wheel,wwwonly
[reload] [reload]
command:/usr/local/opnsense/scripts/rustproxy/service.sh restart command:/usr/local/opnsense/scripts/rustproxy/service.sh reload
parameters: parameters:
type:script type:script
message:reloading RustProxy message:reloading RustProxy
@@ -10,6 +10,8 @@ rustproxy_tls_port="{% if not helpers.empty('OPNsense.rustproxy.general.tlsPort'
rustproxy_admin_address="{% if not helpers.empty('OPNsense.rustproxy.general.adminAddress') %}{{ OPNsense.rustproxy.general.adminAddress }}{% endif %}" rustproxy_admin_address="{% if not helpers.empty('OPNsense.rustproxy.general.adminAddress') %}{{ OPNsense.rustproxy.general.adminAddress }}{% endif %}"
rustproxy_admin_port="{% if not helpers.empty('OPNsense.rustproxy.general.adminPort') %}{{ OPNsense.rustproxy.general.adminPort }}{% endif %}" rustproxy_admin_port="{% if not helpers.empty('OPNsense.rustproxy.general.adminPort') %}{{ OPNsense.rustproxy.general.adminPort }}{% endif %}"
rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}" rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}"
# Existing installations without this setting use the 3600-second default.
rustproxy_update_check_interval_secs="{% if not helpers.empty('OPNsense.rustproxy.general.updateCheckIntervalSecs') %}{{ OPNsense.rustproxy.general.updateCheckIntervalSecs }}{% else %}3600{% endif %}"
# Legacy 1.2.x combined settings. They are only used when the corresponding # Legacy 1.2.x combined settings. They are only used when the corresponding
# new split address/port fields are both empty. # new split address/port fields are both empty.
+19
View File
@@ -0,0 +1,19 @@
#!/bin/sh
set -e
# Do not start the service by default on fresh installation.
if ! /usr/sbin/pw groupshow rustproxy >/dev/null 2>&1; then
/usr/sbin/pw groupadd rustproxy
fi
if ! /usr/sbin/pw usershow rustproxy >/dev/null 2>&1; then
/usr/sbin/pw useradd rustproxy -g rustproxy -c 'RustProxy reverse proxy' -d /var/db/rustproxy -s /usr/sbin/nologin
fi
/usr/bin/install -d -m 0700 -o rustproxy -g rustproxy /var/db/rustproxy
/usr/bin/install -d -m 0750 -o rustproxy -g rustproxy /var/log/rustproxy
/usr/bin/install -d -m 0700 -o rustproxy -g rustproxy /usr/local/etc/rustproxy
# On upgrades pkg executes the former pre-deinstall hook first.
if /usr/sbin/service rustproxy enabled >/dev/null 2>&1; then
/usr/sbin/service rustproxy start || echo 'Warning: RustProxy requires attention; see /var/log/rustproxy/rustproxy.log' >&2
else
echo 'Enable RustProxy with: sysrc rustproxy_enable=YES && service rustproxy start'
fi
exit 0
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# Also stop on upgrade, before the old executable is replaced.
/usr/sbin/service rustproxy onestop >/dev/null 2>&1 || true
exit 0
+5
View File
@@ -0,0 +1,5 @@
# Native FreeBSD RustProxy logs; daemon(8) -H reopens supervised stdout.
# For application-written JSONL, verify that the writer reopens rotated files.
/var/log/rustproxy/*.jsonl rustproxy:rustproxy 0640 14 100M $D0 GBEZPN
/var/log/rustproxy/*.log rustproxy:rustproxy 0640 14 100M $D0 GBEZP /var/run/rustproxy/daemon.pid SIGHUP
/var/log/rustproxy/vhosts/*.jsonl rustproxy:rustproxy 0640 14 100M $D0 GBEZPN
+135
View File
@@ -0,0 +1,135 @@
#!/bin/sh
# PROVIDE: rustproxy
# REQUIRE: LOGIN NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name=rustproxy
rcvar=rustproxy_enable
load_rc_config "$name"
: ${rustproxy_enable:="NO"}
: ${rustproxy_user:="rustproxy"}
: ${rustproxy_group:="rustproxy"}
: ${rustproxy_data_dir:="/var/db/rustproxy"}
: ${rustproxy_log_dir:="/var/log/rustproxy"}
: ${rustproxy_run_dir:="/var/run/rustproxy"}
: ${rustproxy_config:=""}
: ${rustproxy_proxy_listen:="0.0.0.0:8080"}
: ${rustproxy_tls_listen:=""}
: ${rustproxy_admin_listen:="127.0.0.1:9090"}
: ${rustproxy_allow_remote_admin:="NO"}
: ${rustproxy_update_check_interval_secs:="3600"}
# rc.subr expands command_args; permit only simple shell-safe paths and
# IPv4 socket addresses to avoid command injection from rc.conf settings.
rustproxy_safe_path()
{
case "$1" in
/*) ;;
*) echo "RustProxy requires absolute paths: $1" >&2; return 1 ;;
esac
case "$1" in
*[!a-zA-Z0-9_./-]*) echo "Unsupported path characters: $1" >&2; return 1 ;;
esac
}
rustproxy_safe_listen()
{
case "$1" in
*[!0-9.:]*|''|*::*) echo "Only IPv4 host:port listeners are supported by this rc.d script: $1" >&2; return 1 ;;
*:*) ;;
*) echo "Missing listener port: $1" >&2; return 1 ;;
esac
}
case "${rustproxy_user}:${rustproxy_group}" in
*[!a-zA-Z0-9_:-]*) echo 'Unsupported user/group name' >&2; exit 1 ;;
esac
for rp_path in "$rustproxy_data_dir" "$rustproxy_log_dir" "$rustproxy_run_dir"; do
rustproxy_safe_path "$rp_path" || exit 1
done
if [ -n "$rustproxy_config" ]; then
rustproxy_safe_path "$rustproxy_config" || exit 1
fi
rustproxy_safe_listen "$rustproxy_proxy_listen" || exit 1
rustproxy_safe_listen "$rustproxy_admin_listen" || exit 1
if [ -n "$rustproxy_tls_listen" ]; then
rustproxy_safe_listen "$rustproxy_tls_listen" || exit 1
fi
case "$rustproxy_update_check_interval_secs" in
''|*[!0-9]*) echo 'Invalid update interval' >&2; exit 1 ;;
esac
rustproxy_bin="/usr/local/sbin/rust-proxy"
rustproxy_config_arg=""
if [ -n "$rustproxy_config" ]; then
rustproxy_config_arg="--config ${rustproxy_config}"
fi
rustproxy_args="--data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen ${rustproxy_admin_listen} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen}"
if [ -n "$rustproxy_tls_listen" ]; then
rustproxy_args="${rustproxy_args} --tls-listen ${rustproxy_tls_listen}"
fi
if checkyesno rustproxy_allow_remote_admin; then
rustproxy_args="${rustproxy_args} --allow-remote-admin"
fi
pidfile="${rustproxy_run_dir}/daemon.pid"
child_pidfile="${rustproxy_run_dir}/rustproxy.pid"
command="/usr/sbin/daemon"
command_args="-r -R 5 -H -P ${pidfile} -p ${child_pidfile} -u ${rustproxy_user} -o ${rustproxy_log_dir}/rustproxy.log ${rustproxy_bin} ${rustproxy_args}"
required_files="${rustproxy_bin}"
start_precmd="rustproxy_prestart"
stop_postcmd="rustproxy_poststop"
status_cmd="rustproxy_status"
extra_commands="reload"
reload_cmd="rustproxy_reload"
rustproxy_prestart()
{
/usr/bin/install -d -m 0750 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_run_dir}"
/usr/bin/install -d -m 0700 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_data_dir}"
/usr/bin/install -d -m 0750 -o "${rustproxy_user}" -g "${rustproxy_group}" "${rustproxy_log_dir}"
/usr/bin/touch "${rustproxy_log_dir}/rustproxy.log"
/usr/sbin/chown "${rustproxy_user}:${rustproxy_group}" "${rustproxy_log_dir}/rustproxy.log"
/bin/chmod 0640 "${rustproxy_log_dir}/rustproxy.log"
}
rustproxy_child_pid()
{
[ -r "$child_pidfile" ] || return 1
rp_pid=$(/bin/cat "$child_pidfile" 2>/dev/null)
case "$rp_pid" in
''|*[!0-9]*) return 1 ;;
esac
/bin/kill -0 "$rp_pid" 2>/dev/null || return 1
printf '%s\n' "$rp_pid"
}
rustproxy_status()
{
if rp_pid=$(rustproxy_child_pid); then
echo "rustproxy is running as pid $rp_pid"
return 0
fi
echo 'rustproxy is not running'
return 1
}
rustproxy_reload()
{
rustproxy_child_pid >/dev/null || {
echo 'RustProxy is not running; cannot reload' >&2
return 1
}
# Change to the daemon's UID. Reload creates a short-lived SQLite session.
# A root-owned SQLite WAL would make the next reload/save fail.
rp_port="${rustproxy_admin_listen##*:}"
rp_command="${rustproxy_bin} --data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen 127.0.0.1:${rp_port} reload"
/usr/bin/su -m "${rustproxy_user}" -c "exec ${rp_command}"
}
rustproxy_poststop()
{
/bin/rm -f "$child_pidfile" "$pidfile"
}
run_rc_command "$1"
+85
View File
@@ -0,0 +1,85 @@
# RustProxy (DEB/RPM): dostęp do katalogów WWW i certyfikatów
Jednostka z paczki uruchamia `rust-proxy` jako `rustproxy:rustproxy`, z
`ProtectSystem=strict` i `ProtectHome=read-only`. Katalogi `/home`, `/var/www`,
`/srv` i zewnętrzne PEM są **dostępne do odczytu**, jeżeli pozwalają na to
standardowe uprawnienia użytkownika, grupy lub POSIX ACL. Nie jest potrzebny
`systemctl edit` ani dopisywanie `ReadOnlyPaths=`. Dostęp do zapisu poza
`/var/lib/rustproxy`, `/var/log/rustproxy` i dopuszczonym `/etc/rustproxy`
pozostaje zablokowany przez systemd. Nie zmieniaj `ProtectSystem` na `false`.
## 1. Katalog strony WWW w /home
Przykład (zastąp `klient` i ścieżkę nazwą swojej strony):
```bash
sudo setfacl -m u:rustproxy:--x /home/klient
sudo setfacl -R -m u:rustproxy:r-X /home/klient/public_html
sudo -u rustproxy test -r /home/klient/public_html/index.html && echo OK
sudo systemctl restart rustproxy
```
`--x` pozwala jedynie przejść przez prywatny katalog domowy; `r-X` nadaje
odczyt plików i przechodzenie przez podkatalogi. Dla nowo tworzonych plików
ustaw także odpowiednie domyślne ACL na katalogach witryny lub pilnuj grupy
oraz umask narzędzia publikującego.
## 2. Grupa zamiast ACL (np. /var/www)
Nadaj witrynie **osobną, możliwie wąską grupę**, np. `web-readers`, zapewnij
jej `r` do plików i `x` do katalogów, a potem:
```bash
sudo usermod -aG web-readers rustproxy
id rustproxy
sudo systemctl restart rustproxy
```
Grupy dodatkowe użytkownika `rustproxy` są automatycznie uwzględniane przez
systemd przy starcie usługi. Nie wpisuj ich w `SupplementaryGroups=` i nie
udostępniaj plików całemu światu (`chmod 777` / `chmod -R 755` nie są wymagane).
Restart jest konieczny po zmianie grup, ponieważ uruchomiony proces nie
pobiera nowej listy grup samoczynnie.
## 3. Zewnętrzny certyfikat z Certbot / Let's Encrypt
Dla `example.com` nadaj jedynie możliwość przechodzenia przez katalogi i
odczytu bieżących rzeczywistych plików (symlinki `live` prowadzą do `archive`):
```bash
sudo setfacl -m u:rustproxy:--x /etc/letsencrypt /etc/letsencrypt/live \
/etc/letsencrypt/live/example.com /etc/letsencrypt/archive \
/etc/letsencrypt/archive/example.com
sudo setfacl -m u:rustproxy:r-- "$(readlink -f /etc/letsencrypt/live/example.com/fullchain.pem)"
sudo setfacl -m u:rustproxy:r-- "$(readlink -f /etc/letsencrypt/live/example.com/privkey.pem)"
sudo systemctl reload rustproxy
```
**Ważne:** RustProxy celowo wymaga, aby zewnętrzne pliki certyfikatu/klucza oraz
wszystkie rzeczywiste katalogi nadrzędne były własnością `root` albo
`rustproxy`, nie były zapisywalne przez grupę/innych, a klucz prywatny nie był
czytelny przez innych. Katalog WWW może być własnością innego użytkownika;
ścieżka do PEM ma ostrzejszą walidację. Nie zakładaj, że certyfikat trzymany
pod `/home/klient` z katalogiem należącym do `klient` zadziała po samym ACL.
Umieść go w zaufanym katalogu np. `/etc/letsencrypt` i nadaj odczyt ACL/grupą.
Po odnowieniu certyfikatu (nowy plik w `archive/`) odtwórz uprawnienia przez
hook odnowienia, a potem przeładuj RustProxy.
## 4. Diagnostyka
```bash
systemctl show rustproxy -p User -p Group -p ProtectHome -p ProtectSystem -p ReadWritePaths
systemctl cat rustproxy
namei -l /home/klient/public_html/index.html
sudo -u rustproxy test -r /home/klient/public_html/index.html && echo OK
journalctl -u rustproxy -n 60 --no-pager
```
Test `sudo -u` potwierdza prawa POSIX **poza** sandboxem systemd. Jeśli działa,
a RustProxy dalej nie czyta pliku, sprawdź aktywną jednostkę/drop-in, politykę
SELinux lub AppArmor (`ausearch -m avc -ts recent` na SELinux), symlinki i
system plików. Ręczna jednostka w `/etc/systemd/system/rustproxy.service` ma
pierwszeństwo przed pakietową w `/usr/lib/systemd/system` i nie zostanie
nadpisana automatycznie. Web Server ma własną politykę symlinków. ACL nie
pozwoli zapisać strony lub uploadów pod `/home`/`/var/www`, dopóki nie zostanie
jawnie dopuszczony zapis w sandboxie; zapis aplikacji trzymaj osobno.
+11
View File
@@ -0,0 +1,11 @@
/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl {
daily
maxsize 512M
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
su rustproxy rustproxy
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
# SUSE 15 can keep an old /usr/bin/python3 alongside versioned Python >= 3.10.
# Restrict this wrapper to RustProxy's service PATH; never replace system Python.
for executable in /usr/bin/python3.14 /usr/bin/python3.13 /usr/bin/python3.12 /usr/bin/python3.11 /usr/bin/python3.10 /usr/bin/python3; do
if [ -x "$executable" ] && "$executable" -c 'import sys; sys.exit(sys.version_info < (3, 10))' >/dev/null 2>&1; then
exec "$executable" "$@"
fi
done
echo 'RustProxy requires Python 3.10 or newer; install a supported python3XX package.' >&2
exit 127
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
: "${SHA:?SHA required}"
: "${SRC:?SRC required}"
: "${OUT:?OUT required}"
: "${VERSION:?VERSION required}"
: "${SOURCE_REVISION:?SOURCE_REVISION required}"
REPOSITORY="$DATA_DIR/repository"
BRANCH_RELEASE_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}-freebsd"
RELEASE_DIR="$REPOSITORY/releases/$BRANCH_RELEASE_ID"
PUBLISHED=0
cleanup_freebsd_release() {
if [[ "$PUBLISHED" != "1" && -d "$RELEASE_DIR" ]]; then
rm -rf "$RELEASE_DIR"
fi
}
trap cleanup_freebsd_release EXIT
FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
if [[ ! -x "$FREEBSD_BINARY" ]]; then
# Reuse the same FreeBSD cross-build as OPNsense if it succeeded first.
"$BUILDER_ROOT/scripts/build-opnsense.sh" "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK"
fi
FREEBSD_PACKAGE_REVISION=$SOURCE_REVISION
LAST_FREEBSD_PACKAGE_VERSION=""
if [[ -r "$DATA_DIR/state/last_freebsd_package_version" ]]; then
LAST_FREEBSD_PACKAGE_VERSION=$(cat "$DATA_DIR/state/last_freebsd_package_version")
elif [[ -r "$REPOSITORY/current/freebsd/BUILD-INFO.txt" ]]; then
LAST_FREEBSD_PACKAGE_VERSION=$(sed -n 's/^package_version=//p' "$REPOSITORY/current/freebsd/BUILD-INFO.txt" | head -n1)
fi
if [[ "$LAST_FREEBSD_PACKAGE_VERSION" == "${VERSION}_"* ]]; then
LAST_FREEBSD_REVISION=${LAST_FREEBSD_PACKAGE_VERSION#"${VERSION}_"}
if [[ "$LAST_FREEBSD_REVISION" =~ ^[0-9]+$ ]] && (( LAST_FREEBSD_REVISION >= FREEBSD_PACKAGE_REVISION )); then
FREEBSD_PACKAGE_REVISION=$((LAST_FREEBSD_REVISION + 1))
fi
fi
if (( FREEBSD_PACKAGE_REVISION == 0 )); then
FREEBSD_PACKAGE_REVISION=1
fi
FREEBSD_PACKAGE_VERSION="${VERSION}_${FREEBSD_PACKAGE_REVISION}"
FREEBSD_PACKAGE_FILE="$OUT/rustproxy-${FREEBSD_PACKAGE_VERSION}.pkg"
"$BUILDER_ROOT/scripts/package-freebsd.sh" \
"$SRC" "$FREEBSD_BINARY" "$FREEBSD_PACKAGE_FILE" "$FREEBSD_PACKAGE_VERSION"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd"
CURRENT_TARGET=""
if [[ -L "$REPOSITORY/current" ]]; then
CURRENT_LINK=$(readlink "$REPOSITORY/current")
CURRENT_TARGET="$REPOSITORY/$CURRENT_LINK"
fi
log 'staging native FreeBSD repository snapshot'
if [[ -n "$CURRENT_TARGET" ]]; then
for component in apt rpm suse opnsense freebsd; do
if [[ -d "$CURRENT_TARGET/$component" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
fi
done
for copied in repo-key.gpg index.html LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
if [[ -f "$CURRENT_TARGET/$copied" ]]; then
cp -a "$CURRENT_TARGET/$copied" "$RELEASE_DIR/$copied"
fi
done
fi
FREEBSD_PACKAGE_DIR="$RELEASE_DIR/freebsd/$FREEBSD_ABI"
mkdir -p "$FREEBSD_PACKAGE_DIR"
cp -f "$FREEBSD_PACKAGE_FILE" "$FREEBSD_PACKAGE_DIR/"
"$BUILDER_ROOT/scripts/generate-opnsense-repo.py" "$RELEASE_DIR/freebsd" --retention "$FREEBSD_NATIVE_RETENTION" --repo-version 2
NATIVE_URL="${OPNSENSE_REPOSITORY_URL%/}/freebsd"
cat > "$RELEASE_DIR/freebsd/RustProxy-FreeBSD.conf" <<CONF
RustProxyNative: {
url: "${NATIVE_URL}",
enabled: yes,
mirror_type: "none",
signature_type: "none"
}
CONF
cp -f "$FREEBSD_PACKAGE_FILE" "$RELEASE_DIR/freebsd/rustproxy-latest.pkg"
(cd "$RELEASE_DIR/freebsd" && sha256sum "$FREEBSD_ABI/$(basename "$FREEBSD_PACKAGE_FILE")") > "$RELEASE_DIR/freebsd/SHA256"
cat > "$RELEASE_DIR/freebsd/BUILD-INFO.txt" <<INFO
app_version=$VERSION
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
package_version=$FREEBSD_PACKAGE_VERSION
source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH
source_commit=$SHA
build_reason=$BUILD_REASON
source_revision=$SOURCE_REVISION
packaging_revision=$FREEBSD_NATIVE_PACKAGING_REVISION
freebsd_release=$FREEBSD_RELEASE
freebsd_abi=$FREEBSD_ABI
freebsd_arch=$FREEBSD_ARCH
package=$FREEBSD_ABI/$(basename "$FREEBSD_PACKAGE_FILE")
INFO
"$BUILDER_ROOT/scripts/generate-freebsd-index.sh" "$RELEASE_DIR/freebsd/index.html"
[[ -s "$RELEASE_DIR/freebsd/index.html" ]]
[[ -s "$RELEASE_DIR/freebsd/meta.conf" ]]
[[ -s "$RELEASE_DIR/freebsd/packagesite.pkg" ]]
[[ -s "$RELEASE_DIR/freebsd/data.pkg" ]]
grep -Fqx "version: 2;" "$RELEASE_DIR/freebsd/meta.conf" || fatal "unexpected FreeBSD pkg repository format"
[[ -s "$RELEASE_DIR/freebsd/rustproxy-latest.pkg" ]]
bsdtar -tf "$RELEASE_DIR/freebsd/packagesite.pkg" | grep -Fx 'packagesite.yaml' >/dev/null || fatal 'invalid FreeBSD packagesite.pkg'
bsdtar -xOf "$FREEBSD_PACKAGE_DIR/$(basename "$FREEBSD_PACKAGE_FILE")" +COMPACT_MANIFEST >/dev/null || fatal 'invalid FreeBSD package manifest'
# Fail before switching the live repository if the public package is missing or empty.
[[ -s "$FREEBSD_PACKAGE_DIR/$(basename "$FREEBSD_PACKAGE_FILE")" ]] || fatal 'missing FreeBSD native package artifact'
log 'publishing native FreeBSD repository branch'
rm -f "$REPOSITORY/current.new"
ln -s "releases/$BRANCH_RELEASE_ID" "$REPOSITORY/current.new"
mv -Tf "$REPOSITORY/current.new" "$REPOSITORY/current"
PUBLISHED=1
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_freebsd_commit"
printf '%s\n' "$FREEBSD_PACKAGE_VERSION" | atomic_write "$DATA_DIR/state/last_freebsd_package_version"
printf '%s\n' "$BRANCH_RELEASE_ID" | atomic_write "$DATA_DIR/state/last_freebsd_release"
printf '%s\n' "$BRANCH_RELEASE_ID" | atomic_write "$DATA_DIR/state/last_release"
log "FreeBSD native publication completed: package=$FREEBSD_PACKAGE_VERSION commit=$SHA"
+133 -23
View File
@@ -32,8 +32,14 @@ if [[ "$REQUESTED_PACKAGE_RELEASE" == "auto" ]]; then
[[ -r "$DATA_DIR/state/last_version" ]] && LAST_VERSION=$(cat "$DATA_DIR/state/last_version") [[ -r "$DATA_DIR/state/last_version" ]] && LAST_VERSION=$(cat "$DATA_DIR/state/last_version")
[[ -r "$DATA_DIR/state/last_package_release" ]] && LAST_PACKAGE_RELEASE=$(cat "$DATA_DIR/state/last_package_release") [[ -r "$DATA_DIR/state/last_package_release" ]] && LAST_PACKAGE_RELEASE=$(cat "$DATA_DIR/state/last_package_release")
CURRENT_LINUX_PACKAGING_REVISION=""
if [[ -r "$REPOSITORY/current/LINUX-BUILD-INFO.txt" ]]; then
CURRENT_LINUX_PACKAGING_REVISION=$(sed -n 's/^packaging_revision=//p' "$REPOSITORY/current/LINUX-BUILD-INFO.txt" | head -n1)
fi
if [[ "$LAST_LINUX_COMMIT" == "$SHA" \ if [[ "$LAST_LINUX_COMMIT" == "$SHA" \
&& "$LAST_VERSION" == "$VERSION" \ && "$LAST_VERSION" == "$VERSION" \
&& "$CURRENT_LINUX_PACKAGING_REVISION" == "$PACKAGING_REVISION" \
&& "$LAST_PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]]; then && "$LAST_PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]]; then
PACKAGE_RELEASE=$LAST_PACKAGE_RELEASE PACKAGE_RELEASE=$LAST_PACKAGE_RELEASE
log "Linux repair build: reusing package release $PACKAGE_RELEASE" log "Linux repair build: reusing package release $PACKAGE_RELEASE"
@@ -48,23 +54,42 @@ fi
export PACKAGE_RELEASE export PACKAGE_RELEASE
cd "$SRC" cd "$SRC"
LINUX_RUST_TOOLCHAIN=$(/opt/rust-package-builder/scripts/ensure-rust-toolchain.sh "$SRC" linux)
log "Linux branch: version $VERSION release $PACKAGE_RELEASE" log "Linux branch: version $VERSION release $PACKAGE_RELEASE"
if [[ "${RUN_LINUX_TESTS:-1}" == "1" ]]; then TEST_MODE="${RUN_LINUX_TESTS:-compile}"
log "Linux cargo tests started" case "$TEST_MODE" in
0|off|false|no)
log "Linux cargo tests skipped"
;;
1|compile|check|no-run)
log "Linux cargo tests compile check started (--no-run)"
if [[ -f Cargo.lock ]]; then if [[ -f Cargo.lock ]]; then
cargo test --release --locked cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked --no-run
else else
cargo test --release cargo "+$LINUX_RUST_TOOLCHAIN" test --release --no-run
fi fi
log "Linux cargo tests completed" log "Linux cargo tests compile check completed"
;;
full|run)
log "Linux full cargo tests started"
if [[ -f Cargo.lock ]]; then
cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked
else
cargo "+$LINUX_RUST_TOOLCHAIN" test --release
fi fi
log "Linux full cargo tests completed"
;;
*)
fatal "invalid RUN_LINUX_TESTS=$TEST_MODE (use: 0, 1/compile, or full)"
;;
esac
log "Linux cargo build started" log "Linux cargo build started"
if [[ -f Cargo.lock ]]; then if [[ -f Cargo.lock ]]; then
cargo build --release --locked cargo "+$LINUX_RUST_TOOLCHAIN" build --release --locked
else else
cargo build --release cargo "+$LINUX_RUST_TOOLCHAIN" build --release
fi fi
log "Linux cargo build completed" log "Linux cargo build completed"
@@ -79,25 +104,69 @@ if [[ -n "$MAX_GLIBC_FOUND" ]]; then
log "glibc compatibility check: <= $MAX_GLIBC_FOUND" log "glibc compatibility check: <= $MAX_GLIBC_FOUND"
fi fi
# The Go agent is packaged ONLY for Linux. A failure aborts the Linux branch
# before either APT or RPM metadata is published; OPNsense/FreeBSD are separate.
AGENT_SRC="$SRC/docker-agent"
[[ -f "$AGENT_SRC/go.mod" ]] || fatal "RustProxy source is missing docker-agent/go.mod"
AGENT_BINARY="$OUT/rustproxy-docker-agent"
AGENT_PKG="$WORK/package-agent"
log "Linux Docker Agent tests started"
(cd "$AGENT_SRC" && GOWORK=off CGO_ENABLED=0 go test ./...)
log "Linux Docker Agent build started"
(cd "$AGENT_SRC" && GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w -X main.version=$VERSION" -o "$AGENT_BINARY" .)
[[ -x "$AGENT_BINARY" ]] || fatal "built Go agent not found: $AGENT_BINARY"
/opt/rust-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY_PATH" /opt/rust-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY_PATH"
/opt/rust-package-builder/scripts/prepare-agent-package.sh "$SRC" "$AGENT_PKG" "$AGENT_BINARY"
DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb" DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
RPM_FILE="$OUT/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm" RPM_FILE="$OUT/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
AGENT_DEB_FILE="$OUT/rustproxy-docker-agent_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
AGENT_RPM_FILE="$OUT/rustproxy-docker-agent-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
SUSE_RPM_FILE="$OUT/suse/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
SUSE_AGENT_RPM_FILE="$OUT/suse/rustproxy-docker-agent-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
mkdir -p "$OUT/suse"
log "creating deb package" log "creating RustProxy and Docker Agent deb packages"
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB_FILE" nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB_FILE"
log "creating rpm package" nfpm package --config "$AGENT_PKG/nfpm-deb.yaml" --packager deb --target "$AGENT_DEB_FILE"
log "creating RustProxy and Docker Agent rpm packages"
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE" nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE"
nfpm package --config "$AGENT_PKG/nfpm-rpm.yaml" --packager rpm --target "$AGENT_RPM_FILE"
log "creating SUSE/openSUSE RPM packages"
nfpm package --config "$PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE_RPM_FILE"
nfpm package --config "$AGENT_PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE_AGENT_RPM_FILE"
ar t "$DEB_FILE" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package does not contain nFPM GPG origin signature" for deb in "$DEB_FILE" "$AGENT_DEB_FILE"; do
rpm -qpi "$RPM_FILE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed" ar t "$deb" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package is not signed: $deb"
done
for rpm_file in "$RPM_FILE" "$AGENT_RPM_FILE" "$SUSE_RPM_FILE" "$SUSE_AGENT_RPM_FILE"; do
rpm -qpi "$rpm_file" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' \
|| fatal "RPM package is not signed: $rpm_file"
done
log "Linux package signatures verified" log "Linux package signatures verified"
cp -f "$DEB_FILE" "$DATA_DIR/packages/deb/" cp -f "$DEB_FILE" "$AGENT_DEB_FILE" "$DATA_DIR/packages/deb/"
cp -f "$RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/" cp -f "$RPM_FILE" "$AGENT_RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
cp -f "$SUSE_RPM_FILE" "$SUSE_AGENT_RPM_FILE" "$DATA_DIR/packages/suse/x86_64/"
log "updating aptly internal repository" # Retention is per product, not per file pool: keep ten RustProxy versions and
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DEB_FILE" >/dev/null # ten Docker Agent versions in each Linux package family by default.
package_retention=${PACKAGE_RETENTION:-10}
prune_package_files "$DATA_DIR/packages/deb" "${PACKAGE_NAME}_*.deb" "$package_retention"
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy-docker-agent_*.deb' "$package_retention"
prune_package_files "$DATA_DIR/packages/rpm/x86_64" "${PACKAGE_NAME}-*.x86_64.rpm" "$package_retention" 'rustproxy-docker-agent-*.x86_64.rpm'
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$package_retention"
prune_package_files "$DATA_DIR/packages/suse/x86_64" "${PACKAGE_NAME}-*.x86_64.rpm" "$package_retention" 'rustproxy-docker-agent-*.x86_64.rpm'
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$package_retention"
log "updating aptly internal repository from retained DEB packages"
# Make the mutable local repo match the retained raw DEB pool exactly. Existing
# snapshots are immutable and remain valid until the new snapshot is published.
aptly -config="$APTLY_CONF" repo remove package-builder "$PACKAGE_NAME" >/dev/null 2>&1 || true
aptly -config="$APTLY_CONF" repo remove package-builder rustproxy-docker-agent >/dev/null 2>&1 || true
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DATA_DIR/packages/deb" >/dev/null
SNAPSHOT="linux-${VERSION}-${SHORT_SHA}-$(date +%s)" SNAPSHOT="linux-${VERSION}-${SHORT_SHA}-$(date +%s)"
aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo package-builder >/dev/null aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo package-builder >/dev/null
@@ -119,7 +188,19 @@ APT_PUBLIC="$DATA_DIR/aptly/public"
gpg --batch --verify "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 \ gpg --batch --verify "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 \
|| fatal "APT Release signature verification failed" || fatal "APT Release signature verification failed"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" # The new snapshot is now live. Older Linux snapshots are no longer needed;
# removing them lets aptly reclaim unreferenced pool objects.
while IFS= read -r old_snapshot; do
[[ -n "$old_snapshot" && "$old_snapshot" != "$SNAPSHOT" ]] || continue
if ! aptly -config="$APTLY_CONF" snapshot drop "$old_snapshot" >/dev/null 2>&1; then
log "WARN: could not drop old aptly snapshot $old_snapshot"
fi
done < <(aptly -config="$APTLY_CONF" snapshot list -raw | grep '^linux-' || true)
if ! aptly -config="$APTLY_CONF" db cleanup >/dev/null 2>&1; then
log "WARN: aptly database cleanup failed"
fi
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd"
CURRENT_TARGET="" CURRENT_TARGET=""
if [[ -L "$REPOSITORY/current" ]]; then if [[ -L "$REPOSITORY/current" ]]; then
CURRENT_LINK=$(readlink "$REPOSITORY/current") CURRENT_LINK=$(readlink "$REPOSITORY/current")
@@ -127,39 +208,68 @@ if [[ -L "$REPOSITORY/current" ]]; then
fi fi
log "staging Linux repository snapshot" log "staging Linux repository snapshot"
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/opnsense" ]]; then if [[ -n "$CURRENT_TARGET" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/opnsense"/ "$CURRENT_TARGET/opnsense"/ "$RELEASE_DIR/opnsense"/ for component in opnsense freebsd; do
if [[ -d "$CURRENT_TARGET/$component" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
fi
done
fi fi
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/apt" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/apt/" "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
else
rsync -a --delete "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/ rsync -a --delete "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
fi
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/rpm" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/rpm/" "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/
else
rsync -a --delete "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/ rsync -a --delete "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/
rm -rf "$RELEASE_DIR/rpm/x86_64/repodata" fi
createrepo_c --update "$RELEASE_DIR/rpm/x86_64" >/dev/null if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/suse" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/suse/" "$DATA_DIR/packages/suse"/ "$RELEASE_DIR/suse"/
else
rsync -a --delete "$DATA_DIR/packages/suse"/ "$RELEASE_DIR/suse"/
fi
for rpm_root in "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64"; do
rm -rf "$rpm_root/repodata"
createrepo_c --update "$rpm_root" >/dev/null
done
GPG_SIGN_ARGS=(--batch --yes --pinentry-mode loopback) GPG_SIGN_ARGS=(--batch --yes --pinentry-mode loopback)
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE") GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE")
fi fi
for rpm_root in "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64"; do
gpg "${GPG_SIGN_ARGS[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" \ gpg "${GPG_SIGN_ARGS[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" \
--output "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" --output "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml"
gpg --batch --verify "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" >/dev/null 2>&1 \ gpg --batch --verify "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml" >/dev/null 2>&1 \
|| fatal "RPM metadata signature verification failed" || fatal "RPM metadata signature verification failed: $rpm_root"
done
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg" gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
[[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed" [[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed"
/opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME" /opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME"
cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO
app_version=$VERSION app_version=$VERSION
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
package_release=$PACKAGE_RELEASE package_release=$PACKAGE_RELEASE
packaging_revision=$PACKAGING_REVISION
linux_packages=$PACKAGE_NAME,rustproxy-docker-agent
linux_rpm_flavors=dnf,suse
source_url=$SOURCE_REPO source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH source_ref=$SOURCE_BRANCH
source_commit=$SHA source_commit=$SHA
build_reason=$BUILD_REASON build_reason=$BUILD_REASON
INFO INFO
cp -f "$RELEASE_DIR/LINUX-BUILD-INFO.txt" "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
printf '%s\n' 'rpm_flavor=suse' 'platforms=SLES_15_SP6+,SLES_16+,Leap_15.6+,Tumbleweed' >> "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
[[ -s "$RELEASE_DIR/index.html" ]] [[ -s "$RELEASE_DIR/index.html" ]]
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] [[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
[[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]] [[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]]
[[ -f "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml" ]]
[[ -s "$RELEASE_DIR/SUSE-BUILD-INFO.txt" ]]
log "publishing Linux repository branch" log "publishing Linux repository branch"
rm -f "$REPOSITORY/current.new" rm -f "$REPOSITORY/current.new"
+24 -5
View File
@@ -24,16 +24,33 @@ trap cleanup_opnsense_release EXIT
FREEBSD_BINARY="$OUT/rust-proxy-freebsd" FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
/opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK" /opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK"
if [[ "$SOURCE_REVISION" == "0" ]]; then # PKG needs an increasing version for a packaging-only change at the same
# application commit, otherwise pkg upgrade would not install the new payload.
OPNSENSE_PACKAGE_REVISION=$SOURCE_REVISION
LAST_OPNSENSE_PACKAGE_VERSION=""
if [[ -r "$DATA_DIR/state/last_opnsense_package_version" ]]; then
LAST_OPNSENSE_PACKAGE_VERSION=$(cat "$DATA_DIR/state/last_opnsense_package_version")
elif [[ -r "$REPOSITORY/current/opnsense/BUILD-INFO.txt" ]]; then
LAST_OPNSENSE_PACKAGE_VERSION=$(sed -n 's/^package_version=//p' "$REPOSITORY/current/opnsense/BUILD-INFO.txt" | head -n1)
fi
if [[ "$LAST_OPNSENSE_PACKAGE_VERSION" == "${VERSION}_"* ]]; then
LAST_OPNSENSE_REVISION=${LAST_OPNSENSE_PACKAGE_VERSION#"${VERSION}_"}
if [[ "$LAST_OPNSENSE_REVISION" =~ ^[0-9]+$ ]] && (( LAST_OPNSENSE_REVISION >= OPNSENSE_PACKAGE_REVISION )); then
OPNSENSE_PACKAGE_REVISION=$((LAST_OPNSENSE_REVISION + 1))
fi
elif [[ "$LAST_OPNSENSE_PACKAGE_VERSION" == "$VERSION" && "$OPNSENSE_PACKAGE_REVISION" == "0" ]]; then
OPNSENSE_PACKAGE_REVISION=1
fi
if [[ "$OPNSENSE_PACKAGE_REVISION" == "0" ]]; then
OPNSENSE_PACKAGE_VERSION="$VERSION" OPNSENSE_PACKAGE_VERSION="$VERSION"
else else
OPNSENSE_PACKAGE_VERSION="${VERSION}_${SOURCE_REVISION}" OPNSENSE_PACKAGE_VERSION="${VERSION}_${OPNSENSE_PACKAGE_REVISION}"
fi fi
OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg" OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg"
/opt/rust-package-builder/scripts/package-opnsense.sh \ /opt/rust-package-builder/scripts/package-opnsense.sh \
"$SRC" "$FREEBSD_BINARY" "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_VERSION" "$SRC" "$FREEBSD_BINARY" "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_VERSION"
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense" mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd"
CURRENT_TARGET="" CURRENT_TARGET=""
if [[ -L "$REPOSITORY/current" ]]; then if [[ -L "$REPOSITORY/current" ]]; then
CURRENT_LINK=$(readlink "$REPOSITORY/current") CURRENT_LINK=$(readlink "$REPOSITORY/current")
@@ -42,12 +59,12 @@ fi
log "staging OPNsense repository snapshot" log "staging OPNsense repository snapshot"
if [[ -n "$CURRENT_TARGET" ]]; then if [[ -n "$CURRENT_TARGET" ]]; then
for component in apt rpm opnsense; do for component in apt rpm suse opnsense freebsd; do
if [[ -d "$CURRENT_TARGET/$component" ]]; then if [[ -d "$CURRENT_TARGET/$component" ]]; then
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/ rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
fi fi
done done
for file in repo-key.gpg index.html LINUX-BUILD-INFO.txt; do for file in repo-key.gpg index.html LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
if [[ -f "$CURRENT_TARGET/$file" ]]; then if [[ -f "$CURRENT_TARGET/$file" ]]; then
cp -a "$CURRENT_TARGET/$file" "$RELEASE_DIR/$file" cp -a "$CURRENT_TARGET/$file" "$RELEASE_DIR/$file"
fi fi
@@ -77,12 +94,14 @@ cp -f "$OPNSENSE_PACKAGE_FILE" "$RELEASE_DIR/opnsense/os-rustproxy-latest.pkg"
(cd "$RELEASE_DIR/opnsense" && sha256sum "$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")") > "$RELEASE_DIR/opnsense/SHA256" (cd "$RELEASE_DIR/opnsense" && sha256sum "$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")") > "$RELEASE_DIR/opnsense/SHA256"
cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO
app_version=$VERSION app_version=$VERSION
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
package_version=$OPNSENSE_PACKAGE_VERSION package_version=$OPNSENSE_PACKAGE_VERSION
source_url=$SOURCE_REPO source_url=$SOURCE_REPO
source_ref=$SOURCE_BRANCH source_ref=$SOURCE_BRANCH
source_commit=$SHA source_commit=$SHA
build_reason=$BUILD_REASON build_reason=$BUILD_REASON
source_revision=$SOURCE_REVISION source_revision=$SOURCE_REVISION
packaging_revision=$OPNSENSE_PACKAGING_REVISION
opnsense_series=$OPNSENSE_ABI opnsense_series=$OPNSENSE_ABI
freebsd_release=$FREEBSD_RELEASE freebsd_release=$FREEBSD_RELEASE
freebsd_abi=$FREEBSD_ABI freebsd_abi=$FREEBSD_ABI
+4 -2
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
SOURCE_DIR=${1:?source path required} SOURCE_DIR=${1:?source path required}
OUTPUT_BINARY=${2:?output binary required} OUTPUT_BINARY=${2:?output binary required}
@@ -17,7 +18,7 @@ rsync -a --delete \
"$SOURCE_DIR/" "$BUILD_SOURCE/" "$SOURCE_DIR/" "$BUILD_SOURCE/"
if [[ "${APPLY_FREEBSD_PATCHES:-1}" == "1" ]]; then if [[ "${APPLY_FREEBSD_PATCHES:-1}" == "1" ]]; then
/opt/rust-package-builder/scripts/apply-freebsd-patches.sh "$BUILD_SOURCE" "$BUILDER_ROOT/scripts/apply-freebsd-patches.sh" "$BUILD_SOURCE"
else else
log "FreeBSD/OPNsense source patches disabled" log "FreeBSD/OPNsense source patches disabled"
fi fi
@@ -29,6 +30,7 @@ export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_6
log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}" log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}"
cd "$BUILD_SOURCE" cd "$BUILD_SOURCE"
FREEBSD_RUST_TOOLCHAIN=$("$BUILDER_ROOT/scripts/ensure-rust-toolchain.sh" "$BUILD_SOURCE" freebsd)
if [[ -f Cargo.lock ]]; then if [[ -f Cargo.lock ]]; then
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd
else else
+32 -8
View File
@@ -6,28 +6,35 @@ SHA=${1:?commit SHA required}
BUILD_REASON=${2:-new-commit} BUILD_REASON=${2:-new-commit}
BUILD_LINUX=${3:-1} BUILD_LINUX=${3:-1}
BUILD_OPNSENSE=${4:-1} BUILD_OPNSENSE=${4:-1}
BUILD_FREEBSD=${5:-0}
[[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX must be 0 or 1" [[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX must be 0 or 1"
[[ "$BUILD_OPNSENSE" =~ ^[01]$ ]] || fatal "BUILD_OPNSENSE must be 0 or 1" [[ "$BUILD_OPNSENSE" =~ ^[01]$ ]] || fatal "BUILD_OPNSENSE must be 0 or 1"
(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 )) || { log "nothing to build for $SHA"; exit 0; } [[ "$BUILD_FREEBSD" =~ ^[01]$ ]] || fatal "BUILD_FREEBSD must be 0 or 1"
(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 || BUILD_FREEBSD == 1 )) || { log "nothing to build for $SHA"; exit 0; }
SHORT_SHA=${SHA:0:12} SHORT_SHA=${SHA:0:12}
BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}" BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}"
WORK="$DATA_DIR/work/$BUILD_ID" BUILD_TMP_DIR=${BUILD_TMP_DIR:-/tmp/rust-package-builder}
export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-$BUILD_TMP_DIR/cargo-target}
WORK="$BUILD_TMP_DIR/work/$BUILD_ID"
SRC="$WORK/source" SRC="$WORK/source"
PKG="$WORK/package" PKG="$WORK/package"
OUT="$WORK/out" OUT="$WORK/out"
cleanup() { cleanup() {
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
rm -rf "$WORK" rm -rf -- "$WORK"
else else
log "workspace kept: $WORK" log "workspace kept: $WORK"
fi fi
if is_true "${CLEAN_BUILD_CACHE:-1}"; then
rm -rf -- "$CARGO_TARGET_DIR" "${GOCACHE:-$BUILD_TMP_DIR/go-cache}"
fi
} }
trap cleanup EXIT trap cleanup EXIT
mkdir -p "$SRC" "$PKG" "$OUT" mkdir -p "$SRC" "$PKG" "$OUT" "$CARGO_TARGET_DIR"
log "cloning source" log "cloning source"
git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_REPO" "$SRC" git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_REPO" "$SRC"
@@ -37,7 +44,7 @@ ACTUAL_SHA=$(git -C "$SRC" rev-parse HEAD)
SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA") SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA")
cd "$SRC" cd "$SRC"
META_JSON=$(cargo metadata --format-version 1 --no-deps) META_JSON=$(cargo +stable metadata --format-version 1 --no-deps)
export META_JSON SRC export META_JSON SRC
readarray -t META < <(python3 <<'PY' readarray -t META < <(python3 <<'PY'
@@ -102,10 +109,11 @@ HOMEPAGE=${PACKAGE_HOMEPAGE:-${CARGO_HOMEPAGE:-${CARGO_REPOSITORY:-${CONTROL_HOM
export SHA SHORT_SHA BUILD_ID BUILD_REASON SOURCE_REVISION WORK SRC PKG OUT export SHA SHORT_SHA BUILD_ID BUILD_REASON SOURCE_REVISION WORK SRC PKG OUT
export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER
log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE version=$VERSION" log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD version=$VERSION"
LINUX_OK=1 LINUX_OK=1
OPNSENSE_OK=1 OPNSENSE_OK=1
FREEBSD_OK=1
if [[ "$BUILD_LINUX" == "1" ]]; then if [[ "$BUILD_LINUX" == "1" ]]; then
if /opt/rust-package-builder/scripts/build-linux-release.sh; then if /opt/rust-package-builder/scripts/build-linux-release.sh; then
@@ -125,15 +133,31 @@ if [[ "$BUILD_OPNSENSE" == "1" ]]; then
fi fi
fi fi
if [[ "$BUILD_FREEBSD" == "1" ]]; then
if /opt/rust-package-builder/scripts/build-freebsd-release.sh; then
log "FreeBSD native branch completed for $SHA"
else
FREEBSD_OK=0
log "ERROR: FreeBSD native branch failed for $SHA"
fi
fi
LINUX_STATE="" LINUX_STATE=""
OPNSENSE_STATE="" OPNSENSE_STATE=""
FREEBSD_STATE=""
[[ -r "$DATA_DIR/state/last_linux_commit" ]] && LINUX_STATE=$(cat "$DATA_DIR/state/last_linux_commit") [[ -r "$DATA_DIR/state/last_linux_commit" ]] && LINUX_STATE=$(cat "$DATA_DIR/state/last_linux_commit")
[[ -r "$DATA_DIR/state/last_opnsense_commit" ]] && OPNSENSE_STATE=$(cat "$DATA_DIR/state/last_opnsense_commit") [[ -r "$DATA_DIR/state/last_opnsense_commit" ]] && OPNSENSE_STATE=$(cat "$DATA_DIR/state/last_opnsense_commit")
if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" ]]; then [[ -r "$DATA_DIR/state/last_freebsd_commit" ]] && FREEBSD_STATE=$(cat "$DATA_DIR/state/last_freebsd_commit")
if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" && ( "${FREEBSD_NATIVE_ENABLED:-1}" != "1" || "$FREEBSD_STATE" == "$SHA" ) ]]; then
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit" printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit"
log "all branches complete for $SHA" log "all branches complete for $SHA"
fi fi
if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" ]]; then # Every branch publication creates an atomic repository snapshot. Keep only
# the newest configured set while always protecting the snapshot currently
# served through repository/current.
prune_release_dirs "$DATA_DIR/repository/releases" "${RELEASE_RETENTION:-10}" "$(readlink -f "$DATA_DIR/repository/current" 2>/dev/null || true)"
if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" || "$FREEBSD_OK" != "1" ]]; then
exit 1 exit 1
fi fi
+42 -13
View File
@@ -10,8 +10,19 @@ linux_release_complete_for_commit() {
[[ -d "$CURRENT" ]] || return 1 [[ -d "$CURRENT" ]] || return 1
[[ -f "$CURRENT/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] || return 1 [[ -f "$CURRENT/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] || return 1
[[ -f "$CURRENT/rpm/x86_64/repodata/repomd.xml" ]] || return 1 [[ -f "$CURRENT/rpm/x86_64/repodata/repomd.xml" ]] || return 1
[[ -f "$CURRENT/suse/x86_64/repodata/repomd.xml" ]] || return 1
[[ -f "$CURRENT/SUSE-BUILD-INFO.txt" ]] || return 1
# Both standalone packages must exist in the current Linux snapshot.
find "$CURRENT/rpm/x86_64" -maxdepth 1 -type f -name 'rustproxy-docker-agent-*.rpm' -print -quit | grep -q . || return 1
find "$CURRENT/suse/x86_64" -maxdepth 1 -type f -name 'rustproxy-docker-agent-*.rpm' -print -quit | grep -q . || return 1
find "$CURRENT/apt/pool" -type f -name 'rustproxy-docker-agent_*.deb' -print -quit | grep -q . || return 1
# A new builder revision must repackage even when RustProxy's SHA is unchanged.
[[ -f "$CURRENT/LINUX-BUILD-INFO.txt" ]] || return 1
grep -Fqx "packaging_revision=$PACKAGING_REVISION" "$CURRENT/LINUX-BUILD-INFO.txt" || return 1
grep -Fqx "packaging_revision=$PACKAGING_REVISION" "$CURRENT/SUSE-BUILD-INFO.txt" || return 1
grep -Fq ',rustproxy-docker-agent' "$CURRENT/LINUX-BUILD-INFO.txt" || return 1
if [[ -f "$CURRENT/LINUX-BUILD-INFO.txt" ]] && grep -Fqx "source_commit=$sha" "$CURRENT/LINUX-BUILD-INFO.txt"; then if grep -Fqx "source_commit=$sha" "$CURRENT/LINUX-BUILD-INFO.txt"; then
return 0 return 0
fi fi
@@ -41,17 +52,34 @@ opnsense_release_complete_for_commit() {
[[ -f "$opnsense/os-rustproxy-latest.pkg" ]] || return 1 [[ -f "$opnsense/os-rustproxy-latest.pkg" ]] || return 1
grep -Fqx "source_commit=$sha" "$opnsense/BUILD-INFO.txt" || return 1 grep -Fqx "source_commit=$sha" "$opnsense/BUILD-INFO.txt" || return 1
grep -Fqx "opnsense_series=$OPNSENSE_ABI" "$opnsense/BUILD-INFO.txt" || return 1 grep -Fqx "opnsense_series=$OPNSENSE_ABI" "$opnsense/BUILD-INFO.txt" || return 1
grep -Fqx "packaging_revision=$OPNSENSE_PACKAGING_REVISION" "$opnsense/BUILD-INFO.txt" || return 1
[[ -d "$package_dir" ]] || return 1 [[ -d "$package_dir" ]] || return 1
find "$package_dir" -maxdepth 1 -type f -name 'os-rustproxy-*.pkg' -print -quit | grep -q . || return 1 find "$package_dir" -maxdepth 1 -type f -name 'os-rustproxy-*.pkg' -print -quit | grep -q . || return 1
return 0 return 0
} }
freebsd_release_complete_for_commit() {
local sha=$1
local repo="$CURRENT/freebsd"
[[ -d "$CURRENT" ]] || return 1
for file in meta.conf packagesite.pkg data.pkg RustProxy-FreeBSD.conf BUILD-INFO.txt rustproxy-latest.pkg; do
[[ -s "$repo/$file" ]] || return 1
done
grep -Fqx "source_commit=$sha" "$repo/BUILD-INFO.txt" || return 1
grep -Fqx "packaging_revision=$FREEBSD_NATIVE_PACKAGING_REVISION" "$repo/BUILD-INFO.txt" || return 1
grep -Fqx "freebsd_abi=$FREEBSD_ABI" "$repo/BUILD-INFO.txt" || return 1
[[ -d "$repo/$FREEBSD_ABI" ]] || return 1
find "$repo/$FREEBSD_ABI" -maxdepth 1 -type f -name 'rustproxy-*.pkg' -print -quit | grep -q . || return 1
return 0
}
log "checking repository" log "checking repository"
REMOTE_SHA=$(git ls-remote "$SOURCE_REPO" "refs/heads/$SOURCE_BRANCH" | awk 'NR == 1 {print $1}') REMOTE_SHA=$(git ls-remote "$SOURCE_REPO" "refs/heads/$SOURCE_BRANCH" | awk 'NR == 1 {print $1}')
[[ "$REMOTE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || fatal "cannot resolve branch $SOURCE_BRANCH from $SOURCE_REPO" [[ "$REMOTE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || fatal "cannot resolve branch $SOURCE_BRANCH from $SOURCE_REPO"
BUILD_LINUX=1 BUILD_LINUX=1
BUILD_OPNSENSE=1 BUILD_OPNSENSE=1
BUILD_FREEBSD=0
if linux_release_complete_for_commit "$REMOTE_SHA"; then if linux_release_complete_for_commit "$REMOTE_SHA"; then
BUILD_LINUX=0 BUILD_LINUX=0
# Persist migrated per-branch state when an older installation already has # Persist migrated per-branch state when an older installation already has
@@ -63,19 +91,20 @@ if opnsense_release_complete_for_commit "$REMOTE_SHA"; then
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_opnsense_commit" printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_opnsense_commit"
fi fi
if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" ]]; then if [[ "${FREEBSD_NATIVE_ENABLED:-1}" == "1" ]]; then
BUILD_FREEBSD=1
if freebsd_release_complete_for_commit "$REMOTE_SHA"; then
BUILD_FREEBSD=0
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_freebsd_commit"
fi
fi
if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" && "$BUILD_FREEBSD" == "0" ]]; then
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_commit" printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_commit"
log "no changes: Linux and OPNsense are complete for $REMOTE_SHA" log "no changes: all enabled repositories are complete for $REMOTE_SHA"
exit 0 exit 0
fi fi
if [[ "$BUILD_LINUX" == "1" && "$BUILD_OPNSENSE" == "1" ]]; then REASON="missing-or-outdated-channel"
REASON="new-or-missing-both" log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD reason=$REASON"
elif [[ "$BUILD_LINUX" == "1" ]]; then /opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE" "$BUILD_FREEBSD"
REASON="repair-linux"
else
REASON="repair-opnsense"
fi
log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE reason=$REASON"
/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE"
+133
View File
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Select and verify a stable Rust toolchain for this source checkout.
# Prints the resolved channel on stdout. Progress/errors go to stderr.
set -Eeuo pipefail
SOURCE_DIR=${1:?usage: ensure-rust-toolchain.sh SOURCE_DIR linux|freebsd}
PLATFORM=${2:?usage: ensure-rust-toolchain.sh SOURCE_DIR linux|freebsd}
[[ "$PLATFORM" == linux || "$PLATFORM" == freebsd ]] || {
printf 'Unsupported build target: %s\n' "$PLATFORM" >&2
exit 2
}
[[ -r "$SOURCE_DIR/Cargo.toml" ]] || {
printf 'Missing Cargo.toml: %s\n' "$SOURCE_DIR" >&2
exit 1
}
# Rocky Linux 9 builder image supplies Python 3.11 for TOML parsing.
TOML_PYTHON=${TOML_PYTHON:-python3.11}
metadata=$("$TOML_PYTHON" - "$SOURCE_DIR" <<'PY'
import pathlib
import re
import sys
import tomllib
root = pathlib.Path(sys.argv[1])
manifest = tomllib.loads((root / "Cargo.toml").read_text(encoding="utf-8"))
rust_version = manifest.get("package", {}).get("rust-version", "")
if isinstance(rust_version, dict) and rust_version.get("workspace") is True:
rust_version = manifest.get("workspace", {}).get("package", {}).get("rust-version", "")
if not isinstance(rust_version, str):
raise SystemExit("Unsupported Cargo.toml rust-version")
if rust_version and not re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", rust_version):
raise SystemExit(f"Unsupported Cargo.toml rust-version: {rust_version}")
pinned = ""
for filename in ("rust-toolchain.toml", "rust-toolchain"):
path = root / filename
if not path.exists():
continue
if filename.endswith(".toml"):
pinned = tomllib.loads(path.read_text(encoding="utf-8")).get("toolchain", {}).get("channel", "")
else:
value = path.read_text(encoding="utf-8").strip()
pinned = tomllib.loads(value).get("toolchain", {}).get("channel", "") if value.startswith("[") else value
break
if pinned and not isinstance(pinned, str):
raise SystemExit("Invalid rust-toolchain channel")
if pinned and not (pinned == "stable" or re.fullmatch(r"[0-9]+\.[0-9]+(?:\.[0-9]+)?", pinned)):
raise SystemExit(f"Only stable Rust toolchains are supported: {pinned}")
print(rust_version)
print(pinned)
PY
)
mapfile -t requested <<< "$metadata"
MIN_RUST=${requested[0]:-}
PINNED_TOOLCHAIN=${requested[1]:-}
version_components() {
local version=$1
[[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]] || return 1
V_MAJOR=${BASH_REMATCH[1]}
V_MINOR=${BASH_REMATCH[2]}
V_PATCH=${BASH_REMATCH[4]:-0}
}
version_less() {
local a=$1 b=$2 a_major a_minor a_patch
version_components "$a" || return 2
a_major=$V_MAJOR a_minor=$V_MINOR a_patch=$V_PATCH
version_components "$b" || return 2
(( 10#$a_major < 10#$V_MAJOR || \
(10#$a_major == 10#$V_MAJOR && 10#$a_minor < 10#$V_MINOR) || \
(10#$a_major == 10#$V_MAJOR && 10#$a_minor == 10#$V_MINOR && 10#$a_patch < 10#$V_PATCH) ))
}
if [[ -n "$PINNED_TOOLCHAIN" ]]; then
channel=$PINNED_TOOLCHAIN
elif [[ "$PLATFORM" == linux ]]; then
channel=stable
else
channel=${FREEBSD_RUST_TOOLCHAIN:-1.88.0}
if [[ "$channel" != stable ]] && [[ -n "$MIN_RUST" ]] && version_less "$channel" "$MIN_RUST"; then
printf 'FreeBSD base toolchain %s is below project MSRV %s; selecting project MSRV\n' "$channel" "$MIN_RUST" >&2
channel=$MIN_RUST
fi
fi
[[ "$channel" == stable || "$channel" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || {
printf 'Unsupported Rust toolchain: %s\n' "$channel" >&2
exit 1
}
if [[ -n "$PINNED_TOOLCHAIN" && -n "$MIN_RUST" && "$channel" != stable ]] && version_less "$channel" "$MIN_RUST"; then
printf 'Explicit rust-toolchain pin %s is below project MSRV %s; update the source pin\n' "$channel" "$MIN_RUST" >&2
exit 1
fi
rustc_version() {
local result
result=$(rustup run "$channel" rustc --version 2>/dev/null) || return 1
[[ "$result" =~ ^rustc[[:space:]]+([0-9]+\.[0-9]+\.[0-9]+) ]] || return 1
printf '%s' "${BASH_REMATCH[1]}"
}
if ! installed=$(rustc_version); then
printf 'Installing Rust toolchain %s for %s\n' "$channel" "$PLATFORM" >&2
rustup toolchain install "$channel" --profile minimal >&2
installed=$(rustc_version) || { printf 'Toolchain %s unavailable after installation\n' "$channel" >&2; exit 1; }
fi
if [[ -n "$MIN_RUST" ]] && version_less "$installed" "$MIN_RUST"; then
if [[ "$channel" == stable ]]; then
printf 'Updating stable Rust (%s) to satisfy project MSRV %s\n' "$installed" "$MIN_RUST" >&2
rustup toolchain install stable --profile minimal >&2
installed=$(rustc_version) || { printf 'Stable toolchain unavailable after update\n' >&2; exit 1; }
fi
if version_less "$installed" "$MIN_RUST"; then
printf 'Rust %s (%s) does not satisfy project MSRV %s\n' "$installed" "$channel" "$MIN_RUST" >&2
exit 1
fi
fi
if [[ "$PLATFORM" == freebsd ]]; then
target=x86_64-unknown-freebsd
if ! rustup target list --toolchain "$channel" --installed | grep -Fxq "$target"; then
printf 'Installing %s target for Rust %s\n' "$target" "$channel" >&2
rustup target add "$target" --toolchain "$channel" >&2
fi
rustup target list --toolchain "$channel" --installed | grep -Fxq "$target" || {
printf 'FreeBSD target is unavailable for Rust %s\n' "$channel" >&2
exit 1
}
fi
printf 'Selected Rust %s (%s) for %s\n' "$installed" "$channel" "$PLATFORM" >&2
printf '%s\n' "$channel"
+43 -5
View File
@@ -16,6 +16,12 @@ FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN:-1.88.0} FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN:-1.88.0}
APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-1} APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-1}
RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1} RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}
FREEBSD_NATIVE_ENABLED=${FREEBSD_NATIVE_ENABLED:-1}
FREEBSD_NATIVE_RETENTION=${FREEBSD_NATIVE_RETENTION:-10}
RELEASE_RETENTION=${RELEASE_RETENTION:-10}
PACKAGE_RETENTION=${PACKAGE_RETENTION:-10}
CLEAN_BUILD_CACHE=${CLEAN_BUILD_CACHE:-1}
BUILD_TMP_DIR=${BUILD_TMP_DIR:-/tmp/rust-package-builder}
[[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer" [[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer"
(( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1" (( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1"
@@ -23,6 +29,13 @@ RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}
[[ "$OPNSENSE_RETENTION" =~ ^[0-9]+$ ]] || fatal "OPNSENSE_RETENTION must be >= 0" [[ "$OPNSENSE_RETENTION" =~ ^[0-9]+$ ]] || fatal "OPNSENSE_RETENTION must be >= 0"
[[ "$APPLY_FREEBSD_PATCHES" =~ ^[01]$ ]] || fatal "APPLY_FREEBSD_PATCHES must be 0 or 1" [[ "$APPLY_FREEBSD_PATCHES" =~ ^[01]$ ]] || fatal "APPLY_FREEBSD_PATCHES must be 0 or 1"
[[ "$RUN_LINUX_TESTS" =~ ^[01]$ ]] || fatal "RUN_LINUX_TESTS must be 0 or 1" [[ "$RUN_LINUX_TESTS" =~ ^[01]$ ]] || fatal "RUN_LINUX_TESTS must be 0 or 1"
[[ "$FREEBSD_NATIVE_ENABLED" =~ ^[01]$ ]] || fatal "FREEBSD_NATIVE_ENABLED must be 0 or 1"
[[ "$FREEBSD_NATIVE_RETENTION" =~ ^[0-9]+$ ]] || fatal "FREEBSD_NATIVE_RETENTION must be a nonnegative integer"
require_positive_integer RELEASE_RETENTION "$RELEASE_RETENTION"
require_positive_integer PACKAGE_RETENTION "$PACKAGE_RETENTION"
if [[ "$FREEBSD_NATIVE_ENABLED" != "1" ]]; then
log 'WARNING: native FreeBSD build is disabled (FREEBSD_NATIVE_ENABLED=0); no FreeBSD packages will be published'
fi
export DATA_DIR=${DATA_DIR:-/var/lib/package-builder} export DATA_DIR=${DATA_DIR:-/var/lib/package-builder}
export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL
@@ -31,15 +44,18 @@ export REPO_COMPONENT=${REPO_COMPONENT:-main}
export PACKAGE_RELEASE export PACKAGE_RELEASE
export OPNSENSE_ABI OPNSENSE_RETENTION export OPNSENSE_ABI OPNSENSE_RETENTION
export FREEBSD_RELEASE FREEBSD_ABI FREEBSD_ARCH FREEBSD_RUST_TOOLCHAIN export FREEBSD_RELEASE FREEBSD_ABI FREEBSD_ARCH FREEBSD_RUST_TOOLCHAIN
export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS FREEBSD_NATIVE_ENABLED FREEBSD_NATIVE_RETENTION
export RELEASE_RETENTION PACKAGE_RETENTION CLEAN_BUILD_CACHE BUILD_TMP_DIR
export MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.34} export MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.34}
export GPG_PRIVATE_KEY_FILE=${GPG_PRIVATE_KEY_FILE:-/run/secrets/gpg/private-key.asc} export GPG_PRIVATE_KEY_FILE=${GPG_PRIVATE_KEY_FILE:-/run/secrets/gpg/private-key.asc}
export GPG_PASSPHRASE_FILE=${GPG_PASSPHRASE_FILE:-/run/secrets/gpg/passphrase} export GPG_PASSPHRASE_FILE=${GPG_PASSPHRASE_FILE:-/run/secrets/gpg/passphrase}
export GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg} export GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg}
export CARGO_HOME="$DATA_DIR/cargo" export CARGO_HOME="$DATA_DIR/cargo"
export CARGO_TARGET_DIR="$DATA_DIR/cargo/target" export CARGO_TARGET_DIR="$BUILD_TMP_DIR/cargo-target"
export GOPATH="$DATA_DIR/go"
export GOCACHE="$BUILD_TMP_DIR/go-cache"
for cmd in git cargo rustc rustup nfpm aptly createrepo_c gpg rsync python3 python3.11 \ for cmd in git go cargo rustc rustup nfpm aptly createrepo_c gpg rsync python3 python3.11 \
objdump ar rpm clang llvm-readelf llvm-ar llvm-ranlib file tar zstd bsdtar sha256sum; do objdump ar rpm clang llvm-readelf llvm-ar llvm-ranlib file tar zstd bsdtar sha256sum; do
require_cmd "$cmd" require_cmd "$cmd"
done done
@@ -52,12 +68,34 @@ mkdir -p \
"$DATA_DIR/repository/releases" \ "$DATA_DIR/repository/releases" \
"$DATA_DIR/state" \ "$DATA_DIR/state" \
"$DATA_DIR/cargo" \ "$DATA_DIR/cargo" \
"$DATA_DIR/go/cache" \
"$DATA_DIR/work" \ "$DATA_DIR/work" \
"$DATA_DIR/packages/rpm/x86_64" \ "$DATA_DIR/packages/rpm/x86_64" \
"$DATA_DIR/packages/suse/x86_64" \
"$DATA_DIR/packages/deb" "$DATA_DIR/packages/deb"
mkdir -p "$GNUPGHOME" mkdir -p "$BUILD_TMP_DIR/work" "$CARGO_TARGET_DIR" "$GOCACHE" "$GNUPGHOME"
chmod 0700 "$GNUPGHOME" chmod 0700 "$GNUPGHOME"
# Migration/maintenance cleanup for older builder versions. Large build
# products must not accumulate on the persistent /data volume.
rm -rf -- "$DATA_DIR/cargo/target" "$DATA_DIR/go/cache"
find "$DATA_DIR/work" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
find "$BUILD_TMP_DIR/work" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
if is_true "$CLEAN_BUILD_CACHE"; then
rm -rf -- "$CARGO_TARGET_DIR" "$GOCACHE"
mkdir -p "$CARGO_TARGET_DIR" "$GOCACHE"
fi
# Apply retention immediately on startup, before waiting for another source
# change. Main RustProxy and Docker Agent packages are counted separately.
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy_*.deb' "$PACKAGE_RETENTION"
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy-docker-agent_*.deb' "$PACKAGE_RETENTION"
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-*.x86_64.rpm' "$PACKAGE_RETENTION" 'rustproxy-docker-agent-*.x86_64.rpm'
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$PACKAGE_RETENTION"
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-*.x86_64.rpm' "$PACKAGE_RETENTION" 'rustproxy-docker-agent-*.x86_64.rpm'
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$PACKAGE_RETENTION"
prune_release_dirs "$DATA_DIR/repository/releases" "$RELEASE_RETENTION" "$(readlink -f "$DATA_DIR/repository/current" 2>/dev/null || true)"
[[ -r "$GPG_PRIVATE_KEY_FILE" ]] || fatal "GPG private key not found: $GPG_PRIVATE_KEY_FILE" [[ -r "$GPG_PRIVATE_KEY_FILE" ]] || fatal "GPG private key not found: $GPG_PRIVATE_KEY_FILE"
log "importing GPG signing key" log "importing GPG signing key"
gpg --batch --import "$GPG_PRIVATE_KEY_FILE" >/dev/null 2>&1 || fatal "cannot import GPG key" gpg --batch --import "$GPG_PRIVATE_KEY_FILE" >/dev/null 2>&1 || fatal "cannot import GPG key"
@@ -77,7 +115,7 @@ fi
/opt/rust-package-builder/scripts/init-storage.sh /opt/rust-package-builder/scripts/init-storage.sh
log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE" log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE native_freebsd=$FREEBSD_NATIVE_ENABLED"
while true; do while true; do
if ! /opt/rust-package-builder/scripts/check-and-build.sh; then if ! /opt/rust-package-builder/scripts/check-and-build.sh; then
log "ERROR: one or more build branches failed; any successful branch was published independently" log "ERROR: one or more build branches failed; any successful branch was published independently"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
OUT=${1:?output file required}
URL="${OPNSENSE_REPOSITORY_URL%/}/freebsd"
export OUT URL FREEBSD_ABI
python3 <<'PY'
import html, os
h=html.escape
url=h(os.environ['URL']); abi=h(os.environ['FREEBSD_ABI'])
page=f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>RustProxy - native FreeBSD pkg</title>
<style>body{{font:16px/1.55 system-ui,sans-serif;max-width:860px;margin:40px auto;padding:0 20px}}pre{{overflow:auto;background:#171717;color:#eee;padding:18px;border-radius:8px}}</style></head>
<body><h1>RustProxy for native FreeBSD</h1>
<p>This repository contains the <code>rustproxy</code> package and an rc.d service, without OPNsense plugin files. ABI: <code>{abi}</code>.</p>
<p><strong>Development repository: unsigned pkg catalog.</strong> Use only in an environment where you trust the source and transport. Configure signing before production.</p>
<pre><code>mkdir -p /usr/local/etc/pkg/repos
fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf {url}/RustProxy-FreeBSD.conf
pkg update -f
pkg install rustproxy
sysrc rustproxy_enable=YES
service rustproxy start
service rustproxy reload</code></pre>
<p>To use an external JSON configuration, set <code>rustproxy_config="/usr/local/etc/rustproxy/config.json"</code> in <code>/etc/rc.conf</code>. The file and parent directory must be writable by the service account.</p>
<ul><li><a href="{url}/meta.conf">meta.conf</a></li><li><a href="{url}/packagesite.pkg">packagesite.pkg</a></li><li><a href="{url}/RustProxy-FreeBSD.conf">RustProxy-FreeBSD.conf</a></li><li><a href="{url}/rustproxy-latest.pkg">latest package</a></li></ul>
</body></html>'''
with open(os.environ['OUT'],'w',encoding='utf-8') as f:
f.write(page)
PY
+32 -4
View File
@@ -36,8 +36,8 @@ a{{color:inherit}} .meta{{display:flex;gap:16px;flex-wrap:wrap;opacity:.82;font-
</head> </head>
<body> <body>
<h1>{h(package)} — package repository</h1> <h1>{h(package)} — package repository</h1>
<p class="lead">Signed DEB and RPM packages for x86_64/amd64.</p> <p class="lead">Signed DEB, DNF RPM and SUSE RPM packages for x86_64/amd64.</p>
<div class="meta"><span class="ok">Repository active</span><span>{h(url)}</span><span>APT + RPM/DNF</span></div> <div class="meta"><span class="ok">Repository active</span><span>{h(url)}</span><span>APT + RPM/DNF + RPM/Zypper</span></div>
<div class="card"> <div class="card">
<h2>Debian / Ubuntu</h2> <h2>Debian / Ubuntu</h2>
@@ -68,10 +68,37 @@ REPO
sudo dnf install {h(package)}</code></pre> sudo dnf install {h(package)}</code></pre>
</div> </div>
<div class="card">
<h2>SUSE Linux Enterprise / openSUSE Leap / Tumbleweed</h2>
<p>Signed dedicated SUSE RPM repository (x86_64; glibc 2.34 or newer).</p>
<pre><code>sudo rpm --import {h(url)}/repo-key.gpg
sudo zypper addrepo --refresh --gpgcheck {h(url)}/suse/x86_64/ rustproxy-suse
sudo zypper refresh rustproxy-suse
sudo zypper install {h(package)}
# Optional Docker host agent:
sudo zypper install rustproxy-docker-agent</code></pre>
</div>
<div class="card">
<h2>Docker discovery agent — separate Linux package</h2>
<p>Install <code>rustproxy-docker-agent</code> on the Docker host (systemd, Docker Engine, amd64/x86_64). This is independent from the RustProxy server and has no FreeBSD pkg.</p>
<pre><code># Debian / Ubuntu (after adding the signed APT repo above)
sudo apt install rustproxy-docker-agent
# RHEL / Rocky / Alma / Fedora (after adding the DNF repo above)
sudo dnf install rustproxy-docker-agent
# Locally display the token and certificate fingerprint
sudo rustproxy-docker-agent info --show-token
sudo systemctl status rustproxy-docker-agent</code></pre>
<p>Packages create persistent credentials and a systemd service; tokens are never printed to unattended package logs. Restrict the agent's HTTPS port to the RustProxy host.</p>
</div>
<div class="card"> <div class="card">
<h2>OPNsense / FreeBSD pkg</h2> <h2>OPNsense / FreeBSD pkg</h2>
<p>The OPNsense plugin is published separately because it uses the FreeBSD pkg format.</p> <p>The OPNsense plugin is published separately because it uses the FreeBSD pkg format.</p>
<p><a href="{h(opn_url)}/">{h(opn_url)}/</a></p> <p><a href="{h(opn_url)}/">{h(opn_url)}/</a></p>
<p>Native FreeBSD pkg (if enabled): <a href="{h(opn_url)}/freebsd/">{h(opn_url)}/freebsd/</a>.</p>
</div> </div>
<div class="card"> <div class="card">
@@ -79,9 +106,10 @@ sudo dnf install {h(package)}</code></pre>
<ul> <ul>
<li><a href="{h(url)}/repo-key.gpg">Public GPG key</a></li> <li><a href="{h(url)}/repo-key.gpg">Public GPG key</a></li>
<li><a href="{h(url)}/apt/dists/{h(dist)}/InRelease">APT InRelease</a></li> <li><a href="{h(url)}/apt/dists/{h(dist)}/InRelease">APT InRelease</a></li>
<li><a href="{h(url)}/rpm/x86_64/repodata/repomd.xml">RPM repomd.xml</a></li> <li><a href="{h(url)}/rpm/x86_64/repodata/repomd.xml">DNF RPM repomd.xml</a></li>
<li><a href="{h(url)}/suse/x86_64/repodata/repomd.xml">SUSE RPM repomd.xml</a></li>
</ul> </ul>
<p>Supported targets: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10; architecture: amd64/x86_64.</p> <p>Supported targets: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10, SLES 15 SP6+, SLES 16+, openSUSE Leap 15.6+/Tumbleweed; architecture: amd64/x86_64.</p>
</div> </div>
</body> </body>
</html> </html>
+4
View File
@@ -39,6 +39,10 @@ code{{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}}
<p><strong>OPNsense series:</strong> {h(series)} &nbsp; <strong>ABI:</strong> {h(abi)}</p> <p><strong>OPNsense series:</strong> {h(series)} &nbsp; <strong>ABI:</strong> {h(abi)}</p>
</div> </div>
<div class="card"> <div class="card">
<h2>Native FreeBSD (without OPNsense)</h2>
<p>The separate optional native FreeBSD pkg repository is available at <a href="{h(url)}/freebsd/">{h(url)}/freebsd/</a> when FREEBSD_NATIVE_ENABLED=1.</p>
</div>
<div class="card">
<h2>pkg repository configuration</h2> <h2>pkg repository configuration</h2>
<pre><code>mkdir -p /usr/local/etc/pkg/repos <pre><code>mkdir -p /usr/local/etc/pkg/repos
cat &gt; /usr/local/etc/pkg/repos/RustProxy.conf &lt;&lt;'REPO' cat &gt; /usr/local/etc/pkg/repos/RustProxy.conf &lt;&lt;'REPO'
+5 -4
View File
@@ -11,7 +11,7 @@ from collections import defaultdict
from pathlib import Path from pathlib import Path
CATALOG_FILES = {"data.pkg", "packagesite.pkg"} CATALOG_FILES = {"data.pkg", "packagesite.pkg"}
LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg"} LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg", "rustproxy-latest.pkg"}
def sha256(path: Path) -> str: def sha256(path: Path) -> str:
@@ -91,7 +91,7 @@ def make_tar(dst: Path, member_name: str, payload: bytes):
os.replace(tmp, dst) os.replace(tmp, dst)
def rebuild(repo: Path, retention: int): def rebuild(repo: Path, retention: int, repo_version: int = 1):
packages = collect_packages(repo) packages = collect_packages(repo)
if enforce_retention(packages, retention): if enforce_retention(packages, retention):
packages = collect_packages(repo) packages = collect_packages(repo)
@@ -111,7 +111,7 @@ def rebuild(repo: Path, retention: int):
make_tar(repo / "data.pkg", "data", data) make_tar(repo / "data.pkg", "data", data)
meta = ( meta = (
'version: 1;\n' f'version: {repo_version};\n'
'packing_format: "tar";\n' 'packing_format: "tar";\n'
'manifests: "packagesite.yaml";\n' 'manifests: "packagesite.yaml";\n'
'manifests_archive: "packagesite";\n' 'manifests_archive: "packagesite";\n'
@@ -130,11 +130,12 @@ def main():
parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue") parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue")
parser.add_argument("repo", type=Path, help="repository root") parser.add_argument("repo", type=Path, help="repository root")
parser.add_argument("--retention", type=int, default=10, help="versions kept per package/ABI; 0 disables pruning") parser.add_argument("--retention", type=int, default=10, help="versions kept per package/ABI; 0 disables pruning")
parser.add_argument("--repo-version", type=int, choices=(1, 2), default=1, help="pkg repository format; 2 for native FreeBSD 15")
args = parser.parse_args() args = parser.parse_args()
repo = args.repo.resolve() repo = args.repo.resolve()
repo.mkdir(parents=True, exist_ok=True) repo.mkdir(parents=True, exist_ok=True)
rebuild(repo, args.retention) rebuild(repo, args.retention, args.repo_version)
if __name__ == "__main__": if __name__ == "__main__":
+4 -1
View File
@@ -34,7 +34,7 @@ if ! aptly -config="$APTLY_CONF" repo list -raw | grep -Fx package-builder >/dev
package-builder >/dev/null package-builder >/dev/null
fi fi
for item in apt rpm opnsense repo-key.gpg index.html LINUX-BUILD-INFO.txt; do for item in apt rpm suse opnsense freebsd repo-key.gpg index.html LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then
fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite" fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
fi fi
@@ -42,7 +42,10 @@ done
ln -sfn current/apt "$REPOSITORY/apt" ln -sfn current/apt "$REPOSITORY/apt"
ln -sfn current/rpm "$REPOSITORY/rpm" ln -sfn current/rpm "$REPOSITORY/rpm"
ln -sfn current/suse "$REPOSITORY/suse"
ln -sfn current/opnsense "$REPOSITORY/opnsense" ln -sfn current/opnsense "$REPOSITORY/opnsense"
ln -sfn current/freebsd "$REPOSITORY/freebsd"
ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg" ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
ln -sfn current/index.html "$REPOSITORY/index.html" ln -sfn current/index.html "$REPOSITORY/index.html"
ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt" ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
ln -sfn current/SUSE-BUILD-INFO.txt "$REPOSITORY/SUSE-BUILD-INFO.txt"
+63
View File
@@ -1,6 +1,11 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
# Increment when package payload/installation semantics change without a source commit.
PACKAGING_REVISION=7
OPNSENSE_PACKAGING_REVISION=4
FREEBSD_NATIVE_PACKAGING_REVISION=2
log() { log() {
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*" printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
} }
@@ -38,3 +43,61 @@ atomic_write() {
cat > "$tmp" cat > "$tmp"
mv -f "$tmp" "$path" mv -f "$tmp" "$path"
} }
is_true() {
case "${1:-}" in
1|true|TRUE|yes|YES|on|ON) return 0 ;;
*) return 1 ;;
esac
}
require_positive_integer() {
local name=$1 value=$2
[[ "$value" =~ ^[1-9][0-9]*$ ]] || fatal "$name must be a positive integer"
}
# Keep the newest N artifacts matching a package family. An optional exclude
# glob is useful for repositories that contain both rustproxy-* and
# rustproxy-docker-agent-* files in the same directory.
prune_package_files() {
local directory=$1 pattern=$2 keep=$3 exclude_pattern=${4:-}
require_positive_integer PACKAGE_RETENTION "$keep"
[[ -d "$directory" ]] || return 0
local -a find_args=("$directory" -maxdepth 1 -type f -name "$pattern")
if [[ -n "$exclude_pattern" ]]; then
find_args+=( ! -name "$exclude_pattern" )
fi
mapfile -t stale < <(
find "${find_args[@]}" -printf '%T@ %p\n' \
| sort -nr \
| awk -v keep="$keep" 'NR>keep {$1=""; sub(/^ /,""); print}'
)
local path
for path in "${stale[@]}"; do
[[ -n "$path" ]] || continue
log "package retention: removing $path"
rm -f -- "$path"
done
}
prune_release_dirs() {
local releases_dir=$1 keep=$2 protected=${3:-}
require_positive_integer RELEASE_RETENTION "$keep"
[[ -d "$releases_dir" ]] || return 0
mapfile -t stale < <(
find "$releases_dir" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' \
| sort -nr \
| awk -v keep="$keep" 'NR>keep {$1=""; sub(/^ /,""); print}'
)
local path
for path in "${stale[@]}"; do
[[ -n "$path" && "$path" != "$protected" ]] || continue
log "release retention: removing $path"
rm -rf -- "$path"
done
}
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Normalize the packaged RustProxy systemd unit for Linux DEB/RPM."""
from pathlib import Path
import re
import sys
path = Path(sys.argv[1])
name = sys.argv[2]
text = path.read_text()
if not re.search(r'^\[Service\][ \t]*$', text, re.M):
raise SystemExit('unit has no [Service] section')
# This normalization also covers older/source-provided debian/*.service units:
# ProtectHome=true masks /home completely, so neither chgrp nor setfacl can
# make web roots/certificate files there readable. Keep the directories
# read-only, in addition to the existing strict filesystem sandbox.
if re.search(r'^[ \t]*ProtectHome[ \t]*=', text, re.M):
text = re.sub(r'^[ \t]*ProtectHome[ \t]*=.*$', 'ProtectHome=read-only',
text, flags=re.M)
else:
text = re.sub(r'^(\[Service\][ \t]*\n)',
lambda m: m.group(1) + 'ProtectHome=read-only\n',
text, count=1, flags=re.M)
if not re.search(r'^ExecReload=', text, re.M):
if not re.search(r'^ExecStart=', text, re.M):
raise SystemExit('unit has no ExecStart; cannot attach reload')
text = re.sub(
r'^(ExecStart=[^\n]*\n)',
lambda m: m.group(1) +
f'ExecReload=/usr/bin/{name} --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload\n',
text,
count=1,
flags=re.M,
)
if re.search(r'^ProtectSystem=strict\s*$', text, re.M) and not re.search(
r'^ReadWritePaths=.*(?:^|[ \t])-?/etc/rustproxy(?:[ \t]|$)', text, re.M
):
text = re.sub(
r'^(ProtectSystem=strict\s*\n)',
lambda m: m.group(1) + 'ReadWritePaths=-/etc/rustproxy\n',
text, count=1, flags=re.M,
)
path.write_text(text)
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
SOURCE_DIR=${1:?source path required}
BINARY=${2:?FreeBSD binary required}
PACKAGE_FILE=${3:?output package file required}
PACKAGE_VERSION=${4:?FreeBSD package version required}
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
PACKAGING="$BUILDER_ROOT/packaging/freebsd"
for tool in python3 sha256sum tar zstd file; do
require_cmd "$tool"
done
[[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml: $SOURCE_DIR"
[[ -f "$BINARY" ]] || fatal "missing FreeBSD binary: $BINARY"
[[ "$(file -b "$BINARY")" == *FreeBSD* ]] || fatal "refusing non-FreeBSD native package: $BINARY"
WORK=$(mktemp -d "${PACKAGE_FILE}.work.XXXXXX")
trap 'rm -rf "$WORK"' EXIT
STAGE="$WORK/stage"
META="$WORK/meta"
ARCHIVE="$WORK/rustproxy.tar"
mkdir -p "$META" "$(dirname "$PACKAGE_FILE")"
install -D -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
install -D -m 0555 "$PACKAGING/rustproxy.rc" "$STAGE/usr/local/etc/rc.d/rustproxy"
install -D -m 0644 "$PACKAGING/rustproxy.newsyslog" "$STAGE/usr/local/etc/newsyslog.conf.d/rustproxy.conf"
python3 - "$STAGE" "$META" "$PACKAGE_VERSION" "$FREEBSD_ABI" "$FREEBSD_ARCH" \
"$PACKAGING/+POST_INSTALL" "$PACKAGING/+PRE_DEINSTALL" <<'PY'
import hashlib
import json
from pathlib import Path
import sys
stage, meta = map(Path, sys.argv[1:3])
version, abi, arch = sys.argv[3:6]
post_install = Path(sys.argv[6]).read_text(encoding='utf-8')
pre_deinstall = Path(sys.argv[7]).read_text(encoding='utf-8')
files = {}
flatsize = 0
for path in sorted(stage.rglob('*')):
if path.is_symlink():
files['/' + path.relative_to(stage).as_posix()] = '-'
elif path.is_file():
files['/' + path.relative_to(stage).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest()
flatsize += path.stat().st_size
common = {
'name': 'rustproxy',
'version': version,
'origin': 'www/rustproxy',
'comment': 'RustProxy reverse proxy for native FreeBSD',
'desc': 'RustProxy daemon and rc.d service for FreeBSD (no OPNsense plugin or dependency).',
'maintainer': 'local@localhost',
'www': 'https://git.linuxiarz.pl/gru/RustProxy',
'prefix': '/usr/local',
'abi': abi,
'arch': arch,
'flatsize': flatsize,
'licenselogic': 'single',
'licenses': ['UNKNOWN'],
'categories': ['www'],
'deps': {},
'options': {},
}
manifest = dict(common)
manifest['files'] = files
manifest['scripts'] = {'post-install': post_install, 'pre-deinstall': pre_deinstall}
(meta / '+COMPACT_MANIFEST').write_text(json.dumps(common, separators=(',', ':')) + '\n')
(meta / '+MANIFEST').write_text(json.dumps(manifest, separators=(',', ':')) + '\n')
PY
(
cd "$STAGE"
find . \( -type f -o -type l \) -print | LC_ALL=C sort | sed 's#^\./##' > "$WORK/payload.list"
)
[[ -s "$WORK/payload.list" ]] || fatal 'FreeBSD package payload is empty'
tar --format=ustar --owner=0 --group=0 --numeric-owner \
-cf "$ARCHIVE" -C "$META" +COMPACT_MANIFEST +MANIFEST
tar -P --format=ustar --owner=0 --group=0 --numeric-owner --no-recursion \
--transform='s,^,/,S' \
-rf "$ARCHIVE" -C "$STAGE" -T "$WORK/payload.list"
python3 - "$ARCHIVE" "$META/+MANIFEST" <<'PY'
from pathlib import Path
import json
import sys
import tarfile
pkg = Path(sys.argv[1])
manifest = json.loads(Path(sys.argv[2]).read_text())
with tarfile.open(pkg) as tf:
members = tf.getmembers()
assert [m.name for m in members[:2]] == ['+COMPACT_MANIFEST', '+MANIFEST']
assert all(not m.isdir() for m in members[2:]), 'package contains directory records'
actual = {m.name for m in members[2:] if m.isfile() or m.issym() or m.islnk()}
assert actual == set(manifest['files']), 'payload differs from +MANIFEST'
assert all(name.startswith('/') for name in actual), 'non-absolute native FreeBSD payload path'
PY
zstd -q -f -19 -T0 "$ARCHIVE" -o "$PACKAGE_FILE"
zstd -dc "$PACKAGE_FILE" | tar -tf - >/dev/null
log "created native FreeBSD package: $PACKAGE_FILE"
+4 -3
View File
@@ -1,13 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
SOURCE_DIR=${1:?source path required} SOURCE_DIR=${1:?source path required}
BINARY=${2:?FreeBSD binary required} BINARY=${2:?FreeBSD binary required}
PACKAGE_FILE=${3:?output package file required} PACKAGE_FILE=${3:?output package file required}
PACKAGE_VERSION=${4:?OPNsense package version required} PACKAGE_VERSION=${4:?OPNsense package version required}
PLUGIN_ROOT=/opt/rust-package-builder/opnsense/plugin PLUGIN_ROOT="$BUILDER_ROOT/opnsense/plugin"
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64} FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64} FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
@@ -35,7 +36,7 @@ mkdir -p "$STAGE/usr/local/sbin"
install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy" install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy" chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy"
chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/10-rustproxy.start" chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/start/50-rustproxy.start"
chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh" chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh"
POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre" POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre"
+220
View File
@@ -0,0 +1,220 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
SRC=${1:?source path required}
PKG_AGENT=${2:?agent package workspace required}
AGENT_BINARY=${3:?agent binary path required}
: "${VERSION:?RustProxy source version is required}"
: "${PACKAGE_RELEASE:?package release is required}"
: "${GPG_PRIVATE_KEY_FILE:?GPG key path is required}"
[[ -x "$AGENT_BINARY" ]] || fatal "Go agent binary not found: $AGENT_BINARY"
[[ -r "$SRC/docker-agent/rustproxy-docker-agent.service" ]] || fatal 'missing Docker Agent systemd unit'
[[ -r "$SRC/docker-agent/docker-agent.env.example" ]] || fatal 'missing Docker Agent environment template'
AGENT_NAME=rustproxy-docker-agent
rm -rf -- "$PKG_AGENT"
mkdir -p "$PKG_AGENT/payload" "$PKG_AGENT/scripts"
install -D -m 0755 "$AGENT_BINARY" "$PKG_AGENT/payload/usr/bin/$AGENT_NAME"
sed 's@/usr/local/bin/rustproxy-docker-agent@/usr/bin/rustproxy-docker-agent@g' \
"$SRC/docker-agent/rustproxy-docker-agent.service" > "$PKG_AGENT/payload/agent.service"
install -D -m 0644 "$PKG_AGENT/payload/agent.service" "$PKG_AGENT/payload/usr/lib/systemd/system/$AGENT_NAME.service"
install -D -m 0600 "$SRC/docker-agent/docker-agent.env.example" "$PKG_AGENT/payload/etc/rustproxy/docker-agent.env"
install -D -m 0644 "$SRC/docker-agent/README.md" "$PKG_AGENT/payload/usr/share/doc/$AGENT_NAME/README.md"
cat > "$PKG_AGENT/scripts/deb-postinstall.sh" <<'SCRIPT'
#!/bin/sh
set -e
if ! getent group rustproxy-docker-agent >/dev/null 2>&1; then
addgroup --system rustproxy-docker-agent >/dev/null
fi
if ! getent passwd rustproxy-docker-agent >/dev/null 2>&1; then
adduser --system --ingroup rustproxy-docker-agent --home /var/lib/rustproxy-docker-agent \
--no-create-home --disabled-password --shell /usr/sbin/nologin rustproxy-docker-agent >/dev/null
fi
if getent group docker >/dev/null 2>&1; then
usermod -a -G docker rustproxy-docker-agent
fi
install -d -o rustproxy-docker-agent -g rustproxy-docker-agent -m 0750 /var/lib/rustproxy-docker-agent
install -d -m 0750 /etc/rustproxy
/usr/bin/rustproxy-docker-agent init
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl enable rustproxy-docker-agent.service >/dev/null 2>&1 || true
if getent group docker >/dev/null 2>&1 && [ -S /var/run/docker.sock ]; then
systemctl restart rustproxy-docker-agent.service >/dev/null 2>&1 || true
else
echo 'RustProxy Docker Agent installed but not started: Docker group/socket unavailable.' >&2
fi
fi
exit 0
SCRIPT
cat > "$PKG_AGENT/scripts/rpm-postinstall.sh" <<'SCRIPT'
#!/bin/sh
set -e
if ! getent group rustproxy-docker-agent >/dev/null 2>&1; then
groupadd --system rustproxy-docker-agent
fi
if ! getent passwd rustproxy-docker-agent >/dev/null 2>&1; then
useradd --system --gid rustproxy-docker-agent --home-dir /var/lib/rustproxy-docker-agent \
--no-create-home --shell /sbin/nologin rustproxy-docker-agent
fi
if getent group docker >/dev/null 2>&1; then
usermod -a -G docker rustproxy-docker-agent
fi
install -d -o rustproxy-docker-agent -g rustproxy-docker-agent -m 0750 /var/lib/rustproxy-docker-agent
install -d -m 0750 /etc/rustproxy
/usr/bin/rustproxy-docker-agent init
if command -v systemctl >/dev/null 2>&1; then
systemctl daemon-reload >/dev/null 2>&1 || true
systemctl enable rustproxy-docker-agent.service >/dev/null 2>&1 || true
if getent group docker >/dev/null 2>&1 && [ -S /var/run/docker.sock ]; then
systemctl restart rustproxy-docker-agent.service >/dev/null 2>&1 || true
else
echo 'RustProxy Docker Agent installed but not started: Docker group/socket unavailable.' >&2
fi
fi
exit 0
SCRIPT
cat > "$PKG_AGENT/scripts/deb-preremove.sh" <<'SCRIPT'
#!/bin/sh
set -e
case "${1:-}" in
remove|deconfigure) systemctl stop rustproxy-docker-agent.service >/dev/null 2>&1 || true ;;
esac
exit 0
SCRIPT
cat > "$PKG_AGENT/scripts/rpm-preremove.sh" <<'SCRIPT'
#!/bin/sh
set -e
if [ "${1:-1}" -eq 0 ]; then
systemctl stop rustproxy-docker-agent.service >/dev/null 2>&1 || true
systemctl disable rustproxy-docker-agent.service >/dev/null 2>&1 || true
fi
exit 0
SCRIPT
for kind in deb rpm; do
cat > "$PKG_AGENT/scripts/$kind-postremove.sh" <<'SCRIPT'
#!/bin/sh
set -e
systemctl daemon-reload >/dev/null 2>&1 || true
exit 0
SCRIPT
done
chmod 0755 "$PKG_AGENT/scripts/"*.sh
AGENT_NAME_Q=$(yaml_quote "$AGENT_NAME")
VERSION_Q=$(yaml_quote "$VERSION")
MAINTAINER_Q=$(yaml_quote "$MAINTAINER")
HOMEPAGE_Q=$(yaml_quote "$HOMEPAGE")
GPG_Q=$(yaml_quote "$GPG_PRIVATE_KEY_FILE")
cat > "$PKG_AGENT/contents.yaml" <<YAML
- src: $(yaml_quote "$PKG_AGENT/payload/usr/bin/$AGENT_NAME")
dst: /usr/bin/$AGENT_NAME
file_info:
mode: 0755
- src: $(yaml_quote "$PKG_AGENT/payload/usr/lib/systemd/system/$AGENT_NAME.service")
dst: /usr/lib/systemd/system/$AGENT_NAME.service
file_info:
mode: 0644
- src: $(yaml_quote "$PKG_AGENT/payload/etc/rustproxy/docker-agent.env")
dst: /etc/rustproxy/docker-agent.env
type: "config|noreplace"
file_info:
mode: 0600
- src: $(yaml_quote "$PKG_AGENT/payload/usr/share/doc/$AGENT_NAME/README.md")
dst: /usr/share/doc/$AGENT_NAME/README.md
file_info:
mode: 0644
YAML
cat > "$PKG_AGENT/nfpm-deb.yaml" <<YAML
name: $AGENT_NAME_Q
arch: amd64
platform: linux
version: $VERSION_Q
release: "$PACKAGE_RELEASE"
section: net
priority: optional
maintainer: $MAINTAINER_Q
description: "RustProxy Docker discovery agent (HTTPS and bearer token)"
license: "Proprietary"
homepage: $HOMEPAGE_Q
contents:
$(cat "$PKG_AGENT/contents.yaml")
depends:
- adduser
- ca-certificates
- systemd
- passwd
scripts:
postinstall: "$PKG_AGENT/scripts/deb-postinstall.sh"
preremove: "$PKG_AGENT/scripts/deb-preremove.sh"
postremove: "$PKG_AGENT/scripts/deb-postremove.sh"
deb:
compression: xz
signature:
key_file: $GPG_Q
type: origin
YAML
cat > "$PKG_AGENT/nfpm-rpm.yaml" <<YAML
name: $AGENT_NAME_Q
arch: amd64
platform: linux
version: $VERSION_Q
release: "$PACKAGE_RELEASE"
section: net
priority: optional
maintainer: $MAINTAINER_Q
description: "RustProxy Docker discovery agent (HTTPS and bearer token)"
license: "Proprietary"
homepage: $HOMEPAGE_Q
contents:
$(cat "$PKG_AGENT/contents.yaml")
depends:
- ca-certificates
- shadow-utils
- systemd
scripts:
postinstall: "$PKG_AGENT/scripts/rpm-postinstall.sh"
preremove: "$PKG_AGENT/scripts/rpm-preremove.sh"
postremove: "$PKG_AGENT/scripts/rpm-postremove.sh"
rpm:
compression: zstd
signature:
key_file: $GPG_Q
YAML
cat > "$PKG_AGENT/nfpm-suse.yaml" <<YAML
name: $AGENT_NAME_Q
arch: amd64
platform: linux
version: $VERSION_Q
release: "$PACKAGE_RELEASE"
section: net
priority: optional
maintainer: $MAINTAINER_Q
description: "RustProxy Docker discovery agent (HTTPS and bearer token)"
license: "Proprietary"
homepage: $HOMEPAGE_Q
contents:
$(cat "$PKG_AGENT/contents.yaml")
depends:
- ca-certificates
- shadow
- systemd
scripts:
postinstall: "$PKG_AGENT/scripts/rpm-postinstall.sh"
preremove: "$PKG_AGENT/scripts/rpm-preremove.sh"
postremove: "$PKG_AGENT/scripts/rpm-postremove.sh"
rpm:
compression: gzip
signature:
key_file: $GPG_Q
YAML
log "prepared signed Linux DEB/RPM/SUSE metadata for $AGENT_NAME $VERSION-$PACKAGE_RELEASE"
+108 -8
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -Eeuo pipefail set -Eeuo pipefail
source /opt/rust-package-builder/scripts/lib.sh BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
source "$BUILDER_ROOT/scripts/lib.sh"
SRC=${1:?source path required} SRC=${1:?source path required}
PKG=${2:?package workspace required} PKG=${2:?package workspace required}
@@ -12,7 +13,8 @@ install -D -m 0755 "$BINARY_PATH" "$PKG/payload/usr/bin/$BINARY_NAME"
SERVICE_SRC="$SRC/debian/${PACKAGE_NAME}.service" SERVICE_SRC="$SRC/debian/${PACKAGE_NAME}.service"
ENV_SRC="$SRC/debian/${PACKAGE_NAME}.environment" ENV_SRC="$SRC/debian/${PACKAGE_NAME}.environment"
LOGROTATE_SRC="$SRC/debian/${PACKAGE_NAME}.logrotate" LOGROTATE_SRC="$BUILDER_ROOT/packaging/rustproxy.logrotate"
ACCESS_GUIDE_SRC="$BUILDER_ROOT/packaging/rustproxy-filesystem-access.md"
TMPFILES_SRC="$SRC/debian/${PACKAGE_NAME}.tmpfiles" TMPFILES_SRC="$SRC/debian/${PACKAGE_NAME}.tmpfiles"
POSTINST_SRC="$SRC/debian/${PACKAGE_NAME}.postinst" POSTINST_SRC="$SRC/debian/${PACKAGE_NAME}.postinst"
@@ -28,21 +30,60 @@ After=network-online.target
[Service] [Service]
Type=simple Type=simple
ExecStart=/usr/bin/$BINARY_NAME User=rustproxy
Group=rustproxy
WorkingDirectory=/var/lib/rustproxy
UMask=0077
Environment=RUST_LOG=rust_proxy=info,warn
Environment=MALLOC_ARENA_MAX=2
Environment=PATH=/usr/local/bin:/usr/bin:/bin
EnvironmentFile=-/etc/rustproxy/environment
ExecStart=/usr/bin/$BINARY_NAME --data-dir /var/lib/rustproxy serve --admin-listen 0.0.0.0:9090 --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
Restart=on-failure Restart=on-failure
RestartSec=3 RestartSec=3
KillSignal=SIGINT
KillMode=control-group
TimeoutStopSec=120
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
LogsDirectory=rustproxy
LogsDirectoryMode=0700
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
# Allow ACL/group-controlled read access to web roots and external certificates
# under /home; ProtectSystem=strict still makes all other paths read-only.
ProtectHome=read-only
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictRealtime=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
SystemCallArchitectures=native
LockPersonality=true
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
UNIT UNIT
fi fi
# Match the 0.34.x CLI. Avoid changing upstream units that already provide
# their own ExecReload. Linux package revision forces rebuilding old binaries.
python3 "$BUILDER_ROOT/scripts/normalize-systemd-unit.py" \
"$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "$BINARY_NAME"
if [[ -r "$ENV_SRC" ]]; then if [[ -r "$ENV_SRC" ]]; then
install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment" install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment"
fi fi
if [[ -r "$LOGROTATE_SRC" ]]; then [[ -r "$LOGROTATE_SRC" ]] || fatal "missing bundled logrotate configuration: $LOGROTATE_SRC"
install -D -m 0644 "$LOGROTATE_SRC" "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" install -D -m 0644 "$LOGROTATE_SRC" "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}"
fi [[ -r "$ACCESS_GUIDE_SRC" ]] || fatal "missing Linux filesystem access guide: $ACCESS_GUIDE_SRC"
install -D -m 0644 "$ACCESS_GUIDE_SRC" "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md"
if [[ -r "$TMPFILES_SRC" ]]; then if [[ -r "$TMPFILES_SRC" ]]; then
install -D -m 0644 "$TMPFILES_SRC" "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" install -D -m 0644 "$TMPFILES_SRC" "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf"
fi fi
@@ -73,7 +114,7 @@ if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
adduser --system --ingroup "$PACKAGE_NAME" --home "/var/lib/$PACKAGE_NAME" --no-create-home \\ adduser --system --ingroup "$PACKAGE_NAME" --home "/var/lib/$PACKAGE_NAME" --no-create-home \\
--disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null --disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null
fi fi
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0750 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" "/etc/$PACKAGE_NAME"
SCRIPT SCRIPT
fi fi
cat >> "$PKG/scripts/deb-postinstall.sh" <<SCRIPT cat >> "$PKG/scripts/deb-postinstall.sh" <<SCRIPT
@@ -108,7 +149,7 @@ fi
if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
useradd --system --gid "$PACKAGE_NAME" --home-dir "/var/lib/$PACKAGE_NAME" --no-create-home --shell /sbin/nologin "$PACKAGE_NAME" useradd --system --gid "$PACKAGE_NAME" --home-dir "/var/lib/$PACKAGE_NAME" --no-create-home --shell /sbin/nologin "$PACKAGE_NAME"
fi fi
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0750 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" "/etc/$PACKAGE_NAME"
systemd-tmpfiles --create "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" >/dev/null 2>&1 || true systemd-tmpfiles --create "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" >/dev/null 2>&1 || true
systemctl daemon-reload >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true
systemctl enable "${PACKAGE_NAME}.service" >/dev/null 2>&1 || true systemctl enable "${PACKAGE_NAME}.service" >/dev/null 2>&1 || true
@@ -151,7 +192,8 @@ add_content() {
add_content "$PKG/payload/usr/bin/$BINARY_NAME" "/usr/bin/$BINARY_NAME" 0755 add_content "$PKG/payload/usr/bin/$BINARY_NAME" "/usr/bin/$BINARY_NAME" 0755
add_content "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "/usr/lib/systemd/system/${PACKAGE_NAME}.service" 0644 add_content "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "/usr/lib/systemd/system/${PACKAGE_NAME}.service" 0644
[[ -f "$PKG/payload/etc/${PACKAGE_NAME}/environment" ]] && add_content "$PKG/payload/etc/${PACKAGE_NAME}/environment" "/etc/${PACKAGE_NAME}/environment" 0640 'config|noreplace' [[ -f "$PKG/payload/etc/${PACKAGE_NAME}/environment" ]] && add_content "$PKG/payload/etc/${PACKAGE_NAME}/environment" "/etc/${PACKAGE_NAME}/environment" 0640 'config|noreplace'
[[ -f "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" ]] && add_content "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" "/etc/logrotate.d/${PACKAGE_NAME}" 0644 'config|noreplace' add_content "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" "/etc/logrotate.d/${PACKAGE_NAME}" 0644 'config|noreplace'
add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md" "/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md" 0644
[[ -f "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" ]] && add_content "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" 0644 [[ -f "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" ]] && add_content "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" 0644
[[ -f "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" ]] && add_content "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" "/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" 0644 [[ -f "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" ]] && add_content "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" "/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" 0644
[[ -f "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" ]] && add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" "/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" 0644 [[ -f "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" ]] && add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" "/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" 0644
@@ -183,6 +225,8 @@ depends:
- ca-certificates - ca-certificates
- gzip - gzip
- logrotate - logrotate
- python3
- python3-pip
- systemd - systemd
scripts: scripts:
postinstall: "$PKG/scripts/deb-postinstall.sh" postinstall: "$PKG/scripts/deb-postinstall.sh"
@@ -195,6 +239,17 @@ deb:
type: origin type: origin
YAML YAML
# The SUSE flavor keeps the RPM name/version identical but uses native
# dependencies and an isolated Python launcher for Leap/SLES 15 systems.
# SUSE may ship /usr/bin/python3 older than the separately installed python311.
install -D -m 0755 "$BUILDER_ROOT/packaging/suse-python3" \
"$PKG/payload/usr/libexec/$PACKAGE_NAME/python3"
mkdir -p "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d"
cat > "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf" <<SUSE_UNIT
[Service]
Environment=PATH=/usr/libexec/${PACKAGE_NAME}:/usr/local/bin:/usr/bin:/bin
SUSE_UNIT
cat > "$PKG/nfpm-rpm.yaml" <<YAML cat > "$PKG/nfpm-rpm.yaml" <<YAML
name: $NAME_Q name: $NAME_Q
arch: amd64 arch: amd64
@@ -215,6 +270,7 @@ depends:
- gzip - gzip
- logrotate - logrotate
- shadow-utils - shadow-utils
- '((python3 >= 3.10 and python3-pip) or (python3.12 and python3.12-pip) or (python3.11 and python3.11-pip))'
- systemd - systemd
scripts: scripts:
postinstall: "$PKG/scripts/rpm-postinstall.sh" postinstall: "$PKG/scripts/rpm-postinstall.sh"
@@ -225,3 +281,47 @@ rpm:
signature: signature:
key_file: $GPG_KEY_Q key_file: $GPG_KEY_Q
YAML YAML
# SUSE Linux Enterprise, openSUSE Leap and Tumbleweed use shadow (not
# RHEL's shadow-utils). Versioned Python enables Leap/SLES 15 installations.
# Compression gzip is understood by older rpm clients as well as current ones.
cat > "$PKG/nfpm-suse.yaml" <<YAML
name: $NAME_Q
arch: amd64
platform: linux
version: $VERSION_Q
release: "$PACKAGE_RELEASE"
section: net
priority: optional
maintainer: $MAINTAINER_Q
description: $DESC_Q
license: $LICENSE_Q
homepage: $HOMEPAGE_Q
vendor: $NAME_Q
contents:
$(cat "$CONTENTS_FILE")
- src: $(yaml_quote "$PKG/payload/usr/libexec/$PACKAGE_NAME/python3")
dst: /usr/libexec/$PACKAGE_NAME/python3
file_info:
mode: 0755
- src: $(yaml_quote "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf")
dst: /usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf
file_info:
mode: 0644
depends:
- ca-certificates
- 'glibc >= 2.34'
- gzip
- logrotate
- shadow
- systemd
- '((python310 and python310-pip) or (python311 and python311-pip) or (python312 and python312-pip) or (python313 and python313-pip) or (python314 and python314-pip) or (python3 >= 3.10 and python3-pip))'
scripts:
postinstall: "$PKG/scripts/rpm-postinstall.sh"
preremove: "$PKG/scripts/rpm-preremove.sh"
postremove: "$PKG/scripts/rpm-postremove.sh"
rpm:
compression: gzip
signature:
key_file: $GPG_KEY_Q
YAML
+9 -4
View File
@@ -104,6 +104,9 @@ fi
[[ -f "$ENV_FILE" ]] || write_env_file [[ -f "$ENV_FILE" ]] || write_env_file
load_env_file load_env_file
if [[ "${FREEBSD_NATIVE_ENABLED:-1}" == "0" ]]; then
warn "Native FreeBSD builds are DISABLED by existing .env; set FREEBSD_NATIVE_ENABLED=1 and rerun ./start.sh to publish native PKG"
fi
: "${SOURCE_REPO:?Uzupelnij SOURCE_REPO w .env albo ustaw zmienna przed uruchomieniem start.sh}" : "${SOURCE_REPO:?Uzupelnij SOURCE_REPO w .env albo ustaw zmienna przed uruchomieniem start.sh}"
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}" : "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}"
@@ -114,15 +117,15 @@ case "$SOURCE_REPO" in
*) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;; *) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;;
esac esac
normalize_repo_url REPOSITORY_URL HTTP_PORT "$DEFAULT_HTTP_PORT" "APT/RPM" normalize_repo_url REPOSITORY_URL HTTP_PORT "$DEFAULT_HTTP_PORT" "APT/RPM/SUSE"
normalize_repo_url OPNSENSE_REPOSITORY_URL OPNSENSE_HTTP_PORT "$DEFAULT_OPNSENSE_HTTP_PORT" "OPNsense" normalize_repo_url OPNSENSE_REPOSITORY_URL OPNSENSE_HTTP_PORT "$DEFAULT_OPNSENSE_HTTP_PORT" "OPNsense"
[[ "$HTTP_PORT" != "$OPNSENSE_HTTP_PORT" ]] || die "HTTP_PORT i OPNSENSE_HTTP_PORT musza byc rozne" [[ "$HTTP_PORT" != "$OPNSENSE_HTTP_PORT" ]] || die "HTTP_PORT i OPNSENSE_HTTP_PORT musza byc rozne"
mkdir -p "$DATA_DIR/repository" "$DATA_DIR/state" "$DATA_DIR/cargo" "$DATA_DIR/work" "$GPG_DIR" mkdir -p "$DATA_DIR/repository" "$DATA_DIR/state" "$DATA_DIR/cargo" "$DATA_DIR/work" "$GPG_DIR"
chmod 0700 "$GPG_DIR" 2>/dev/null || true chmod 0700 "$GPG_DIR" 2>/dev/null || true
log "budowanie obrazu buildera (Linux + FreeBSD cross-toolchain)" log "budowanie obrazu buildera oraz panelu repozytorium"
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE" "${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE" repository-dashboard
if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM" log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM"
@@ -173,7 +176,7 @@ else
log "uzywanie istniejacego klucza GPG" log "uzywanie istniejacego klucza GPG"
fi fi
log "uruchamianie buildera i wspolnego nginx przez Docker Compose" log "uruchamianie buildera, panelu i nginx przez Docker Compose"
"${COMPOSE[@]}" --env-file "$ENV_FILE" up -d "$SERVICE" "$HTTP_SERVICE" "${COMPOSE[@]}" --env-file "$ENV_FILE" up -d "$SERVICE" "$HTTP_SERVICE"
fingerprint="" fingerprint=""
@@ -182,9 +185,11 @@ fingerprint=""
log "gotowe" log "gotowe"
printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO" printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO"
printf 'APT/RPM URL: %s/\n' "${REPOSITORY_URL%/}" printf 'APT/RPM URL: %s/\n' "${REPOSITORY_URL%/}"
printf 'SUSE/Zypper RPM URL: %s/suse/x86_64/\n' "${REPOSITORY_URL%/}"
printf 'APT/RPM local HTTP port: %s\n' "$HTTP_PORT" printf 'APT/RPM local HTTP port: %s\n' "$HTTP_PORT"
printf 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}" printf 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}"
printf 'OPNsense local HTTP port: %s\n' "$OPNSENSE_HTTP_PORT" printf 'OPNsense local HTTP port: %s\n' "$OPNSENSE_HTTP_PORT"
[[ "${FREEBSD_NATIVE_ENABLED:-1}" == "1" ]] && printf 'Native FreeBSD pkg URL: %s/freebsd/\n' "${OPNSENSE_REPOSITORY_URL%/}"
[[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint" [[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint"
printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository" printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository"
printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT" printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
-21
View File
@@ -1,21 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
SRC=${1:?usage: check-rustproxy-source.sh /path/to/RustProxy}
for f in \
Cargo.toml \
debian/rustproxy.service \
debian/rustproxy.environment \
debian/rustproxy.logrotate \
debian/rustproxy.tmpfiles \
debian/rustproxy.postinst \
grafana/rustproxy-overview.json; do
[[ -f "$SRC/$f" ]] || { echo "missing: $f" >&2; exit 1; }
done
grep -q '^name = "rust-proxy"' "$SRC/Cargo.toml"
grep -q '^version = "0.26.6"' "$SRC/Cargo.toml"
grep -q '^ExecStart=/usr/bin/rust-proxy serve' "$SRC/debian/rustproxy.service"
grep -q '^RUST_PROXY_DATA_DIR=/var/lib/rustproxy' "$SRC/debian/rustproxy.environment"
echo 'RustProxy 0.26.6 packaging layout: OK'
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
for f in "$ROOT"/start.sh "$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh; do
bash -n "$f"
done
python3 -m py_compile "$ROOT/scripts/generate-opnsense-repo.py"
# Main image/build dependencies.
grep -q 'FROM rockylinux:9' "$ROOT/Dockerfile"
grep -q 'FREEBSD_RELEASE=15.1-RELEASE' "$ROOT/Dockerfile"
grep -q 'x86_64-unknown-freebsd' "$ROOT/Dockerfile"
grep -q '/opt/freebsd-sysroot' "$ROOT/Dockerfile"
# Coordinator uses one checkout but independent branches.
grep -q 'build-linux-release.sh' "$ROOT/scripts/build-release.sh"
grep -q 'build-opnsense-release.sh' "$ROOT/scripts/build-release.sh"
grep -q 'OPNsense branch will still run' "$ROOT/scripts/build-release.sh"
# Linux tests gate Linux packaging only.
grep -q 'cargo test --release' "$ROOT/scripts/build-linux-release.sh"
grep -q 'nfpm package' "$ROOT/scripts/build-linux-release.sh"
grep -q 'aptly .*publish' "$ROOT/scripts/build-linux-release.sh"
grep -q 'createrepo_c' "$ROOT/scripts/build-linux-release.sh"
! grep -q 'cargo test' "$ROOT/scripts/build-opnsense-release.sh"
! grep -q 'cargo test' "$ROOT/scripts/build-opnsense.sh"
# FreeBSD/OPNsense branch.
grep -q 'build-opnsense.sh' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'package-opnsense.sh' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'generate-opnsense-repo.py' "$ROOT/scripts/build-opnsense-release.sh"
grep -q 'last_opnsense_commit' "$ROOT/scripts/build-opnsense-release.sh"
test -f "$ROOT/opnsense/plugin/src/etc/rc.d/rustproxy"
test -f "$ROOT/opnsense/patches/0.20.0/0001-opnsense-build.patch"
# Per-branch state/retry logic.
grep -q 'last_linux_commit' "$ROOT/scripts/check-and-build.sh"
grep -q 'last_opnsense_commit' "$ROOT/scripts/check-and-build.sh"
grep -q 'BUILD_LINUX' "$ROOT/scripts/check-and-build.sh"
grep -q 'BUILD_OPNSENSE' "$ROOT/scripts/check-and-build.sh"
grep -q 'RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}' "$ROOT/scripts/entrypoint.sh"
grep -Fq 'RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"' "$ROOT/docker-compose.yml"
# Two public endpoints from one nginx container.
grep -Fq '${REPOSITORY_URL:?REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -Fq '${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}' "$ROOT/docker-compose.yml"
grep -Fq '0.0.0.0:${HTTP_PORT:-18765}:8080' "$ROOT/docker-compose.yml"
grep -Fq '0.0.0.0:${OPNSENSE_HTTP_PORT:-18766}:8081' "$ROOT/docker-compose.yml"
grep -q 'listen 8080' "$ROOT/http/nginx.conf"
grep -q 'listen 8081' "$ROOT/http/nginx.conf"
grep -q 'root /srv/repository/opnsense' "$ROOT/http/nginx.conf"
grep -q 'DEFAULT_HTTP_PORT=18765' "$ROOT/start.sh"
grep -q 'DEFAULT_OPNSENSE_HTTP_PORT=18766' "$ROOT/start.sh"
grep -q 'OPNSENSE_REPOSITORY_URL=https://opn-rustproxy.linuxiarz.pl' "$ROOT/.env.example"
grep -q 'RUN_LINUX_TESTS=1' "$ROOT/.env.example"
echo 'static checks: OK'
+115
View File
@@ -0,0 +1,115 @@
from pathlib import Path
import os
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class CleanupRetentionTests(unittest.TestCase):
def test_runtime_target_is_not_persistent_data(self):
dockerfile = (ROOT / "Dockerfile").read_text()
entrypoint = (ROOT / "scripts/entrypoint.sh").read_text()
build = (ROOT / "scripts/build-release.sh").read_text()
runtime = dockerfile.split("# Cargo registry/git cache stays persistent", 1)[1]
self.assertIn("CARGO_HOME=/var/lib/package-builder/cargo", runtime)
self.assertIn("CARGO_TARGET_DIR=/tmp/rust-package-builder/cargo-target", runtime)
self.assertIn("GOCACHE=/tmp/rust-package-builder/go-cache", dockerfile)
self.assertNotIn("CARGO_TARGET_DIR=/var/lib/package-builder/cargo/target", runtime)
self.assertNotIn("GOCACHE=/var/lib/package-builder/go/cache", dockerfile)
self.assertIn('export CARGO_TARGET_DIR="$BUILD_TMP_DIR/cargo-target"', entrypoint)
self.assertIn('rm -rf -- "$DATA_DIR/cargo/target" "$DATA_DIR/go/cache"', entrypoint)
self.assertIn('WORK="$BUILD_TMP_DIR/work/$BUILD_ID"', build)
self.assertIn('if is_true "${CLEAN_BUILD_CACHE:-1}"; then', build)
self.assertIn('rm -rf -- "$CARGO_TARGET_DIR" "${GOCACHE:-$BUILD_TMP_DIR/go-cache}"', build)
def test_default_retention_is_ten_and_exported_to_compose(self):
env = (ROOT / ".env.example").read_text()
compose = (ROOT / "docker-compose.yml").read_text()
entrypoint = (ROOT / "scripts/entrypoint.sh").read_text()
self.assertIn("RELEASE_RETENTION=10", env)
self.assertIn("PACKAGE_RETENTION=10", env)
self.assertIn("CLEAN_BUILD_CACHE=1", env)
self.assertIn('RELEASE_RETENTION: "${RELEASE_RETENTION:-10}"', compose)
self.assertIn('PACKAGE_RETENTION: "${PACKAGE_RETENTION:-10}"', compose)
self.assertIn('CLEAN_BUILD_CACHE: "${CLEAN_BUILD_CACHE:-1}"', compose)
self.assertIn('RELEASE_RETENTION=${RELEASE_RETENTION:-10}', entrypoint)
self.assertIn('PACKAGE_RETENTION=${PACKAGE_RETENTION:-10}', entrypoint)
def test_linux_retention_and_aptly_cleanup_are_enabled(self):
linux = (ROOT / "scripts/build-linux-release.sh").read_text()
self.assertIn('package_retention=${PACKAGE_RETENTION:-10}', linux)
self.assertIn("'rustproxy-docker-agent_*.deb'", linux)
self.assertIn("'rustproxy-docker-agent-*.x86_64.rpm'", linux)
self.assertIn('repo add -force-replace package-builder "$DATA_DIR/packages/deb"', linux)
self.assertIn('snapshot list -raw', linux)
self.assertIn('aptly -config="$APTLY_CONF" db cleanup', linux)
self.assertIn('--link-dest="$CURRENT_TARGET/apt/"', linux)
self.assertIn('--link-dest="$CURRENT_TARGET/rpm/"', linux)
self.assertIn('--link-dest="$CURRENT_TARGET/suse/"', linux)
def test_package_retention_keeps_ten_versions_per_product(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
rpm = root / "rpm"
rpm.mkdir()
for idx in range(12):
main = rpm / f"rustproxy-0.2.{idx}-1.x86_64.rpm"
agent = rpm / f"rustproxy-docker-agent-0.2.{idx}-1.x86_64.rpm"
main.write_bytes(b"main")
agent.write_bytes(b"agent")
ts = 1_700_000_000 + idx
os.utime(main, (ts, ts))
os.utime(agent, (ts, ts))
script = "\n".join([
"set -Eeuo pipefail",
f"source {ROOT / 'scripts/lib.sh'}",
f"prune_package_files {rpm} 'rustproxy-*.x86_64.rpm' 10 'rustproxy-docker-agent-*.x86_64.rpm'",
f"prune_package_files {rpm} 'rustproxy-docker-agent-*.x86_64.rpm' 10",
])
subprocess.run(["bash", "-lc", script], check=True)
main = [p for p in rpm.glob("rustproxy-*.x86_64.rpm") if not p.name.startswith("rustproxy-docker-agent-")]
agent = list(rpm.glob("rustproxy-docker-agent-*.x86_64.rpm"))
self.assertEqual(len(main), 10)
self.assertEqual(len(agent), 10)
self.assertFalse((rpm / "rustproxy-0.2.0-1.x86_64.rpm").exists())
self.assertFalse((rpm / "rustproxy-docker-agent-0.2.0-1.x86_64.rpm").exists())
self.assertTrue((rpm / "rustproxy-0.2.11-1.x86_64.rpm").exists())
self.assertTrue((rpm / "rustproxy-docker-agent-0.2.11-1.x86_64.rpm").exists())
def test_release_retention_keeps_newest_ten(self):
with tempfile.TemporaryDirectory() as tmp:
releases = Path(tmp) / "releases"
releases.mkdir()
for idx in range(12):
path = releases / f"release-{idx:02d}"
path.mkdir()
ts = 1_700_000_000 + idx
os.utime(path, (ts, ts))
protected = releases / "release-11"
script = "\n".join([
"set -Eeuo pipefail",
f"source {ROOT / 'scripts/lib.sh'}",
f"prune_release_dirs {releases} 10 {protected}",
])
subprocess.run(["bash", "-lc", script], check=True)
remaining = sorted(p.name for p in releases.iterdir() if p.is_dir())
self.assertEqual(len(remaining), 10)
self.assertNotIn("release-00", remaining)
self.assertNotIn("release-01", remaining)
self.assertIn("release-11", remaining)
def test_packaging_revision_forces_one_new_linux_build(self):
lib = (ROOT / "scripts/lib.sh").read_text()
self.assertIn("PACKAGING_REVISION=7", lib)
if __name__ == "__main__":
unittest.main()
+134
View File
@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""Offline verification of SUSE nFPM metadata and catalog integration.
Run: python3 -m unittest discover -s tests -v
Does not require nfpm, root, Docker, or access to the RustProxy upstream.
"""
import importlib.util
import os
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class SusePackagingTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.tmp = tempfile.TemporaryDirectory()
cls.work = Path(cls.tmp.name)
cls.src = cls.work / "source"
(cls.src / "docker-agent").mkdir(parents=True)
agent = cls.src / "docker-agent"
(agent / "rustproxy-docker-agent.service").write_text(
"[Unit]\nDescription=Test Agent\n[Service]\nExecStart=/usr/local/bin/rustproxy-docker-agent\n")
(agent / "docker-agent.env.example").write_text("BIND_ADDRESS=127.0.0.1\n")
(agent / "README.md").write_text("Test fixture")
(cls.work / "server").write_text("#!/bin/sh\necho server\n")
(cls.work / "agent").write_text("#!/bin/sh\necho agent\n")
(cls.work / "server").chmod(0o755)
(cls.work / "agent").chmod(0o755)
cls.env = {
**os.environ,
"BUILDER_ROOT": str(ROOT),
"PACKAGE_NAME": "rustproxy",
"BINARY_NAME": "rust-proxy",
"VERSION": "0.99.1",
"PACKAGE_RELEASE": "7",
"MAINTAINER": "Test Builder <test@example.invalid>",
"DESCRIPTION": "Test RPM",
"LICENSE": "MIT",
"HOMEPAGE": "https://example.invalid",
"GPG_PRIVATE_KEY_FILE": "/not/needed/until-nfpm-signs",
}
subprocess.run(
["bash", str(ROOT / "scripts/prepare-package.sh"), str(cls.src),
str(cls.work / "package"), str(cls.work / "server")],
env=cls.env, check=True, capture_output=True, text=True,
)
subprocess.run(
["bash", str(ROOT / "scripts/prepare-agent-package.sh"), str(cls.src),
str(cls.work / "package-agent"), str(cls.work / "agent")],
env=cls.env, check=True, capture_output=True, text=True,
)
@classmethod
def tearDownClass(cls):
cls.tmp.cleanup()
def test_suse_rpm_has_native_dependencies_and_paths(self):
contents = (self.work / "package/nfpm-suse.yaml").read_text()
self.assertIn(" - shadow\n", contents)
self.assertIn(" - 'glibc >= 2.34'", contents)
self.assertNotIn("shadow-utils", contents)
self.assertIn("(python311 and python311-pip)", contents)
self.assertIn("/usr/libexec/rustproxy/python3", contents)
self.assertIn("50-suse-python.conf", contents)
self.assertIn("compression: gzip", contents)
self.assertIn("key_file:", contents)
self.assertTrue((self.work / "package/payload/usr/lib/systemd/system/rustproxy.service.d/50-suse-python.conf").exists())
self.assertIn("/usr/libexec/rustproxy", (self.work / "package/payload/usr/lib/systemd/system/rustproxy.service.d/50-suse-python.conf").read_text())
def test_suse_agent_has_native_dependencies(self):
contents = (self.work / "package-agent/nfpm-suse.yaml").read_text()
self.assertIn(" - shadow\n", contents)
self.assertNotIn("shadow-utils", contents)
self.assertIn("compression: gzip", contents)
self.assertIn("name: \"rustproxy-docker-agent\"", contents)
self.assertIn("/usr/lib/systemd/system/rustproxy-docker-agent.service", contents)
def test_suse_python_launcher(self):
wrapper = self.work / "package/payload/usr/libexec/rustproxy/python3"
result = subprocess.run([str(wrapper), "-c", "import sys; print(sys.version_info.major)"],
capture_output=True, text=True, timeout=5)
# Builder test machine has Python 3; a host without Python >=3.10
# will deliberately fail the wrapper rather than run obsolete Python.
if result.returncode == 0:
self.assertEqual(result.stdout.strip(), "3")
else:
self.assertIn("Python 3.10", result.stderr)
class SuseCatalogTests(unittest.TestCase):
def setUp(self):
spec = importlib.util.spec_from_file_location("rustproxy_dashboard", ROOT / "dashboard/app.py")
self.assertIsNotNone(spec)
self.api = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.api)
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.repo = Path(self.tmp.name) / "repository"
release = self.repo / "releases" / "new"
(release / "suse/x86_64/repodata").mkdir(parents=True)
(release / "suse/x86_64/repodata/repomd.xml").write_text("<repomd/>")
(release / "suse/x86_64/rustproxy-0.99.1-7.x86_64.rpm").write_bytes(b"rpm-test")
(release / "suse/x86_64/rustproxy-docker-agent-0.99.1-7.x86_64.rpm").write_bytes(b"rpm-test")
(release / "SUSE-BUILD-INFO.txt").write_text(
"app_version=0.99.1\npackage_release=7\n"
"built_at=2026-10-22T12:00:00Z\nsource_commit=abcdef123456\n"
)
(self.repo / "current").symlink_to("releases/new")
self.release = release
def test_catalog_publishes_suse_packages_only_in_suse_channel(self):
catalog = self.api.generate_catalog(self.repo)
self.assertTrue(catalog["builds"]["suse"]["published"])
self.assertEqual(catalog["builds"]["suse"]["package_version"], "0.99.1-7")
suse_pkgs = [p for p in catalog["packages"] if p["channel"] == "suse"]
self.assertEqual(len(suse_pkgs), 2)
self.assertTrue(all(p["format"] == "RPM" for p in suse_pkgs))
self.assertTrue(all(p["download_url"].endswith(".x86_64.rpm") for p in suse_pkgs))
self.assertTrue(all("/suse/x86_64/" in p["download_url"] for p in suse_pkgs))
self.assertEqual(catalog["summary"]["channel_counts"]["suse"], 2)
def test_old_build_does_not_fake_suse_publication(self):
(self.release / "SUSE-BUILD-INFO.txt").unlink()
catalog = self.api.generate_catalog(self.repo)
self.assertFalse(catalog["builds"]["suse"]["published"])
if __name__ == "__main__":
unittest.main()