Compare commits
25
Commits
222775a043
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7168bc4683 | ||
|
|
fb5f01d291 | ||
|
|
f86a62b8b1 | ||
|
|
1f0daa0679 | ||
|
|
feb9d91a3d | ||
|
|
cd64b9a14e | ||
|
|
ccccdc2031 | ||
|
|
3b317a4f0c | ||
|
|
a1389a6a61 | ||
|
|
a946c38194 | ||
|
|
910bb563f0 | ||
|
|
af78f6528c | ||
|
|
17f1de681e | ||
|
|
c507e9e12a | ||
|
|
86aebcfc05 | ||
|
|
f9b8c43f5f | ||
|
|
c48ddedad3 | ||
|
|
f77f692001 | ||
|
|
5807d11550 | ||
|
|
743f70675b | ||
|
|
e046b56322 | ||
|
|
8a9d63fa95 | ||
|
|
fd8a7dd629 | ||
|
|
34d7b656f5 | ||
|
|
02870305bc |
No files matched your search
@@ -2,3 +2,4 @@ SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git
|
||||
SOURCE_BRANCH=master
|
||||
REPOSITORY_URL=https://packages.linuxiarz.pl
|
||||
POLL_INTERVAL=300
|
||||
RUN_LINUX_TESTS=0
|
||||
+12
-3
@@ -18,6 +18,8 @@ RUN set -eux; \
|
||||
mkdir -p /freebsd-sysroot; \
|
||||
tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib
|
||||
|
||||
FROM golang:1.26-bookworm AS go-toolchain
|
||||
|
||||
FROM rockylinux:9
|
||||
|
||||
ARG RUST_TOOLCHAIN=stable
|
||||
@@ -68,8 +70,12 @@ RUN set -eux; \
|
||||
rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version
|
||||
|
||||
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 \
|
||||
CC_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" \
|
||||
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.
|
||||
ENV CARGO_HOME=/var/lib/package-builder/cargo
|
||||
# Cargo registry/git cache stays persistent; build target is disposable and
|
||||
# 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 packaging/ /opt/rust-package-builder/packaging/
|
||||
COPY opnsense/ /opt/rust-package-builder/opnsense/
|
||||
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py
|
||||
|
||||
|
||||
@@ -1,13 +1,59 @@
|
||||
# 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:
|
||||
|
||||
- Linux x86_64: DEB + podpisane repo APT,
|
||||
- Linux x86_64: RPM + podpisane repo DNF/YUM,
|
||||
- Linux x86_64: DEB + podpisane repo APT (`rustproxy` **oraz osobny** `rustproxy-docker-agent`),
|
||||
- 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,
|
||||
- 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
|
||||
|
||||
@@ -30,7 +76,7 @@ Dla `http://` bez portu `start.sh` dopisze domyślny port. Dla `https://` zakła
|
||||
Domyślne lokalne porty:
|
||||
|
||||
```text
|
||||
APT/RPM: 18765 -> nginx :8080
|
||||
APT/RPM/Zypper: 18765 -> nginx :8080
|
||||
OPNsense: 18766 -> nginx :8081
|
||||
```
|
||||
|
||||
@@ -49,17 +95,80 @@ opn-rustproxy.linuxiarz.pl -> http://builder:18766
|
||||
wspólny checkout
|
||||
/ \
|
||||
/ \
|
||||
Linux branch OPNsense branch
|
||||
cargo test izolowana kopia źródła
|
||||
cargo build poprawki FreeBSD
|
||||
DEB + RPM cross-build FreeBSD
|
||||
APT + DNF os-rustproxy.pkg
|
||||
| |
|
||||
v v
|
||||
publikacja Linux publikacja OPNsense
|
||||
Linux branch FreeBSD cross-build
|
||||
cargo test |
|
||||
cargo build +---+---+
|
||||
go test/build agent | |
|
||||
2x DEB + 2x RPM + 2x SUSE RPM | |
|
||||
APT + DNF + Zypper OPNsense FreeBSD native*
|
||||
| | |
|
||||
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
|
||||
|
||||
@@ -110,6 +219,9 @@ POLL_INTERVAL=300
|
||||
PACKAGE_RELEASE=auto
|
||||
MAX_GLIBC_VERSION=2.34
|
||||
RUN_LINUX_TESTS=1
|
||||
RELEASE_RETENTION=10
|
||||
PACKAGE_RETENTION=10
|
||||
CLEAN_BUILD_CACHE=1
|
||||
|
||||
OPNSENSE_ABI=26.7
|
||||
OPNSENSE_RETENTION=10
|
||||
@@ -120,10 +232,43 @@ FREEBSD_RUST_TOOLCHAIN=1.88.0
|
||||
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.
|
||||
|
||||
`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
|
||||
|
||||
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: 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
|
||||
|
||||
```text
|
||||
data/
|
||||
├── aptly/
|
||||
├── cargo/
|
||||
├── cargo/ # registry/git cache; bez target/
|
||||
├── packages/
|
||||
│ ├── deb/
|
||||
│ └── rpm/x86_64/
|
||||
@@ -162,16 +383,17 @@ data/
|
||||
│ ├── apt -> current/apt
|
||||
│ ├── rpm -> current/rpm
|
||||
│ ├── opnsense -> current/opnsense
|
||||
│ ├── freebsd -> current/freebsd
|
||||
│ └── releases/
|
||||
├── state/
|
||||
└── 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
|
||||
|
||||
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
|
||||
docker compose logs -f rust-package-builder
|
||||
@@ -183,10 +405,7 @@ curl -f http://127.0.0.1:18766/healthz
|
||||
|
||||
## Testy statyczne
|
||||
|
||||
```bash
|
||||
./tests/static.sh
|
||||
./tests/check-rustproxy-source.sh /sciezka/do/RustProxy
|
||||
```
|
||||
Skrypty diagnostyczne są dostępne w pełnym repozytorium developerskim; wydanie wdrożeniowe zawiera tylko pliki potrzebne do uruchomienia.
|
||||
|
||||
## Klucz GPG
|
||||
|
||||
@@ -202,3 +421,79 @@ data/state/last_opnsense_commit
|
||||
```
|
||||
|
||||
Jeżeli istnieje starsze `last_commit` oraz poprawne APT/RPM dla tego SHA, zostanie ono potraktowane jako stan gałęzi Linux. Brak repo OPNsense uruchomi wyłącznie gałąź OPNsense. Po udanym OPNsense kolejny cykl nie będzie ponownie kompilował FreeBSD, nawet jeśli testy Linux nadal nie przechodzą.
|
||||
|
||||
## 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.
|
||||
|
||||
### 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
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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"]
|
||||
@@ -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()
|
||||
@@ -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) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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();
|
||||
})();
|
||||
@@ -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 |
@@ -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 & 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>
|
||||
@@ -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}
|
||||
@@ -17,6 +17,10 @@ services:
|
||||
PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}"
|
||||
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
|
||||
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_RETENTION: "${OPNSENSE_RETENTION:-10}"
|
||||
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
|
||||
@@ -25,12 +29,35 @@ services:
|
||||
FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}"
|
||||
APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}"
|
||||
RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"
|
||||
FREEBSD_NATIVE_ENABLED: "${FREEBSD_NATIVE_ENABLED:-1}"
|
||||
FREEBSD_NATIVE_RETENTION: "${FREEBSD_NATIVE_RETENTION:-10}"
|
||||
volumes:
|
||||
- ./data:/var/lib/package-builder
|
||||
- ./gpg:/run/secrets/gpg:ro
|
||||
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:
|
||||
depends_on:
|
||||
- repository-dashboard
|
||||
image: nginxinc/nginx-unprivileged:stable-alpine-slim
|
||||
ports:
|
||||
- "0.0.0.0:${HTTP_PORT:-18765}:8080"
|
||||
|
||||
+65
-21
@@ -23,7 +23,11 @@ http {
|
||||
uwsgi_temp_path /tmp/uwsgi_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 {
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
@@ -32,11 +36,6 @@ http {
|
||||
index index.html;
|
||||
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)$) {
|
||||
return 405;
|
||||
}
|
||||
@@ -47,12 +46,21 @@ http {
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /index.html @linux_initializing;
|
||||
proxy_set_header X-Repo-Site linux;
|
||||
proxy_pass http://repository-dashboard:8000/;
|
||||
}
|
||||
|
||||
location @linux_initializing {
|
||||
default_type text/html;
|
||||
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>';
|
||||
location = /index.html {
|
||||
proxy_set_header X-Repo-Site linux;
|
||||
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 ~ (^|/)\. {
|
||||
@@ -60,12 +68,17 @@ http {
|
||||
}
|
||||
|
||||
location / {
|
||||
expires 10m;
|
||||
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,
|
||||
# but follows the same atomic repository/current switch as the Linux repo.
|
||||
# OPNsense / FreeBSD repository
|
||||
server {
|
||||
listen 8081;
|
||||
server_name _;
|
||||
@@ -74,11 +87,6 @@ http {
|
||||
index index.html;
|
||||
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)$) {
|
||||
return 405;
|
||||
}
|
||||
@@ -89,12 +97,42 @@ http {
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /index.html @opnsense_initializing;
|
||||
proxy_set_header X-Repo-Site opnsense;
|
||||
proxy_pass http://repository-dashboard:8000/;
|
||||
}
|
||||
|
||||
location @opnsense_initializing {
|
||||
default_type text/html;
|
||||
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>';
|
||||
location = /index.html {
|
||||
proxy_set_header X-Repo-Site opnsense;
|
||||
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 ~ (^|/)\. {
|
||||
@@ -102,7 +140,13 @@ http {
|
||||
}
|
||||
|
||||
location / {
|
||||
expires 10m;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# Redirect 404 to homepage
|
||||
location @redirect_home {
|
||||
return 302 /;
|
||||
}
|
||||
}
|
||||
}
|
||||
+70
@@ -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()
|
||||
Executable → Regular
File mode changed.
Executable → Regular
File mode changed.
@@ -1,6 +1,6 @@
|
||||
PLUGIN_NAME= rustproxy
|
||||
PLUGIN_VERSION?= 0.23.3
|
||||
PLUGIN_REVISION?= 1
|
||||
PLUGIN_REVISION?= 2
|
||||
PLUGIN_COMMENT= RustProxy reverse proxy service
|
||||
PLUGIN_MAINTAINER?= local@localhost
|
||||
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
|
||||
@@ -18,12 +18,15 @@ load_rc_config "$name"
|
||||
: ${rustproxy_user:="root"}
|
||||
: ${rustproxy_group:="wheel"}
|
||||
: ${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_file:="${rustproxy_log_dir}/rustproxy.log"}
|
||||
: ${rustproxy_run_dir:="/var/run/rustproxy"}
|
||||
: ${rustproxy_command:="/usr/local/sbin/rust-proxy"}
|
||||
: ${rustproxy_restart_delay:="5"}
|
||||
: ${rustproxy_allow_remote_admin:="1"}
|
||||
: ${rustproxy_update_check_interval_secs:="3600"}
|
||||
# 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
|
||||
# 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" \
|
||||
"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
|
||||
rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}"
|
||||
fi
|
||||
@@ -163,12 +194,15 @@ rustproxy_daemon_user_arg=""
|
||||
if [ "$rustproxy_user" != "root" ]; then
|
||||
rustproxy_daemon_user_arg="-u ${rustproxy_user}"
|
||||
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}"
|
||||
start_precmd="rustproxy_prestart"
|
||||
start_postcmd="rustproxy_poststart"
|
||||
stop_postcmd="rustproxy_poststop"
|
||||
status_cmd="rustproxy_status"
|
||||
extra_commands="reload"
|
||||
reload_cmd="rustproxy_reload"
|
||||
|
||||
rustproxy_prestart()
|
||||
{
|
||||
@@ -200,6 +234,7 @@ rustproxy_prestart()
|
||||
if [ -n "${rustproxy_malloc_conf}" ]; then
|
||||
echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
|
||||
fi
|
||||
echo "RustProxy update check interval: ${rustproxy_update_check_interval_secs}s"
|
||||
echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
|
||||
printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
|
||||
/bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
|
||||
@@ -268,6 +303,25 @@ rustproxy_status()
|
||||
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()
|
||||
{
|
||||
/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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -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>
|
||||
<mount>//OPNsense/rustproxy</mount>
|
||||
<version>1.4.0</version>
|
||||
<version>1.5.0</version>
|
||||
<description>RustProxy service settings</description>
|
||||
<items>
|
||||
<general>
|
||||
@@ -57,6 +57,13 @@
|
||||
<Default>1</Default>
|
||||
<Required>Y</Required>
|
||||
</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. -->
|
||||
<proxyListen type="IPPortField"/>
|
||||
|
||||
@@ -16,6 +16,11 @@ start_service()
|
||||
echo "RustProxy is disabled in OPNsense configuration"
|
||||
return 0
|
||||
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
|
||||
}
|
||||
|
||||
@@ -31,10 +36,19 @@ case "${1:-}" in
|
||||
stop)
|
||||
stop_service
|
||||
;;
|
||||
restart|reload)
|
||||
restart)
|
||||
stop_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)
|
||||
"$RC_SCRIPT" status
|
||||
;;
|
||||
|
||||
@@ -20,7 +20,7 @@ message:restarting RustProxy
|
||||
allowed_groups:wheel,wwwonly
|
||||
|
||||
[reload]
|
||||
command:/usr/local/opnsense/scripts/rustproxy/service.sh restart
|
||||
command:/usr/local/opnsense/scripts/rustproxy/service.sh reload
|
||||
parameters:
|
||||
type:script
|
||||
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_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 %}"
|
||||
# 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
|
||||
# new split address/port fields are both empty.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
Executable
+10
@@ -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
|
||||
@@ -7,17 +7,28 @@ PATCH_ROOT=/opt/rust-package-builder/opnsense/patches
|
||||
|
||||
[[ -f "$SOURCE_DIR/Cargo.toml" ]] || fatal "missing Cargo.toml in FreeBSD build source: $SOURCE_DIR"
|
||||
|
||||
VERSION=$(python3 - "$SOURCE_DIR/Cargo.toml" <<'PY'
|
||||
import sys, tomllib
|
||||
# build-release.sh already resolves the Cargo package version and exports VERSION.
|
||||
# Reuse it here so the FreeBSD branch does not need to parse Cargo.toml again.
|
||||
SOURCE_VERSION=${VERSION:-}
|
||||
if [[ -z "$SOURCE_VERSION" ]]; then
|
||||
# Fallback for standalone/manual use of this helper. Rocky Linux 9 exposes
|
||||
# python3 as Python 3.9, while tomllib is part of the stdlib from Python 3.11.
|
||||
PYTHON_BIN=${RUST_PROXY_PYTHON:-/usr/bin/python3.11}
|
||||
[[ -x "$PYTHON_BIN" ]] || fatal "Python 3.11 not found: $PYTHON_BIN"
|
||||
SOURCE_VERSION=$("$PYTHON_BIN" - "$SOURCE_DIR/Cargo.toml" <<'PY'
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
with Path(sys.argv[1]).open('rb') as f:
|
||||
print(tomllib.load(f)['package']['version'])
|
||||
PY
|
||||
)
|
||||
fi
|
||||
|
||||
PATCH_DIR="$PATCH_ROOT/$VERSION"
|
||||
[[ -n "$SOURCE_VERSION" ]] || fatal "cannot determine RustProxy version for FreeBSD patch selection"
|
||||
PATCH_DIR="$PATCH_ROOT/$SOURCE_VERSION"
|
||||
if [[ ! -d "$PATCH_DIR" ]]; then
|
||||
log "no FreeBSD/OPNsense patch set for RustProxy $VERSION; using upstream source"
|
||||
log "no FreeBSD/OPNsense patch set for RustProxy $SOURCE_VERSION; using upstream source"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -25,7 +36,7 @@ shopt -s nullglob
|
||||
patches=("$PATCH_DIR"/*.patch)
|
||||
(( ${#patches[@]} > 0 )) || fatal "FreeBSD patch directory is empty: $PATCH_DIR"
|
||||
|
||||
log "applying FreeBSD/OPNsense patches for RustProxy $VERSION"
|
||||
log "applying FreeBSD/OPNsense patches for RustProxy $SOURCE_VERSION"
|
||||
for patch_file in "${patches[@]}"; do
|
||||
patch_name=$(basename "$patch_file")
|
||||
if git -C "$SOURCE_DIR" apply --check "$patch_file" 2>/dev/null; then
|
||||
@@ -34,6 +45,6 @@ for patch_file in "${patches[@]}"; do
|
||||
elif git -C "$SOURCE_DIR" apply --reverse --check "$patch_file" 2>/dev/null; then
|
||||
log "patch already present: $patch_name"
|
||||
else
|
||||
fatal "patch does not apply cleanly: $patch_name (RustProxy $VERSION)"
|
||||
fatal "patch does not apply cleanly: $patch_name (RustProxy $SOURCE_VERSION)"
|
||||
fi
|
||||
done
|
||||
Executable
+123
@@ -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
@@ -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_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" \
|
||||
&& "$LAST_VERSION" == "$VERSION" \
|
||||
&& "$CURRENT_LINUX_PACKAGING_REVISION" == "$PACKAGING_REVISION" \
|
||||
&& "$LAST_PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
PACKAGE_RELEASE=$LAST_PACKAGE_RELEASE
|
||||
log "Linux repair build: reusing package release $PACKAGE_RELEASE"
|
||||
@@ -48,23 +54,42 @@ fi
|
||||
export PACKAGE_RELEASE
|
||||
|
||||
cd "$SRC"
|
||||
LINUX_RUST_TOOLCHAIN=$(/opt/rust-package-builder/scripts/ensure-rust-toolchain.sh "$SRC" linux)
|
||||
log "Linux branch: version $VERSION release $PACKAGE_RELEASE"
|
||||
|
||||
if [[ "${RUN_LINUX_TESTS:-1}" == "1" ]]; then
|
||||
log "Linux cargo tests started"
|
||||
TEST_MODE="${RUN_LINUX_TESTS:-compile}"
|
||||
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
|
||||
cargo test --release --locked
|
||||
cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked --no-run
|
||||
else
|
||||
cargo test --release
|
||||
cargo "+$LINUX_RUST_TOOLCHAIN" test --release --no-run
|
||||
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
|
||||
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"
|
||||
if [[ -f Cargo.lock ]]; then
|
||||
cargo build --release --locked
|
||||
cargo "+$LINUX_RUST_TOOLCHAIN" build --release --locked
|
||||
else
|
||||
cargo build --release
|
||||
cargo "+$LINUX_RUST_TOOLCHAIN" build --release
|
||||
fi
|
||||
log "Linux cargo build completed"
|
||||
|
||||
@@ -79,25 +104,69 @@ if [[ -n "$MAX_GLIBC_FOUND" ]]; then
|
||||
log "glibc compatibility check: <= $MAX_GLIBC_FOUND"
|
||||
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-agent-package.sh "$SRC" "$AGENT_PKG" "$AGENT_BINARY"
|
||||
|
||||
DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
|
||||
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"
|
||||
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 "$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"
|
||||
rpm -qpi "$RPM_FILE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
|
||||
for deb in "$DEB_FILE" "$AGENT_DEB_FILE"; do
|
||||
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"
|
||||
|
||||
cp -f "$DEB_FILE" "$DATA_DIR/packages/deb/"
|
||||
cp -f "$RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
|
||||
cp -f "$DEB_FILE" "$AGENT_DEB_FILE" "$DATA_DIR/packages/deb/"
|
||||
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"
|
||||
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DEB_FILE" >/dev/null
|
||||
# Retention is per product, not per file pool: keep ten RustProxy versions and
|
||||
# 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)"
|
||||
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 \
|
||||
|| 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=""
|
||||
if [[ -L "$REPOSITORY/current" ]]; then
|
||||
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
||||
@@ -127,39 +208,68 @@ if [[ -L "$REPOSITORY/current" ]]; then
|
||||
fi
|
||||
|
||||
log "staging Linux repository snapshot"
|
||||
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/opnsense" ]]; then
|
||||
rsync -a --delete --link-dest="$CURRENT_TARGET/opnsense"/ "$CURRENT_TARGET/opnsense"/ "$RELEASE_DIR/opnsense"/
|
||||
if [[ -n "$CURRENT_TARGET" ]]; then
|
||||
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
|
||||
|
||||
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"/
|
||||
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"/
|
||||
rm -rf "$RELEASE_DIR/rpm/x86_64/repodata"
|
||||
createrepo_c --update "$RELEASE_DIR/rpm/x86_64" >/dev/null
|
||||
fi
|
||||
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)
|
||||
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
|
||||
GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE")
|
||||
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" \
|
||||
--output "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/rpm/x86_64/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 \
|
||||
|| fatal "RPM metadata signature verification failed"
|
||||
--output "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml"
|
||||
gpg --batch --verify "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml" >/dev/null 2>&1 \
|
||||
|| fatal "RPM metadata signature verification failed: $rpm_root"
|
||||
done
|
||||
|
||||
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
|
||||
[[ -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"
|
||||
cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO
|
||||
app_version=$VERSION
|
||||
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
|
||||
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_ref=$SOURCE_BRANCH
|
||||
source_commit=$SHA
|
||||
build_reason=$BUILD_REASON
|
||||
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" ]]
|
||||
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
|
||||
[[ -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"
|
||||
rm -f "$REPOSITORY/current.new"
|
||||
|
||||
@@ -24,16 +24,33 @@ trap cleanup_opnsense_release EXIT
|
||||
FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
|
||||
/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"
|
||||
else
|
||||
OPNSENSE_PACKAGE_VERSION="${VERSION}_${SOURCE_REVISION}"
|
||||
OPNSENSE_PACKAGE_VERSION="${VERSION}_${OPNSENSE_PACKAGE_REVISION}"
|
||||
fi
|
||||
OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg"
|
||||
/opt/rust-package-builder/scripts/package-opnsense.sh \
|
||||
"$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=""
|
||||
if [[ -L "$REPOSITORY/current" ]]; then
|
||||
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
||||
@@ -42,12 +59,12 @@ fi
|
||||
|
||||
log "staging OPNsense repository snapshot"
|
||||
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
|
||||
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
|
||||
fi
|
||||
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
|
||||
cp -a "$CURRENT_TARGET/$file" "$RELEASE_DIR/$file"
|
||||
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"
|
||||
cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO
|
||||
app_version=$VERSION
|
||||
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
|
||||
package_version=$OPNSENSE_PACKAGE_VERSION
|
||||
source_url=$SOURCE_REPO
|
||||
source_ref=$SOURCE_BRANCH
|
||||
source_commit=$SHA
|
||||
build_reason=$BUILD_REASON
|
||||
source_revision=$SOURCE_REVISION
|
||||
packaging_revision=$OPNSENSE_PACKAGING_REVISION
|
||||
opnsense_series=$OPNSENSE_ABI
|
||||
freebsd_release=$FREEBSD_RELEASE
|
||||
freebsd_abi=$FREEBSD_ABI
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
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}
|
||||
OUTPUT_BINARY=${2:?output binary required}
|
||||
@@ -17,7 +18,7 @@ rsync -a --delete \
|
||||
"$SOURCE_DIR/" "$BUILD_SOURCE/"
|
||||
|
||||
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
|
||||
log "FreeBSD/OPNsense source patches disabled"
|
||||
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}"
|
||||
cd "$BUILD_SOURCE"
|
||||
FREEBSD_RUST_TOOLCHAIN=$("$BUILDER_ROOT/scripts/ensure-rust-toolchain.sh" "$BUILD_SOURCE" freebsd)
|
||||
if [[ -f Cargo.lock ]]; then
|
||||
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd
|
||||
else
|
||||
|
||||
@@ -6,28 +6,35 @@ SHA=${1:?commit SHA required}
|
||||
BUILD_REASON=${2:-new-commit}
|
||||
BUILD_LINUX=${3:-1}
|
||||
BUILD_OPNSENSE=${4:-1}
|
||||
BUILD_FREEBSD=${5:-0}
|
||||
|
||||
[[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX 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}
|
||||
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"
|
||||
PKG="$WORK/package"
|
||||
OUT="$WORK/out"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
|
||||
rm -rf "$WORK"
|
||||
rm -rf -- "$WORK"
|
||||
else
|
||||
log "workspace kept: $WORK"
|
||||
fi
|
||||
if is_true "${CLEAN_BUILD_CACHE:-1}"; then
|
||||
rm -rf -- "$CARGO_TARGET_DIR" "${GOCACHE:-$BUILD_TMP_DIR/go-cache}"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$SRC" "$PKG" "$OUT"
|
||||
mkdir -p "$SRC" "$PKG" "$OUT" "$CARGO_TARGET_DIR"
|
||||
|
||||
log "cloning source"
|
||||
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")
|
||||
|
||||
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
|
||||
|
||||
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 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
|
||||
OPNSENSE_OK=1
|
||||
FREEBSD_OK=1
|
||||
|
||||
if [[ "$BUILD_LINUX" == "1" ]]; then
|
||||
if /opt/rust-package-builder/scripts/build-linux-release.sh; then
|
||||
@@ -125,15 +133,31 @@ if [[ "$BUILD_OPNSENSE" == "1" ]]; then
|
||||
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=""
|
||||
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_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"
|
||||
log "all branches complete for $SHA"
|
||||
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
|
||||
fi
|
||||
+42
-13
@@ -10,8 +10,19 @@ linux_release_complete_for_commit() {
|
||||
[[ -d "$CURRENT" ]] || return 1
|
||||
[[ -f "$CURRENT/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] || 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
|
||||
fi
|
||||
|
||||
@@ -41,17 +52,34 @@ opnsense_release_complete_for_commit() {
|
||||
[[ -f "$opnsense/os-rustproxy-latest.pkg" ]] || 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 "packaging_revision=$OPNSENSE_PACKAGING_REVISION" "$opnsense/BUILD-INFO.txt" || return 1
|
||||
[[ -d "$package_dir" ]] || return 1
|
||||
find "$package_dir" -maxdepth 1 -type f -name 'os-rustproxy-*.pkg' -print -quit | grep -q . || return 1
|
||||
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"
|
||||
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"
|
||||
|
||||
BUILD_LINUX=1
|
||||
BUILD_OPNSENSE=1
|
||||
BUILD_FREEBSD=0
|
||||
if linux_release_complete_for_commit "$REMOTE_SHA"; then
|
||||
BUILD_LINUX=0
|
||||
# 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"
|
||||
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"
|
||||
log "no changes: Linux and OPNsense are complete for $REMOTE_SHA"
|
||||
log "no changes: all enabled repositories are complete for $REMOTE_SHA"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$BUILD_LINUX" == "1" && "$BUILD_OPNSENSE" == "1" ]]; then
|
||||
REASON="new-or-missing-both"
|
||||
elif [[ "$BUILD_LINUX" == "1" ]]; then
|
||||
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"
|
||||
REASON="missing-or-outdated-channel"
|
||||
log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD reason=$REASON"
|
||||
/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE" "$BUILD_FREEBSD"
|
||||
Executable
+133
@@ -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
@@ -16,6 +16,12 @@ FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
|
||||
FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN:-1.88.0}
|
||||
APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-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 >= 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"
|
||||
[[ "$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"
|
||||
[[ "$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 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 OPNSENSE_ABI OPNSENSE_RETENTION
|
||||
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 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 GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg}
|
||||
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
|
||||
require_cmd "$cmd"
|
||||
done
|
||||
@@ -52,12 +68,34 @@ mkdir -p \
|
||||
"$DATA_DIR/repository/releases" \
|
||||
"$DATA_DIR/state" \
|
||||
"$DATA_DIR/cargo" \
|
||||
"$DATA_DIR/go/cache" \
|
||||
"$DATA_DIR/work" \
|
||||
"$DATA_DIR/packages/rpm/x86_64" \
|
||||
"$DATA_DIR/packages/suse/x86_64" \
|
||||
"$DATA_DIR/packages/deb"
|
||||
mkdir -p "$GNUPGHOME"
|
||||
mkdir -p "$BUILD_TMP_DIR/work" "$CARGO_TARGET_DIR" "$GOCACHE" "$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"
|
||||
log "importing GPG signing 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
|
||||
|
||||
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
|
||||
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"
|
||||
|
||||
Executable
+30
@@ -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
|
||||
@@ -36,8 +36,8 @@ a{{color:inherit}} .meta{{display:flex;gap:16px;flex-wrap:wrap;opacity:.82;font-
|
||||
</head>
|
||||
<body>
|
||||
<h1>{h(package)} — package repository</h1>
|
||||
<p class="lead">Signed DEB and 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>
|
||||
<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 + RPM/Zypper</span></div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Debian / Ubuntu</h2>
|
||||
@@ -68,10 +68,37 @@ REPO
|
||||
sudo dnf install {h(package)}</code></pre>
|
||||
</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">
|
||||
<h2>OPNsense / FreeBSD pkg</h2>
|
||||
<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>Native FreeBSD pkg (if enabled): <a href="{h(opn_url)}/freebsd/">{h(opn_url)}/freebsd/</a>.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@@ -79,9 +106,10 @@ sudo dnf install {h(package)}</code></pre>
|
||||
<ul>
|
||||
<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)}/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>
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -39,6 +39,10 @@ code{{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}}
|
||||
<p><strong>OPNsense series:</strong> {h(series)} <strong>ABI:</strong> {h(abi)}</p>
|
||||
</div>
|
||||
<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>
|
||||
<pre><code>mkdir -p /usr/local/etc/pkg/repos
|
||||
cat > /usr/local/etc/pkg/repos/RustProxy.conf <<'REPO'
|
||||
|
||||
@@ -11,7 +11,7 @@ from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
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:
|
||||
@@ -91,7 +91,7 @@ def make_tar(dst: Path, member_name: str, payload: bytes):
|
||||
os.replace(tmp, dst)
|
||||
|
||||
|
||||
def rebuild(repo: Path, retention: int):
|
||||
def rebuild(repo: Path, retention: int, repo_version: int = 1):
|
||||
packages = collect_packages(repo)
|
||||
if enforce_retention(packages, retention):
|
||||
packages = collect_packages(repo)
|
||||
@@ -111,7 +111,7 @@ def rebuild(repo: Path, retention: int):
|
||||
make_tar(repo / "data.pkg", "data", data)
|
||||
|
||||
meta = (
|
||||
'version: 1;\n'
|
||||
f'version: {repo_version};\n'
|
||||
'packing_format: "tar";\n'
|
||||
'manifests: "packagesite.yaml";\n'
|
||||
'manifests_archive: "packagesite";\n'
|
||||
@@ -130,11 +130,12 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue")
|
||||
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("--repo-version", type=int, choices=(1, 2), default=1, help="pkg repository format; 2 for native FreeBSD 15")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo = args.repo.resolve()
|
||||
repo.mkdir(parents=True, exist_ok=True)
|
||||
rebuild(repo, args.retention)
|
||||
rebuild(repo, args.retention, args.repo_version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -34,7 +34,7 @@ if ! aptly -config="$APTLY_CONF" repo list -raw | grep -Fx package-builder >/dev
|
||||
package-builder >/dev/null
|
||||
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
|
||||
fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
|
||||
fi
|
||||
@@ -42,7 +42,10 @@ done
|
||||
|
||||
ln -sfn current/apt "$REPOSITORY/apt"
|
||||
ln -sfn current/rpm "$REPOSITORY/rpm"
|
||||
ln -sfn current/suse "$REPOSITORY/suse"
|
||||
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/index.html "$REPOSITORY/index.html"
|
||||
ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
|
||||
ln -sfn current/SUSE-BUILD-INFO.txt "$REPOSITORY/SUSE-BUILD-INFO.txt"
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
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() {
|
||||
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||
}
|
||||
@@ -38,3 +43,61 @@ atomic_write() {
|
||||
cat > "$tmp"
|
||||
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
|
||||
}
|
||||
@@ -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)
|
||||
Executable
+103
@@ -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"
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
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}
|
||||
BINARY=${2:?FreeBSD binary required}
|
||||
PACKAGE_FILE=${3:?output package file 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_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"
|
||||
|
||||
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"
|
||||
|
||||
POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre"
|
||||
|
||||
Executable
+220
@@ -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
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
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}
|
||||
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"
|
||||
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"
|
||||
POSTINST_SRC="$SRC/debian/${PACKAGE_NAME}.postinst"
|
||||
|
||||
@@ -28,21 +30,60 @@ After=network-online.target
|
||||
|
||||
[Service]
|
||||
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
|
||||
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]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
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
|
||||
install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment"
|
||||
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}"
|
||||
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
|
||||
install -D -m 0644 "$TMPFILES_SRC" "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf"
|
||||
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 \\
|
||||
--disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null
|
||||
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
|
||||
fi
|
||||
cat >> "$PKG/scripts/deb-postinstall.sh" <<SCRIPT
|
||||
@@ -108,7 +149,7 @@ fi
|
||||
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"
|
||||
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
|
||||
systemctl daemon-reload >/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/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/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/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
|
||||
@@ -183,6 +225,8 @@ depends:
|
||||
- ca-certificates
|
||||
- gzip
|
||||
- logrotate
|
||||
- python3
|
||||
- python3-pip
|
||||
- systemd
|
||||
scripts:
|
||||
postinstall: "$PKG/scripts/deb-postinstall.sh"
|
||||
@@ -195,6 +239,17 @@ deb:
|
||||
type: origin
|
||||
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
|
||||
name: $NAME_Q
|
||||
arch: amd64
|
||||
@@ -215,6 +270,7 @@ depends:
|
||||
- gzip
|
||||
- logrotate
|
||||
- shadow-utils
|
||||
- '((python3 >= 3.10 and python3-pip) or (python3.12 and python3.12-pip) or (python3.11 and python3.11-pip))'
|
||||
- systemd
|
||||
scripts:
|
||||
postinstall: "$PKG/scripts/rpm-postinstall.sh"
|
||||
@@ -225,3 +281,47 @@ rpm:
|
||||
signature:
|
||||
key_file: $GPG_KEY_Q
|
||||
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
|
||||
@@ -104,6 +104,9 @@ fi
|
||||
|
||||
[[ -f "$ENV_FILE" ]] || write_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}"
|
||||
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}"
|
||||
@@ -114,15 +117,15 @@ case "$SOURCE_REPO" in
|
||||
*) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;;
|
||||
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"
|
||||
[[ "$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"
|
||||
chmod 0700 "$GPG_DIR" 2>/dev/null || true
|
||||
|
||||
log "budowanie obrazu buildera (Linux + FreeBSD cross-toolchain)"
|
||||
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE"
|
||||
log "budowanie obrazu buildera oraz panelu repozytorium"
|
||||
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE" repository-dashboard
|
||||
|
||||
if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
|
||||
log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM"
|
||||
@@ -173,7 +176,7 @@ else
|
||||
log "uzywanie istniejacego klucza GPG"
|
||||
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"
|
||||
|
||||
fingerprint=""
|
||||
@@ -182,9 +185,11 @@ fingerprint=""
|
||||
log "gotowe"
|
||||
printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO"
|
||||
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 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}"
|
||||
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"
|
||||
printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository"
|
||||
printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user