feat(betrieb): Docker Compose und LXC-Installer
Ein Laufzeit-Paket (scripts/paket-bauen.sh) für beide Wege: Next standalone, worker als Bundle mit nur den nötigen Paketen, Migrationen ohne Prisma-CLI. Compose lokal komplett hochgefahren; der Installer auf pve-demo in einem echten Debian-13-LXC durchgelaufen. Closes #019 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
node_modules
|
||||
.next
|
||||
paket
|
||||
proxdeploy-*.tar.gz
|
||||
cli/bin
|
||||
test-results
|
||||
playwright-report
|
||||
coverage
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
*.tsbuildinfo
|
||||
src/generated
|
||||
+5
-1
@@ -1,7 +1,11 @@
|
||||
# ── Datenbank ────────────────────────────────────────────────────────────────
|
||||
DATABASE_URL=postgresql://proxdeploy:proxdeploy@localhost:5437/proxdeploy
|
||||
# Nur Docker Compose: Passwort der mitgelieferten PostgreSQL (DATABASE_URL setzt Compose selbst).
|
||||
POSTGRES_PASSWORT=
|
||||
# Nur für Tests; wird vor jedem Test geleert.
|
||||
TEST_DATABASE_URL=postgresql://proxdeploy:proxdeploy@localhost:5437/proxdeploy_test
|
||||
TEST_DATABASE_URL=postgresql://proxdeploy:proxdeploy@localhost:5437/proxdeploy
|
||||
# Nur Docker Compose: Passwort der mitgelieferten PostgreSQL (DATABASE_URL setzt Compose selbst).
|
||||
POSTGRES_PASSWORT=_test
|
||||
|
||||
# ── Geheimnisse ──────────────────────────────────────────────────────────────
|
||||
# Schlüssel für AES-256-GCM (Proxmox-Token, SSH-Schlüssel, OIDC-Secret): genau 32 Byte, base64.
|
||||
|
||||
@@ -13,3 +13,5 @@ cli/bin/
|
||||
playwright-report/
|
||||
test-results/
|
||||
.DS_Store
|
||||
paket/
|
||||
proxdeploy-*.tar.gz
|
||||
|
||||
@@ -10,3 +10,4 @@ cli/
|
||||
catalog/
|
||||
test-results
|
||||
public/brand
|
||||
paket
|
||||
|
||||
@@ -8,6 +8,10 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
## [Unveröffentlicht]
|
||||
|
||||
### Hinzugefügt
|
||||
- Betrieb per Docker Compose (postgres, migrate, app, worker) mit einem Image für alles, non-root (#019)
|
||||
- LXC-Installer `deploy/install-lxc.sh`: legt auf dem Proxmox-Host einen Debian-13-Container an, installiert Node.js und PostgreSQL, richtet systemd-Dienste ein; mit Update-Pfad (#019)
|
||||
- Migrationen im Betrieb ohne Prisma-CLI, kompatibel zu Prisma (#019)
|
||||
- Übersicht zeigt Nodes, CPUs und freien RAM je Server sowie die letzten Deployments (#018)
|
||||
- Marken-Dateien nach Vorgabe unter `public/brand/`: Symbol (dunkel/hell), kleines Icon, Avatar, Logo (dunkel/hell), Banner 1280×640, Favicons, Apple-Touch- und Manifest-Icons; erzeugt mit `pnpm marke` (#017)
|
||||
- Web-App-Manifest; Favicon passt sich hellem und dunklem Browser an (#017)
|
||||
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
# ProxDeploy – ein Image für app, worker und migrate. Gebaut wird mit scripts/paket-bauen.sh,
|
||||
# derselben Struktur wie im LXC-Installer.
|
||||
#
|
||||
# docker build --build-arg VERSION=0.6.0 -t proxdeploy:0.6.0 .
|
||||
|
||||
FROM node:24.21.0-trixie-slim AS bau
|
||||
ENV PNPM_HOME=/pnpm PATH=/pnpm:$PATH CI=true
|
||||
RUN corepack enable && corepack prepare pnpm@12.6.0 --activate
|
||||
WORKDIR /quelle
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
COPY prisma ./prisma
|
||||
COPY prisma.config.ts ./
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
ARG VERSION
|
||||
# Ohne .git im Kontext gibt es keinen Commit; dann steht nur die Version da.
|
||||
RUN scripts/paket-bauen.sh "${VERSION:-$(node -p 'require("./package.json").version')}"
|
||||
|
||||
FROM node:24.21.0-trixie-slim
|
||||
LABEL org.opencontainers.image.title="ProxDeploy" \
|
||||
org.opencontainers.image.description="VMs und LXC-Container auf Proxmox VE ausrollen, per Web oder CLI." \
|
||||
org.opencontainers.image.source="https://git.wilde-server.de/florian.wilde/proxdeploy"
|
||||
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0 NEXT_TELEMETRY_DISABLED=1
|
||||
WORKDIR /opt/proxdeploy
|
||||
COPY --from=bau --chown=root:root /quelle/paket ./
|
||||
# Läuft als „node“ (UID 1000), nie als root; der Code gehört root und ist nicht beschreibbar.
|
||||
USER node
|
||||
WORKDIR /opt/proxdeploy/app
|
||||
EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s \
|
||||
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3000)+'/api/v1/gesundheit').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
# app; worker und migrate setzen ihr Kommando in docker-compose.yml.
|
||||
CMD ["node", "server.js"]
|
||||
Executable
+214
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env bash
|
||||
# Läuft im Debian-13-Container (von install-lxc.sh per `pct exec` aufgerufen):
|
||||
#
|
||||
# im-container.sh installieren --url http://10.0.10.50:3000
|
||||
# im-container.sh aktualisieren
|
||||
#
|
||||
# Erwartet das Paket unter /root/proxdeploy-paket.tar.gz.
|
||||
set -euo pipefail
|
||||
# Über SSH/pct exec reisen die Locales des Aufrufers mit; im frischen Container gibt es sie nicht.
|
||||
export LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||
unset LANGUAGE
|
||||
|
||||
NODE_VERSION="24.21.0"
|
||||
BASIS="/opt/proxdeploy"
|
||||
ENV_DATEI="/etc/proxdeploy/proxdeploy.env"
|
||||
PAKET="/root/proxdeploy-paket.tar.gz"
|
||||
|
||||
fehler() { echo "✗ $*" >&2; exit 1; }
|
||||
schritt() { echo "◐ $*"; }
|
||||
fertig() { echo "✓ $*"; }
|
||||
|
||||
node_installieren() {
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=x64 ;;
|
||||
aarch64) arch=arm64 ;;
|
||||
*) fehler "Architektur $(uname -m) wird nicht unterstützt." ;;
|
||||
esac
|
||||
if [ -x /opt/node/bin/node ] && [ "$(/opt/node/bin/node --version)" = "v$NODE_VERSION" ]; then return; fi
|
||||
schritt "Node.js $NODE_VERSION"
|
||||
local datei="node-v$NODE_VERSION-linux-$arch.tar.xz" tmp
|
||||
tmp="$(mktemp -d)"
|
||||
curl -fsSL -o "$tmp/$datei" "https://nodejs.org/dist/v$NODE_VERSION/$datei"
|
||||
curl -fsSL -o "$tmp/SHASUMS256.txt" "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt"
|
||||
(cd "$tmp" && grep " $datei\$" SHASUMS256.txt | sha256sum -c --quiet) || fehler "Prüfsumme von $datei stimmt nicht."
|
||||
rm -rf /opt/node && mkdir -p /opt/node
|
||||
tar -xJf "$tmp/$datei" -C /opt/node --strip-components=1
|
||||
rm -rf "$tmp"
|
||||
fertig "Node.js $(/opt/node/bin/node --version)"
|
||||
}
|
||||
|
||||
paket_auspacken() {
|
||||
[ -f "$PAKET" ] || fehler "$PAKET fehlt."
|
||||
local version ziel
|
||||
version="$(tar -xzOf "$PAKET" ./VERSION)"
|
||||
ziel="$BASIS/versionen/$version"
|
||||
rm -rf "$ziel" && mkdir -p "$ziel"
|
||||
tar -xzf "$PAKET" -C "$ziel"
|
||||
chown -R root:root "$ziel"
|
||||
ln -sfn "$ziel" "$BASIS/aktuell"
|
||||
rm -f "$PAKET"
|
||||
# Die letzten drei Fassungen behalten – für ein Zurück per Symlink.
|
||||
find "$BASIS/versionen" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -rn | tail -n +4 |
|
||||
cut -d' ' -f2- | xargs -r rm -rf
|
||||
echo "$version"
|
||||
}
|
||||
|
||||
dienste_schreiben() {
|
||||
cat > /etc/systemd/system/proxdeploy-migrate.service <<EOF
|
||||
[Unit]
|
||||
Description=ProxDeploy – Datenbank-Migrationen
|
||||
After=postgresql.service network-online.target
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
User=proxdeploy
|
||||
EnvironmentFile=$ENV_DATEI
|
||||
WorkingDirectory=$BASIS/aktuell/app
|
||||
ExecStart=/opt/node/bin/node ../worker/migrieren.mjs
|
||||
EOF
|
||||
for dienst in app worker; do
|
||||
if [ "$dienst" = app ]; then start="server.js"; beschreibung="Web-GUI und REST-API"; else start="../worker/worker.mjs"; beschreibung="worker (Aufträge)"; fi
|
||||
cat > "/etc/systemd/system/proxdeploy-$dienst.service" <<EOF
|
||||
[Unit]
|
||||
Description=ProxDeploy – $beschreibung
|
||||
After=proxdeploy-migrate.service
|
||||
Requires=proxdeploy-migrate.service
|
||||
|
||||
[Service]
|
||||
User=proxdeploy
|
||||
EnvironmentFile=$ENV_DATEI
|
||||
Environment=NODE_ENV=production
|
||||
WorkingDirectory=$BASIS/aktuell/app
|
||||
ExecStart=/opt/node/bin/node $start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
TimeoutStopSec=40
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectControlGroups=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
done
|
||||
systemctl daemon-reload
|
||||
}
|
||||
|
||||
auf_gesundheit_warten() {
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS -o /dev/null "http://127.0.0.1:3000/api/v1/gesundheit" 2>/dev/null; then return 0; fi
|
||||
sleep 2
|
||||
done
|
||||
journalctl -u proxdeploy-app -u proxdeploy-migrate --no-pager -n 30 >&2
|
||||
fehler "Die app antwortet nicht."
|
||||
}
|
||||
|
||||
installieren() {
|
||||
local url=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--url) url="$2"; shift 2 ;;
|
||||
*) fehler "Unbekannte Option $1" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$url" ] || fehler "--url fehlt."
|
||||
[ -f "$ENV_DATEI" ] && fehler "ProxDeploy ist hier schon eingerichtet – zum Aktualisieren: install-lxc.sh --aktualisieren <ctid>"
|
||||
|
||||
schritt "Pakete (PostgreSQL, curl)"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends ca-certificates curl xz-utils openssl postgresql >/dev/null
|
||||
fertig "PostgreSQL $(psql --version | awk '{print $3}')"
|
||||
node_installieren
|
||||
|
||||
schritt "Benutzer und Datenbank"
|
||||
id proxdeploy >/dev/null 2>&1 || useradd --system --home-dir /var/lib/proxdeploy --create-home --shell /usr/sbin/nologin proxdeploy
|
||||
local db_passwort admin_passwort
|
||||
db_passwort="$(openssl rand -hex 24)"
|
||||
admin_passwort="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)"
|
||||
su postgres -c "psql -q -v ON_ERROR_STOP=1" <<SQL
|
||||
DO \$\$ BEGIN
|
||||
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'proxdeploy') THEN
|
||||
CREATE ROLE proxdeploy LOGIN PASSWORD '$db_passwort';
|
||||
ELSE
|
||||
ALTER ROLE proxdeploy PASSWORD '$db_passwort';
|
||||
END IF;
|
||||
END \$\$;
|
||||
SQL
|
||||
su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname = 'proxdeploy'\"" | grep -q 1 ||
|
||||
su postgres -c "createdb -O proxdeploy proxdeploy"
|
||||
fertig "Datenbank proxdeploy"
|
||||
|
||||
schritt "Konfiguration $ENV_DATEI"
|
||||
mkdir -p /etc/proxdeploy
|
||||
umask 027
|
||||
cat > "$ENV_DATEI" <<EOF
|
||||
# ProxDeploy – erzeugt von im-container.sh am $(date -Iseconds)
|
||||
DATABASE_URL=postgresql://proxdeploy:$db_passwort@127.0.0.1:5432/proxdeploy
|
||||
PROXDEPLOY_SCHLUESSEL=$(openssl rand -base64 32)
|
||||
SITZUNG_GEHEIMNIS=$(openssl rand -base64 48)
|
||||
APP_URL=$url
|
||||
# Hinter einem Reverse-Proxy auf true setzen (wertet X-Forwarded-* aus).
|
||||
TRUST_PROXY=false
|
||||
PORT=3000
|
||||
HOSTNAME=0.0.0.0
|
||||
LOG_LEVEL=info
|
||||
WORKER_PARALLEL=4
|
||||
ADMIN_BENUTZER=admin
|
||||
ADMIN_PASSWORT=$admin_passwort
|
||||
EOF
|
||||
chown root:proxdeploy "$ENV_DATEI"
|
||||
chmod 0640 "$ENV_DATEI"
|
||||
umask 022
|
||||
|
||||
schritt "ProxDeploy"
|
||||
local version
|
||||
version="$(paket_auspacken)"
|
||||
dienste_schreiben
|
||||
systemctl enable --now proxdeploy-app proxdeploy-worker >/dev/null 2>&1
|
||||
auf_gesundheit_warten
|
||||
# Der erste Admin ist angelegt – das Passwort hat in der Datei nichts mehr zu suchen.
|
||||
sed -i '/^ADMIN_PASSWORT=/d' "$ENV_DATEI"
|
||||
fertig "ProxDeploy $version läuft"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
ProxDeploy ist bereit: $url
|
||||
Benutzer: admin
|
||||
Passwort: $admin_passwort
|
||||
(wird nur jetzt angezeigt – nach der Anmeldung unter
|
||||
Einstellungen → Passwort ändern)
|
||||
Konfiguration: $ENV_DATEI
|
||||
Logs: journalctl -u proxdeploy-app -u proxdeploy-worker
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
EOF
|
||||
}
|
||||
|
||||
aktualisieren() {
|
||||
[ -f "$ENV_DATEI" ] || fehler "Hier ist ProxDeploy noch nicht eingerichtet."
|
||||
node_installieren
|
||||
local alt version
|
||||
alt="$(cat "$BASIS/aktuell/VERSION" 2>/dev/null || echo –)"
|
||||
systemctl stop proxdeploy-app proxdeploy-worker
|
||||
version="$(paket_auspacken)"
|
||||
dienste_schreiben
|
||||
# migrate bleibt nach dem ersten Lauf „aktiv“ – für die neue Fassung ausdrücklich neu starten.
|
||||
systemctl restart proxdeploy-migrate
|
||||
systemctl start proxdeploy-app proxdeploy-worker
|
||||
auf_gesundheit_warten
|
||||
fertig "ProxDeploy $alt → $version"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
installieren) shift; installieren "$@" ;;
|
||||
aktualisieren) shift; aktualisieren "$@" ;;
|
||||
*) fehler "Aufruf: $0 installieren --url URL | aktualisieren" ;;
|
||||
esac
|
||||
Executable
+121
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# ProxDeploy in einem Debian-13-LXC einrichten – auf dem Proxmox-Host als root ausführen.
|
||||
#
|
||||
# ./install-lxc.sh --paket proxdeploy-0.6.0.tar.gz [Optionen]
|
||||
# ./install-lxc.sh --aktualisieren <ctid> --paket proxdeploy-0.6.1.tar.gz
|
||||
#
|
||||
# Optionen (alle optional außer --paket):
|
||||
# --ctid N Container-ID (Standard: nächste freie)
|
||||
# --hostname NAME Hostname (Standard: proxdeploy)
|
||||
# --storage NAME Storage für die Disk (Standard: local-lvm)
|
||||
# --disk GB Disk in GB (Standard: 8)
|
||||
# --cores N CPU-Kerne (Standard: 2)
|
||||
# --memory MB RAM (Standard: 2048)
|
||||
# --bridge NAME Bridge (Standard: vmbr0)
|
||||
# --vlan TAG VLAN-Tag
|
||||
# --ip CIDR feste Adresse, z. B. 10.0.10.50/24 (Standard: DHCP); dazu --gw
|
||||
# --gw ADRESSE Gateway bei fester Adresse
|
||||
# --url URL öffentliche Adresse (APP_URL), z. B. https://proxdeploy.example
|
||||
# (Standard: http://<IP des Containers>:3000)
|
||||
# --tags LISTE zusätzliche Proxmox-Tags, z. B. proxdeploy-test
|
||||
#
|
||||
# Das Paket baut `scripts/paket-bauen.sh <version> --tar` im Repository.
|
||||
set -euo pipefail
|
||||
|
||||
fehler() { echo "✗ $*" >&2; exit 1; }
|
||||
schritt() { echo "◐ $*"; }
|
||||
fertig() { echo "✓ $*"; }
|
||||
|
||||
[ "$(id -u)" = 0 ] || fehler "Bitte als root auf dem Proxmox-Host ausführen."
|
||||
command -v pct >/dev/null || fehler "pct fehlt – läuft das auf einem Proxmox-VE-Host?"
|
||||
|
||||
ctid="" hostname="proxdeploy" storage="local-lvm" disk=8 cores=2 memory=2048 bridge="vmbr0"
|
||||
vlan="" ip="dhcp" gw="" url="" paket="" aktualisieren="" tags=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--ctid) ctid="$2"; shift 2 ;;
|
||||
--hostname) hostname="$2"; shift 2 ;;
|
||||
--storage) storage="$2"; shift 2 ;;
|
||||
--disk) disk="$2"; shift 2 ;;
|
||||
--cores) cores="$2"; shift 2 ;;
|
||||
--memory) memory="$2"; shift 2 ;;
|
||||
--bridge) bridge="$2"; shift 2 ;;
|
||||
--vlan) vlan="$2"; shift 2 ;;
|
||||
--ip) ip="$2"; shift 2 ;;
|
||||
--gw) gw="$2"; shift 2 ;;
|
||||
--url) url="$2"; shift 2 ;;
|
||||
--tags) tags="$2"; shift 2 ;;
|
||||
--paket) paket="$2"; shift 2 ;;
|
||||
--aktualisieren) aktualisieren="$2"; shift 2 ;;
|
||||
-h|--help) sed -n '2,25p' "$0"; exit 0 ;;
|
||||
*) fehler "Unbekannte Option $1 (siehe --help)" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$paket" ] || fehler "--paket fehlt (proxdeploy-<version>.tar.gz)."
|
||||
[ -f "$paket" ] || fehler "$paket gibt es nicht."
|
||||
tar -tzf "$paket" ./VERSION >/dev/null 2>&1 || fehler "$paket ist kein ProxDeploy-Paket (VERSION fehlt)."
|
||||
version="$(tar -xzOf "$paket" ./VERSION)"
|
||||
innen="$(dirname "$0")/im-container.sh"
|
||||
[ -f "$innen" ] || fehler "im-container.sh fehlt neben diesem Skript."
|
||||
|
||||
pushen() {
|
||||
pct push "$1" "$paket" /root/proxdeploy-paket.tar.gz
|
||||
pct push "$1" "$innen" /root/proxdeploy-im-container.sh --perms 0700
|
||||
}
|
||||
|
||||
# ── Aktualisieren ───────────────────────────────────────────────────────────────────────────────
|
||||
if [ -n "$aktualisieren" ]; then
|
||||
pct status "$aktualisieren" | grep -q running || fehler "Container $aktualisieren läuft nicht."
|
||||
schritt "Aktualisiere Container $aktualisieren auf $version"
|
||||
pushen "$aktualisieren"
|
||||
pct exec "$aktualisieren" -- bash /root/proxdeploy-im-container.sh aktualisieren
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Neu einrichten ──────────────────────────────────────────────────────────────────────────────
|
||||
[ -n "$ctid" ] || ctid="$(pvesh get /cluster/nextid)"
|
||||
pct status "$ctid" >/dev/null 2>&1 && fehler "Container $ctid gibt es schon."
|
||||
if [ "$ip" != dhcp ]; then
|
||||
[[ "$ip" =~ ^[0-9.]+/[0-9]+$ ]] || fehler "--ip: Format 10.0.10.50/24"
|
||||
[ -n "$gw" ] || fehler "Bei fester Adresse fehlt --gw."
|
||||
fi
|
||||
|
||||
schritt "LXC-Template Debian 13"
|
||||
pveam update >/dev/null
|
||||
vorlage="$(pveam available --section system | awk '/debian-13-standard_.*_amd64\.tar\.zst/ {print $2}' | sort -V | tail -1)"
|
||||
[ -n "$vorlage" ] || fehler "Kein Debian-13-Template in pveam gefunden."
|
||||
template_storage="$(pvesm status --content vztmpl | awk 'NR>1 {print $1; exit}')"
|
||||
[ -n "$template_storage" ] || fehler "Kein Storage für Container-Templates (Inhalt vztmpl)."
|
||||
if ! pveam list "$template_storage" | grep -q "$vorlage"; then
|
||||
pveam download "$template_storage" "$vorlage" >/dev/null
|
||||
fi
|
||||
fertig "$vorlage auf $template_storage"
|
||||
|
||||
netz="name=eth0,bridge=$bridge,firewall=1"
|
||||
[ -n "$vlan" ] && netz="$netz,tag=$vlan"
|
||||
if [ "$ip" = dhcp ]; then netz="$netz,ip=dhcp"; else netz="$netz,ip=$ip,gw=$gw"; fi
|
||||
|
||||
schritt "Container $ctid anlegen"
|
||||
pct create "$ctid" "$template_storage:vztmpl/$vorlage" \
|
||||
--hostname "$hostname" --cores "$cores" --memory "$memory" --swap 512 \
|
||||
--rootfs "$storage:$disk" --net0 "$netz" \
|
||||
--unprivileged 1 --features nesting=1 --onboot 1 \
|
||||
--tags "proxdeploy${tags:+;$tags}" \
|
||||
--description "ProxDeploy $version – eingerichtet mit install-lxc.sh" >/dev/null
|
||||
pct start "$ctid"
|
||||
fertig "Container $ctid ($hostname) läuft"
|
||||
|
||||
schritt "Netzwerk"
|
||||
adresse=""
|
||||
for _ in $(seq 1 60); do
|
||||
adresse="$(pct exec "$ctid" -- ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)"
|
||||
[ -n "$adresse" ] && pct exec "$ctid" -- getent hosts deb.debian.org >/dev/null 2>&1 && break
|
||||
adresse=""
|
||||
sleep 2
|
||||
done
|
||||
[ -n "$adresse" ] || fehler "Container $ctid bekommt keine Adresse oder kein DNS."
|
||||
fertig "IP $adresse"
|
||||
|
||||
[ -n "$url" ] || url="http://$adresse:3000"
|
||||
pushen "$ctid"
|
||||
pct exec "$ctid" -- bash /root/proxdeploy-im-container.sh installieren --url "$url"
|
||||
@@ -0,0 +1,65 @@
|
||||
# ProxDeploy im Betrieb: postgres, migrate (einmalig), app, worker.
|
||||
#
|
||||
# cp .env.example .env # Schlüssel erzeugen, ADMIN_PASSWORT und APP_URL setzen
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# Die app lauscht auf 127.0.0.1:3000 – davor gehört ein Reverse-Proxy mit TLS (TRUST_PROXY=true).
|
||||
|
||||
name: proxdeploy
|
||||
|
||||
x-proxdeploy: &proxdeploy
|
||||
image: proxdeploy:${PROXDEPLOY_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
VERSION: ${PROXDEPLOY_VERSION:-}
|
||||
env_file: ${PROXDEPLOY_ENV_DATEI:-.env}
|
||||
environment:
|
||||
DATABASE_URL: postgresql://proxdeploy:${POSTGRES_PASSWORT:?POSTGRES_PASSWORT in .env setzen}@postgres:5432/proxdeploy
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17.11-trixie
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: proxdeploy
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORT:?POSTGRES_PASSWORT in .env setzen}
|
||||
POSTGRES_DB: proxdeploy
|
||||
volumes:
|
||||
- datenbank:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U proxdeploy"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 20
|
||||
|
||||
migrate:
|
||||
<<: *proxdeploy
|
||||
command: ["node", "../worker/migrieren.mjs"]
|
||||
restart: "no"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
app:
|
||||
<<: *proxdeploy
|
||||
ports:
|
||||
- "${PROXDEPLOY_BIND:-127.0.0.1}:${PROXDEPLOY_PORT:-3000}:3000"
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
worker:
|
||||
<<: *proxdeploy
|
||||
command: ["node", "../worker/worker.mjs"]
|
||||
healthcheck:
|
||||
disable: true
|
||||
# Lange Aufträge (Template-Bau) dürfen beim Stoppen zu Ende laufen.
|
||||
stop_grace_period: 40s
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
|
||||
volumes:
|
||||
datenbank:
|
||||
@@ -1,5 +1,5 @@
|
||||
# #019 Docker-Image und LXC-Installer
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.6.0
|
||||
**Labels:** infrastruktur
|
||||
**Geschätzt:** 6 h **Tatsächlich:** –
|
||||
@@ -8,14 +8,22 @@
|
||||
Produktionsbetrieb per Docker Compose oder in einem Debian-LXC.
|
||||
|
||||
## Aufgaben
|
||||
- [ ] Dockerfile (standalone, non-root), Compose mit migrate
|
||||
- [ ] `deploy/install-lxc.sh` (auf dem Proxmox-Host), Update-Pfad
|
||||
- [x] Dockerfile (standalone, non-root), Compose mit migrate – lokal komplett hochgefahren (migrate → app healthy, worker bereit, Anmeldung)
|
||||
- [x] `deploy/install-lxc.sh` (auf dem Proxmox-Host) + `deploy/im-container.sh`, Update-Pfad (`--aktualisieren`, drei Fassungen zum Zurückgehen)
|
||||
- [x] Gemeinsames Laufzeit-Paket `scripts/paket-bauen.sh` für beide Wege
|
||||
- [x] Migrationen ohne Prisma-CLI (`src/betrieb/migrieren.ts`), kompatibel zu `_prisma_migrations`
|
||||
|
||||
## Akzeptanzkriterien
|
||||
- [ ] Installer in echtem LXC durchgelaufen
|
||||
- [x] Installer in echtem LXC durchgelaufen (pve-demo, CT 109 `pd-installer`); darin Server angelegt, Verbindungstest, CLI-Login, Dry-Run und ein echter LXC-Auftrag
|
||||
|
||||
## Abhängigkeiten
|
||||
#001
|
||||
|
||||
## Notizen / Entscheidungen
|
||||
–
|
||||
- **Eigener Migrationslauf statt `prisma migrate deploy`:** spart Prisma-CLI und Schema-Engine im
|
||||
Betrieb; schreibt `_prisma_migrations` wie Prisma (Test prüft `prisma migrate status`).
|
||||
- **worker als esbuild-Bundle** mit nur den Paketen, die er importiert (aus der Metadatei, exakte
|
||||
Versionen aus der Entwicklung): 103 MB statt 741 MB.
|
||||
- **Node im LXC von nodejs.org** mit SHA-256-Prüfung (Debian 13 hat nur Node 20).
|
||||
- `ADMIN_PASSWORT` wird nach dem ersten Start aus der Env-Datei entfernt; das Passwort steht nur in der Ausgabe.
|
||||
- Image 559 MB (node:24.21.0-trixie-slim), läuft als UID 1000; PostgreSQL 17.11.
|
||||
|
||||
@@ -22,7 +22,7 @@ Details und Architektur: [../architektur.md](../architektur.md). Zeiten werden a
|
||||
| [#016](016-oidc.md) | Anmeldung per OIDC | erledigt | v0.5.0 | 4 h | 0 h 08 min |
|
||||
| [#017](017-branding.md) | Branding-Assets | erledigt | v0.6.0 | 4 h | – |
|
||||
| [#018](018-readme.md) | README und Dokumentation | offen | v0.6.0 | 4 h | – |
|
||||
| [#019](019-installer.md) | Docker-Image und LXC-Installer | offen | v0.6.0 | 6 h | – |
|
||||
| [#019](019-installer.md) | Docker-Image und LXC-Installer | erledigt | v0.6.0 | 6 h | – |
|
||||
| [#020](020-feldtest.md) | Feldtest 8.x und 9.x, Version 1.0.0 | offen | v1.0.0 | 6 h | – |
|
||||
| [#021](021-template-bau-ohne-ssh.md) | Template-Bau ohne SSH (Bau-Adresse) | umgesetzt, Abnahme offen | v0.3.0 | 3 h | 0 h 19 min |
|
||||
| [#022](022-template-profile.md) | Templates mit eingebackenen Profilen | erledigt | v0.3.0 | 4 h | 0 h 22 min |
|
||||
|
||||
@@ -43,5 +43,7 @@ export default defineConfig([
|
||||
"playwright-report/**",
|
||||
"dist/**",
|
||||
"cli/**",
|
||||
// Laufzeit-Paket aus scripts/paket-bauen.sh.
|
||||
"paket/**",
|
||||
]),
|
||||
]);
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# Baut das Laufzeit-Paket von ProxDeploy – dieselbe Struktur für Docker-Image und LXC-Installer:
|
||||
#
|
||||
# paket/
|
||||
# app/ Next standalone (server.js, .next, node_modules), dazu public/, catalog/,
|
||||
# prisma/migrations/ – Arbeitsverzeichnis für app und worker
|
||||
# worker/worker.mjs worker als ein Bundle (esbuild); Pakete aus worker/node_modules
|
||||
# worker/migrieren.mjs Migrationen ohne Prisma-CLI
|
||||
# worker/node_modules/ Produktions-Abhängigkeiten, flach (ohne Symlinks)
|
||||
# VERSION
|
||||
#
|
||||
# scripts/paket-bauen.sh [version] → paket/
|
||||
# scripts/paket-bauen.sh 0.6.0 --tar → zusätzlich proxdeploy-0.6.0.tar.gz
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
version="${1:-$(node -p 'require("./package.json").version')}"
|
||||
commit="$(git rev-parse --short HEAD 2>/dev/null || echo unbekannt)"
|
||||
|
||||
rm -rf paket
|
||||
mkdir -p paket/worker
|
||||
|
||||
# 1. Web-App (standalone). Version und Commit landen im Build (Seitenleiste, /api/v1/ich).
|
||||
NEXT_PUBLIC_APP_VERSION="$version" NEXT_PUBLIC_GIT_SHA="$commit" pnpm build >/dev/null
|
||||
cp -a .next/standalone paket/app
|
||||
cp -a .next/static paket/app/.next/static
|
||||
cp -a public paket/app/public
|
||||
cp -a catalog paket/app/catalog
|
||||
mkdir -p paket/app/prisma && cp -a prisma/migrations paket/app/prisma/migrations
|
||||
|
||||
# 2. worker und Migrationslauf als Bundles; eigener Code (inkl. Prisma-Client aus src/generated)
|
||||
# wird gebündelt, Pakete bleiben extern.
|
||||
for eintrag in "src/worker/main.ts:worker.mjs" "src/betrieb/migrieren-start.ts:migrieren.mjs"; do
|
||||
pnpm exec esbuild "${eintrag%%:*}" --bundle --platform=node --format=esm --target=node24 \
|
||||
--packages=external --log-level=warning \
|
||||
--define:process.env.NEXT_PUBLIC_APP_VERSION="\"$version\"" \
|
||||
--define:process.env.NEXT_PUBLIC_GIT_SHA="\"$commit\"" \
|
||||
--metafile="paket/worker/${eintrag##*:}.meta.json" \
|
||||
--outfile="paket/worker/${eintrag##*:}"
|
||||
done
|
||||
|
||||
# 3. Nur die Pakete, die die Bundles wirklich importieren (aus den esbuild-Metadateien), in den
|
||||
# Versionen aus package.json – flach, ohne Build-Skripte. Next, React & Co. braucht der worker nicht.
|
||||
tmp="$(mktemp -d)"
|
||||
node - "$tmp" paket/worker/*.meta.json <<'JS'
|
||||
const fs = require("node:fs");
|
||||
const [ziel, ...metas] = process.argv.slice(2);
|
||||
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
|
||||
const benoetigt = new Set();
|
||||
for (const datei of metas) {
|
||||
const meta = JSON.parse(fs.readFileSync(datei, "utf8"));
|
||||
for (const eingabe of Object.values(meta.inputs))
|
||||
for (const imp of eingabe.imports ?? [])
|
||||
if (imp.external && !imp.path.startsWith("node:")) {
|
||||
const teile = imp.path.split("/");
|
||||
benoetigt.add(imp.path.startsWith("@") ? teile.slice(0, 2).join("/") : teile[0]);
|
||||
}
|
||||
}
|
||||
const abhaengigkeiten = {};
|
||||
for (const name of [...benoetigt].sort()) {
|
||||
if (!pkg.dependencies[name]) throw new Error(`${name} wird vom worker importiert, steht aber nicht in dependencies`);
|
||||
// Exakt die Version, die in der Entwicklung installiert ist (aus dem Lockfile aufgelöst).
|
||||
abhaengigkeiten[name] = JSON.parse(fs.readFileSync(`node_modules/${name}/package.json`, "utf8")).version;
|
||||
}
|
||||
fs.writeFileSync(`${ziel}/package.json`, JSON.stringify({ name: "proxdeploy-worker", private: true, type: "module", dependencies: abhaengigkeiten }, null, 2));
|
||||
console.log(`worker braucht: ${Object.entries(abhaengigkeiten).map(([n, v]) => `${n}@${v}`).join(", ")}`);
|
||||
JS
|
||||
rm -f paket/worker/*.meta.json
|
||||
cp pnpm-workspace.yaml "$tmp/"
|
||||
(cd "$tmp" && pnpm install --prod --ignore-scripts --config.node-linker=hoisted >/dev/null)
|
||||
mv "$tmp/node_modules" paket/worker/node_modules
|
||||
rm -rf "$tmp"
|
||||
|
||||
echo "$version+$commit" > paket/VERSION
|
||||
echo "paket/ gebaut ($version+$commit, $(du -sh paket | cut -f1))"
|
||||
|
||||
if [ "${2:-}" = "--tar" ]; then
|
||||
tar -czf "proxdeploy-$version.tar.gz" -C paket .
|
||||
sha256sum "proxdeploy-$version.tar.gz"
|
||||
fi
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Einstieg für den Betrieb: `node migrieren.mjs` (Docker: Dienst „migrate“, LXC: vor app und worker).
|
||||
* Liest DATABASE_URL und den Ordner der Migrationen (Standard: prisma/migrations neben dem Aufruf).
|
||||
*/
|
||||
import { migrieren } from "./migrieren";
|
||||
|
||||
const url = process.env["DATABASE_URL"];
|
||||
if (!url) {
|
||||
console.error("DATABASE_URL fehlt.");
|
||||
process.exit(2);
|
||||
}
|
||||
const ordner = process.env["MIGRATIONEN"] ?? "prisma/migrations";
|
||||
try {
|
||||
const { eingespielt, vorhanden } = await migrieren(url, ordner, (text) => console.log(text));
|
||||
console.log(
|
||||
eingespielt.length > 0
|
||||
? `${eingespielt.length} Migration(en) eingespielt, ${vorhanden} waren schon da.`
|
||||
: `Datenbank aktuell (${vorhanden} Migrationen).`,
|
||||
);
|
||||
} catch (fehler) {
|
||||
console.error((fehler as Error).message);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import pg from "pg";
|
||||
|
||||
/**
|
||||
* Spielt die Migrationen aus `prisma/migrations/` ein – im Betrieb statt `prisma migrate deploy`,
|
||||
* damit weder Docker-Image noch LXC das Prisma-CLI samt Schema-Engine brauchen.
|
||||
*
|
||||
* Kompatibel zu Prisma: dieselbe Tabelle `_prisma_migrations`, Prüfsumme = SHA-256 der
|
||||
* migration.sql. In der Entwicklung bleibt `prisma migrate dev` also benutzbar, und beide
|
||||
* erkennen, was schon eingespielt ist.
|
||||
*
|
||||
* Jede Migration läuft in einer eigenen Transaktion; eine Sperre (`pg_advisory_lock`) verhindert,
|
||||
* dass app und worker gleichzeitig migrieren. Eine eingespielte Migration, deren Datei sich
|
||||
* geändert hat, ist ein Fehler – dann stimmt der Codestand nicht zur Datenbank.
|
||||
*/
|
||||
const SPERRE = 724_196_331; // beliebig, aber fest: „proxdeploy-migration“
|
||||
|
||||
export type MigrationsErgebnis = { eingespielt: string[]; vorhanden: number };
|
||||
|
||||
export async function migrieren(
|
||||
datenbankUrl: string,
|
||||
ordner: string,
|
||||
log: (text: string) => void = () => {},
|
||||
): Promise<MigrationsErgebnis> {
|
||||
const migrationen = readdirSync(ordner, { withFileTypes: true })
|
||||
.filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
const client = new pg.Client({ connectionString: datenbankUrl });
|
||||
await client.connect();
|
||||
try {
|
||||
await client.query("SELECT pg_advisory_lock($1)", [SPERRE]);
|
||||
await client.query(`CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
|
||||
"id" VARCHAR(36) PRIMARY KEY NOT NULL,
|
||||
"checksum" VARCHAR(64) NOT NULL,
|
||||
"finished_at" TIMESTAMPTZ,
|
||||
"migration_name" VARCHAR(255) NOT NULL,
|
||||
"logs" TEXT,
|
||||
"rolled_back_at" TIMESTAMPTZ,
|
||||
"started_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
"applied_steps_count" INTEGER NOT NULL DEFAULT 0
|
||||
)`);
|
||||
const { rows } = await client.query<{
|
||||
migration_name: string;
|
||||
checksum: string;
|
||||
finished_at: Date | null;
|
||||
}>(`SELECT migration_name, checksum, finished_at FROM "_prisma_migrations" WHERE rolled_back_at IS NULL`);
|
||||
const bekannt = new Map(rows.map((r) => [r.migration_name, r]));
|
||||
const unfertig = rows.filter((r) => !r.finished_at);
|
||||
if (unfertig.length > 0) {
|
||||
throw new Error(
|
||||
`Migration ${unfertig.map((r) => r.migration_name).join(", ")} ist unvollständig eingespielt – bitte prüfen.`,
|
||||
);
|
||||
}
|
||||
|
||||
const eingespielt: string[] = [];
|
||||
for (const name of migrationen) {
|
||||
const sql = readFileSync(join(ordner, name, "migration.sql"));
|
||||
const pruefsumme = createHash("sha256").update(sql).digest("hex");
|
||||
const vorhanden = bekannt.get(name);
|
||||
if (vorhanden) {
|
||||
if (vorhanden.checksum !== pruefsumme) {
|
||||
throw new Error(`Migration ${name} wurde nach dem Einspielen verändert (Prüfsumme weicht ab).`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
log(`Migration ${name} …`);
|
||||
await client.query("BEGIN");
|
||||
try {
|
||||
const id = randomUUID();
|
||||
await client.query(
|
||||
`INSERT INTO "_prisma_migrations" (id, checksum, migration_name, started_at) VALUES ($1, $2, $3, now())`,
|
||||
[id, pruefsumme, name],
|
||||
);
|
||||
await client.query(sql.toString("utf8"));
|
||||
await client.query(
|
||||
`UPDATE "_prisma_migrations" SET finished_at = now(), applied_steps_count = 1 WHERE id = $1`,
|
||||
[id],
|
||||
);
|
||||
await client.query("COMMIT");
|
||||
} catch (fehler) {
|
||||
await client.query("ROLLBACK");
|
||||
throw new Error(`Migration ${name} fehlgeschlagen: ${(fehler as Error).message}`);
|
||||
}
|
||||
eingespielt.push(name);
|
||||
}
|
||||
return { eingespielt, vorhanden: migrationen.length - eingespielt.length };
|
||||
} finally {
|
||||
await client.query("SELECT pg_advisory_unlock($1)", [SPERRE]).catch(() => {});
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
@@ -52,4 +52,36 @@ describe.runIf(shellcheckVerfuegbar())("shellcheck", () => {
|
||||
})();
|
||||
expect(ausgabe).toBe("");
|
||||
}, 60_000);
|
||||
|
||||
it("findet in den Betriebsskripten nichts (Installer, Paket, CLI-Build)", () => {
|
||||
const dateien = [
|
||||
"deploy/install-lxc.sh",
|
||||
"deploy/im-container.sh",
|
||||
"scripts/paket-bauen.sh",
|
||||
"scripts/cli-pruefen.sh",
|
||||
"cli/bauen.sh",
|
||||
];
|
||||
const ausgabe = (() => {
|
||||
try {
|
||||
execFileSync(
|
||||
"docker",
|
||||
[
|
||||
"run",
|
||||
"--rm",
|
||||
"-v",
|
||||
`${process.cwd()}:/mnt:ro`,
|
||||
"-w",
|
||||
"/mnt",
|
||||
"koalaman/shellcheck:stable",
|
||||
...dateien,
|
||||
],
|
||||
{ stdio: "pipe" },
|
||||
);
|
||||
return "";
|
||||
} catch (fehler) {
|
||||
return String((fehler as { stdout?: Buffer }).stdout ?? fehler);
|
||||
}
|
||||
})();
|
||||
expect(ausgabe).toBe("");
|
||||
}, 60_000);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import pg from "pg";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { migrieren } from "@/betrieb/migrieren";
|
||||
import { datenbankVerfuegbar } from "./helpers/db";
|
||||
|
||||
const NAME = "proxdeploy_migtest";
|
||||
|
||||
describe.runIf(datenbankVerfuegbar)("Migrationen im Betrieb (ohne Prisma-CLI)", () => {
|
||||
const basis = new URL(process.env["TEST_DATABASE_URL"]!);
|
||||
const ziel = new URL(basis);
|
||||
ziel.pathname = `/${NAME}`;
|
||||
const verwaltung = async (sql: string) => {
|
||||
const c = new pg.Client({ connectionString: basis.href });
|
||||
await c.connect();
|
||||
try {
|
||||
await c.query(sql);
|
||||
} finally {
|
||||
await c.end();
|
||||
}
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await verwaltung(`DROP DATABASE IF EXISTS ${NAME} WITH (FORCE)`);
|
||||
await verwaltung(`CREATE DATABASE ${NAME}`);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await verwaltung(`DROP DATABASE IF EXISTS ${NAME} WITH (FORCE)`);
|
||||
});
|
||||
|
||||
it("spielt alle ein, beim zweiten Mal nichts – und Prisma hält den Stand für aktuell", async () => {
|
||||
const erstes = await migrieren(ziel.href, "prisma/migrations");
|
||||
expect(erstes.eingespielt.length).toBeGreaterThan(5);
|
||||
expect(erstes.eingespielt[0]).toMatch(/_grundgeruest$/);
|
||||
expect(await migrieren(ziel.href, "prisma/migrations")).toEqual({
|
||||
eingespielt: [],
|
||||
vorhanden: erstes.eingespielt.length,
|
||||
});
|
||||
|
||||
const status = execFileSync("pnpm", ["exec", "prisma", "migrate", "status"], {
|
||||
env: { ...process.env, DATABASE_URL: ziel.href },
|
||||
encoding: "utf8",
|
||||
});
|
||||
expect(status).toMatch(/Database schema is up to date/);
|
||||
|
||||
// Auch die Regeln aus den Migrationen (Trigger) sind da: das Audit-Log ist nur anhängbar.
|
||||
const c = new pg.Client({ connectionString: ziel.href });
|
||||
await c.connect();
|
||||
try {
|
||||
await c.query(`INSERT INTO "Protokoll" (quelle, aktion) VALUES ('SYSTEM', 'Test')`);
|
||||
await expect(c.query(`DELETE FROM "Protokoll"`)).rejects.toThrow();
|
||||
} finally {
|
||||
await c.end();
|
||||
}
|
||||
});
|
||||
|
||||
it("verweigert, wenn eine eingespielte Migration verändert wurde", async () => {
|
||||
const c = new pg.Client({ connectionString: ziel.href });
|
||||
await c.connect();
|
||||
await c.query(`UPDATE "_prisma_migrations" SET checksum = 'x' WHERE migration_name LIKE '%_server'`);
|
||||
await c.end();
|
||||
await expect(migrieren(ziel.href, "prisma/migrations")).rejects.toThrow(/verändert/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user