Compare commits
23
Commits
34d7b656f5
..
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 |
No files matched your search
@@ -2,3 +2,4 @@ SOURCE_REPO=https://git.linuxiarz.pl/gru/RustProxy.git
|
|||||||
SOURCE_BRANCH=master
|
SOURCE_BRANCH=master
|
||||||
REPOSITORY_URL=https://packages.linuxiarz.pl
|
REPOSITORY_URL=https://packages.linuxiarz.pl
|
||||||
POLL_INTERVAL=300
|
POLL_INTERVAL=300
|
||||||
|
RUN_LINUX_TESTS=0
|
||||||
+13
-4
@@ -18,6 +18,8 @@ RUN set -eux; \
|
|||||||
mkdir -p /freebsd-sysroot; \
|
mkdir -p /freebsd-sysroot; \
|
||||||
tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib
|
tar -xJf base.txz -C /freebsd-sysroot ./lib ./usr/include ./usr/lib
|
||||||
|
|
||||||
|
FROM golang:1.26-bookworm AS go-toolchain
|
||||||
|
|
||||||
FROM rockylinux:9
|
FROM rockylinux:9
|
||||||
|
|
||||||
ARG RUST_TOOLCHAIN=stable
|
ARG RUST_TOOLCHAIN=stable
|
||||||
@@ -30,7 +32,7 @@ ARG APTLY_ZIP_SHA256=535ed24eb706bc25a316b59d709a7e74e10165f2ffab8552097ba7e4816
|
|||||||
RUN dnf -y install --allowerasing \
|
RUN dnf -y install --allowerasing \
|
||||||
bash binutils ca-certificates clang cmake coreutils createrepo_c curl file findutils \
|
bash binutils ca-certificates clang cmake coreutils createrepo_c curl file findutils \
|
||||||
gcc gcc-c++ git gnupg2 gzip libarchive lld llvm make openssl pkgconf-pkg-config \
|
gcc gcc-c++ git gnupg2 gzip libarchive lld llvm make openssl pkgconf-pkg-config \
|
||||||
python3 python3.11 rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 zstd \
|
python3 python3.11 rpm rsync shadow-utils sqlite-devel tar unzip xz bzip2 zstd bsdtar \
|
||||||
&& dnf clean all \
|
&& dnf clean all \
|
||||||
&& rm -rf /var/cache/dnf
|
&& rm -rf /var/cache/dnf
|
||||||
|
|
||||||
@@ -68,8 +70,12 @@ RUN set -eux; \
|
|||||||
rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version
|
rustc "+${FREEBSD_RUST_TOOLCHAIN}" --version
|
||||||
|
|
||||||
COPY --from=freebsd-sysroot /freebsd-sysroot /opt/freebsd-sysroot
|
COPY --from=freebsd-sysroot /freebsd-sysroot /opt/freebsd-sysroot
|
||||||
|
COPY --from=go-toolchain /usr/local/go /usr/local/go
|
||||||
|
|
||||||
ENV FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
|
ENV PATH=/usr/local/go/bin:${PATH} \
|
||||||
|
GOPATH=/var/lib/package-builder/go \
|
||||||
|
GOCACHE=/tmp/rust-package-builder/go-cache \
|
||||||
|
FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
|
||||||
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \
|
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_LINKER=clang \
|
||||||
CC_x86_64_unknown_freebsd=clang \
|
CC_x86_64_unknown_freebsd=clang \
|
||||||
CXX_x86_64_unknown_freebsd=clang++ \
|
CXX_x86_64_unknown_freebsd=clang++ \
|
||||||
@@ -79,10 +85,13 @@ ENV FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN} \
|
|||||||
CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \
|
CXXFLAGS_x86_64_unknown_freebsd="--target=x86_64-unknown-freebsd15.1 --sysroot=/opt/freebsd-sysroot" \
|
||||||
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd15.1 -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld"
|
CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_64-unknown-freebsd15.1 -C link-arg=--sysroot=/opt/freebsd-sysroot -C link-arg=-fuse-ld=lld"
|
||||||
|
|
||||||
# Registry/git/target cache is persistent, while rustup/toolchains stay in the image.
|
# Cargo registry/git cache stays persistent; build target is disposable and
|
||||||
ENV CARGO_HOME=/var/lib/package-builder/cargo
|
# must not grow on the /data volume between package builds.
|
||||||
|
ENV CARGO_HOME=/var/lib/package-builder/cargo \
|
||||||
|
CARGO_TARGET_DIR=/tmp/rust-package-builder/cargo-target
|
||||||
|
|
||||||
COPY scripts/ /opt/rust-package-builder/scripts/
|
COPY scripts/ /opt/rust-package-builder/scripts/
|
||||||
|
COPY packaging/ /opt/rust-package-builder/packaging/
|
||||||
COPY opnsense/ /opt/rust-package-builder/opnsense/
|
COPY opnsense/ /opt/rust-package-builder/opnsense/
|
||||||
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py
|
RUN chmod 0755 /opt/rust-package-builder/scripts/*.sh /opt/rust-package-builder/scripts/*.py
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,59 @@
|
|||||||
# RustProxy unified package builder
|
# RustProxy unified package builder
|
||||||
|
|
||||||
|
Wydanie wdrożeniowe (odchudzone): zawiera panel, konfigurację Nginx oraz wszystkie skrypty budowania Linux, OPNsense i FreeBSD. Nie zawiera pustych szkieletów projektów; dołączono lekkie testy offline dla pakowania SUSE. Istniejące katalogi `data/`, `gpg/` i plik `.env` pozostają nienaruszone podczas rozpakowywania.
|
||||||
|
|
||||||
Jeden kontener buildera buduje z jednego commita RustProxy:
|
Jeden kontener buildera buduje z jednego commita RustProxy:
|
||||||
|
|
||||||
- Linux x86_64: DEB + podpisane repo APT,
|
- Linux x86_64: DEB + podpisane repo APT (`rustproxy` **oraz osobny** `rustproxy-docker-agent`),
|
||||||
- Linux x86_64: RPM + podpisane repo DNF/YUM,
|
- Linux x86_64: RPM + podpisane repo DNF/YUM (`rustproxy` **oraz osobny** `rustproxy-docker-agent`),
|
||||||
|
- **SUSE / openSUSE / openSUSE Leap x86_64**: oddzielne paczki RPM dostosowane do `zypper`, podpisane repo `suse/x86_64` (`rustproxy` i `rustproxy-docker-agent`),
|
||||||
- FreeBSD x86_64: cross-build bez VM FreeBSD,
|
- FreeBSD x86_64: cross-build bez VM FreeBSD,
|
||||||
- OPNsense: `os-rustproxy-<version>_<revision>.pkg` + statyczne repo `pkg`.
|
- OPNsense: `os-rustproxy-<version>_<revision>.pkg` + statyczne repo `pkg`,
|
||||||
|
- **natywne FreeBSD** (domyślnie włączone): `rustproxy-<version>_<revision>.pkg` + osobny katalog repo `pkg` pod `/freebsd/` (bez zależności OPNsense).
|
||||||
|
|
||||||
Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux i gałąź OPNsense są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD.
|
**Agent Docker jest pakowany wyłącznie na Linux (DEB/RPM); brak paczki FreeBSD pkg dla agenta.** Nie ma Docker-in-Docker, SCP ani osobnego watchera repo OPNsense. FreeBSD sysroot, clang/lld i Rust target są częścią głównego obrazu buildera. OPNsense dostaje osobny publiczny adres. Gałąź Linux, gałąź OPNsense i gałąź natywnego FreeBSD są budowane oraz publikowane niezależnie, więc błąd testów Linux nie blokuje pakietu FreeBSD.
|
||||||
|
|
||||||
|
## Panel repozytorium (RustProxy Package Registry)
|
||||||
|
|
||||||
|
The dashboard interface defaults to **English (`en-GB`)**, including its dates, navigation, notifications, and installation instructions. The **Docker discovery agent** installation card is shown only for **Linux (DEB/RPM, including SUSE/Zypper)**, never for OPNsense or native FreeBSD. Package listings/API also omit any stray agent PKG files for the FreeBSD channels.
|
||||||
|
|
||||||
|
Strony startowe repozytoriów (Linux DNF/APT, SUSE, OPNsense, FreeBSD) działają jako **lekka aplikacja Python** (`dashboard/`) za istniejącym Nginx. Wygląd nawiązuje do panelu RustProxy 0.34.6 (ciemny, jasny i kremowy motyw). **APT, RPM i pkg są nadal obsługiwane bezpośrednio przez Nginx**, więc mechanizm aktualizacji paczek, ścieżki plików, podpisy GPG i atomowe przełączanie `current` nie zmieniają się.
|
||||||
|
|
||||||
|
Panel udostępnia:
|
||||||
|
|
||||||
|
- **Przegląd**: najnowsza wersja, czas buildu, commit, stan Linux / SUSE / OPNsense / FreeBSD, statystyki i ostatnie paczki.
|
||||||
|
- **Paczki i wersje**: pełny katalog z nazwą, wersją, architekturą, datą, rozmiarem i linkiem pobrania; wyszukiwanie, sortowanie oraz filtry formatów/kanałów.
|
||||||
|
- **Instalacja**: aktualne polecenia dla APT, DNF, Zypper, OPNsense i natywnego FreeBSD oraz przyciski kopiowania.
|
||||||
|
- **Repozytoria**: linki do metadanych, katalogów i klucza GPG. Automatyczne odświeżanie co **60 sekund** oraz ręczne odświeżenie.
|
||||||
|
|
||||||
|
Adresy paneli (bez zmian domen i portów):
|
||||||
|
|
||||||
|
```text
|
||||||
|
REPOSITORY_URL/ Linux DEB + RPM (DNF) + SUSE RPM (Zypper)
|
||||||
|
REPOSITORY_URL/suse/x86_64/ podpisane RPM i metadane dla SUSE/openSUSE
|
||||||
|
OPNSENSE_REPOSITORY_URL/ OPNsense pkg
|
||||||
|
OPNSENSE_REPOSITORY_URL/freebsd/ Native FreeBSD pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Publiczne API, np. `https://twoje-repo.example/api/v1/packages`, zwraca JSON z `builds`, `packages` i `summary`. API jest dostępne na obu hostach. Panel nie zapisuje nic w repo; odczytuje wyłącznie opublikowany snapshot `data/repository/current`. Kontener Python nie publikuje portu hosta i ma dostęp **tylko do odczytu**.
|
||||||
|
|
||||||
|
Nowe buildy zapisują `built_at` w `LINUX-BUILD-INFO.txt` oraz `BUILD-INFO.txt` dla OPNsense/FreeBSD (UTC, ISO 8601). **Dla wydań sprzed tej zmiany** panel pokazuje czas modyfikacji pliku, oznaczony w podpowiedzi; nie twierdzi, że to potwierdzony czas buildu. Przełączanie wersji działa automatycznie i nie wymaga ponownego uruchamiania panelu.
|
||||||
|
|
||||||
|
Uruchomienie po aktualizacji projektu:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./start.sh
|
||||||
|
# lub ręcznie (przy ustawionym .env):
|
||||||
|
docker compose --env-file .env build repository-dashboard
|
||||||
|
docker compose --env-file .env up -d repository-dashboard repository-http
|
||||||
|
|
||||||
|
# API i zdrowie nginx
|
||||||
|
curl -fsS http://127.0.0.1:18765/api/v1/packages
|
||||||
|
curl -fsS http://127.0.0.1:18765/healthz
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Panel zależy wyłącznie od biblioteki standardowej Pythona, bez dodatkowych pakietów pip. Jeżeli nie ma jeszcze buildów, pokaże komunikat **brak paczek**, a nie fikcyjne wersje.
|
||||||
|
|
||||||
## Szybki start
|
## Szybki start
|
||||||
|
|
||||||
@@ -30,7 +76,7 @@ Dla `http://` bez portu `start.sh` dopisze domyślny port. Dla `https://` zakła
|
|||||||
Domyślne lokalne porty:
|
Domyślne lokalne porty:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
APT/RPM: 18765 -> nginx :8080
|
APT/RPM/Zypper: 18765 -> nginx :8080
|
||||||
OPNsense: 18766 -> nginx :8081
|
OPNsense: 18766 -> nginx :8081
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -49,17 +95,80 @@ opn-rustproxy.linuxiarz.pl -> http://builder:18766
|
|||||||
wspólny checkout
|
wspólny checkout
|
||||||
/ \
|
/ \
|
||||||
/ \
|
/ \
|
||||||
Linux branch OPNsense branch
|
Linux branch FreeBSD cross-build
|
||||||
cargo test izolowana kopia źródła
|
cargo test |
|
||||||
cargo build poprawki FreeBSD
|
cargo build +---+---+
|
||||||
DEB + RPM cross-build FreeBSD
|
go test/build agent | |
|
||||||
APT + DNF os-rustproxy.pkg
|
2x DEB + 2x RPM + 2x SUSE RPM | |
|
||||||
| |
|
APT + DNF + Zypper OPNsense FreeBSD native*
|
||||||
v v
|
| | |
|
||||||
publikacja Linux publikacja OPNsense
|
v v v
|
||||||
|
publikacja Linux os-rustproxy rustproxy.pkg
|
||||||
|
pkg repo pkg repo
|
||||||
|
|
||||||
|
* FreeBSD native: enabled by default; FREEBSD_NATIVE_ENABLED=0 disables it
|
||||||
```
|
```
|
||||||
|
|
||||||
`cargo test` należy tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit` i `last_opnsense_commit`, więc kolejny cykl ponawia tylko brakującą gałąź.
|
`cargo test` i `go test ./...` (agenta) należą tylko do gałęzi Linux. Jeśli testy nie przejdą, DEB/RPM/SUSE nie są publikowane, ale builder nadal uruchamia cross-build FreeBSD i może opublikować repo OPNsense. Stan jest śledzony osobno w `last_linux_commit`, `last_opnsense_commit` i (jeśli włączone) `last_freebsd_commit`; kolejny cykl ponawia tylko brakującą gałąź.
|
||||||
|
|
||||||
|
## Docker Agent: osobne pakiety Linux
|
||||||
|
|
||||||
|
Builder sprawdza `docker-agent/go.mod`, uruchamia `go test ./...`, kompiluje statyczny agent z `CGO_ENABLED=0` dla `linux/amd64` i pakuje **niezależnie od głównego RustProxy**. Wersja paczki oraz wydrukowana wersja Go agenta są zgodne z `Cargo.toml` danego commita; `PACKAGE_RELEASE` jest wspólne dla obu produktów w jednym wydaniu. Build gałęzi Linux kończy się błędem, jeśli budowanie/testy agenta nie przejdą, więc nie opublikuje repo z niekompletną parą. OPNsense i natywne FreeBSD pozostają niezależne i bez agenta.
|
||||||
|
|
||||||
|
Zawartość katalogu `data/packages/` po udanym cyklu:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/packages/deb/rustproxy_<version>-<release>_amd64.deb
|
||||||
|
data/packages/deb/rustproxy-docker-agent_<version>-<release>_amd64.deb
|
||||||
|
data/packages/rpm/x86_64/rustproxy-<version>-<release>.x86_64.rpm
|
||||||
|
data/packages/rpm/x86_64/rustproxy-docker-agent-<version>-<release>.x86_64.rpm
|
||||||
|
data/packages/suse/x86_64/rustproxy-<version>-<release>.x86_64.rpm
|
||||||
|
data/packages/suse/x86_64/rustproxy-docker-agent-<version>-<release>.x86_64.rpm
|
||||||
|
```
|
||||||
|
|
||||||
|
Wszystkie pakiety (DEB, DNF RPM i SUSE RPM) są podpisane istniejącym kluczem GPG repozytorium; metadane obu repozytoriów RPM również są podpisane. Każde repo zawiera `rustproxy` i `rustproxy-docker-agent`. Zmiana rewizji pakowania Linux (`PACKAGING_REVISION=6`) wymusza przebudowę także dla niezmienionego commita.
|
||||||
|
|
||||||
|
Instalacja na hoście Docker (uruchom Docker Engine wcześniej):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian / Ubuntu, po dodaniu repo APT
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install rustproxy-docker-agent
|
||||||
|
|
||||||
|
# RHEL / Rocky / Alma / Fedora, po dodaniu repo DNF
|
||||||
|
sudo dnf install rustproxy-docker-agent
|
||||||
|
|
||||||
|
# Konfiguracja w panelu RustProxy: Settings -> Docker agents
|
||||||
|
sudo rustproxy-docker-agent info
|
||||||
|
sudo rustproxy-docker-agent info --show-token
|
||||||
|
sudo rustproxy-docker-agent print-fingerprint
|
||||||
|
sudo systemctl status rustproxy-docker-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Instalacja z lokalnego pliku: `sudo apt install ./rustproxy-docker-agent_*.deb` lub `sudo dnf install ./rustproxy-docker-agent-*.rpm` (jedna pasująca wersja w katalogu). Dla pakietu nie trzeba mieć kompilatora Go. Skrypty `postinstall` zakładają konto systemowe, zabezpieczają `/etc/rustproxy/docker-agent.env`, inicjalizują długi losowy token oraz samopodpisany certyfikat i uruchamiają systemd, jeśli Docker socket jest dostępny. Tokenu **nie wypisują do logów dpkg/rpm**. `info --show-token` wywołuje administrator świadomie, lokalnie.
|
||||||
|
|
||||||
|
Konfiguracja i klucze pozostają po aktualizacji: `/etc/rustproxy/docker-agent.env` (`config|noreplace`, finalnie `0600`) oraz `/var/lib/rustproxy-docker-agent/agent.crt` i `agent.key` (klucz `0600`). Rotacja przez `sudo rustproxy-docker-agent rotate-token` wymaga `sudo systemctl restart rustproxy-docker-agent` i aktualizacji tokenu w panelu RustProxy. Do ręcznej instalacji, bez pakietu, pozostaje `cd docker-agent && sudo ./install.sh` po sklonowaniu projektu z Go 1.22+; instrukcje: `docker-agent/README.md` w repo RustProxy.
|
||||||
|
|
||||||
|
W przypadku przechodzenia z instalacji ręcznej na paczkę: jednostka w `/etc/systemd/system/rustproxy-docker-agent.service` ma pierwszeństwo przed jednostką paczkowaną w `/usr/lib/systemd/system`. Należy zachować token/certyfikat, zatrzymać usługę, przenieść ręczną jednostkę poza katalog systemd i wykonać `systemctl daemon-reload` przed zainstalowaniem paczki. Głównego `rustproxy` i jego konfiguracji to nie dotyczy.
|
||||||
|
|
||||||
|
## SUSE / openSUSE / openSUSE Leap — RPM / Zypper
|
||||||
|
|
||||||
|
Gałąź Linux buduje dodatkowo **dwie natywne paczki RPM dla SUSE** z tego samego commita: `rustproxy` i `rustproxy-docker-agent`. Mają inne zależności niż paczki dla RHEL (m.in. `shadow` zamiast `shadow-utils`) i oddzielne, podpisane repozytorium `suse/x86_64` z metadanymi `repomd.xml.asc` oraz informacją `SUSE-BUILD-INFO.txt`. DNF/YUM nie jest zmieniany. Paczki SUSE mają kompresję gzip dla zgodności ze starszymi klientami RPM.
|
||||||
|
|
||||||
|
Dla systemów SUSE z Pythonem systemowym starszym niż 3.10 paczka serwera dopuszcza doinstalowanie wersjonowanego Pythona (z `pip`), np. `python311` i `python311-pip`. Lokalny wrapper `/usr/libexec/rustproxy/python3` jest dodawany wyłącznie do ścieżki `PATH` usługi RustProxy przez drop-in systemd. **Nie podmienia** systemowego `/usr/bin/python3`. Zalecane środowiska: **SLES 15 SP6/SP7 i nowsze**, **SLES 16**, **openSUSE Leap 15.6 i 16.x**, **openSUSE Tumbleweed** z architekturą x86_64 i `glibc >= 2.34`. Dla starszych wydań z glibc 2.31 (np. SLES 15 SP5) paczka nie jest kompatybilna bez osobnego builda na starszym toolchainie. Zależności dla konkretnej wersji sprawdź przez `zypper` przed wdrożeniem.
|
||||||
|
|
||||||
|
Instalacja po pierwszej publikacji (zamień `REPOSITORY_URL` na adres serwera pakietów):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo rpm --import REPOSITORY_URL/repo-key.gpg
|
||||||
|
sudo zypper addrepo --refresh --gpgcheck REPOSITORY_URL/suse/x86_64/ rustproxy-suse
|
||||||
|
sudo zypper refresh rustproxy-suse
|
||||||
|
sudo zypper install rustproxy
|
||||||
|
# Opcjonalnie, na hoście z Docker Engine:
|
||||||
|
sudo zypper install rustproxy-docker-agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Aktualizacja: `sudo zypper update rustproxy rustproxy-docker-agent` (agent tylko gdy zainstalowany). Pakiety i repozytorium są publikowane atomowo wraz z APT/DNF jako część gałęzi Linux. Jeśli builder miał już wydanie z tą samą wersją, `PACKAGING_REVISION=6` wymusi jednorazową przebudowę; gałęzie OPNsense/FreeBSD zachowują dotychczasowy stan. **Paczki RPM nie znajdują się w samym archiwum projektu** — powstają po uruchomieniu buildera na źródłach RustProxy.
|
||||||
|
|
||||||
## OPNsense repo
|
## OPNsense repo
|
||||||
|
|
||||||
@@ -110,6 +219,9 @@ POLL_INTERVAL=300
|
|||||||
PACKAGE_RELEASE=auto
|
PACKAGE_RELEASE=auto
|
||||||
MAX_GLIBC_VERSION=2.34
|
MAX_GLIBC_VERSION=2.34
|
||||||
RUN_LINUX_TESTS=1
|
RUN_LINUX_TESTS=1
|
||||||
|
RELEASE_RETENTION=10
|
||||||
|
PACKAGE_RETENTION=10
|
||||||
|
CLEAN_BUILD_CACHE=1
|
||||||
|
|
||||||
OPNSENSE_ABI=26.7
|
OPNSENSE_ABI=26.7
|
||||||
OPNSENSE_RETENTION=10
|
OPNSENSE_RETENTION=10
|
||||||
@@ -120,10 +232,43 @@ FREEBSD_RUST_TOOLCHAIN=1.88.0
|
|||||||
APPLY_FREEBSD_PATCHES=1
|
APPLY_FREEBSD_PATCHES=1
|
||||||
```
|
```
|
||||||
|
|
||||||
`RUN_LINUX_TESTS=1` jest domyślne. Testy są bramką tylko dla DEB/RPM i nie wpływają na gałąź OPNsense. Ustaw `0`, jeśli chcesz całkowicie pominąć testy Linux.
|
`RUN_LINUX_TESTS=1` jest domyślne. Testy są bramką tylko dla DEB/RPM/SUSE i nie wpływają na gałąź OPNsense. Ustaw `0`, jeśli chcesz całkowicie pominąć testy Linux.
|
||||||
|
|
||||||
|
`PACKAGE_RETENTION=10` zachowuje ostatnie 10 wersji **osobno dla RustProxy i Docker Agenta** w każdym kanale Linux: DEB, RHEL/DNF RPM i SUSE RPM. Oznacza to do 10 kompletnych wersji obu produktów, a nie 10 plików łącznie.
|
||||||
|
|
||||||
|
`RELEASE_RETENTION=10` zachowuje 10 najnowszych atomowych snapshotów `data/repository/releases/`. Aktualnie serwowany snapshot jest zawsze chroniony przed usunięciem.
|
||||||
|
|
||||||
|
`CLEAN_BUILD_CACHE=1` powoduje usunięcie `cargo target` oraz cache kompilacji Go po każdym pełnym cyklu budowania. `CARGO_TARGET_DIR` i `GOCACHE` znajdują się pod `/tmp/rust-package-builder`, więc duże artefakty kompilacji nie trafiają już do trwałego `data/`. Ustaw `0`, jeśli świadomie chcesz zachować te cache pomiędzy buildami w działającym kontenerze.
|
||||||
|
|
||||||
`OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji.
|
`OPNSENSE_RETENTION=10` zachowuje ostatnie 10 wersji pakietu dla danej serii/architektury OPNsense. `0` wyłącza usuwanie starych wersji.
|
||||||
|
|
||||||
|
`FREEBSD_NATIVE_ENABLED=1` (domyślnie) włącza natywny pakiet FreeBSD i oddzielny kanał publikacji. `0` wyłącza budowanie natywnego FreeBSD. **Uwaga przy aktualizacji:** istniejący plik `.env` z `FREEBSD_NATIVE_ENABLED=0` ma pierwszeństwo przed nową wartością domyślną — zmień go ręcznie na `1`, inaczej pakiet FreeBSD nadal nie powstanie. `FREEBSD_NATIVE_RETENTION=10` ogranicza liczbę zachowanych wersji. Repo używa tego samego publicznego hosta/portu co OPNsense, ale pod oddzielnym prefiksem `OPNSENSE_REPOSITORY_URL/freebsd/`.
|
||||||
|
|
||||||
|
## Automatyczny Rust toolchain podczas budowania
|
||||||
|
|
||||||
|
Builder sprawdza `rust-version` w `Cargo.toml` **pobranego commita** i opcjonalny
|
||||||
|
`rust-toolchain.toml`/`rust-toolchain` przed kompilacją każdej gałęzi.
|
||||||
|
|
||||||
|
- **Linux DEB/RPM:** używa istniejącego `stable`; wywołuje `rustup` tylko,
|
||||||
|
gdy brakuje toolchainu albo wersja jest starsza od wymaganej przez projekt.
|
||||||
|
- **FreeBSD/OPNsense:** `FREEBSD_RUST_TOOLCHAIN` pozostaje bazowym toolchainem.
|
||||||
|
Gdy jest starszy od `rust-version`, builder automatycznie dobiera wersję
|
||||||
|
minimalną projektu oraz doinstalowuje target `x86_64-unknown-freebsd`.
|
||||||
|
- Jeśli projekt przypina konkretny toolchain, builder **respektuje pin**.
|
||||||
|
Sprzeczny pin i `rust-version` powodują czytelny błąd zamiast cichej zmiany.
|
||||||
|
- Testy i build danej gałęzi używają tego samego wybranego toolchainu.
|
||||||
|
- Aktualizacja nie zmienia globalnego `rustup default`, systemowych pakietów
|
||||||
|
ani wersji już opublikowanych pakietów RustProxy.
|
||||||
|
- Toolchainy pobrane w trakcie działania są w warstwie kontenera
|
||||||
|
(`/opt/rustup`); po **odtworzeniu kontenera** mogą wymagać ponownego pobrania.
|
||||||
|
Cache Cargo w `data/cargo` pozostaje trwały.
|
||||||
|
- Jeśli pobranie toolchainu lub kompilacja się nie powiedzie, dany kanał
|
||||||
|
nie zostaje opublikowany; pozostały kanał builder obsługuje niezależnie.
|
||||||
|
|
||||||
|
Działanie nie wymaga nowej opcji w `.env`. Uruchom `./start.sh`, żeby
|
||||||
|
przebudować obraz z nowym skryptem. Aktualizacje toolchainu na podstawie
|
||||||
|
źródeł będą potem automatyczne przy kolejnych buildach.
|
||||||
|
|
||||||
## FreeBSD cross-build
|
## FreeBSD cross-build
|
||||||
|
|
||||||
Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa:
|
Obraz buildera pobiera i weryfikuje `base.txz` dla `FREEBSD_RELEASE`, a następnie używa:
|
||||||
@@ -148,12 +293,88 @@ Kod pluginu OPNsense znajduje się w:
|
|||||||
opnsense/plugin/
|
opnsense/plugin/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## OPNsense: autostart i sprawdzanie aktualizacji aplikacji
|
||||||
|
|
||||||
|
W **Services -> RustProxy** zaznacz **Enabled** i naciśnij **Apply**. Pakiet
|
||||||
|
instaluje wykonywalny syshook w
|
||||||
|
`/usr/local/etc/rc.syshook.d/start/50-rustproxy.start`. Po restarcie systemu,
|
||||||
|
gdy sieć już działa, hook odświeża konfigurację z OPNsense i uruchamia usługę
|
||||||
|
tylko wtedy, gdy jest włączona. Powtórny start nie tworzy drugiego procesu.
|
||||||
|
|
||||||
|
Pole **Update check interval (seconds)** steruje argumentem RustProxy
|
||||||
|
`--update-check-interval-secs`. Domyślnie `3600` (godzina); dopuszczalny zakres
|
||||||
|
to `60`–`604800` sekund. Wartość nie dotyczy `POLL_INTERVAL` buildera,
|
||||||
|
który nadal odpowiada wyłącznie za sprawdzanie repozytorium kodu.
|
||||||
|
|
||||||
|
Sprawdzenie po restarcie OPNsense:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ls -l /usr/local/etc/rc.syshook.d/start/50-rustproxy.start
|
||||||
|
configctl rustproxy status
|
||||||
|
cat /var/run/rustproxy/command.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
W `command.txt` powinien wystąpić aktualnie ustawiony parametr
|
||||||
|
`--update-check-interval-secs <liczba>`. Jeśli usługa nie wystartuje, sprawdź
|
||||||
|
`/var/log/rustproxy/rustproxy.log` oraz uruchom ręcznie:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
configctl template reload OPNsense/RustProxy
|
||||||
|
configctl rustproxy start
|
||||||
|
```
|
||||||
|
|
||||||
|
Obsługa miękkiego przeładowania zwiększa `OPNSENSE_PACKAGING_REVISION` do `4`.
|
||||||
|
Pakowanie Linux ma `PACKAGING_REVISION=6`, a natywne FreeBSD `FREEBSD_NATIVE_PACKAGING_REVISION=2`. Każdy kanał otrzymuje osobną aktualizację przy zmianie swojej rewizji.
|
||||||
|
|
||||||
|
## DEB/RPM: dostęp do plików strony i zewnętrznych certyfikatów
|
||||||
|
|
||||||
|
Od rewizji pakowania **5** jednostka `rustproxy.service` używa
|
||||||
|
`ProtectHome=read-only` zamiast `ProtectHome=true`. Dzięki temu katalogi
|
||||||
|
`/home/...`, `/var/www/...` i `/srv/...` mogą być czytane na podstawie
|
||||||
|
normalnych uprawnień grupowych lub ACL, bez lokalnych wyjątków systemd.
|
||||||
|
`ProtectSystem=strict` nadal chroni te lokalizacje przed zapisem. Builder
|
||||||
|
normalizuje również `ProtectHome` w starszym `debian/rustproxy.service`, jeśli
|
||||||
|
taki plik istnieje w pobranym źródle; sama rewizja pakowania wystarcza do
|
||||||
|
ponownego zbudowania DEB/RPM przy niezmienionym kodzie aplikacji.
|
||||||
|
|
||||||
|
Instrukcja trafia **do obu paczek** jako
|
||||||
|
`/usr/share/doc/rustproxy/FILESYSTEM_ACCESS.md` (źródło:
|
||||||
|
[`packaging/rustproxy-filesystem-access.md`](packaging/rustproxy-filesystem-access.md)).
|
||||||
|
Opisuje bezpieczne przyznanie grupy lub ACL, odświeżenie grup przez restart,
|
||||||
|
obsługę certyfikatów Certbot i ich symlinków oraz ograniczenie aplikacyjne:
|
||||||
|
zewnętrzne PEM wymagają zaufanych katalogów właściciela `root`/`rustproxy`.
|
||||||
|
Pakiet nie przypisuje automatycznie szerokich grup do użytkownika usługi,
|
||||||
|
nie zmienia uprawnień witryn ani nie nadpisuje istniejących drop-inów.
|
||||||
|
|
||||||
|
Test pakowania jest dostępny w pełnym repozytorium developerskim (nie jest dołączony do archiwum wdrożeniowego).
|
||||||
|
|
||||||
|
## Rotacja logów RustProxy
|
||||||
|
|
||||||
|
- **DEB/RPM:** builder zawsze umieszcza `packaging/rustproxy.logrotate` w `/etc/logrotate.d/rustproxy` (nFPM: `config|noreplace`). Logrotate jest zależnością obu pakietów. Reguła obejmuje `/var/log/rustproxy/*.jsonl`, `*.log` i `vhosts/*.jsonl`, z rotacją codzienną lub po przekroczeniu 100 MiB, 14 archiwami, gzip z opóźnioną kompresją, `copytruncate` oraz `su rustproxy rustproxy`.
|
||||||
|
- **OPNsense / natywne FreeBSD PKG:** pakiet dostarcza `/usr/local/etc/newsyslog.conf.d/rustproxy.conf` dla tych samych ścieżek; `newsyslog` wykonuje rotację codziennie lub przy rozmiarze 100 MiB, przechowuje 14 archiwów, pomija puste pliki i używa gzip z niekompresowanym najnowszym archiwum. Plik `*.log` jest ponownie otwierany przez `daemon(8)` po `SIGHUP` (opcja `-H` w skrypcie rc.d). OPNsense uruchamia usługę jako `root` (`root:wheel` w `newsyslog`); natywne FreeBSD działa jako `rustproxy` (`rustproxy:rustproxy` w `newsyslog`).
|
||||||
|
|
||||||
|
**Uwaga FreeBSD:** `newsyslog` działa przez zmianę nazwy pliku, nie ma `copytruncate`. Pliki JSONL i inne logi otwarte bezpośrednio przez aplikację muszą być przez nią ponownie otwierane po rotacji. W przeciwnym razie proces może pisać do poprzedniego (przeniesionego) pliku; zachowanie nie jest identyczne z Linux. Reguły są instalowane, ale na działającym OPNsense należy zweryfikować sposób otwierania plików przez RustProxy.
|
||||||
|
|
||||||
|
Bieżąca rewizja pakowania Linux wynosi `PACKAGING_REVISION=6` i `OPNSENSE_PACKAGING_REVISION=4`; nowy kanał używa `FREEBSD_NATIVE_PACKAGING_REVISION=2`. `check-and-build.sh` sprawdza osobno rewizje pakowania Linux, OPNsense i FreeBSD native; nowa wersja samej wtyczki przebudowuje **tylko OPNsense**, nawet przy niezmienionym SHA aplikacji. Rewizja pakietu PKG zwiększy się automatycznie. W przypadku zmiany rewizji Linux analogicznie odświeżane są DEB/RPM. Konfiguracja w `/etc/logrotate.d/rustproxy` jest oznaczona jako `noreplace`, więc lokalna wersja zmodyfikowana przez administratora może zostać zachowana (zależnie od menedżera pakietów).
|
||||||
|
|
||||||
|
Kontrola po zainstalowaniu:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# DEB / RPM (nie wykonuje rotacji)
|
||||||
|
logrotate -d /etc/logrotate.d/rustproxy
|
||||||
|
|
||||||
|
# OPNsense (nie wykonuje rotacji)
|
||||||
|
newsyslog -n -v -f /usr/local/etc/newsyslog.conf.d/rustproxy.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Testy integracyjne są dostępne w pełnym repozytorium developerskim.
|
||||||
|
|
||||||
## Dane
|
## Dane
|
||||||
|
|
||||||
```text
|
```text
|
||||||
data/
|
data/
|
||||||
├── aptly/
|
├── aptly/
|
||||||
├── cargo/
|
├── cargo/ # registry/git cache; bez target/
|
||||||
├── packages/
|
├── packages/
|
||||||
│ ├── deb/
|
│ ├── deb/
|
||||||
│ └── rpm/x86_64/
|
│ └── rpm/x86_64/
|
||||||
@@ -162,16 +383,17 @@ data/
|
|||||||
│ ├── apt -> current/apt
|
│ ├── apt -> current/apt
|
||||||
│ ├── rpm -> current/rpm
|
│ ├── rpm -> current/rpm
|
||||||
│ ├── opnsense -> current/opnsense
|
│ ├── opnsense -> current/opnsense
|
||||||
|
│ ├── freebsd -> current/freebsd
|
||||||
│ └── releases/
|
│ └── releases/
|
||||||
├── state/
|
├── state/
|
||||||
└── work/
|
└── work/
|
||||||
```
|
```
|
||||||
|
|
||||||
Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji Linux poprzedni katalog OPNsense jest zachowywany, a przy publikacji OPNsense zachowywane są aktualne APT/RPM. Dzięki temu obie gałęzie są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu.
|
Każda publikacja tworzy nowy snapshot `repository/releases/...` i atomowo przełącza `current`. Przy publikacji jednego kanału poprzednie katalogi pozostałych kanałów są zachowywane. Dzięki temu wszystkie aktywne kanały są niezależne, ale nginx nadal serwuje jeden spójny snapshot katalogu.
|
||||||
|
|
||||||
## HTTP i logi
|
## HTTP i logi
|
||||||
|
|
||||||
Jeden kontener nginx obsługuje oba repozytoria na dwóch lokalnych portach:
|
Jeden kontener nginx obsługuje repo Linux i repo OPNsense na dwóch lokalnych portach; natywne FreeBSD jest serwowane jako `/freebsd/` na porcie OPNsense:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose logs -f rust-package-builder
|
docker compose logs -f rust-package-builder
|
||||||
@@ -183,10 +405,7 @@ curl -f http://127.0.0.1:18766/healthz
|
|||||||
|
|
||||||
## Testy statyczne
|
## Testy statyczne
|
||||||
|
|
||||||
```bash
|
Skrypty diagnostyczne są dostępne w pełnym repozytorium developerskim; wydanie wdrożeniowe zawiera tylko pliki potrzebne do uruchomienia.
|
||||||
./tests/static.sh
|
|
||||||
./tests/check-rustproxy-source.sh /sciezka/do/RustProxy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Klucz GPG
|
## Klucz GPG
|
||||||
|
|
||||||
@@ -206,3 +425,75 @@ Jeżeli istnieje starsze `last_commit` oraz poprawne APT/RPM dla tego SHA, zosta
|
|||||||
## FreeBSD patch version detection
|
## FreeBSD patch version detection
|
||||||
|
|
||||||
The OPNsense branch reuses the Cargo package `VERSION` already detected by the main build pipeline. If `apply-freebsd-patches.sh` is run manually, it falls back to `RUST_PROXY_PYTHON` (`/usr/bin/python3.11`) so Rocky Linux 9's default Python 3.9 does not cause a missing `tomllib` error.
|
The OPNsense branch reuses the Cargo package `VERSION` already detected by the main build pipeline. If `apply-freebsd-patches.sh` is run manually, it falls back to `RUST_PROXY_PYTHON` (`/usr/bin/python3.11`) so Rocky Linux 9's default Python 3.9 does not cause a missing `tomllib` error.
|
||||||
|
|
||||||
|
### Czyszczenie danych po kompilacji i retencja
|
||||||
|
|
||||||
|
Builder nie zapisuje już `cargo target` w `data/cargo/target` ani `GOCACHE` w `data/go/cache`. Rejestr Cargo/cache Git oraz pobrane moduły Go mogą pozostać w `data/`, ale duże artefakty kompilacji trafiają pod `/tmp/rust-package-builder/`. Przy `CLEAN_BUILD_CACHE=1` (domyślnie) są usuwane po każdym cyklu, także gdy jedna z gałęzi zakończy się błędem. Workspace źródła i pliki pośrednie również są tworzone pod `/tmp/rust-package-builder/work` i standardowo usuwane po cyklu.
|
||||||
|
|
||||||
|
Przy starcie builder automatycznie usuwa pozostałości po starszych wydaniach: `data/cargo/target`, `data/go/cache` oraz stare katalogi w `data/work/`. Dzięki temu aktualizacja buildera odzyskuje miejsce bez ręcznego czyszczenia.
|
||||||
|
|
||||||
|
Retencja Linux jest kontrolowana przez `PACKAGE_RETENTION=10`: osobno zachowywane jest 10 DEB RustProxy, 10 DEB Docker Agenta, 10 RHEL RPM każdego produktu i 10 SUSE RPM każdego produktu. Po zmianie zestawu DEB lokalne repo Aptly jest synchronizowane z zachowanymi paczkami, stare snapshoty Aptly są usuwane, a `aptly db cleanup` zwalnia nieużywane obiekty.
|
||||||
|
|
||||||
|
`RELEASE_RETENTION=10` ogranicza liczbę atomowych katalogów `repository/releases`. Kolejne snapshoty korzystają z hardlinków do niezmienionych plików poprzedniego wydania, więc te same RPM-y i dane repozytorium nie są fizycznie kopiowane przy każdym buildzie.
|
||||||
|
|
||||||
|
Retencja OPNsense (`OPNSENSE_RETENTION=10`) i natywnego FreeBSD (`FREEBSD_NATIVE_RETENTION=10`) pozostaje niezależna.
|
||||||
|
|
||||||
|
|
||||||
|
## RustProxy 0.34.x: plik `--config` i miękki reload
|
||||||
|
|
||||||
|
- **Linux DEB/RPM:** gdy usługa systemd nie ma własnego `ExecReload`, builder dodaje wywołanie `rust-proxy --data-dir /var/lib/rustproxy --admin-listen 127.0.0.1:9090 reload`. `systemctl reload rustproxy` wykonuje przeładowanie przez administracyjne API, nie restartuje procesu. Usługi z `ProtectSystem=strict` otrzymują dodatkowo zapis do `-/etc/rustproxy`. Aby panel mógł zapisywać wskazany `--config`, plik i jego katalog muszą być własnością użytkownika usługi. Przykładowo można ustawić `RUST_PROXY_CONFIG=/etc/rustproxy/config.json` w `/etc/rustproxy/environment`, po uprzednim utworzeniu poprawnego pliku JSON.
|
||||||
|
- **OPNsense:** `configctl rustproxy reload` wykonuje miękkie przeładowanie z CLI. Skrypt `service.sh` nie robi już stop/start dla `reload`. Listener GUI po zmianie adresu/portu wymaga nadal `restart`, ponieważ samo przeładowanie nie zmienia gniazd nasłuchujących. Opcjonalna ścieżka `rustproxy_config_path` w `rc.conf` (absolutna, bez spacji) jest przekazywana jako `--config`; panel OPNsense nie ma jeszcze osobnego pola do jej edycji. CLI wymaga osiągalnego adresu administratora na loopback.
|
||||||
|
- **Czystemu FreeBSD** służy osobna paczka `rustproxy`, bez modeli MVC, configd, syshooków i pluginu OPNsense. Jej natywne `rc.d` uruchamia usługę na koncie `rustproxy`, domyślnie nasłuchuje na HTTP `8080`, admin na `127.0.0.1:9090`, a TLS jest domyślnie wyłączony. Po `sysrc rustproxy_enable=YES` można wykonać `service rustproxy start`, `service rustproxy reload` i `service rustproxy restart`. Ścieżkę JSON można przekazać przez `sysrc rustproxy_config=/usr/local/etc/rustproxy/config.json` po przygotowaniu prawidłowego pliku z uprawnieniami użytkownika `rustproxy`.
|
||||||
|
|
||||||
|
**Ważne:** CLI `reload` wymaga uruchomionego RustProxy, zainicjalizowanego administratora i API osiągalnego przez adres loopback. Zmiana adresów/portów nasłuchujących wymaga `restart`, a nie `reload`.
|
||||||
|
|
||||||
|
## Natywne repo FreeBSD — włączone domyślnie
|
||||||
|
|
||||||
|
Dla nowej instalacji FreeBSD native włącza się automatycznie. Jeśli masz już `.env`, **sprawdź**, czy ma:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FREEBSD_NATIVE_ENABLED=1
|
||||||
|
FREEBSD_NATIVE_RETENTION=10
|
||||||
|
```
|
||||||
|
|
||||||
|
Następnie `./start.sh` (przebudowa kontenerów buildera i panelu). Natywna paczka pojawi się pod:
|
||||||
|
|
||||||
|
```text
|
||||||
|
${OPNSENSE_REPOSITORY_URL}/freebsd/
|
||||||
|
RustProxy-FreeBSD.conf
|
||||||
|
meta.conf
|
||||||
|
packagesite.pkg
|
||||||
|
data.pkg
|
||||||
|
rustproxy-latest.pkg
|
||||||
|
FreeBSD:15:amd64/rustproxy-<wersja>_<revision>.pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Na **czystym FreeBSD 15 amd64**:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p /usr/local/etc/pkg/repos
|
||||||
|
fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf \
|
||||||
|
https://YOUR-OPNSENSE-REPO-HOST/freebsd/RustProxy-FreeBSD.conf
|
||||||
|
pkg update -f
|
||||||
|
pkg install rustproxy
|
||||||
|
sysrc rustproxy_enable=YES
|
||||||
|
service rustproxy start
|
||||||
|
service rustproxy reload
|
||||||
|
```
|
||||||
|
|
||||||
|
To osobny pakiet od `os-rustproxy`: nie należy instalować obu pakietów naraz, bo oba używają nazwy usługi i ścieżek RustProxy.
|
||||||
|
|
||||||
|
**Bezpieczeństwo repo:** aktualnie katalog `pkg` jest **niesygnowany**, analogicznie do istniejącego repo OPNsense (`signature_type: none`). To nadaje się do testów DEV na zaufanej infrastrukturze. Przed produkcyjnym wdrożeniem należy dodać podpis katalogu `pkg`, bezpieczną dystrybucję klucza publicznego i wykonać instalację oraz aktualizację na rzeczywistym FreeBSD. Generator katalogu korzysta z formatu repozytorium `pkg` w wersji **2** dla FreeBSD 15 (OPNsense zachowuje dotychczasowy format), ale testy offline nie zastępują testu klienta `pkg` na prawdziwym FreeBSD.
|
||||||
|
|
||||||
|
Pakietowe testy offline są dostępne w pełnym repozytorium developerskim (nie w wydaniu wdrożeniowym).
|
||||||
|
|
||||||
|
|
||||||
|
## Troubleshooting: no native FreeBSD build
|
||||||
|
|
||||||
|
1. Ensure `.env` contains `FREEBSD_NATIVE_ENABLED=1` (older installations often set `0`).
|
||||||
|
2. Rebuild/start: `docker compose --env-file .env up -d --build rust-package-builder repository-dashboard`.
|
||||||
|
3. Follow the builder logs: `docker compose --env-file .env logs -f rust-package-builder`.
|
||||||
|
4. Look for `build plan: ... freebsd=1` and then `FreeBSD native publication completed:`. If compilation fails, inspect the **first** `error:` in the same log. The OPNsense package uses the same FreeBSD cross-compiled executable but native FreeBSD publishes independently.
|
||||||
|
5. Check `data/repository/current/freebsd/BUILD-INFO.txt` and `data/repository/current/freebsd/meta.conf` (`version: 2;`). The API `/api/v1/packages` then shows the published FreeBSD package/version/build time.
|
||||||
|
|
||||||
|
The development test `tests/freebsd-release.sh` uses a simulated ELF to verify packaging/catalog/atomic publication without Docker. It **does not prove Rust compilation or installation on real FreeBSD**. For end-to-end validation, run `pkg update -f` and `pkg install rustproxy` on FreeBSD 15.1 amd64 against the published URL. The catalog is **not signed**, so only use a trusted HTTPS source during testing.
|
||||||
+69
@@ -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}"
|
PACKAGE_RELEASE: "${PACKAGE_RELEASE:-auto}"
|
||||||
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
|
MAX_GLIBC_VERSION: "${MAX_GLIBC_VERSION:-2.34}"
|
||||||
KEEP_WORKSPACE: "${KEEP_WORKSPACE:-0}"
|
KEEP_WORKSPACE: "${KEEP_WORKSPACE:-0}"
|
||||||
|
RELEASE_RETENTION: "${RELEASE_RETENTION:-10}"
|
||||||
|
PACKAGE_RETENTION: "${PACKAGE_RETENTION:-10}"
|
||||||
|
CLEAN_BUILD_CACHE: "${CLEAN_BUILD_CACHE:-1}"
|
||||||
|
BUILD_TMP_DIR: "${BUILD_TMP_DIR:-/tmp/rust-package-builder}"
|
||||||
OPNSENSE_ABI: "${OPNSENSE_ABI:-26.7}"
|
OPNSENSE_ABI: "${OPNSENSE_ABI:-26.7}"
|
||||||
OPNSENSE_RETENTION: "${OPNSENSE_RETENTION:-10}"
|
OPNSENSE_RETENTION: "${OPNSENSE_RETENTION:-10}"
|
||||||
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
|
FREEBSD_RELEASE: "${FREEBSD_RELEASE:-15.1-RELEASE}"
|
||||||
@@ -25,12 +29,35 @@ services:
|
|||||||
FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}"
|
FREEBSD_RUST_TOOLCHAIN: "${FREEBSD_RUST_TOOLCHAIN:-1.88.0}"
|
||||||
APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}"
|
APPLY_FREEBSD_PATCHES: "${APPLY_FREEBSD_PATCHES:-1}"
|
||||||
RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"
|
RUN_LINUX_TESTS: "${RUN_LINUX_TESTS:-1}"
|
||||||
|
FREEBSD_NATIVE_ENABLED: "${FREEBSD_NATIVE_ENABLED:-1}"
|
||||||
|
FREEBSD_NATIVE_RETENTION: "${FREEBSD_NATIVE_RETENTION:-10}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/var/lib/package-builder
|
- ./data:/var/lib/package-builder
|
||||||
- ./gpg:/run/secrets/gpg:ro
|
- ./gpg:/run/secrets/gpg:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
repository-dashboard:
|
||||||
|
build:
|
||||||
|
context: ./dashboard
|
||||||
|
image: rustproxy/repository-dashboard:local
|
||||||
|
environment:
|
||||||
|
REPOSITORY_ROOT: /srv/repository
|
||||||
|
REPOSITORY_URL: "${REPOSITORY_URL:?REPOSITORY_URL is required}"
|
||||||
|
OPNSENSE_REPOSITORY_URL: "${OPNSENSE_REPOSITORY_URL:?OPNSENSE_REPOSITORY_URL is required}"
|
||||||
|
REPO_DISTRIBUTION: "${REPO_DISTRIBUTION:-stable}"
|
||||||
|
REPO_COMPONENT: "${REPO_COMPONENT:-main}"
|
||||||
|
volumes:
|
||||||
|
- ./data/repository:/srv/repository:ro
|
||||||
|
read_only: true
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
repository-http:
|
repository-http:
|
||||||
|
depends_on:
|
||||||
|
- repository-dashboard
|
||||||
image: nginxinc/nginx-unprivileged:stable-alpine-slim
|
image: nginxinc/nginx-unprivileged:stable-alpine-slim
|
||||||
ports:
|
ports:
|
||||||
- "0.0.0.0:${HTTP_PORT:-18765}:8080"
|
- "0.0.0.0:${HTTP_PORT:-18765}:8080"
|
||||||
|
|||||||
+65
-21
@@ -23,7 +23,11 @@ http {
|
|||||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||||
scgi_temp_path /tmp/scgi_temp;
|
scgi_temp_path /tmp/scgi_temp;
|
||||||
|
|
||||||
# Linux APT/RPM repository.
|
# Redirect all 404 responses, including upstream errors.
|
||||||
|
proxy_intercept_errors on;
|
||||||
|
error_page 404 = @redirect_home;
|
||||||
|
|
||||||
|
# Linux repository
|
||||||
server {
|
server {
|
||||||
listen 8080 default_server;
|
listen 8080 default_server;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -32,11 +36,6 @@ http {
|
|||||||
index index.html;
|
index index.html;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "DENY" always;
|
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
|
||||||
add_header Cache-Control "max-age=600" always;
|
|
||||||
|
|
||||||
if ($request_method !~ ^(GET|HEAD)$) {
|
if ($request_method !~ ^(GET|HEAD)$) {
|
||||||
return 405;
|
return 405;
|
||||||
}
|
}
|
||||||
@@ -47,12 +46,21 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
try_files /index.html @linux_initializing;
|
proxy_set_header X-Repo-Site linux;
|
||||||
|
proxy_pass http://repository-dashboard:8000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location @linux_initializing {
|
location = /index.html {
|
||||||
default_type text/html;
|
proxy_set_header X-Repo-Site linux;
|
||||||
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy repository</title></head><body><h1>RustProxy Linux repository</h1><p>Repository is being initialized.</p></body></html>';
|
proxy_pass http://repository-dashboard:8000/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /ui/ {
|
||||||
|
proxy_pass http://repository-dashboard:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/v1/packages {
|
||||||
|
proxy_pass http://repository-dashboard:8000;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ (^|/)\. {
|
location ~ (^|/)\. {
|
||||||
@@ -60,12 +68,17 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
expires 10m;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Redirect 404 to homepage
|
||||||
|
location @redirect_home {
|
||||||
|
return 302 /;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# OPNsense/FreeBSD pkg repository. It has a separate public URL and local port,
|
# OPNsense / FreeBSD repository
|
||||||
# but follows the same atomic repository/current switch as the Linux repo.
|
|
||||||
server {
|
server {
|
||||||
listen 8081;
|
listen 8081;
|
||||||
server_name _;
|
server_name _;
|
||||||
@@ -74,11 +87,6 @@ http {
|
|||||||
index index.html;
|
index index.html;
|
||||||
autoindex off;
|
autoindex off;
|
||||||
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-Frame-Options "DENY" always;
|
|
||||||
add_header Referrer-Policy "no-referrer" always;
|
|
||||||
add_header Cache-Control "max-age=600" always;
|
|
||||||
|
|
||||||
if ($request_method !~ ^(GET|HEAD)$) {
|
if ($request_method !~ ^(GET|HEAD)$) {
|
||||||
return 405;
|
return 405;
|
||||||
}
|
}
|
||||||
@@ -89,12 +97,42 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
try_files /index.html @opnsense_initializing;
|
proxy_set_header X-Repo-Site opnsense;
|
||||||
|
proxy_pass http://repository-dashboard:8000/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location @opnsense_initializing {
|
location = /index.html {
|
||||||
default_type text/html;
|
proxy_set_header X-Repo-Site opnsense;
|
||||||
return 200 '<!doctype html><html><head><meta charset="utf-8"><title>RustProxy OPNsense repository</title></head><body><h1>RustProxy OPNsense repository</h1><p>Repository is being initialized.</p></body></html>';
|
proxy_pass http://repository-dashboard:8000/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /freebsd {
|
||||||
|
return 301 /freebsd/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /freebsd/ {
|
||||||
|
proxy_set_header X-Repo-Site freebsd;
|
||||||
|
proxy_pass http://repository-dashboard:8000/freebsd/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /freebsd/index.html {
|
||||||
|
proxy_set_header X-Repo-Site freebsd;
|
||||||
|
proxy_pass http://repository-dashboard:8000/freebsd/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ^~ /ui/ {
|
||||||
|
proxy_pass http://repository-dashboard:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /api/v1/packages {
|
||||||
|
proxy_pass http://repository-dashboard:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Serve FreeBSD package catalogs directly
|
||||||
|
location /freebsd/ {
|
||||||
|
alias /srv/repository/freebsd/;
|
||||||
|
expires 10m;
|
||||||
|
autoindex off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ (^|/)\. {
|
location ~ (^|/)\. {
|
||||||
@@ -102,7 +140,13 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
expires 10m;
|
||||||
try_files $uri =404;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Redirect 404 to homepage
|
||||||
|
location @redirect_home {
|
||||||
|
return 302 /;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+70
@@ -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_NAME= rustproxy
|
||||||
PLUGIN_VERSION?= 0.23.3
|
PLUGIN_VERSION?= 0.23.3
|
||||||
PLUGIN_REVISION?= 1
|
PLUGIN_REVISION?= 2
|
||||||
PLUGIN_COMMENT= RustProxy reverse proxy service
|
PLUGIN_COMMENT= RustProxy reverse proxy service
|
||||||
PLUGIN_MAINTAINER?= local@localhost
|
PLUGIN_MAINTAINER?= local@localhost
|
||||||
PLUGIN_WWW?= https://git.linuxiarz.pl/gru/RustProxy
|
PLUGIN_WWW?= https://git.linuxiarz.pl/gru/RustProxy
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# FreeBSD/OPNsense newsyslog equivalent for RustProxy log retention.
|
||||||
|
# Fields: path owner:group mode count size when flags [pidfile signal]
|
||||||
|
# 14 archives; rotate daily or above 100 MiB; gzip, keep newest uncompressed.
|
||||||
|
# G=glob, B=no injected text (important for JSONL), E=skip empty, P=delay gzip.
|
||||||
|
# The supervised *.log output is reopened on SIGHUP via daemon(8) -H.
|
||||||
|
# JSONL files and other application-owned logs cannot use Linux copytruncate:
|
||||||
|
# their writers must close/reopen the files to avoid writing to old inodes.
|
||||||
|
/var/log/rustproxy/*.jsonl root:wheel 0640 14 100M $D0 GBEZPN
|
||||||
|
/var/log/rustproxy/*.log root:wheel 0640 14 100M $D0 GBEZP /var/run/rustproxy/daemon.pid SIGHUP
|
||||||
|
/var/log/rustproxy/vhosts/*.jsonl root:wheel 0640 14 100M $D0 GBEZPN
|
||||||
@@ -18,12 +18,15 @@ load_rc_config "$name"
|
|||||||
: ${rustproxy_user:="root"}
|
: ${rustproxy_user:="root"}
|
||||||
: ${rustproxy_group:="wheel"}
|
: ${rustproxy_group:="wheel"}
|
||||||
: ${rustproxy_data_dir:="/var/db/rustproxy"}
|
: ${rustproxy_data_dir:="/var/db/rustproxy"}
|
||||||
|
# Optional external canonical JSON (absolute path without spaces).
|
||||||
|
: ${rustproxy_config_path:=""}
|
||||||
: ${rustproxy_log_dir:="/var/log/rustproxy"}
|
: ${rustproxy_log_dir:="/var/log/rustproxy"}
|
||||||
: ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"}
|
: ${rustproxy_log_file:="${rustproxy_log_dir}/rustproxy.log"}
|
||||||
: ${rustproxy_run_dir:="/var/run/rustproxy"}
|
: ${rustproxy_run_dir:="/var/run/rustproxy"}
|
||||||
: ${rustproxy_command:="/usr/local/sbin/rust-proxy"}
|
: ${rustproxy_command:="/usr/local/sbin/rust-proxy"}
|
||||||
: ${rustproxy_restart_delay:="5"}
|
: ${rustproxy_restart_delay:="5"}
|
||||||
: ${rustproxy_allow_remote_admin:="1"}
|
: ${rustproxy_allow_remote_admin:="1"}
|
||||||
|
: ${rustproxy_update_check_interval_secs:="3600"}
|
||||||
# FreeBSD uses jemalloc rather than glibc. Keep the same low-arena intent as
|
# FreeBSD uses jemalloc rather than glibc. Keep the same low-arena intent as
|
||||||
# Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob. rc.conf can override
|
# Linux MALLOC_ARENA_MAX=2 with the native jemalloc knob. rc.conf can override
|
||||||
# rustproxy_malloc_conf or set it to an empty value to disable the tuning.
|
# rustproxy_malloc_conf or set it to an empty value to disable the tuning.
|
||||||
@@ -147,7 +150,35 @@ rustproxy_admin_listen_resolved="$(rustproxy_resolve_listener \
|
|||||||
"$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \
|
"$rustproxy_admin_listen_legacy" "$rustproxy_admin_listen" \
|
||||||
"0.0.0.0" "9090" "admin")" || exit 1
|
"0.0.0.0" "9090" "admin")" || exit 1
|
||||||
|
|
||||||
rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs 0 serve --proxy-listen ${rustproxy_proxy_listen_resolved}"
|
# The CLI expects a positive interval (seconds), not a value hardcoded in rc.d.
|
||||||
|
# Validate again at runtime for hand-written or stale configurations.
|
||||||
|
rustproxy_valid_update_interval()
|
||||||
|
{
|
||||||
|
case "$1" in
|
||||||
|
''|*[!0-9]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
[ "$1" -ge 60 ] 2>/dev/null && [ "$1" -le 604800 ] 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! rustproxy_valid_update_interval "$rustproxy_update_check_interval_secs"; then
|
||||||
|
echo "Invalid RustProxy update check interval: ${rustproxy_update_check_interval_secs} (60-604800 seconds)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restrict to shell-safe absolute paths because rc.subr expands command_args.
|
||||||
|
rustproxy_config_arg=""
|
||||||
|
if [ -n "${rustproxy_config_path}" ]; then
|
||||||
|
case "${rustproxy_config_path}" in
|
||||||
|
/*) ;;
|
||||||
|
*) echo "rustproxy_config_path must be an absolute path" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "${rustproxy_config_path}" in
|
||||||
|
*[!a-zA-Z0-9_./-]*) echo "rustproxy_config_path contains unsupported characters" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
rustproxy_config_arg="--config ${rustproxy_config_path}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rustproxy_runtime_args="--data-dir ${rustproxy_data_dir} ${rustproxy_config_arg} --admin-listen ${rustproxy_admin_listen_resolved} --update-check-interval-secs ${rustproxy_update_check_interval_secs} serve --proxy-listen ${rustproxy_proxy_listen_resolved}"
|
||||||
if [ "$rustproxy_tls_enabled" = "1" ]; then
|
if [ "$rustproxy_tls_enabled" = "1" ]; then
|
||||||
rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}"
|
rustproxy_runtime_args="${rustproxy_runtime_args} --tls-listen ${rustproxy_tls_listen_resolved}"
|
||||||
fi
|
fi
|
||||||
@@ -163,12 +194,15 @@ rustproxy_daemon_user_arg=""
|
|||||||
if [ "$rustproxy_user" != "root" ]; then
|
if [ "$rustproxy_user" != "root" ]; then
|
||||||
rustproxy_daemon_user_arg="-u ${rustproxy_user}"
|
rustproxy_daemon_user_arg="-u ${rustproxy_user}"
|
||||||
fi
|
fi
|
||||||
command_args="-r -R ${rustproxy_restart_delay} -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}"
|
# -H allows daemon(8) to reopen the stdout/stderr log when newsyslog sends SIGHUP.
|
||||||
|
command_args="-r -R ${rustproxy_restart_delay} -H -P ${pidfile} -p ${child_pidfile} ${rustproxy_daemon_user_arg} -o ${rustproxy_log_file} ${rustproxy_command} ${rustproxy_runtime_args}"
|
||||||
required_files="${rustproxy_command}"
|
required_files="${rustproxy_command}"
|
||||||
start_precmd="rustproxy_prestart"
|
start_precmd="rustproxy_prestart"
|
||||||
start_postcmd="rustproxy_poststart"
|
start_postcmd="rustproxy_poststart"
|
||||||
stop_postcmd="rustproxy_poststop"
|
stop_postcmd="rustproxy_poststop"
|
||||||
status_cmd="rustproxy_status"
|
status_cmd="rustproxy_status"
|
||||||
|
extra_commands="reload"
|
||||||
|
reload_cmd="rustproxy_reload"
|
||||||
|
|
||||||
rustproxy_prestart()
|
rustproxy_prestart()
|
||||||
{
|
{
|
||||||
@@ -200,6 +234,7 @@ rustproxy_prestart()
|
|||||||
if [ -n "${rustproxy_malloc_conf}" ]; then
|
if [ -n "${rustproxy_malloc_conf}" ]; then
|
||||||
echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
|
echo "RustProxy allocator: MALLOC_CONF=${rustproxy_malloc_conf}"
|
||||||
fi
|
fi
|
||||||
|
echo "RustProxy update check interval: ${rustproxy_update_check_interval_secs}s"
|
||||||
echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
|
echo "RustProxy exec: ${rustproxy_command} ${rustproxy_runtime_args}"
|
||||||
printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
|
printf '%s\n' "${rustproxy_command} ${rustproxy_runtime_args}" > "${rustproxy_run_dir}/command.txt"
|
||||||
/bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
|
/bin/chmod 0640 "${rustproxy_run_dir}/command.txt"
|
||||||
@@ -268,6 +303,25 @@ rustproxy_status()
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rustproxy_reload()
|
||||||
|
{
|
||||||
|
# rust-proxy reload authenticates locally via SQLite; it must run as the
|
||||||
|
# same account as the daemon and address the listener through loopback.
|
||||||
|
rustproxy_child_pid >/dev/null || {
|
||||||
|
echo "RustProxy is not running; nothing to reload" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
_rp_port="${rustproxy_admin_listen_resolved##*:}"
|
||||||
|
case "${rustproxy_admin_listen_resolved}" in
|
||||||
|
\[*\]:*) _rp_local_admin="[::1]:${_rp_port}" ;;
|
||||||
|
*:*) _rp_local_admin="127.0.0.1:${_rp_port}" ;;
|
||||||
|
*) echo "Invalid RustProxy administration listen address" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
# The optional path is already checked for safe characters above.
|
||||||
|
${rustproxy_command} --data-dir "${rustproxy_data_dir}" ${rustproxy_config_arg} \
|
||||||
|
--admin-listen "${_rp_local_admin}" reload
|
||||||
|
}
|
||||||
|
|
||||||
rustproxy_poststop()
|
rustproxy_poststop()
|
||||||
{
|
{
|
||||||
/bin/rm -f "${child_pidfile}" "${pidfile}"
|
/bin/rm -f "${child_pidfile}" "${pidfile}"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# OPNsense invokes this hook after networking and configd are available.
|
||||||
|
# Regenerate from config.xml on every boot: /var and /usr/local/etc may have
|
||||||
|
# been restored, or the administrator may have changed the GUI settings.
|
||||||
|
# Retry briefly if configd is still finishing startup on slower firewalls.
|
||||||
|
attempt=1
|
||||||
|
while ! /usr/local/sbin/configctl template reload OPNsense/RustProxy; do
|
||||||
|
if [ "$attempt" -ge 3 ]; then
|
||||||
|
echo "RustProxy: failed to render configuration during boot (3 attempts)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
/bin/sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Use the helper directly: its enabled check is based on the freshly rendered
|
||||||
|
# configuration and it does not rely on configd reloading our action registry.
|
||||||
|
exec /usr/local/opnsense/scripts/rustproxy/service.sh boot
|
||||||
@@ -76,4 +76,16 @@
|
|||||||
<type>checkbox</type>
|
<type>checkbox</type>
|
||||||
<help>Pass --allow-remote-admin so the RustProxy panel accepts non-loopback clients. Keep firewall access restricted to trusted networks; do not expose the admin port to WAN unless this is intentional.</help>
|
<help>Pass --allow-remote-admin so the RustProxy panel accepts non-loopback clients. Keep firewall access restricted to trusted networks; do not expose the admin port to WAN unless this is intentional.</help>
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
|
<field>
|
||||||
|
<label>Updates</label>
|
||||||
|
<type>header</type>
|
||||||
|
<static>true</static>
|
||||||
|
</field>
|
||||||
|
<field>
|
||||||
|
<id>rustproxy.general.updateCheckIntervalSecs</id>
|
||||||
|
<label>Update check interval (seconds)</label>
|
||||||
|
<type>text</type>
|
||||||
|
<help>How often RustProxy checks for new application versions. Default: 3600 seconds (1 hour). Allowed range: 60-604800 seconds. Changes take effect after Apply.</help>
|
||||||
|
</field>
|
||||||
</form>
|
</form>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OPNsense\RustProxy\Migrations;
|
||||||
|
|
||||||
|
use OPNsense\Base\BaseModelMigration;
|
||||||
|
|
||||||
|
/** Fill the new update-check interval for existing installations. */
|
||||||
|
class M1_5_0 extends BaseModelMigration
|
||||||
|
{
|
||||||
|
public function run($model)
|
||||||
|
{
|
||||||
|
parent::run($model);
|
||||||
|
if (trim((string)$model->general->updateCheckIntervalSecs) === '') {
|
||||||
|
$model->general->updateCheckIntervalSecs = '3600';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<model>
|
<model>
|
||||||
<mount>//OPNsense/rustproxy</mount>
|
<mount>//OPNsense/rustproxy</mount>
|
||||||
<version>1.4.0</version>
|
<version>1.5.0</version>
|
||||||
<description>RustProxy service settings</description>
|
<description>RustProxy service settings</description>
|
||||||
<items>
|
<items>
|
||||||
<general>
|
<general>
|
||||||
@@ -57,6 +57,13 @@
|
|||||||
<Default>1</Default>
|
<Default>1</Default>
|
||||||
<Required>Y</Required>
|
<Required>Y</Required>
|
||||||
</allowRemoteAdmin>
|
</allowRemoteAdmin>
|
||||||
|
<updateCheckIntervalSecs type="IntegerField">
|
||||||
|
<Default>3600</Default>
|
||||||
|
<MinimumValue>60</MinimumValue>
|
||||||
|
<MaximumValue>604800</MaximumValue>
|
||||||
|
<Required>Y</Required>
|
||||||
|
<ValidationMessage>Enter the number of seconds between update checks (60-604800).</ValidationMessage>
|
||||||
|
</updateCheckIntervalSecs>
|
||||||
|
|
||||||
<!-- Legacy combined listener values retained for seamless upgrades. -->
|
<!-- Legacy combined listener values retained for seamless upgrades. -->
|
||||||
<proxyListen type="IPPortField"/>
|
<proxyListen type="IPPortField"/>
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ start_service()
|
|||||||
echo "RustProxy is disabled in OPNsense configuration"
|
echo "RustProxy is disabled in OPNsense configuration"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
# OPNsense may invoke rc.d in addition to this boot hook. Avoid a second
|
||||||
|
# daemon supervisor when the service is already running.
|
||||||
|
if "$RC_SCRIPT" status >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
"$RC_SCRIPT" onestart
|
"$RC_SCRIPT" onestart
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +36,19 @@ case "${1:-}" in
|
|||||||
stop)
|
stop)
|
||||||
stop_service
|
stop_service
|
||||||
;;
|
;;
|
||||||
restart|reload)
|
restart)
|
||||||
stop_service
|
stop_service
|
||||||
start_service
|
start_service
|
||||||
;;
|
;;
|
||||||
|
reload)
|
||||||
|
if [ "$enabled" != "1" ]; then
|
||||||
|
echo "RustProxy is disabled in OPNsense configuration" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# Soft reload is performed by rust-proxy CLI through the live admin API.
|
||||||
|
# Listener changes from the GUI still require the explicit restart action.
|
||||||
|
"$RC_SCRIPT" reload
|
||||||
|
;;
|
||||||
status)
|
status)
|
||||||
"$RC_SCRIPT" status
|
"$RC_SCRIPT" status
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ message:restarting RustProxy
|
|||||||
allowed_groups:wheel,wwwonly
|
allowed_groups:wheel,wwwonly
|
||||||
|
|
||||||
[reload]
|
[reload]
|
||||||
command:/usr/local/opnsense/scripts/rustproxy/service.sh restart
|
command:/usr/local/opnsense/scripts/rustproxy/service.sh reload
|
||||||
parameters:
|
parameters:
|
||||||
type:script
|
type:script
|
||||||
message:reloading RustProxy
|
message:reloading RustProxy
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ rustproxy_tls_port="{% if not helpers.empty('OPNsense.rustproxy.general.tlsPort'
|
|||||||
rustproxy_admin_address="{% if not helpers.empty('OPNsense.rustproxy.general.adminAddress') %}{{ OPNsense.rustproxy.general.adminAddress }}{% endif %}"
|
rustproxy_admin_address="{% if not helpers.empty('OPNsense.rustproxy.general.adminAddress') %}{{ OPNsense.rustproxy.general.adminAddress }}{% endif %}"
|
||||||
rustproxy_admin_port="{% if not helpers.empty('OPNsense.rustproxy.general.adminPort') %}{{ OPNsense.rustproxy.general.adminPort }}{% endif %}"
|
rustproxy_admin_port="{% if not helpers.empty('OPNsense.rustproxy.general.adminPort') %}{{ OPNsense.rustproxy.general.adminPort }}{% endif %}"
|
||||||
rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}"
|
rustproxy_allow_remote_admin="{% if not helpers.empty('OPNsense.rustproxy.general.allowRemoteAdmin') %}1{% else %}0{% endif %}"
|
||||||
|
# Existing installations without this setting use the 3600-second default.
|
||||||
|
rustproxy_update_check_interval_secs="{% if not helpers.empty('OPNsense.rustproxy.general.updateCheckIntervalSecs') %}{{ OPNsense.rustproxy.general.updateCheckIntervalSecs }}{% else %}3600{% endif %}"
|
||||||
|
|
||||||
# Legacy 1.2.x combined settings. They are only used when the corresponding
|
# Legacy 1.2.x combined settings. They are only used when the corresponding
|
||||||
# new split address/port fields are both empty.
|
# new split address/port fields are both empty.
|
||||||
|
|||||||
@@ -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
|
||||||
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"
|
||||||
+137
-27
@@ -32,8 +32,14 @@ if [[ "$REQUESTED_PACKAGE_RELEASE" == "auto" ]]; then
|
|||||||
[[ -r "$DATA_DIR/state/last_version" ]] && LAST_VERSION=$(cat "$DATA_DIR/state/last_version")
|
[[ -r "$DATA_DIR/state/last_version" ]] && LAST_VERSION=$(cat "$DATA_DIR/state/last_version")
|
||||||
[[ -r "$DATA_DIR/state/last_package_release" ]] && LAST_PACKAGE_RELEASE=$(cat "$DATA_DIR/state/last_package_release")
|
[[ -r "$DATA_DIR/state/last_package_release" ]] && LAST_PACKAGE_RELEASE=$(cat "$DATA_DIR/state/last_package_release")
|
||||||
|
|
||||||
|
CURRENT_LINUX_PACKAGING_REVISION=""
|
||||||
|
if [[ -r "$REPOSITORY/current/LINUX-BUILD-INFO.txt" ]]; then
|
||||||
|
CURRENT_LINUX_PACKAGING_REVISION=$(sed -n 's/^packaging_revision=//p' "$REPOSITORY/current/LINUX-BUILD-INFO.txt" | head -n1)
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$LAST_LINUX_COMMIT" == "$SHA" \
|
if [[ "$LAST_LINUX_COMMIT" == "$SHA" \
|
||||||
&& "$LAST_VERSION" == "$VERSION" \
|
&& "$LAST_VERSION" == "$VERSION" \
|
||||||
|
&& "$CURRENT_LINUX_PACKAGING_REVISION" == "$PACKAGING_REVISION" \
|
||||||
&& "$LAST_PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]]; then
|
&& "$LAST_PACKAGE_RELEASE" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
PACKAGE_RELEASE=$LAST_PACKAGE_RELEASE
|
PACKAGE_RELEASE=$LAST_PACKAGE_RELEASE
|
||||||
log "Linux repair build: reusing package release $PACKAGE_RELEASE"
|
log "Linux repair build: reusing package release $PACKAGE_RELEASE"
|
||||||
@@ -48,23 +54,42 @@ fi
|
|||||||
export PACKAGE_RELEASE
|
export PACKAGE_RELEASE
|
||||||
|
|
||||||
cd "$SRC"
|
cd "$SRC"
|
||||||
|
LINUX_RUST_TOOLCHAIN=$(/opt/rust-package-builder/scripts/ensure-rust-toolchain.sh "$SRC" linux)
|
||||||
log "Linux branch: version $VERSION release $PACKAGE_RELEASE"
|
log "Linux branch: version $VERSION release $PACKAGE_RELEASE"
|
||||||
|
|
||||||
if [[ "${RUN_LINUX_TESTS:-1}" == "1" ]]; then
|
TEST_MODE="${RUN_LINUX_TESTS:-compile}"
|
||||||
log "Linux cargo tests started"
|
case "$TEST_MODE" in
|
||||||
|
0|off|false|no)
|
||||||
|
log "Linux cargo tests skipped"
|
||||||
|
;;
|
||||||
|
1|compile|check|no-run)
|
||||||
|
log "Linux cargo tests compile check started (--no-run)"
|
||||||
if [[ -f Cargo.lock ]]; then
|
if [[ -f Cargo.lock ]]; then
|
||||||
cargo test --release --locked
|
cargo "+$LINUX_RUST_TOOLCHAIN" test --release --locked --no-run
|
||||||
else
|
else
|
||||||
cargo test --release
|
cargo "+$LINUX_RUST_TOOLCHAIN" test --release --no-run
|
||||||
fi
|
fi
|
||||||
log "Linux cargo tests completed"
|
log "Linux cargo tests compile check completed"
|
||||||
fi
|
;;
|
||||||
|
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"
|
log "Linux cargo build started"
|
||||||
if [[ -f Cargo.lock ]]; then
|
if [[ -f Cargo.lock ]]; then
|
||||||
cargo build --release --locked
|
cargo "+$LINUX_RUST_TOOLCHAIN" build --release --locked
|
||||||
else
|
else
|
||||||
cargo build --release
|
cargo "+$LINUX_RUST_TOOLCHAIN" build --release
|
||||||
fi
|
fi
|
||||||
log "Linux cargo build completed"
|
log "Linux cargo build completed"
|
||||||
|
|
||||||
@@ -79,25 +104,69 @@ if [[ -n "$MAX_GLIBC_FOUND" ]]; then
|
|||||||
log "glibc compatibility check: <= $MAX_GLIBC_FOUND"
|
log "glibc compatibility check: <= $MAX_GLIBC_FOUND"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The Go agent is packaged ONLY for Linux. A failure aborts the Linux branch
|
||||||
|
# before either APT or RPM metadata is published; OPNsense/FreeBSD are separate.
|
||||||
|
AGENT_SRC="$SRC/docker-agent"
|
||||||
|
[[ -f "$AGENT_SRC/go.mod" ]] || fatal "RustProxy source is missing docker-agent/go.mod"
|
||||||
|
AGENT_BINARY="$OUT/rustproxy-docker-agent"
|
||||||
|
AGENT_PKG="$WORK/package-agent"
|
||||||
|
log "Linux Docker Agent tests started"
|
||||||
|
(cd "$AGENT_SRC" && GOWORK=off CGO_ENABLED=0 go test ./...)
|
||||||
|
log "Linux Docker Agent build started"
|
||||||
|
(cd "$AGENT_SRC" && GOWORK=off CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w -X main.version=$VERSION" -o "$AGENT_BINARY" .)
|
||||||
|
[[ -x "$AGENT_BINARY" ]] || fatal "built Go agent not found: $AGENT_BINARY"
|
||||||
|
|
||||||
/opt/rust-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY_PATH"
|
/opt/rust-package-builder/scripts/prepare-package.sh "$SRC" "$PKG" "$BINARY_PATH"
|
||||||
|
/opt/rust-package-builder/scripts/prepare-agent-package.sh "$SRC" "$AGENT_PKG" "$AGENT_BINARY"
|
||||||
|
|
||||||
DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
|
DEB_FILE="$OUT/${PACKAGE_NAME}_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
|
||||||
RPM_FILE="$OUT/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
RPM_FILE="$OUT/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
||||||
|
AGENT_DEB_FILE="$OUT/rustproxy-docker-agent_${VERSION}-${PACKAGE_RELEASE}_amd64.deb"
|
||||||
|
AGENT_RPM_FILE="$OUT/rustproxy-docker-agent-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
||||||
|
SUSE_RPM_FILE="$OUT/suse/${PACKAGE_NAME}-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
||||||
|
SUSE_AGENT_RPM_FILE="$OUT/suse/rustproxy-docker-agent-${VERSION}-${PACKAGE_RELEASE}.x86_64.rpm"
|
||||||
|
mkdir -p "$OUT/suse"
|
||||||
|
|
||||||
log "creating deb package"
|
log "creating RustProxy and Docker Agent deb packages"
|
||||||
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB_FILE"
|
nfpm package --config "$PKG/nfpm-deb.yaml" --packager deb --target "$DEB_FILE"
|
||||||
log "creating rpm package"
|
nfpm package --config "$AGENT_PKG/nfpm-deb.yaml" --packager deb --target "$AGENT_DEB_FILE"
|
||||||
|
log "creating RustProxy and Docker Agent rpm packages"
|
||||||
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE"
|
nfpm package --config "$PKG/nfpm-rpm.yaml" --packager rpm --target "$RPM_FILE"
|
||||||
|
nfpm package --config "$AGENT_PKG/nfpm-rpm.yaml" --packager rpm --target "$AGENT_RPM_FILE"
|
||||||
|
log "creating SUSE/openSUSE RPM packages"
|
||||||
|
nfpm package --config "$PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE_RPM_FILE"
|
||||||
|
nfpm package --config "$AGENT_PKG/nfpm-suse.yaml" --packager rpm --target "$SUSE_AGENT_RPM_FILE"
|
||||||
|
|
||||||
ar t "$DEB_FILE" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package does not contain nFPM GPG origin signature"
|
for deb in "$DEB_FILE" "$AGENT_DEB_FILE"; do
|
||||||
rpm -qpi "$RPM_FILE" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' || fatal "RPM package is not signed"
|
ar t "$deb" | grep '^_gpgorigin$' >/dev/null || fatal "DEB package is not signed: $deb"
|
||||||
|
done
|
||||||
|
for rpm_file in "$RPM_FILE" "$AGENT_RPM_FILE" "$SUSE_RPM_FILE" "$SUSE_AGENT_RPM_FILE"; do
|
||||||
|
rpm -qpi "$rpm_file" | awk -F: '/^Signature/ && $0 !~ /\(none\)/ {ok=1} END{exit !ok}' \
|
||||||
|
|| fatal "RPM package is not signed: $rpm_file"
|
||||||
|
done
|
||||||
log "Linux package signatures verified"
|
log "Linux package signatures verified"
|
||||||
|
|
||||||
cp -f "$DEB_FILE" "$DATA_DIR/packages/deb/"
|
cp -f "$DEB_FILE" "$AGENT_DEB_FILE" "$DATA_DIR/packages/deb/"
|
||||||
cp -f "$RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
|
cp -f "$RPM_FILE" "$AGENT_RPM_FILE" "$DATA_DIR/packages/rpm/x86_64/"
|
||||||
|
cp -f "$SUSE_RPM_FILE" "$SUSE_AGENT_RPM_FILE" "$DATA_DIR/packages/suse/x86_64/"
|
||||||
|
|
||||||
log "updating aptly internal repository"
|
# Retention is per product, not per file pool: keep ten RustProxy versions and
|
||||||
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DEB_FILE" >/dev/null
|
# ten Docker Agent versions in each Linux package family by default.
|
||||||
|
package_retention=${PACKAGE_RETENTION:-10}
|
||||||
|
prune_package_files "$DATA_DIR/packages/deb" "${PACKAGE_NAME}_*.deb" "$package_retention"
|
||||||
|
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy-docker-agent_*.deb' "$package_retention"
|
||||||
|
prune_package_files "$DATA_DIR/packages/rpm/x86_64" "${PACKAGE_NAME}-*.x86_64.rpm" "$package_retention" 'rustproxy-docker-agent-*.x86_64.rpm'
|
||||||
|
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$package_retention"
|
||||||
|
prune_package_files "$DATA_DIR/packages/suse/x86_64" "${PACKAGE_NAME}-*.x86_64.rpm" "$package_retention" 'rustproxy-docker-agent-*.x86_64.rpm'
|
||||||
|
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$package_retention"
|
||||||
|
|
||||||
|
log "updating aptly internal repository from retained DEB packages"
|
||||||
|
# Make the mutable local repo match the retained raw DEB pool exactly. Existing
|
||||||
|
# snapshots are immutable and remain valid until the new snapshot is published.
|
||||||
|
aptly -config="$APTLY_CONF" repo remove package-builder "$PACKAGE_NAME" >/dev/null 2>&1 || true
|
||||||
|
aptly -config="$APTLY_CONF" repo remove package-builder rustproxy-docker-agent >/dev/null 2>&1 || true
|
||||||
|
aptly -config="$APTLY_CONF" repo add -force-replace package-builder "$DATA_DIR/packages/deb" >/dev/null
|
||||||
SNAPSHOT="linux-${VERSION}-${SHORT_SHA}-$(date +%s)"
|
SNAPSHOT="linux-${VERSION}-${SHORT_SHA}-$(date +%s)"
|
||||||
aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo package-builder >/dev/null
|
aptly -config="$APTLY_CONF" snapshot create "$SNAPSHOT" from repo package-builder >/dev/null
|
||||||
|
|
||||||
@@ -119,7 +188,19 @@ APT_PUBLIC="$DATA_DIR/aptly/public"
|
|||||||
gpg --batch --verify "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 \
|
gpg --batch --verify "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release.gpg" "$APT_PUBLIC/dists/$REPO_DISTRIBUTION/Release" >/dev/null 2>&1 \
|
||||||
|| fatal "APT Release signature verification failed"
|
|| fatal "APT Release signature verification failed"
|
||||||
|
|
||||||
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense"
|
# The new snapshot is now live. Older Linux snapshots are no longer needed;
|
||||||
|
# removing them lets aptly reclaim unreferenced pool objects.
|
||||||
|
while IFS= read -r old_snapshot; do
|
||||||
|
[[ -n "$old_snapshot" && "$old_snapshot" != "$SNAPSHOT" ]] || continue
|
||||||
|
if ! aptly -config="$APTLY_CONF" snapshot drop "$old_snapshot" >/dev/null 2>&1; then
|
||||||
|
log "WARN: could not drop old aptly snapshot $old_snapshot"
|
||||||
|
fi
|
||||||
|
done < <(aptly -config="$APTLY_CONF" snapshot list -raw | grep '^linux-' || true)
|
||||||
|
if ! aptly -config="$APTLY_CONF" db cleanup >/dev/null 2>&1; then
|
||||||
|
log "WARN: aptly database cleanup failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd"
|
||||||
CURRENT_TARGET=""
|
CURRENT_TARGET=""
|
||||||
if [[ -L "$REPOSITORY/current" ]]; then
|
if [[ -L "$REPOSITORY/current" ]]; then
|
||||||
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
||||||
@@ -127,39 +208,68 @@ if [[ -L "$REPOSITORY/current" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
log "staging Linux repository snapshot"
|
log "staging Linux repository snapshot"
|
||||||
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/opnsense" ]]; then
|
if [[ -n "$CURRENT_TARGET" ]]; then
|
||||||
rsync -a --delete --link-dest="$CURRENT_TARGET/opnsense"/ "$CURRENT_TARGET/opnsense"/ "$RELEASE_DIR/opnsense"/
|
for component in opnsense freebsd; do
|
||||||
|
if [[ -d "$CURRENT_TARGET/$component" ]]; then
|
||||||
|
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rsync -a --delete "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
|
if [[ -n "$CURRENT_TARGET" && -d "$CURRENT_TARGET/apt" ]]; then
|
||||||
rsync -a --delete "$DATA_DIR/packages/rpm"/ "$RELEASE_DIR/rpm"/
|
rsync -a --delete --link-dest="$CURRENT_TARGET/apt/" "$APT_PUBLIC"/ "$RELEASE_DIR/apt"/
|
||||||
rm -rf "$RELEASE_DIR/rpm/x86_64/repodata"
|
else
|
||||||
createrepo_c --update "$RELEASE_DIR/rpm/x86_64" >/dev/null
|
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"/
|
||||||
|
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)
|
GPG_SIGN_ARGS=(--batch --yes --pinentry-mode loopback)
|
||||||
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
|
if [[ -r "$GPG_PASSPHRASE_FILE" ]]; then
|
||||||
GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE")
|
GPG_SIGN_ARGS+=(--passphrase-file "$GPG_PASSPHRASE_FILE")
|
||||||
fi
|
fi
|
||||||
gpg "${GPG_SIGN_ARGS[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" \
|
for rpm_root in "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64"; do
|
||||||
--output "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml"
|
gpg "${GPG_SIGN_ARGS[@]}" --armor --detach-sign --local-user "$GPG_KEY_ID" \
|
||||||
gpg --batch --verify "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml.asc" "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" >/dev/null 2>&1 \
|
--output "$rpm_root/repodata/repomd.xml.asc" "$rpm_root/repodata/repomd.xml"
|
||||||
|| fatal "RPM metadata signature verification failed"
|
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"
|
gpg --batch --armor --export "$GPG_KEY_ID" > "$RELEASE_DIR/repo-key.gpg"
|
||||||
[[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed"
|
[[ -s "$RELEASE_DIR/repo-key.gpg" ]] || fatal "public GPG key export failed"
|
||||||
/opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME"
|
/opt/rust-package-builder/scripts/generate-index.sh "$RELEASE_DIR/index.html" "$PACKAGE_NAME"
|
||||||
cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO
|
cat > "$RELEASE_DIR/LINUX-BUILD-INFO.txt" <<INFO
|
||||||
app_version=$VERSION
|
app_version=$VERSION
|
||||||
|
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
|
||||||
package_release=$PACKAGE_RELEASE
|
package_release=$PACKAGE_RELEASE
|
||||||
|
packaging_revision=$PACKAGING_REVISION
|
||||||
|
linux_packages=$PACKAGE_NAME,rustproxy-docker-agent
|
||||||
|
linux_rpm_flavors=dnf,suse
|
||||||
source_url=$SOURCE_REPO
|
source_url=$SOURCE_REPO
|
||||||
source_ref=$SOURCE_BRANCH
|
source_ref=$SOURCE_BRANCH
|
||||||
source_commit=$SHA
|
source_commit=$SHA
|
||||||
build_reason=$BUILD_REASON
|
build_reason=$BUILD_REASON
|
||||||
INFO
|
INFO
|
||||||
|
cp -f "$RELEASE_DIR/LINUX-BUILD-INFO.txt" "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
|
||||||
|
printf '%s\n' 'rpm_flavor=suse' 'platforms=SLES_15_SP6+,SLES_16+,Leap_15.6+,Tumbleweed' >> "$RELEASE_DIR/SUSE-BUILD-INFO.txt"
|
||||||
|
|
||||||
[[ -s "$RELEASE_DIR/index.html" ]]
|
[[ -s "$RELEASE_DIR/index.html" ]]
|
||||||
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
|
[[ -f "$RELEASE_DIR/apt/dists/$REPO_DISTRIBUTION/InRelease" ]]
|
||||||
[[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]]
|
[[ -f "$RELEASE_DIR/rpm/x86_64/repodata/repomd.xml" ]]
|
||||||
|
[[ -f "$RELEASE_DIR/suse/x86_64/repodata/repomd.xml" ]]
|
||||||
|
[[ -s "$RELEASE_DIR/SUSE-BUILD-INFO.txt" ]]
|
||||||
|
|
||||||
log "publishing Linux repository branch"
|
log "publishing Linux repository branch"
|
||||||
rm -f "$REPOSITORY/current.new"
|
rm -f "$REPOSITORY/current.new"
|
||||||
|
|||||||
@@ -24,16 +24,33 @@ trap cleanup_opnsense_release EXIT
|
|||||||
FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
|
FREEBSD_BINARY="$OUT/rust-proxy-freebsd"
|
||||||
/opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK"
|
/opt/rust-package-builder/scripts/build-opnsense.sh "$SRC" "$FREEBSD_BINARY" "$BINARY_NAME" "$WORK"
|
||||||
|
|
||||||
if [[ "$SOURCE_REVISION" == "0" ]]; then
|
# PKG needs an increasing version for a packaging-only change at the same
|
||||||
|
# application commit, otherwise pkg upgrade would not install the new payload.
|
||||||
|
OPNSENSE_PACKAGE_REVISION=$SOURCE_REVISION
|
||||||
|
LAST_OPNSENSE_PACKAGE_VERSION=""
|
||||||
|
if [[ -r "$DATA_DIR/state/last_opnsense_package_version" ]]; then
|
||||||
|
LAST_OPNSENSE_PACKAGE_VERSION=$(cat "$DATA_DIR/state/last_opnsense_package_version")
|
||||||
|
elif [[ -r "$REPOSITORY/current/opnsense/BUILD-INFO.txt" ]]; then
|
||||||
|
LAST_OPNSENSE_PACKAGE_VERSION=$(sed -n 's/^package_version=//p' "$REPOSITORY/current/opnsense/BUILD-INFO.txt" | head -n1)
|
||||||
|
fi
|
||||||
|
if [[ "$LAST_OPNSENSE_PACKAGE_VERSION" == "${VERSION}_"* ]]; then
|
||||||
|
LAST_OPNSENSE_REVISION=${LAST_OPNSENSE_PACKAGE_VERSION#"${VERSION}_"}
|
||||||
|
if [[ "$LAST_OPNSENSE_REVISION" =~ ^[0-9]+$ ]] && (( LAST_OPNSENSE_REVISION >= OPNSENSE_PACKAGE_REVISION )); then
|
||||||
|
OPNSENSE_PACKAGE_REVISION=$((LAST_OPNSENSE_REVISION + 1))
|
||||||
|
fi
|
||||||
|
elif [[ "$LAST_OPNSENSE_PACKAGE_VERSION" == "$VERSION" && "$OPNSENSE_PACKAGE_REVISION" == "0" ]]; then
|
||||||
|
OPNSENSE_PACKAGE_REVISION=1
|
||||||
|
fi
|
||||||
|
if [[ "$OPNSENSE_PACKAGE_REVISION" == "0" ]]; then
|
||||||
OPNSENSE_PACKAGE_VERSION="$VERSION"
|
OPNSENSE_PACKAGE_VERSION="$VERSION"
|
||||||
else
|
else
|
||||||
OPNSENSE_PACKAGE_VERSION="${VERSION}_${SOURCE_REVISION}"
|
OPNSENSE_PACKAGE_VERSION="${VERSION}_${OPNSENSE_PACKAGE_REVISION}"
|
||||||
fi
|
fi
|
||||||
OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg"
|
OPNSENSE_PACKAGE_FILE="$OUT/os-rustproxy-${OPNSENSE_PACKAGE_VERSION}.pkg"
|
||||||
/opt/rust-package-builder/scripts/package-opnsense.sh \
|
/opt/rust-package-builder/scripts/package-opnsense.sh \
|
||||||
"$SRC" "$FREEBSD_BINARY" "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_VERSION"
|
"$SRC" "$FREEBSD_BINARY" "$OPNSENSE_PACKAGE_FILE" "$OPNSENSE_PACKAGE_VERSION"
|
||||||
|
|
||||||
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/opnsense"
|
mkdir -p "$RELEASE_DIR/apt" "$RELEASE_DIR/rpm/x86_64" "$RELEASE_DIR/suse/x86_64" "$RELEASE_DIR/opnsense" "$RELEASE_DIR/freebsd"
|
||||||
CURRENT_TARGET=""
|
CURRENT_TARGET=""
|
||||||
if [[ -L "$REPOSITORY/current" ]]; then
|
if [[ -L "$REPOSITORY/current" ]]; then
|
||||||
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
CURRENT_LINK=$(readlink "$REPOSITORY/current")
|
||||||
@@ -42,12 +59,12 @@ fi
|
|||||||
|
|
||||||
log "staging OPNsense repository snapshot"
|
log "staging OPNsense repository snapshot"
|
||||||
if [[ -n "$CURRENT_TARGET" ]]; then
|
if [[ -n "$CURRENT_TARGET" ]]; then
|
||||||
for component in apt rpm opnsense; do
|
for component in apt rpm suse opnsense freebsd; do
|
||||||
if [[ -d "$CURRENT_TARGET/$component" ]]; then
|
if [[ -d "$CURRENT_TARGET/$component" ]]; then
|
||||||
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
|
rsync -a --delete --link-dest="$CURRENT_TARGET/$component"/ "$CURRENT_TARGET/$component"/ "$RELEASE_DIR/$component"/
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
for file in repo-key.gpg index.html LINUX-BUILD-INFO.txt; do
|
for file in repo-key.gpg index.html LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
|
||||||
if [[ -f "$CURRENT_TARGET/$file" ]]; then
|
if [[ -f "$CURRENT_TARGET/$file" ]]; then
|
||||||
cp -a "$CURRENT_TARGET/$file" "$RELEASE_DIR/$file"
|
cp -a "$CURRENT_TARGET/$file" "$RELEASE_DIR/$file"
|
||||||
fi
|
fi
|
||||||
@@ -77,12 +94,14 @@ cp -f "$OPNSENSE_PACKAGE_FILE" "$RELEASE_DIR/opnsense/os-rustproxy-latest.pkg"
|
|||||||
(cd "$RELEASE_DIR/opnsense" && sha256sum "$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")") > "$RELEASE_DIR/opnsense/SHA256"
|
(cd "$RELEASE_DIR/opnsense" && sha256sum "$OPNSENSE_ABI/amd64/$(basename "$OPNSENSE_PACKAGE_FILE")") > "$RELEASE_DIR/opnsense/SHA256"
|
||||||
cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO
|
cat > "$RELEASE_DIR/opnsense/BUILD-INFO.txt" <<INFO
|
||||||
app_version=$VERSION
|
app_version=$VERSION
|
||||||
|
built_at=$(date -u "+%Y-%m-%dT%H:%M:%SZ")
|
||||||
package_version=$OPNSENSE_PACKAGE_VERSION
|
package_version=$OPNSENSE_PACKAGE_VERSION
|
||||||
source_url=$SOURCE_REPO
|
source_url=$SOURCE_REPO
|
||||||
source_ref=$SOURCE_BRANCH
|
source_ref=$SOURCE_BRANCH
|
||||||
source_commit=$SHA
|
source_commit=$SHA
|
||||||
build_reason=$BUILD_REASON
|
build_reason=$BUILD_REASON
|
||||||
source_revision=$SOURCE_REVISION
|
source_revision=$SOURCE_REVISION
|
||||||
|
packaging_revision=$OPNSENSE_PACKAGING_REVISION
|
||||||
opnsense_series=$OPNSENSE_ABI
|
opnsense_series=$OPNSENSE_ABI
|
||||||
freebsd_release=$FREEBSD_RELEASE
|
freebsd_release=$FREEBSD_RELEASE
|
||||||
freebsd_abi=$FREEBSD_ABI
|
freebsd_abi=$FREEBSD_ABI
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
source /opt/rust-package-builder/scripts/lib.sh
|
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
|
||||||
|
source "$BUILDER_ROOT/scripts/lib.sh"
|
||||||
|
|
||||||
SOURCE_DIR=${1:?source path required}
|
SOURCE_DIR=${1:?source path required}
|
||||||
OUTPUT_BINARY=${2:?output binary required}
|
OUTPUT_BINARY=${2:?output binary required}
|
||||||
@@ -17,7 +18,7 @@ rsync -a --delete \
|
|||||||
"$SOURCE_DIR/" "$BUILD_SOURCE/"
|
"$SOURCE_DIR/" "$BUILD_SOURCE/"
|
||||||
|
|
||||||
if [[ "${APPLY_FREEBSD_PATCHES:-1}" == "1" ]]; then
|
if [[ "${APPLY_FREEBSD_PATCHES:-1}" == "1" ]]; then
|
||||||
/opt/rust-package-builder/scripts/apply-freebsd-patches.sh "$BUILD_SOURCE"
|
"$BUILDER_ROOT/scripts/apply-freebsd-patches.sh" "$BUILD_SOURCE"
|
||||||
else
|
else
|
||||||
log "FreeBSD/OPNsense source patches disabled"
|
log "FreeBSD/OPNsense source patches disabled"
|
||||||
fi
|
fi
|
||||||
@@ -29,6 +30,7 @@ export CARGO_TARGET_X86_64_UNKNOWN_FREEBSD_RUSTFLAGS="-C link-arg=--target=x86_6
|
|||||||
|
|
||||||
log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}"
|
log "cross-compiling $BINARY_NAME for x86_64-unknown-freebsd${FREEBSD_TARGET_VERSION}"
|
||||||
cd "$BUILD_SOURCE"
|
cd "$BUILD_SOURCE"
|
||||||
|
FREEBSD_RUST_TOOLCHAIN=$("$BUILDER_ROOT/scripts/ensure-rust-toolchain.sh" "$BUILD_SOURCE" freebsd)
|
||||||
if [[ -f Cargo.lock ]]; then
|
if [[ -f Cargo.lock ]]; then
|
||||||
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd
|
cargo "+$FREEBSD_RUST_TOOLCHAIN" build --release --locked --target x86_64-unknown-freebsd
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -6,28 +6,35 @@ SHA=${1:?commit SHA required}
|
|||||||
BUILD_REASON=${2:-new-commit}
|
BUILD_REASON=${2:-new-commit}
|
||||||
BUILD_LINUX=${3:-1}
|
BUILD_LINUX=${3:-1}
|
||||||
BUILD_OPNSENSE=${4:-1}
|
BUILD_OPNSENSE=${4:-1}
|
||||||
|
BUILD_FREEBSD=${5:-0}
|
||||||
|
|
||||||
[[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX must be 0 or 1"
|
[[ "$BUILD_LINUX" =~ ^[01]$ ]] || fatal "BUILD_LINUX must be 0 or 1"
|
||||||
[[ "$BUILD_OPNSENSE" =~ ^[01]$ ]] || fatal "BUILD_OPNSENSE must be 0 or 1"
|
[[ "$BUILD_OPNSENSE" =~ ^[01]$ ]] || fatal "BUILD_OPNSENSE must be 0 or 1"
|
||||||
(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 )) || { log "nothing to build for $SHA"; exit 0; }
|
[[ "$BUILD_FREEBSD" =~ ^[01]$ ]] || fatal "BUILD_FREEBSD must be 0 or 1"
|
||||||
|
(( BUILD_LINUX == 1 || BUILD_OPNSENSE == 1 || BUILD_FREEBSD == 1 )) || { log "nothing to build for $SHA"; exit 0; }
|
||||||
|
|
||||||
SHORT_SHA=${SHA:0:12}
|
SHORT_SHA=${SHA:0:12}
|
||||||
BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}"
|
BUILD_ID="$(date -u '+%Y%m%dT%H%M%SZ')-${SHORT_SHA}"
|
||||||
WORK="$DATA_DIR/work/$BUILD_ID"
|
BUILD_TMP_DIR=${BUILD_TMP_DIR:-/tmp/rust-package-builder}
|
||||||
|
export CARGO_TARGET_DIR=${CARGO_TARGET_DIR:-$BUILD_TMP_DIR/cargo-target}
|
||||||
|
WORK="$BUILD_TMP_DIR/work/$BUILD_ID"
|
||||||
SRC="$WORK/source"
|
SRC="$WORK/source"
|
||||||
PKG="$WORK/package"
|
PKG="$WORK/package"
|
||||||
OUT="$WORK/out"
|
OUT="$WORK/out"
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
|
if [[ "${KEEP_WORKSPACE:-0}" != "1" ]]; then
|
||||||
rm -rf "$WORK"
|
rm -rf -- "$WORK"
|
||||||
else
|
else
|
||||||
log "workspace kept: $WORK"
|
log "workspace kept: $WORK"
|
||||||
fi
|
fi
|
||||||
|
if is_true "${CLEAN_BUILD_CACHE:-1}"; then
|
||||||
|
rm -rf -- "$CARGO_TARGET_DIR" "${GOCACHE:-$BUILD_TMP_DIR/go-cache}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
mkdir -p "$SRC" "$PKG" "$OUT"
|
mkdir -p "$SRC" "$PKG" "$OUT" "$CARGO_TARGET_DIR"
|
||||||
|
|
||||||
log "cloning source"
|
log "cloning source"
|
||||||
git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_REPO" "$SRC"
|
git clone --quiet --no-tags --single-branch --branch "$SOURCE_BRANCH" "$SOURCE_REPO" "$SRC"
|
||||||
@@ -37,7 +44,7 @@ ACTUAL_SHA=$(git -C "$SRC" rev-parse HEAD)
|
|||||||
SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA")
|
SOURCE_REVISION=$(git -C "$SRC" rev-list --count "$SHA")
|
||||||
|
|
||||||
cd "$SRC"
|
cd "$SRC"
|
||||||
META_JSON=$(cargo metadata --format-version 1 --no-deps)
|
META_JSON=$(cargo +stable metadata --format-version 1 --no-deps)
|
||||||
export META_JSON SRC
|
export META_JSON SRC
|
||||||
|
|
||||||
readarray -t META < <(python3 <<'PY'
|
readarray -t META < <(python3 <<'PY'
|
||||||
@@ -102,10 +109,11 @@ HOMEPAGE=${PACKAGE_HOMEPAGE:-${CARGO_HOMEPAGE:-${CARGO_REPOSITORY:-${CONTROL_HOM
|
|||||||
export SHA SHORT_SHA BUILD_ID BUILD_REASON SOURCE_REVISION WORK SRC PKG OUT
|
export SHA SHORT_SHA BUILD_ID BUILD_REASON SOURCE_REVISION WORK SRC PKG OUT
|
||||||
export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER
|
export PACKAGE_NAME VERSION BINARY_NAME DESCRIPTION LICENSE HOMEPAGE MAINTAINER
|
||||||
|
|
||||||
log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE version=$VERSION"
|
log "build plan: commit=$SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD version=$VERSION"
|
||||||
|
|
||||||
LINUX_OK=1
|
LINUX_OK=1
|
||||||
OPNSENSE_OK=1
|
OPNSENSE_OK=1
|
||||||
|
FREEBSD_OK=1
|
||||||
|
|
||||||
if [[ "$BUILD_LINUX" == "1" ]]; then
|
if [[ "$BUILD_LINUX" == "1" ]]; then
|
||||||
if /opt/rust-package-builder/scripts/build-linux-release.sh; then
|
if /opt/rust-package-builder/scripts/build-linux-release.sh; then
|
||||||
@@ -125,15 +133,31 @@ if [[ "$BUILD_OPNSENSE" == "1" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "$BUILD_FREEBSD" == "1" ]]; then
|
||||||
|
if /opt/rust-package-builder/scripts/build-freebsd-release.sh; then
|
||||||
|
log "FreeBSD native branch completed for $SHA"
|
||||||
|
else
|
||||||
|
FREEBSD_OK=0
|
||||||
|
log "ERROR: FreeBSD native branch failed for $SHA"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
LINUX_STATE=""
|
LINUX_STATE=""
|
||||||
OPNSENSE_STATE=""
|
OPNSENSE_STATE=""
|
||||||
|
FREEBSD_STATE=""
|
||||||
[[ -r "$DATA_DIR/state/last_linux_commit" ]] && LINUX_STATE=$(cat "$DATA_DIR/state/last_linux_commit")
|
[[ -r "$DATA_DIR/state/last_linux_commit" ]] && LINUX_STATE=$(cat "$DATA_DIR/state/last_linux_commit")
|
||||||
[[ -r "$DATA_DIR/state/last_opnsense_commit" ]] && OPNSENSE_STATE=$(cat "$DATA_DIR/state/last_opnsense_commit")
|
[[ -r "$DATA_DIR/state/last_opnsense_commit" ]] && OPNSENSE_STATE=$(cat "$DATA_DIR/state/last_opnsense_commit")
|
||||||
if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" ]]; then
|
[[ -r "$DATA_DIR/state/last_freebsd_commit" ]] && FREEBSD_STATE=$(cat "$DATA_DIR/state/last_freebsd_commit")
|
||||||
|
if [[ "$LINUX_STATE" == "$SHA" && "$OPNSENSE_STATE" == "$SHA" && ( "${FREEBSD_NATIVE_ENABLED:-1}" != "1" || "$FREEBSD_STATE" == "$SHA" ) ]]; then
|
||||||
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit"
|
printf '%s\n' "$SHA" | atomic_write "$DATA_DIR/state/last_commit"
|
||||||
log "all branches complete for $SHA"
|
log "all branches complete for $SHA"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" ]]; then
|
# Every branch publication creates an atomic repository snapshot. Keep only
|
||||||
|
# the newest configured set while always protecting the snapshot currently
|
||||||
|
# served through repository/current.
|
||||||
|
prune_release_dirs "$DATA_DIR/repository/releases" "${RELEASE_RETENTION:-10}" "$(readlink -f "$DATA_DIR/repository/current" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
if [[ "$LINUX_OK" != "1" || "$OPNSENSE_OK" != "1" || "$FREEBSD_OK" != "1" ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
+42
-13
@@ -10,8 +10,19 @@ linux_release_complete_for_commit() {
|
|||||||
[[ -d "$CURRENT" ]] || return 1
|
[[ -d "$CURRENT" ]] || return 1
|
||||||
[[ -f "$CURRENT/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] || return 1
|
[[ -f "$CURRENT/apt/dists/$REPO_DISTRIBUTION/InRelease" ]] || return 1
|
||||||
[[ -f "$CURRENT/rpm/x86_64/repodata/repomd.xml" ]] || return 1
|
[[ -f "$CURRENT/rpm/x86_64/repodata/repomd.xml" ]] || return 1
|
||||||
|
[[ -f "$CURRENT/suse/x86_64/repodata/repomd.xml" ]] || return 1
|
||||||
|
[[ -f "$CURRENT/SUSE-BUILD-INFO.txt" ]] || return 1
|
||||||
|
# Both standalone packages must exist in the current Linux snapshot.
|
||||||
|
find "$CURRENT/rpm/x86_64" -maxdepth 1 -type f -name 'rustproxy-docker-agent-*.rpm' -print -quit | grep -q . || return 1
|
||||||
|
find "$CURRENT/suse/x86_64" -maxdepth 1 -type f -name 'rustproxy-docker-agent-*.rpm' -print -quit | grep -q . || return 1
|
||||||
|
find "$CURRENT/apt/pool" -type f -name 'rustproxy-docker-agent_*.deb' -print -quit | grep -q . || return 1
|
||||||
|
# A new builder revision must repackage even when RustProxy's SHA is unchanged.
|
||||||
|
[[ -f "$CURRENT/LINUX-BUILD-INFO.txt" ]] || return 1
|
||||||
|
grep -Fqx "packaging_revision=$PACKAGING_REVISION" "$CURRENT/LINUX-BUILD-INFO.txt" || return 1
|
||||||
|
grep -Fqx "packaging_revision=$PACKAGING_REVISION" "$CURRENT/SUSE-BUILD-INFO.txt" || return 1
|
||||||
|
grep -Fq ',rustproxy-docker-agent' "$CURRENT/LINUX-BUILD-INFO.txt" || return 1
|
||||||
|
|
||||||
if [[ -f "$CURRENT/LINUX-BUILD-INFO.txt" ]] && grep -Fqx "source_commit=$sha" "$CURRENT/LINUX-BUILD-INFO.txt"; then
|
if grep -Fqx "source_commit=$sha" "$CURRENT/LINUX-BUILD-INFO.txt"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -41,17 +52,34 @@ opnsense_release_complete_for_commit() {
|
|||||||
[[ -f "$opnsense/os-rustproxy-latest.pkg" ]] || return 1
|
[[ -f "$opnsense/os-rustproxy-latest.pkg" ]] || return 1
|
||||||
grep -Fqx "source_commit=$sha" "$opnsense/BUILD-INFO.txt" || return 1
|
grep -Fqx "source_commit=$sha" "$opnsense/BUILD-INFO.txt" || return 1
|
||||||
grep -Fqx "opnsense_series=$OPNSENSE_ABI" "$opnsense/BUILD-INFO.txt" || return 1
|
grep -Fqx "opnsense_series=$OPNSENSE_ABI" "$opnsense/BUILD-INFO.txt" || return 1
|
||||||
|
grep -Fqx "packaging_revision=$OPNSENSE_PACKAGING_REVISION" "$opnsense/BUILD-INFO.txt" || return 1
|
||||||
[[ -d "$package_dir" ]] || return 1
|
[[ -d "$package_dir" ]] || return 1
|
||||||
find "$package_dir" -maxdepth 1 -type f -name 'os-rustproxy-*.pkg' -print -quit | grep -q . || return 1
|
find "$package_dir" -maxdepth 1 -type f -name 'os-rustproxy-*.pkg' -print -quit | grep -q . || return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
freebsd_release_complete_for_commit() {
|
||||||
|
local sha=$1
|
||||||
|
local repo="$CURRENT/freebsd"
|
||||||
|
[[ -d "$CURRENT" ]] || return 1
|
||||||
|
for file in meta.conf packagesite.pkg data.pkg RustProxy-FreeBSD.conf BUILD-INFO.txt rustproxy-latest.pkg; do
|
||||||
|
[[ -s "$repo/$file" ]] || return 1
|
||||||
|
done
|
||||||
|
grep -Fqx "source_commit=$sha" "$repo/BUILD-INFO.txt" || return 1
|
||||||
|
grep -Fqx "packaging_revision=$FREEBSD_NATIVE_PACKAGING_REVISION" "$repo/BUILD-INFO.txt" || return 1
|
||||||
|
grep -Fqx "freebsd_abi=$FREEBSD_ABI" "$repo/BUILD-INFO.txt" || return 1
|
||||||
|
[[ -d "$repo/$FREEBSD_ABI" ]] || return 1
|
||||||
|
find "$repo/$FREEBSD_ABI" -maxdepth 1 -type f -name 'rustproxy-*.pkg' -print -quit | grep -q . || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
log "checking repository"
|
log "checking repository"
|
||||||
REMOTE_SHA=$(git ls-remote "$SOURCE_REPO" "refs/heads/$SOURCE_BRANCH" | awk 'NR == 1 {print $1}')
|
REMOTE_SHA=$(git ls-remote "$SOURCE_REPO" "refs/heads/$SOURCE_BRANCH" | awk 'NR == 1 {print $1}')
|
||||||
[[ "$REMOTE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || fatal "cannot resolve branch $SOURCE_BRANCH from $SOURCE_REPO"
|
[[ "$REMOTE_SHA" =~ ^[0-9a-fA-F]{40}$ ]] || fatal "cannot resolve branch $SOURCE_BRANCH from $SOURCE_REPO"
|
||||||
|
|
||||||
BUILD_LINUX=1
|
BUILD_LINUX=1
|
||||||
BUILD_OPNSENSE=1
|
BUILD_OPNSENSE=1
|
||||||
|
BUILD_FREEBSD=0
|
||||||
if linux_release_complete_for_commit "$REMOTE_SHA"; then
|
if linux_release_complete_for_commit "$REMOTE_SHA"; then
|
||||||
BUILD_LINUX=0
|
BUILD_LINUX=0
|
||||||
# Persist migrated per-branch state when an older installation already has
|
# Persist migrated per-branch state when an older installation already has
|
||||||
@@ -63,19 +91,20 @@ if opnsense_release_complete_for_commit "$REMOTE_SHA"; then
|
|||||||
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_opnsense_commit"
|
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_opnsense_commit"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" ]]; then
|
if [[ "${FREEBSD_NATIVE_ENABLED:-1}" == "1" ]]; then
|
||||||
|
BUILD_FREEBSD=1
|
||||||
|
if freebsd_release_complete_for_commit "$REMOTE_SHA"; then
|
||||||
|
BUILD_FREEBSD=0
|
||||||
|
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_freebsd_commit"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$BUILD_LINUX" == "0" && "$BUILD_OPNSENSE" == "0" && "$BUILD_FREEBSD" == "0" ]]; then
|
||||||
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_commit"
|
printf '%s\n' "$REMOTE_SHA" | atomic_write "$DATA_DIR/state/last_commit"
|
||||||
log "no changes: Linux and OPNsense are complete for $REMOTE_SHA"
|
log "no changes: all enabled repositories are complete for $REMOTE_SHA"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$BUILD_LINUX" == "1" && "$BUILD_OPNSENSE" == "1" ]]; then
|
REASON="missing-or-outdated-channel"
|
||||||
REASON="new-or-missing-both"
|
log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE freebsd=$BUILD_FREEBSD reason=$REASON"
|
||||||
elif [[ "$BUILD_LINUX" == "1" ]]; then
|
/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE" "$BUILD_FREEBSD"
|
||||||
REASON="repair-linux"
|
|
||||||
else
|
|
||||||
REASON="repair-opnsense"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "build required: commit=$REMOTE_SHA linux=$BUILD_LINUX opnsense=$BUILD_OPNSENSE reason=$REASON"
|
|
||||||
/opt/rust-package-builder/scripts/build-release.sh "$REMOTE_SHA" "$REASON" "$BUILD_LINUX" "$BUILD_OPNSENSE"
|
|
||||||
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}
|
FREEBSD_RUST_TOOLCHAIN=${FREEBSD_RUST_TOOLCHAIN:-1.88.0}
|
||||||
APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-1}
|
APPLY_FREEBSD_PATCHES=${APPLY_FREEBSD_PATCHES:-1}
|
||||||
RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}
|
RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}
|
||||||
|
FREEBSD_NATIVE_ENABLED=${FREEBSD_NATIVE_ENABLED:-1}
|
||||||
|
FREEBSD_NATIVE_RETENTION=${FREEBSD_NATIVE_RETENTION:-10}
|
||||||
|
RELEASE_RETENTION=${RELEASE_RETENTION:-10}
|
||||||
|
PACKAGE_RETENTION=${PACKAGE_RETENTION:-10}
|
||||||
|
CLEAN_BUILD_CACHE=${CLEAN_BUILD_CACHE:-1}
|
||||||
|
BUILD_TMP_DIR=${BUILD_TMP_DIR:-/tmp/rust-package-builder}
|
||||||
|
|
||||||
[[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer"
|
[[ "$POLL_INTERVAL" =~ ^[0-9]+$ ]] || fatal "POLL_INTERVAL must be an integer"
|
||||||
(( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1"
|
(( POLL_INTERVAL >= 1 )) || fatal "POLL_INTERVAL must be >= 1"
|
||||||
@@ -23,6 +29,13 @@ RUN_LINUX_TESTS=${RUN_LINUX_TESTS:-1}
|
|||||||
[[ "$OPNSENSE_RETENTION" =~ ^[0-9]+$ ]] || fatal "OPNSENSE_RETENTION must be >= 0"
|
[[ "$OPNSENSE_RETENTION" =~ ^[0-9]+$ ]] || fatal "OPNSENSE_RETENTION must be >= 0"
|
||||||
[[ "$APPLY_FREEBSD_PATCHES" =~ ^[01]$ ]] || fatal "APPLY_FREEBSD_PATCHES must be 0 or 1"
|
[[ "$APPLY_FREEBSD_PATCHES" =~ ^[01]$ ]] || fatal "APPLY_FREEBSD_PATCHES must be 0 or 1"
|
||||||
[[ "$RUN_LINUX_TESTS" =~ ^[01]$ ]] || fatal "RUN_LINUX_TESTS must be 0 or 1"
|
[[ "$RUN_LINUX_TESTS" =~ ^[01]$ ]] || fatal "RUN_LINUX_TESTS must be 0 or 1"
|
||||||
|
[[ "$FREEBSD_NATIVE_ENABLED" =~ ^[01]$ ]] || fatal "FREEBSD_NATIVE_ENABLED must be 0 or 1"
|
||||||
|
[[ "$FREEBSD_NATIVE_RETENTION" =~ ^[0-9]+$ ]] || fatal "FREEBSD_NATIVE_RETENTION must be a nonnegative integer"
|
||||||
|
require_positive_integer RELEASE_RETENTION "$RELEASE_RETENTION"
|
||||||
|
require_positive_integer PACKAGE_RETENTION "$PACKAGE_RETENTION"
|
||||||
|
if [[ "$FREEBSD_NATIVE_ENABLED" != "1" ]]; then
|
||||||
|
log 'WARNING: native FreeBSD build is disabled (FREEBSD_NATIVE_ENABLED=0); no FreeBSD packages will be published'
|
||||||
|
fi
|
||||||
|
|
||||||
export DATA_DIR=${DATA_DIR:-/var/lib/package-builder}
|
export DATA_DIR=${DATA_DIR:-/var/lib/package-builder}
|
||||||
export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL
|
export SOURCE_REPO SOURCE_BRANCH REPOSITORY_URL OPNSENSE_REPOSITORY_URL POLL_INTERVAL
|
||||||
@@ -31,15 +44,18 @@ export REPO_COMPONENT=${REPO_COMPONENT:-main}
|
|||||||
export PACKAGE_RELEASE
|
export PACKAGE_RELEASE
|
||||||
export OPNSENSE_ABI OPNSENSE_RETENTION
|
export OPNSENSE_ABI OPNSENSE_RETENTION
|
||||||
export FREEBSD_RELEASE FREEBSD_ABI FREEBSD_ARCH FREEBSD_RUST_TOOLCHAIN
|
export FREEBSD_RELEASE FREEBSD_ABI FREEBSD_ARCH FREEBSD_RUST_TOOLCHAIN
|
||||||
export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS
|
export APPLY_FREEBSD_PATCHES RUN_LINUX_TESTS FREEBSD_NATIVE_ENABLED FREEBSD_NATIVE_RETENTION
|
||||||
|
export RELEASE_RETENTION PACKAGE_RETENTION CLEAN_BUILD_CACHE BUILD_TMP_DIR
|
||||||
export MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.34}
|
export MAX_GLIBC_VERSION=${MAX_GLIBC_VERSION:-2.34}
|
||||||
export GPG_PRIVATE_KEY_FILE=${GPG_PRIVATE_KEY_FILE:-/run/secrets/gpg/private-key.asc}
|
export GPG_PRIVATE_KEY_FILE=${GPG_PRIVATE_KEY_FILE:-/run/secrets/gpg/private-key.asc}
|
||||||
export GPG_PASSPHRASE_FILE=${GPG_PASSPHRASE_FILE:-/run/secrets/gpg/passphrase}
|
export GPG_PASSPHRASE_FILE=${GPG_PASSPHRASE_FILE:-/run/secrets/gpg/passphrase}
|
||||||
export GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg}
|
export GNUPGHOME=${GNUPGHOME:-/run/package-builder/gnupg}
|
||||||
export CARGO_HOME="$DATA_DIR/cargo"
|
export CARGO_HOME="$DATA_DIR/cargo"
|
||||||
export CARGO_TARGET_DIR="$DATA_DIR/cargo/target"
|
export CARGO_TARGET_DIR="$BUILD_TMP_DIR/cargo-target"
|
||||||
|
export GOPATH="$DATA_DIR/go"
|
||||||
|
export GOCACHE="$BUILD_TMP_DIR/go-cache"
|
||||||
|
|
||||||
for cmd in git cargo rustc rustup nfpm aptly createrepo_c gpg rsync python3 python3.11 \
|
for cmd in git go cargo rustc rustup nfpm aptly createrepo_c gpg rsync python3 python3.11 \
|
||||||
objdump ar rpm clang llvm-readelf llvm-ar llvm-ranlib file tar zstd bsdtar sha256sum; do
|
objdump ar rpm clang llvm-readelf llvm-ar llvm-ranlib file tar zstd bsdtar sha256sum; do
|
||||||
require_cmd "$cmd"
|
require_cmd "$cmd"
|
||||||
done
|
done
|
||||||
@@ -52,12 +68,34 @@ mkdir -p \
|
|||||||
"$DATA_DIR/repository/releases" \
|
"$DATA_DIR/repository/releases" \
|
||||||
"$DATA_DIR/state" \
|
"$DATA_DIR/state" \
|
||||||
"$DATA_DIR/cargo" \
|
"$DATA_DIR/cargo" \
|
||||||
|
"$DATA_DIR/go/cache" \
|
||||||
"$DATA_DIR/work" \
|
"$DATA_DIR/work" \
|
||||||
"$DATA_DIR/packages/rpm/x86_64" \
|
"$DATA_DIR/packages/rpm/x86_64" \
|
||||||
|
"$DATA_DIR/packages/suse/x86_64" \
|
||||||
"$DATA_DIR/packages/deb"
|
"$DATA_DIR/packages/deb"
|
||||||
mkdir -p "$GNUPGHOME"
|
mkdir -p "$BUILD_TMP_DIR/work" "$CARGO_TARGET_DIR" "$GOCACHE" "$GNUPGHOME"
|
||||||
chmod 0700 "$GNUPGHOME"
|
chmod 0700 "$GNUPGHOME"
|
||||||
|
|
||||||
|
# Migration/maintenance cleanup for older builder versions. Large build
|
||||||
|
# products must not accumulate on the persistent /data volume.
|
||||||
|
rm -rf -- "$DATA_DIR/cargo/target" "$DATA_DIR/go/cache"
|
||||||
|
find "$DATA_DIR/work" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
|
||||||
|
find "$BUILD_TMP_DIR/work" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + 2>/dev/null || true
|
||||||
|
if is_true "$CLEAN_BUILD_CACHE"; then
|
||||||
|
rm -rf -- "$CARGO_TARGET_DIR" "$GOCACHE"
|
||||||
|
mkdir -p "$CARGO_TARGET_DIR" "$GOCACHE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Apply retention immediately on startup, before waiting for another source
|
||||||
|
# change. Main RustProxy and Docker Agent packages are counted separately.
|
||||||
|
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy_*.deb' "$PACKAGE_RETENTION"
|
||||||
|
prune_package_files "$DATA_DIR/packages/deb" 'rustproxy-docker-agent_*.deb' "$PACKAGE_RETENTION"
|
||||||
|
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-*.x86_64.rpm' "$PACKAGE_RETENTION" 'rustproxy-docker-agent-*.x86_64.rpm'
|
||||||
|
prune_package_files "$DATA_DIR/packages/rpm/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$PACKAGE_RETENTION"
|
||||||
|
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-*.x86_64.rpm' "$PACKAGE_RETENTION" 'rustproxy-docker-agent-*.x86_64.rpm'
|
||||||
|
prune_package_files "$DATA_DIR/packages/suse/x86_64" 'rustproxy-docker-agent-*.x86_64.rpm' "$PACKAGE_RETENTION"
|
||||||
|
prune_release_dirs "$DATA_DIR/repository/releases" "$RELEASE_RETENTION" "$(readlink -f "$DATA_DIR/repository/current" 2>/dev/null || true)"
|
||||||
|
|
||||||
[[ -r "$GPG_PRIVATE_KEY_FILE" ]] || fatal "GPG private key not found: $GPG_PRIVATE_KEY_FILE"
|
[[ -r "$GPG_PRIVATE_KEY_FILE" ]] || fatal "GPG private key not found: $GPG_PRIVATE_KEY_FILE"
|
||||||
log "importing GPG signing key"
|
log "importing GPG signing key"
|
||||||
gpg --batch --import "$GPG_PRIVATE_KEY_FILE" >/dev/null 2>&1 || fatal "cannot import GPG key"
|
gpg --batch --import "$GPG_PRIVATE_KEY_FILE" >/dev/null 2>&1 || fatal "cannot import GPG key"
|
||||||
@@ -77,7 +115,7 @@ fi
|
|||||||
|
|
||||||
/opt/rust-package-builder/scripts/init-storage.sh
|
/opt/rust-package-builder/scripts/init-storage.sh
|
||||||
|
|
||||||
log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE"
|
log "builder started: branch=$SOURCE_BRANCH interval=${POLL_INTERVAL}s opnsense=$OPNSENSE_ABI freebsd=$FREEBSD_RELEASE native_freebsd=$FREEBSD_NATIVE_ENABLED"
|
||||||
while true; do
|
while true; do
|
||||||
if ! /opt/rust-package-builder/scripts/check-and-build.sh; then
|
if ! /opt/rust-package-builder/scripts/check-and-build.sh; then
|
||||||
log "ERROR: one or more build branches failed; any successful branch was published independently"
|
log "ERROR: one or more build branches failed; any successful branch was published independently"
|
||||||
|
|||||||
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{h(package)} — package repository</h1>
|
<h1>{h(package)} — package repository</h1>
|
||||||
<p class="lead">Signed DEB and RPM packages for x86_64/amd64.</p>
|
<p class="lead">Signed DEB, DNF RPM and SUSE RPM packages for x86_64/amd64.</p>
|
||||||
<div class="meta"><span class="ok">Repository active</span><span>{h(url)}</span><span>APT + RPM/DNF</span></div>
|
<div class="meta"><span class="ok">Repository active</span><span>{h(url)}</span><span>APT + RPM/DNF + RPM/Zypper</span></div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Debian / Ubuntu</h2>
|
<h2>Debian / Ubuntu</h2>
|
||||||
@@ -68,10 +68,37 @@ REPO
|
|||||||
sudo dnf install {h(package)}</code></pre>
|
sudo dnf install {h(package)}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>SUSE Linux Enterprise / openSUSE Leap / Tumbleweed</h2>
|
||||||
|
<p>Signed dedicated SUSE RPM repository (x86_64; glibc 2.34 or newer).</p>
|
||||||
|
<pre><code>sudo rpm --import {h(url)}/repo-key.gpg
|
||||||
|
sudo zypper addrepo --refresh --gpgcheck {h(url)}/suse/x86_64/ rustproxy-suse
|
||||||
|
sudo zypper refresh rustproxy-suse
|
||||||
|
sudo zypper install {h(package)}
|
||||||
|
# Optional Docker host agent:
|
||||||
|
sudo zypper install rustproxy-docker-agent</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h2>Docker discovery agent — separate Linux package</h2>
|
||||||
|
<p>Install <code>rustproxy-docker-agent</code> on the Docker host (systemd, Docker Engine, amd64/x86_64). This is independent from the RustProxy server and has no FreeBSD pkg.</p>
|
||||||
|
<pre><code># Debian / Ubuntu (after adding the signed APT repo above)
|
||||||
|
sudo apt install rustproxy-docker-agent
|
||||||
|
|
||||||
|
# RHEL / Rocky / Alma / Fedora (after adding the DNF repo above)
|
||||||
|
sudo dnf install rustproxy-docker-agent
|
||||||
|
|
||||||
|
# Locally display the token and certificate fingerprint
|
||||||
|
sudo rustproxy-docker-agent info --show-token
|
||||||
|
sudo systemctl status rustproxy-docker-agent</code></pre>
|
||||||
|
<p>Packages create persistent credentials and a systemd service; tokens are never printed to unattended package logs. Restrict the agent's HTTPS port to the RustProxy host.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>OPNsense / FreeBSD pkg</h2>
|
<h2>OPNsense / FreeBSD pkg</h2>
|
||||||
<p>The OPNsense plugin is published separately because it uses the FreeBSD pkg format.</p>
|
<p>The OPNsense plugin is published separately because it uses the FreeBSD pkg format.</p>
|
||||||
<p><a href="{h(opn_url)}/">{h(opn_url)}/</a></p>
|
<p><a href="{h(opn_url)}/">{h(opn_url)}/</a></p>
|
||||||
|
<p>Native FreeBSD pkg (if enabled): <a href="{h(opn_url)}/freebsd/">{h(opn_url)}/freebsd/</a>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@@ -79,9 +106,10 @@ sudo dnf install {h(package)}</code></pre>
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href="{h(url)}/repo-key.gpg">Public GPG key</a></li>
|
<li><a href="{h(url)}/repo-key.gpg">Public GPG key</a></li>
|
||||||
<li><a href="{h(url)}/apt/dists/{h(dist)}/InRelease">APT InRelease</a></li>
|
<li><a href="{h(url)}/apt/dists/{h(dist)}/InRelease">APT InRelease</a></li>
|
||||||
<li><a href="{h(url)}/rpm/x86_64/repodata/repomd.xml">RPM repomd.xml</a></li>
|
<li><a href="{h(url)}/rpm/x86_64/repodata/repomd.xml">DNF RPM repomd.xml</a></li>
|
||||||
|
<li><a href="{h(url)}/suse/x86_64/repodata/repomd.xml">SUSE RPM repomd.xml</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Supported targets: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10; architecture: amd64/x86_64.</p>
|
<p>Supported targets: Debian 12/13, Ubuntu 24.04/26.04, RHEL 9/10, SLES 15 SP6+, SLES 16+, openSUSE Leap 15.6+/Tumbleweed; architecture: amd64/x86_64.</p>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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>
|
<p><strong>OPNsense series:</strong> {h(series)} <strong>ABI:</strong> {h(abi)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<h2>Native FreeBSD (without OPNsense)</h2>
|
||||||
|
<p>The separate optional native FreeBSD pkg repository is available at <a href="{h(url)}/freebsd/">{h(url)}/freebsd/</a> when FREEBSD_NATIVE_ENABLED=1.</p>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
<h2>pkg repository configuration</h2>
|
<h2>pkg repository configuration</h2>
|
||||||
<pre><code>mkdir -p /usr/local/etc/pkg/repos
|
<pre><code>mkdir -p /usr/local/etc/pkg/repos
|
||||||
cat > /usr/local/etc/pkg/repos/RustProxy.conf <<'REPO'
|
cat > /usr/local/etc/pkg/repos/RustProxy.conf <<'REPO'
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from collections import defaultdict
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
CATALOG_FILES = {"data.pkg", "packagesite.pkg"}
|
CATALOG_FILES = {"data.pkg", "packagesite.pkg"}
|
||||||
LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg"}
|
LATEST_NAMES = {"latest.pkg", "os-rustproxy-latest.pkg", "rustproxy-latest.pkg"}
|
||||||
|
|
||||||
|
|
||||||
def sha256(path: Path) -> str:
|
def sha256(path: Path) -> str:
|
||||||
@@ -91,7 +91,7 @@ def make_tar(dst: Path, member_name: str, payload: bytes):
|
|||||||
os.replace(tmp, dst)
|
os.replace(tmp, dst)
|
||||||
|
|
||||||
|
|
||||||
def rebuild(repo: Path, retention: int):
|
def rebuild(repo: Path, retention: int, repo_version: int = 1):
|
||||||
packages = collect_packages(repo)
|
packages = collect_packages(repo)
|
||||||
if enforce_retention(packages, retention):
|
if enforce_retention(packages, retention):
|
||||||
packages = collect_packages(repo)
|
packages = collect_packages(repo)
|
||||||
@@ -111,7 +111,7 @@ def rebuild(repo: Path, retention: int):
|
|||||||
make_tar(repo / "data.pkg", "data", data)
|
make_tar(repo / "data.pkg", "data", data)
|
||||||
|
|
||||||
meta = (
|
meta = (
|
||||||
'version: 1;\n'
|
f'version: {repo_version};\n'
|
||||||
'packing_format: "tar";\n'
|
'packing_format: "tar";\n'
|
||||||
'manifests: "packagesite.yaml";\n'
|
'manifests: "packagesite.yaml";\n'
|
||||||
'manifests_archive: "packagesite";\n'
|
'manifests_archive: "packagesite";\n'
|
||||||
@@ -130,11 +130,12 @@ def main():
|
|||||||
parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue")
|
parser = argparse.ArgumentParser(description="Build a static FreeBSD pkg repository catalogue")
|
||||||
parser.add_argument("repo", type=Path, help="repository root")
|
parser.add_argument("repo", type=Path, help="repository root")
|
||||||
parser.add_argument("--retention", type=int, default=10, help="versions kept per package/ABI; 0 disables pruning")
|
parser.add_argument("--retention", type=int, default=10, help="versions kept per package/ABI; 0 disables pruning")
|
||||||
|
parser.add_argument("--repo-version", type=int, choices=(1, 2), default=1, help="pkg repository format; 2 for native FreeBSD 15")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
repo = args.repo.resolve()
|
repo = args.repo.resolve()
|
||||||
repo.mkdir(parents=True, exist_ok=True)
|
repo.mkdir(parents=True, exist_ok=True)
|
||||||
rebuild(repo, args.retention)
|
rebuild(repo, args.retention, args.repo_version)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ if ! aptly -config="$APTLY_CONF" repo list -raw | grep -Fx package-builder >/dev
|
|||||||
package-builder >/dev/null
|
package-builder >/dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for item in apt rpm opnsense repo-key.gpg index.html LINUX-BUILD-INFO.txt; do
|
for item in apt rpm suse opnsense freebsd repo-key.gpg index.html LINUX-BUILD-INFO.txt SUSE-BUILD-INFO.txt; do
|
||||||
if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then
|
if [[ -e "$REPOSITORY/$item" && ! -L "$REPOSITORY/$item" ]]; then
|
||||||
fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
|
fatal "$REPOSITORY/$item exists and is not a symlink; refusing to overwrite"
|
||||||
fi
|
fi
|
||||||
@@ -42,7 +42,10 @@ done
|
|||||||
|
|
||||||
ln -sfn current/apt "$REPOSITORY/apt"
|
ln -sfn current/apt "$REPOSITORY/apt"
|
||||||
ln -sfn current/rpm "$REPOSITORY/rpm"
|
ln -sfn current/rpm "$REPOSITORY/rpm"
|
||||||
|
ln -sfn current/suse "$REPOSITORY/suse"
|
||||||
ln -sfn current/opnsense "$REPOSITORY/opnsense"
|
ln -sfn current/opnsense "$REPOSITORY/opnsense"
|
||||||
|
ln -sfn current/freebsd "$REPOSITORY/freebsd"
|
||||||
ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
|
ln -sfn current/repo-key.gpg "$REPOSITORY/repo-key.gpg"
|
||||||
ln -sfn current/index.html "$REPOSITORY/index.html"
|
ln -sfn current/index.html "$REPOSITORY/index.html"
|
||||||
ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
|
ln -sfn current/LINUX-BUILD-INFO.txt "$REPOSITORY/LINUX-BUILD-INFO.txt"
|
||||||
|
ln -sfn current/SUSE-BUILD-INFO.txt "$REPOSITORY/SUSE-BUILD-INFO.txt"
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
# Increment when package payload/installation semantics change without a source commit.
|
||||||
|
PACKAGING_REVISION=7
|
||||||
|
OPNSENSE_PACKAGING_REVISION=4
|
||||||
|
FREEBSD_NATIVE_PACKAGING_REVISION=2
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
printf '%s %s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" "$*"
|
||||||
}
|
}
|
||||||
@@ -38,3 +43,61 @@ atomic_write() {
|
|||||||
cat > "$tmp"
|
cat > "$tmp"
|
||||||
mv -f "$tmp" "$path"
|
mv -f "$tmp" "$path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is_true() {
|
||||||
|
case "${1:-}" in
|
||||||
|
1|true|TRUE|yes|YES|on|ON) return 0 ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
require_positive_integer() {
|
||||||
|
local name=$1 value=$2
|
||||||
|
[[ "$value" =~ ^[1-9][0-9]*$ ]] || fatal "$name must be a positive integer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep the newest N artifacts matching a package family. An optional exclude
|
||||||
|
# glob is useful for repositories that contain both rustproxy-* and
|
||||||
|
# rustproxy-docker-agent-* files in the same directory.
|
||||||
|
prune_package_files() {
|
||||||
|
local directory=$1 pattern=$2 keep=$3 exclude_pattern=${4:-}
|
||||||
|
require_positive_integer PACKAGE_RETENTION "$keep"
|
||||||
|
[[ -d "$directory" ]] || return 0
|
||||||
|
|
||||||
|
local -a find_args=("$directory" -maxdepth 1 -type f -name "$pattern")
|
||||||
|
if [[ -n "$exclude_pattern" ]]; then
|
||||||
|
find_args+=( ! -name "$exclude_pattern" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
mapfile -t stale < <(
|
||||||
|
find "${find_args[@]}" -printf '%T@ %p\n' \
|
||||||
|
| sort -nr \
|
||||||
|
| awk -v keep="$keep" 'NR>keep {$1=""; sub(/^ /,""); print}'
|
||||||
|
)
|
||||||
|
|
||||||
|
local path
|
||||||
|
for path in "${stale[@]}"; do
|
||||||
|
[[ -n "$path" ]] || continue
|
||||||
|
log "package retention: removing $path"
|
||||||
|
rm -f -- "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prune_release_dirs() {
|
||||||
|
local releases_dir=$1 keep=$2 protected=${3:-}
|
||||||
|
require_positive_integer RELEASE_RETENTION "$keep"
|
||||||
|
[[ -d "$releases_dir" ]] || return 0
|
||||||
|
|
||||||
|
mapfile -t stale < <(
|
||||||
|
find "$releases_dir" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' \
|
||||||
|
| sort -nr \
|
||||||
|
| awk -v keep="$keep" 'NR>keep {$1=""; sub(/^ /,""); print}'
|
||||||
|
)
|
||||||
|
|
||||||
|
local path
|
||||||
|
for path in "${stale[@]}"; do
|
||||||
|
[[ -n "$path" && "$path" != "$protected" ]] || continue
|
||||||
|
log "release retention: removing $path"
|
||||||
|
rm -rf -- "$path"
|
||||||
|
done
|
||||||
|
}
|
||||||
@@ -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
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
source /opt/rust-package-builder/scripts/lib.sh
|
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
|
||||||
|
source "$BUILDER_ROOT/scripts/lib.sh"
|
||||||
|
|
||||||
SOURCE_DIR=${1:?source path required}
|
SOURCE_DIR=${1:?source path required}
|
||||||
BINARY=${2:?FreeBSD binary required}
|
BINARY=${2:?FreeBSD binary required}
|
||||||
PACKAGE_FILE=${3:?output package file required}
|
PACKAGE_FILE=${3:?output package file required}
|
||||||
PACKAGE_VERSION=${4:?OPNsense package version required}
|
PACKAGE_VERSION=${4:?OPNsense package version required}
|
||||||
|
|
||||||
PLUGIN_ROOT=/opt/rust-package-builder/opnsense/plugin
|
PLUGIN_ROOT="$BUILDER_ROOT/opnsense/plugin"
|
||||||
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
|
FREEBSD_ABI=${FREEBSD_ABI:-FreeBSD:15:amd64}
|
||||||
FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
|
FREEBSD_ARCH=${FREEBSD_ARCH:-freebsd:15:x86:64}
|
||||||
|
|
||||||
@@ -35,7 +36,7 @@ mkdir -p "$STAGE/usr/local/sbin"
|
|||||||
install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
|
install -m 0555 "$BINARY" "$STAGE/usr/local/sbin/rust-proxy"
|
||||||
|
|
||||||
chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy"
|
chmod 0555 "$STAGE/usr/local/etc/rc.d/rustproxy"
|
||||||
chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/10-rustproxy.start"
|
chmod 0555 "$STAGE/usr/local/etc/rc.syshook.d/start/50-rustproxy.start"
|
||||||
chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh"
|
chmod 0555 "$STAGE/usr/local/opnsense/scripts/rustproxy/service.sh"
|
||||||
|
|
||||||
POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre"
|
POST_INSTALL_FILE="$PLUGIN_ROOT/+POST_INSTALL.pre"
|
||||||
|
|||||||
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"
|
||||||
+109
-9
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -Eeuo pipefail
|
set -Eeuo pipefail
|
||||||
source /opt/rust-package-builder/scripts/lib.sh
|
BUILDER_ROOT=${BUILDER_ROOT:-/opt/rust-package-builder}
|
||||||
|
source "$BUILDER_ROOT/scripts/lib.sh"
|
||||||
|
|
||||||
SRC=${1:?source path required}
|
SRC=${1:?source path required}
|
||||||
PKG=${2:?package workspace required}
|
PKG=${2:?package workspace required}
|
||||||
@@ -12,7 +13,8 @@ install -D -m 0755 "$BINARY_PATH" "$PKG/payload/usr/bin/$BINARY_NAME"
|
|||||||
|
|
||||||
SERVICE_SRC="$SRC/debian/${PACKAGE_NAME}.service"
|
SERVICE_SRC="$SRC/debian/${PACKAGE_NAME}.service"
|
||||||
ENV_SRC="$SRC/debian/${PACKAGE_NAME}.environment"
|
ENV_SRC="$SRC/debian/${PACKAGE_NAME}.environment"
|
||||||
LOGROTATE_SRC="$SRC/debian/${PACKAGE_NAME}.logrotate"
|
LOGROTATE_SRC="$BUILDER_ROOT/packaging/rustproxy.logrotate"
|
||||||
|
ACCESS_GUIDE_SRC="$BUILDER_ROOT/packaging/rustproxy-filesystem-access.md"
|
||||||
TMPFILES_SRC="$SRC/debian/${PACKAGE_NAME}.tmpfiles"
|
TMPFILES_SRC="$SRC/debian/${PACKAGE_NAME}.tmpfiles"
|
||||||
POSTINST_SRC="$SRC/debian/${PACKAGE_NAME}.postinst"
|
POSTINST_SRC="$SRC/debian/${PACKAGE_NAME}.postinst"
|
||||||
|
|
||||||
@@ -28,21 +30,60 @@ After=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/bin/$BINARY_NAME
|
User=rustproxy
|
||||||
|
Group=rustproxy
|
||||||
|
WorkingDirectory=/var/lib/rustproxy
|
||||||
|
UMask=0077
|
||||||
|
Environment=RUST_LOG=rust_proxy=info,warn
|
||||||
|
Environment=MALLOC_ARENA_MAX=2
|
||||||
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
||||||
|
EnvironmentFile=-/etc/rustproxy/environment
|
||||||
|
ExecStart=/usr/bin/$BINARY_NAME --data-dir /var/lib/rustproxy serve --admin-listen 0.0.0.0:9090 --proxy-listen 0.0.0.0:80 --tls-listen 0.0.0.0:443 --allow-remote-admin
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=3
|
RestartSec=3
|
||||||
|
KillSignal=SIGINT
|
||||||
|
KillMode=control-group
|
||||||
|
TimeoutStopSec=120
|
||||||
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||||
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
PrivateDevices=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
LogsDirectory=rustproxy
|
||||||
|
LogsDirectoryMode=0700
|
||||||
|
ReadWritePaths=/var/lib/rustproxy /var/log/rustproxy
|
||||||
|
# Allow ACL/group-controlled read access to web roots and external certificates
|
||||||
|
# under /home; ProtectSystem=strict still makes all other paths read-only.
|
||||||
|
ProtectHome=read-only
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectKernelLogs=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
RestrictSUIDSGID=true
|
||||||
|
RestrictNamespaces=true
|
||||||
|
RestrictRealtime=true
|
||||||
|
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||||
|
SystemCallArchitectures=native
|
||||||
|
LockPersonality=true
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
UNIT
|
UNIT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Match the 0.34.x CLI. Avoid changing upstream units that already provide
|
||||||
|
# their own ExecReload. Linux package revision forces rebuilding old binaries.
|
||||||
|
python3 "$BUILDER_ROOT/scripts/normalize-systemd-unit.py" \
|
||||||
|
"$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "$BINARY_NAME"
|
||||||
|
|
||||||
if [[ -r "$ENV_SRC" ]]; then
|
if [[ -r "$ENV_SRC" ]]; then
|
||||||
install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment"
|
install -D -m 0640 "$ENV_SRC" "$PKG/payload/etc/${PACKAGE_NAME}/environment"
|
||||||
fi
|
fi
|
||||||
if [[ -r "$LOGROTATE_SRC" ]]; then
|
[[ -r "$LOGROTATE_SRC" ]] || fatal "missing bundled logrotate configuration: $LOGROTATE_SRC"
|
||||||
install -D -m 0644 "$LOGROTATE_SRC" "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}"
|
install -D -m 0644 "$LOGROTATE_SRC" "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}"
|
||||||
fi
|
[[ -r "$ACCESS_GUIDE_SRC" ]] || fatal "missing Linux filesystem access guide: $ACCESS_GUIDE_SRC"
|
||||||
|
install -D -m 0644 "$ACCESS_GUIDE_SRC" "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md"
|
||||||
if [[ -r "$TMPFILES_SRC" ]]; then
|
if [[ -r "$TMPFILES_SRC" ]]; then
|
||||||
install -D -m 0644 "$TMPFILES_SRC" "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf"
|
install -D -m 0644 "$TMPFILES_SRC" "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf"
|
||||||
fi
|
fi
|
||||||
@@ -73,7 +114,7 @@ if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
|
|||||||
adduser --system --ingroup "$PACKAGE_NAME" --home "/var/lib/$PACKAGE_NAME" --no-create-home \\
|
adduser --system --ingroup "$PACKAGE_NAME" --home "/var/lib/$PACKAGE_NAME" --no-create-home \\
|
||||||
--disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null
|
--disabled-password --shell /usr/sbin/nologin "$PACKAGE_NAME" >/dev/null
|
||||||
fi
|
fi
|
||||||
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0750 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME"
|
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" "/etc/$PACKAGE_NAME"
|
||||||
SCRIPT
|
SCRIPT
|
||||||
fi
|
fi
|
||||||
cat >> "$PKG/scripts/deb-postinstall.sh" <<SCRIPT
|
cat >> "$PKG/scripts/deb-postinstall.sh" <<SCRIPT
|
||||||
@@ -108,7 +149,7 @@ fi
|
|||||||
if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
|
if ! getent passwd "$PACKAGE_NAME" >/dev/null 2>&1; then
|
||||||
useradd --system --gid "$PACKAGE_NAME" --home-dir "/var/lib/$PACKAGE_NAME" --no-create-home --shell /sbin/nologin "$PACKAGE_NAME"
|
useradd --system --gid "$PACKAGE_NAME" --home-dir "/var/lib/$PACKAGE_NAME" --no-create-home --shell /sbin/nologin "$PACKAGE_NAME"
|
||||||
fi
|
fi
|
||||||
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0750 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME"
|
install -d -o "$PACKAGE_NAME" -g "$PACKAGE_NAME" -m 0700 "/var/lib/$PACKAGE_NAME" "/var/log/$PACKAGE_NAME" "/etc/$PACKAGE_NAME"
|
||||||
systemd-tmpfiles --create "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" >/dev/null 2>&1 || true
|
systemd-tmpfiles --create "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" >/dev/null 2>&1 || true
|
||||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
systemctl enable "${PACKAGE_NAME}.service" >/dev/null 2>&1 || true
|
systemctl enable "${PACKAGE_NAME}.service" >/dev/null 2>&1 || true
|
||||||
@@ -151,7 +192,8 @@ add_content() {
|
|||||||
add_content "$PKG/payload/usr/bin/$BINARY_NAME" "/usr/bin/$BINARY_NAME" 0755
|
add_content "$PKG/payload/usr/bin/$BINARY_NAME" "/usr/bin/$BINARY_NAME" 0755
|
||||||
add_content "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "/usr/lib/systemd/system/${PACKAGE_NAME}.service" 0644
|
add_content "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service" "/usr/lib/systemd/system/${PACKAGE_NAME}.service" 0644
|
||||||
[[ -f "$PKG/payload/etc/${PACKAGE_NAME}/environment" ]] && add_content "$PKG/payload/etc/${PACKAGE_NAME}/environment" "/etc/${PACKAGE_NAME}/environment" 0640 'config|noreplace'
|
[[ -f "$PKG/payload/etc/${PACKAGE_NAME}/environment" ]] && add_content "$PKG/payload/etc/${PACKAGE_NAME}/environment" "/etc/${PACKAGE_NAME}/environment" 0640 'config|noreplace'
|
||||||
[[ -f "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" ]] && add_content "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" "/etc/logrotate.d/${PACKAGE_NAME}" 0644 'config|noreplace'
|
add_content "$PKG/payload/etc/logrotate.d/${PACKAGE_NAME}" "/etc/logrotate.d/${PACKAGE_NAME}" 0644 'config|noreplace'
|
||||||
|
add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md" "/usr/share/doc/${PACKAGE_NAME}/FILESYSTEM_ACCESS.md" 0644
|
||||||
[[ -f "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" ]] && add_content "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" 0644
|
[[ -f "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" ]] && add_content "$PKG/payload/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" "/usr/lib/tmpfiles.d/${PACKAGE_NAME}.conf" 0644
|
||||||
[[ -f "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" ]] && add_content "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" "/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" 0644
|
[[ -f "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" ]] && add_content "$PKG/payload/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" "/usr/share/${PACKAGE_NAME}/grafana/rustproxy-overview.json" 0644
|
||||||
[[ -f "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" ]] && add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" "/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" 0644
|
[[ -f "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" ]] && add_content "$PKG/payload/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" "/usr/share/doc/${PACKAGE_NAME}/grafana/README.md" 0644
|
||||||
@@ -183,6 +225,8 @@ depends:
|
|||||||
- ca-certificates
|
- ca-certificates
|
||||||
- gzip
|
- gzip
|
||||||
- logrotate
|
- logrotate
|
||||||
|
- python3
|
||||||
|
- python3-pip
|
||||||
- systemd
|
- systemd
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: "$PKG/scripts/deb-postinstall.sh"
|
postinstall: "$PKG/scripts/deb-postinstall.sh"
|
||||||
@@ -195,6 +239,17 @@ deb:
|
|||||||
type: origin
|
type: origin
|
||||||
YAML
|
YAML
|
||||||
|
|
||||||
|
# The SUSE flavor keeps the RPM name/version identical but uses native
|
||||||
|
# dependencies and an isolated Python launcher for Leap/SLES 15 systems.
|
||||||
|
# SUSE may ship /usr/bin/python3 older than the separately installed python311.
|
||||||
|
install -D -m 0755 "$BUILDER_ROOT/packaging/suse-python3" \
|
||||||
|
"$PKG/payload/usr/libexec/$PACKAGE_NAME/python3"
|
||||||
|
mkdir -p "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d"
|
||||||
|
cat > "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf" <<SUSE_UNIT
|
||||||
|
[Service]
|
||||||
|
Environment=PATH=/usr/libexec/${PACKAGE_NAME}:/usr/local/bin:/usr/bin:/bin
|
||||||
|
SUSE_UNIT
|
||||||
|
|
||||||
cat > "$PKG/nfpm-rpm.yaml" <<YAML
|
cat > "$PKG/nfpm-rpm.yaml" <<YAML
|
||||||
name: $NAME_Q
|
name: $NAME_Q
|
||||||
arch: amd64
|
arch: amd64
|
||||||
@@ -215,6 +270,7 @@ depends:
|
|||||||
- gzip
|
- gzip
|
||||||
- logrotate
|
- logrotate
|
||||||
- shadow-utils
|
- shadow-utils
|
||||||
|
- '((python3 >= 3.10 and python3-pip) or (python3.12 and python3.12-pip) or (python3.11 and python3.11-pip))'
|
||||||
- systemd
|
- systemd
|
||||||
scripts:
|
scripts:
|
||||||
postinstall: "$PKG/scripts/rpm-postinstall.sh"
|
postinstall: "$PKG/scripts/rpm-postinstall.sh"
|
||||||
@@ -225,3 +281,47 @@ rpm:
|
|||||||
signature:
|
signature:
|
||||||
key_file: $GPG_KEY_Q
|
key_file: $GPG_KEY_Q
|
||||||
YAML
|
YAML
|
||||||
|
|
||||||
|
# SUSE Linux Enterprise, openSUSE Leap and Tumbleweed use shadow (not
|
||||||
|
# RHEL's shadow-utils). Versioned Python enables Leap/SLES 15 installations.
|
||||||
|
# Compression gzip is understood by older rpm clients as well as current ones.
|
||||||
|
cat > "$PKG/nfpm-suse.yaml" <<YAML
|
||||||
|
name: $NAME_Q
|
||||||
|
arch: amd64
|
||||||
|
platform: linux
|
||||||
|
version: $VERSION_Q
|
||||||
|
release: "$PACKAGE_RELEASE"
|
||||||
|
section: net
|
||||||
|
priority: optional
|
||||||
|
maintainer: $MAINTAINER_Q
|
||||||
|
description: $DESC_Q
|
||||||
|
license: $LICENSE_Q
|
||||||
|
homepage: $HOMEPAGE_Q
|
||||||
|
vendor: $NAME_Q
|
||||||
|
contents:
|
||||||
|
$(cat "$CONTENTS_FILE")
|
||||||
|
- src: $(yaml_quote "$PKG/payload/usr/libexec/$PACKAGE_NAME/python3")
|
||||||
|
dst: /usr/libexec/$PACKAGE_NAME/python3
|
||||||
|
file_info:
|
||||||
|
mode: 0755
|
||||||
|
- src: $(yaml_quote "$PKG/payload/usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf")
|
||||||
|
dst: /usr/lib/systemd/system/${PACKAGE_NAME}.service.d/50-suse-python.conf
|
||||||
|
file_info:
|
||||||
|
mode: 0644
|
||||||
|
depends:
|
||||||
|
- ca-certificates
|
||||||
|
- 'glibc >= 2.34'
|
||||||
|
- gzip
|
||||||
|
- logrotate
|
||||||
|
- shadow
|
||||||
|
- systemd
|
||||||
|
- '((python310 and python310-pip) or (python311 and python311-pip) or (python312 and python312-pip) or (python313 and python313-pip) or (python314 and python314-pip) or (python3 >= 3.10 and python3-pip))'
|
||||||
|
scripts:
|
||||||
|
postinstall: "$PKG/scripts/rpm-postinstall.sh"
|
||||||
|
preremove: "$PKG/scripts/rpm-preremove.sh"
|
||||||
|
postremove: "$PKG/scripts/rpm-postremove.sh"
|
||||||
|
rpm:
|
||||||
|
compression: gzip
|
||||||
|
signature:
|
||||||
|
key_file: $GPG_KEY_Q
|
||||||
|
YAML
|
||||||
@@ -104,6 +104,9 @@ fi
|
|||||||
|
|
||||||
[[ -f "$ENV_FILE" ]] || write_env_file
|
[[ -f "$ENV_FILE" ]] || write_env_file
|
||||||
load_env_file
|
load_env_file
|
||||||
|
if [[ "${FREEBSD_NATIVE_ENABLED:-1}" == "0" ]]; then
|
||||||
|
warn "Native FreeBSD builds are DISABLED by existing .env; set FREEBSD_NATIVE_ENABLED=1 and rerun ./start.sh to publish native PKG"
|
||||||
|
fi
|
||||||
|
|
||||||
: "${SOURCE_REPO:?Uzupelnij SOURCE_REPO w .env albo ustaw zmienna przed uruchomieniem start.sh}"
|
: "${SOURCE_REPO:?Uzupelnij SOURCE_REPO w .env albo ustaw zmienna przed uruchomieniem start.sh}"
|
||||||
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}"
|
: "${REPOSITORY_URL:?Uzupelnij REPOSITORY_URL dla APT/RPM}"
|
||||||
@@ -114,15 +117,15 @@ case "$SOURCE_REPO" in
|
|||||||
*) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;;
|
*) die "SOURCE_REPO ma nieobslugiwany format: $SOURCE_REPO" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
normalize_repo_url REPOSITORY_URL HTTP_PORT "$DEFAULT_HTTP_PORT" "APT/RPM"
|
normalize_repo_url REPOSITORY_URL HTTP_PORT "$DEFAULT_HTTP_PORT" "APT/RPM/SUSE"
|
||||||
normalize_repo_url OPNSENSE_REPOSITORY_URL OPNSENSE_HTTP_PORT "$DEFAULT_OPNSENSE_HTTP_PORT" "OPNsense"
|
normalize_repo_url OPNSENSE_REPOSITORY_URL OPNSENSE_HTTP_PORT "$DEFAULT_OPNSENSE_HTTP_PORT" "OPNsense"
|
||||||
[[ "$HTTP_PORT" != "$OPNSENSE_HTTP_PORT" ]] || die "HTTP_PORT i OPNSENSE_HTTP_PORT musza byc rozne"
|
[[ "$HTTP_PORT" != "$OPNSENSE_HTTP_PORT" ]] || die "HTTP_PORT i OPNSENSE_HTTP_PORT musza byc rozne"
|
||||||
|
|
||||||
mkdir -p "$DATA_DIR/repository" "$DATA_DIR/state" "$DATA_DIR/cargo" "$DATA_DIR/work" "$GPG_DIR"
|
mkdir -p "$DATA_DIR/repository" "$DATA_DIR/state" "$DATA_DIR/cargo" "$DATA_DIR/work" "$GPG_DIR"
|
||||||
chmod 0700 "$GPG_DIR" 2>/dev/null || true
|
chmod 0700 "$GPG_DIR" 2>/dev/null || true
|
||||||
|
|
||||||
log "budowanie obrazu buildera (Linux + FreeBSD cross-toolchain)"
|
log "budowanie obrazu buildera oraz panelu repozytorium"
|
||||||
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE"
|
"${COMPOSE[@]}" --env-file "$ENV_FILE" build "$SERVICE" repository-dashboard
|
||||||
|
|
||||||
if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
|
if [[ ! -s "$GPG_DIR/private-key.asc" ]]; then
|
||||||
log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM"
|
log "brak klucza GPG - generowanie stalego klucza podpisujacego Linux APT/RPM"
|
||||||
@@ -173,7 +176,7 @@ else
|
|||||||
log "uzywanie istniejacego klucza GPG"
|
log "uzywanie istniejacego klucza GPG"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "uruchamianie buildera i wspolnego nginx przez Docker Compose"
|
log "uruchamianie buildera, panelu i nginx przez Docker Compose"
|
||||||
"${COMPOSE[@]}" --env-file "$ENV_FILE" up -d "$SERVICE" "$HTTP_SERVICE"
|
"${COMPOSE[@]}" --env-file "$ENV_FILE" up -d "$SERVICE" "$HTTP_SERVICE"
|
||||||
|
|
||||||
fingerprint=""
|
fingerprint=""
|
||||||
@@ -182,9 +185,11 @@ fingerprint=""
|
|||||||
log "gotowe"
|
log "gotowe"
|
||||||
printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO"
|
printf '\nSOURCE_REPO: %s\n' "$SOURCE_REPO"
|
||||||
printf 'APT/RPM URL: %s/\n' "${REPOSITORY_URL%/}"
|
printf 'APT/RPM URL: %s/\n' "${REPOSITORY_URL%/}"
|
||||||
|
printf 'SUSE/Zypper RPM URL: %s/suse/x86_64/\n' "${REPOSITORY_URL%/}"
|
||||||
printf 'APT/RPM local HTTP port: %s\n' "$HTTP_PORT"
|
printf 'APT/RPM local HTTP port: %s\n' "$HTTP_PORT"
|
||||||
printf 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}"
|
printf 'OPNsense pkg URL: %s/\n' "${OPNSENSE_REPOSITORY_URL%/}"
|
||||||
printf 'OPNsense local HTTP port: %s\n' "$OPNSENSE_HTTP_PORT"
|
printf 'OPNsense local HTTP port: %s\n' "$OPNSENSE_HTTP_PORT"
|
||||||
|
[[ "${FREEBSD_NATIVE_ENABLED:-1}" == "1" ]] && printf 'Native FreeBSD pkg URL: %s/freebsd/\n' "${OPNSENSE_REPOSITORY_URL%/}"
|
||||||
[[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint"
|
[[ -n "$fingerprint" ]] && printf 'GPG fingerprint: %s\n' "$fingerprint"
|
||||||
printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository"
|
printf 'Publiczny katalog: %s\n\n' "$DATA_DIR/repository"
|
||||||
printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
|
printf 'Health Linux: http://127.0.0.1:%s/healthz\n' "$HTTP_PORT"
|
||||||
|
|||||||
@@ -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