release 2026.07.07-24520d4

This commit is contained in:
gitea-actions
2026-07-07 13:46:12 +00:00
parent ecb8d90ce6
commit 37a853d12f
6 changed files with 674 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
# Kopie als deploy/.env ablegen und Werte setzen. .env NIE committen.
# --- Postgres ---
# Nur URL-sichere Zeichen (A-Za-z0-9_-): das Passwort landet auch in Hydras DSN-URL.
PG_PASSWORD=CHANGE_ME
# --- Hydra ---
# openssl rand -hex 32 — nach Erstinbetriebnahme NICHT mehr ändern.
SECRETS_SYSTEM=CHANGE_ME
# Öffentlicher OAuth-Issuer (Reverse-Proxy-vHost auf hydra:4444). MUSS im Betrieb
# per HTTPS erreichbar sein — der MCP-Server validiert JWTs dagegen mit
# RequireHttpsMetadata=true. Für reinen lokalen Smoke ohne Token-Flow:
# http://127.0.0.1:4444 (der MCP-200-Tool-Call gelingt damit NICHT, siehe README).
PUBLIC_ISSUER=https://mcp-oauth.example.com
# Basis-URL des Login-Services (Reverse-Proxy-vHost auf login:8081).
LOGIN_URL=https://mcp-login.example.com
# --- MCP ---
# Öffentliche Basis-URL des MCP-Resource-Servers (Reverse-Proxy-vHost auf mcp:8080).
MCP_RESOURCE_URL=https://mcp.example.com
# --- SAP Service Layer ---
# Trailing /b1s/ ist Pflicht (relative Pfade wie v2/Login).
SL_BASE_URL=https://sap-server.example.com:50000/b1s/
SL_COMPANYDB=CHANGE_ME
# true nur bei self-signed SAP-Zertifikat.
SL_INSECURE=false
# --- Admin-UI ---
# Nur dieser SAP-Benutzer darf das Admin-UI öffnen (gegen den Service Layer geprüft).
# PFLICHT — ohne diesen Wert lässt der Admin-Dienst niemanden hinein (fail closed).
ADMIN_SAP_USER=manager
# --- MCP-Zugriff (Allowlist) ---
# Kommagetrennte SAP-UserCodes, die den MCP IMMER nutzen dürfen. Alle anderen sind
# per Default AUS und werden im Admin-UI unter „Zugriff" freigeschaltet.
MCP_ALLOWED_USERS=manager
# --- Host-Ports / Bind-IPs (für den Reverse Proxy) ---
MCP_HOST_PORT=8080
LOGIN_HOST_PORT=8081
ADMIN_HOST_PORT=8082
HYDRA_PUBLIC_HOST_PORT=4444
# NUR an die interne Host-IP binden, die der Reverse Proxy erreicht — NIE 0.0.0.0.
# Default 127.0.0.1 ist sicher (Proxy auf demselben Host).
#BIND_ADDR=127.0.0.1
# Bind-IP der Admin-UI (inkl. /audit hinter dem SAP-Login-Gate): Default 127.0.0.1.
#ADMIN_BIND=127.0.0.1
# --- Deployment (setzt setup.sh automatisch) ---
# Host-Pfad des Deployments — braucht die Update-Funktion des Admin-UI.
PROJECT_DIR=
# Hinweis: CREDS_KEY kommt NICHT aus .env, sondern als Secret-Datei
# deploy/secrets/creds_key (32 Byte base64: openssl rand -base64 32).
+105
View File
@@ -0,0 +1,105 @@
# EasyMCP / SapB1Mcp — Installation (Kunden-Deployment)
Der Stack wird direkt aus der öffentlichen Registry von `git.kraemerweb.de`
installiert und aktualisiert — es wird kein Installationspaket kopiert.
## Voraussetzungen
- Linux-Host, x86_64 (getestet: Debian 12/13)
- Docker Engine ≥ 24 mit Compose-Plugin v2 (`docker compose version`)
— alternativ ein vorhandenes `docker-compose`-Binary
- `curl`
- **HTTPS-Zugriff auf `git.kraemerweb.de`** (Release-Dateien + Container-Registry)
- Ein Reverse Proxy mit **TLS** (z. B. Caddy, nginx, Traefik) vor dem Stack.
HTTPS ist Pflicht: der MCP-Server validiert OAuth-Tokens nur gegen einen
HTTPS-Issuer.
- Erreichbarer SAP Business One Service Layer (`https://<sap-host>:50000/b1s/`)
## Installation
1. **Setup-Skript holen** (z. B. nach `/opt/sapb1mcp`):
```
mkdir -p /opt/sapb1mcp && cd /opt/sapb1mcp
curl -fsSLO https://git.kraemerweb.de/kraemerweb/easymcp-release/raw/branch/main/setup.sh
bash setup.sh
```
`bash setup.sh <version>` installiert eine bestimmte Version (Tags siehe
Release-Repo); ohne Angabe wird `latest` verwendet. Das Skript lädt
`docker-compose.yml`, `hydra.yml` und `.env.example`, erzeugt
`secrets/creds_key` und eine `.env` mit fertigen Zufalls-Secrets.
2. **`.env` anpassen** — Pflichtwerte:
| Variable | Bedeutung |
|---|---|
| `PUBLIC_ISSUER` | öffentliche HTTPS-URL des OAuth-Issuers (vHost → Port 4444) |
| `LOGIN_URL` | öffentliche HTTPS-URL des Login-Dienstes (vHost → Port 8081) |
| `MCP_RESOURCE_URL` | öffentliche HTTPS-URL des MCP-Servers (vHost → Port 8080) |
| `SL_BASE_URL` | SAP Service Layer, trailing `/b1s/` ist Pflicht |
| `SL_COMPANYDB` | SAP-Firmendatenbank |
| `ADMIN_SAP_USER` | SAP-Benutzer mit Zugang zum Admin-UI (Port 8082) |
| `MCP_ALLOWED_USERS` | SAP-Benutzer mit MCP-Dauerzugriff (Allowlist, default-deny) |
`PG_PASSWORD`, `SECRETS_SYSTEM`, `APP_VERSION` und `PROJECT_DIR` hat
`setup.sh` bereits gesetzt. `SL_INSECURE=true` nur bei self-signed
SAP-Zertifikat.
3. **Stack starten:**
```
docker compose pull
docker compose up -d
```
4. **Verifizieren:**
```
docker compose ps # alle Dauerläufer "healthy"
curl -s http://127.0.0.1:8080/.well-known/oauth-protected-resource
```
## Reverse Proxy
Alle Dienste binden per Default **nur an 127.0.0.1**. Läuft der Reverse Proxy
auf einem anderen Host, in `.env` `BIND_ADDR`/`ADMIN_BIND` auf die interne
Host-IP setzen — **nie 0.0.0.0**.
| vHost (Beispiel) | Backend-Port | Dienst |
|---|---|---|
| `mcp.<domain>` | 8080 | MCP-Resource-Server |
| `mcp-oauth.<domain>` | 4444 | Hydra (OAuth-Issuer) |
| `mcp-login.<domain>` | 8081 | Login/Consent |
| `mcp-admin.<domain>` (intern!) | 8082 | Admin-UI |
Der Proxy muss `X-Forwarded-Proto: https` setzen (TLS-Terminierung).
Das Admin-UI gehört NICHT ins öffentliche Internet.
## Update auf eine neue Version
**Weg 1 — über das Admin-UI (empfohlen):** Seite **Update** öffnen, optional
eine Zielversion eintragen (leer = `latest`), „Update starten" klicken. Der
Stack holt die neuen Images aus der Registry und erneuert alle Dienste selbst
(`.env`, `secrets/` und die Datenbank bleiben erhalten). Dabei startet auch
das Admin-UI neu — danach neu anmelden.
**Weg 2 — manuell per SSH:**
```
bash setup.sh [version] # aktualisiert Deployment-Dateien + APP_VERSION
docker compose pull
docker compose up -d
```
## Rollback
Wie ein Update, nur mit einer älteren Versionsnummer als Zielversion
(Admin-UI-Feld „Zielversion" bzw. `bash setup.sh <version>`).
## Backup
- Docker-Volume `sapb1mcp_pgdata` (Postgres: Benutzer, Audit, Hydra-Clients)
- `secrets/creds_key` — ohne diese Datei sind gespeicherte SAP-Zugangsdaten
unwiederbringlich verloren
- `.env` (enthält `SECRETS_SYSTEM` — nach Inbetriebnahme nie ändern)
+1
View File
@@ -0,0 +1 @@
2026.07.07-24520d4
+291
View File
@@ -0,0 +1,291 @@
# SAP-MCP-Stack (C#) — RELEASE-Compose für Kunden-Deployments (liegt via CI als
# docker-compose.yml im öffentlichen Repo kraemerweb/easymcp-release). Referenziert
# NUR vorgebaute Images aus der öffentlichen Gitea-Registry (kein build:) —
# der Kunde zieht Updates per 'docker compose pull && docker compose up -d'.
# APP_VERSION setzt setup.sh in der .env (Default: latest, pinbar auf Versionstags).
#
# Härtung wie im Entwicklungs-Compose: alle Dienste no-new-privileges; die
# .NET-Dienste + Hydra zusätzlich cap_drop ALL; login/mcp/admin read_only
# (nur /tmp als tmpfs). Postgres behält Default-Caps und bleibt schreibbar.
name: sapb1mcp
# Log-Rotation für die Dauerläufer (Docker-Default rotiert nicht). Max ~30 MB/Dienst.
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
postgres:
image: postgres:17-alpine
container_name: sapb1mcp-postgres
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
environment:
POSTGRES_USER: mcp
POSTGRES_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD fehlt in .env — setup.sh ausführen}
POSTGRES_DB: mcp
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mcp -d mcp"]
interval: 5s
timeout: 3s
retries: 12
# App-Schema (sap_credentials, mcp_user_access, audit_entries) via EF Core.
# Nutzt das mcp-Image, ruft es aber im --migrate-Modus auf und beendet sich.
db-migrate:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest}
container_name: sapb1mcp-db-migrate
restart: "no"
security_opt:
- no-new-privileges:true
depends_on:
postgres:
condition: service_healthy
environment:
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
entrypoint: ["dotnet", "SapB1McpServer.dll", "--migrate"]
# Hydra-Schema (eigene Tabellen in derselben DB).
hydra-migrate:
image: oryd/hydra:v2.3.0
container_name: sapb1mcp-hydra-migrate
restart: "no"
security_opt:
- no-new-privileges:true
depends_on:
postgres:
condition: service_healthy
environment:
DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable
volumes:
- ./hydra.yml:/etc/hydra/hydra.yml:ro
command: migrate sql -e --yes --config /etc/hydra/hydra.yml
hydra:
image: oryd/hydra:v2.3.0
container_name: sapb1mcp-hydra
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
depends_on:
hydra-migrate:
condition: service_completed_successfully
environment:
DSN: postgres://mcp:${PG_PASSWORD}@postgres:5432/mcp?sslmode=disable
SECRETS_SYSTEM: ${SECRETS_SYSTEM:?SECRETS_SYSTEM fehlt in .env — setup.sh ausführen}
URLS_SELF_ISSUER: ${PUBLIC_ISSUER:?PUBLIC_ISSUER fehlt in .env}
URLS_LOGIN: ${LOGIN_URL}/login
URLS_CONSENT: ${LOGIN_URL}/consent
# Hydra leitet registration_endpoint NICHT automatisch ab — ohne diesen
# Eintrag fehlt es im Discovery-Dokument und Clients halten DCR für
# nicht unterstützt.
WEBFINGER_OIDC_DISCOVERY_CLIENT_REGISTRATION_URL: ${PUBLIC_ISSUER}/oauth2/register
volumes:
- ./hydra.yml:/etc/hydra/hydra.yml:ro
command: serve all --config /etc/hydra/hydra.yml
ports:
# Nur an die interne Host-IP (BIND_ADDR) binden, die der Reverse Proxy
# erreicht — nie 0.0.0.0. Default 127.0.0.1 = sicher (Proxy auf demselben Host).
- "${BIND_ADDR:-127.0.0.1}:${HYDRA_PUBLIC_HOST_PORT:-4444}:4444" # öffentlich (OAuth-Issuer)
# Admin-API 4445 bleibt intern im Compose-Netz (nur login/admin-Container).
login:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-login:${APP_VERSION:-latest}
container_name: sapb1mcp-login
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
hydra:
condition: service_started
environment:
ASPNETCORE_URLS: http://0.0.0.0:8081
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
Hydra__AdminUrl: http://hydra:4445
SAPServiceLayer__BaseUrl: ${SL_BASE_URL:?SL_BASE_URL fehlt in .env}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB:?SL_COMPANYDB fehlt in .env}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
CREDS_KEY_FILE: /run/secrets/creds_key
# MCP-Zugriff: immer erlaubte SAP-Benutzer (Allowlist, default-deny für den Rest).
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
secrets:
- creds_key
ports:
- "${BIND_ADDR:-127.0.0.1}:${LOGIN_HOST_PORT:-8081}:8081"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8081/healthz"]
interval: 10s
timeout: 3s
retries: 6
mcp:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-mcp:${APP_VERSION:-latest}
container_name: sapb1mcp-mcp
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
hydra:
condition: service_started
environment:
McpHttp__Port: "8080"
# Öffentliche Resource-URL dieses MCP-Servers (RFC-9728-Metadaten + WWW-Authenticate).
MCP_RESOURCE_URL: ${MCP_RESOURCE_URL:?MCP_RESOURCE_URL fehlt in .env}
# Hydra-Issuer für JWT-Validierung (Authority). MUSS per HTTPS erreichbar sein
# (RequireHttpsMetadata=true) — im Betrieb der Reverse-Proxy-vHost.
Hydra__PublicIssuer: ${PUBLIC_ISSUER}
SAPServiceLayer__BaseUrl: ${SL_BASE_URL}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
CREDS_KEY_FILE: /run/secrets/creds_key
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
secrets:
- creds_key
ports:
- "${BIND_ADDR:-127.0.0.1}:${MCP_HOST_PORT:-8080}:8080"
healthcheck:
# Der MCP-Server hat kein /healthz; die anonyme RFC-9728-Metadaten-Route
# antwortet ohne Token mit 200 und dient als Health-Signal.
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/.well-known/oauth-protected-resource"]
interval: 10s
timeout: 3s
retries: 6
# Docker-Socket-Proxy: EINZIGER Container, der den Docker-Socket sieht. Filtert die
# Engine-API auf das Minimum, das das Admin-UI braucht (Container listen/Logs/Restart).
docker-socket-proxy:
image: tecnativa/docker-socket-proxy:0.3.0
container_name: sapb1mcp-docker-proxy
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
# Kein read_only: der tecnativa-Entrypoint generiert seine haproxy.cfg zur Laufzeit
# aus der im Image liegenden Template — read_only dort crasht den Proxy.
environment:
# Nur Container-Bereich + POST (für /restart) freigeben, alles andere bleibt 0.
CONTAINERS: 1
POST: 1
# Explizit geschlossen halten (Defaults, zur Klarheit dokumentiert):
EXEC: 0
IMAGES: 0
VOLUMES: 0
NETWORKS: 0
INFO: 0
SERVICES: 0
TASKS: 0
SWARM: 0
SYSTEM: 0
AUTH: 0
SECRETS: 0
CONFIGS: 0
NODES: 0
PLUGINS: 0
DISTRIBUTION: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- dockerproxy
admin:
image: git.kraemerweb.de/kraemerweb/sapb1mcp-admin:${APP_VERSION:-latest}
container_name: sapb1mcp-admin
restart: unless-stopped
logging: *default-logging
security_opt:
- no-new-privileges:true
# Kein Docker-Socket-Mount: der Admin erreicht die Engine-API ausschließlich
# über den gefilterten Proxy (siehe DOCKER_HOST).
cap_drop:
- ALL
read_only: true
tmpfs:
- /tmp
depends_on:
db-migrate:
condition: service_completed_successfully
docker-socket-proxy:
condition: service_started
environment:
ASPNETCORE_URLS: http://0.0.0.0:8082
POSTGRES_CONNECTION: "Host=postgres;Port=5432;Database=mcp;Username=mcp;Password=${PG_PASSWORD}"
Hydra__AdminUrl: http://hydra:4445
# Login-Gate: nur dieser SAP-User darf das Admin-UI öffnen (SL-verifiziert).
# Pflicht (fail-closed) — der Dienst lässt sonst niemanden hinein.
ADMIN_SAP_USER: ${ADMIN_SAP_USER:?ADMIN_SAP_USER fehlt in .env}
SAPServiceLayer__BaseUrl: ${SL_BASE_URL}
SAPServiceLayer__CompanyDB: ${SL_COMPANYDB}
SAPServiceLayer__IgnoreSslErrors: ${SL_INSECURE:-false}
MCP_ALLOWED_USERS: ${MCP_ALLOWED_USERS:-manager}
# Container-Steuerung/Logs NUR über den gefilterten Proxy (nicht über den rohen Socket).
COMPOSE_PROJECT: sapb1mcp
DOCKER_HOST: tcp://docker-socket-proxy:2375
# Update-Seite („Pull & Update"): startet den Einweg-Updater-Container.
# PROJECT_DIR = Host-Pfad des Deployments (setzt setup.sh); der Updater mountet
# ihn pfad-identisch, damit die relativen Binds der Compose aufgehen.
APP_VERSION: ${APP_VERSION:-latest}
PROJECT_DIR: ${PROJECT_DIR:?PROJECT_DIR fehlt in .env — setup.sh ausführen}
UPDATES_VOLUME: sapb1mcp_updates
# Updater bewusst auf :latest — er ist kein Compose-Dienst; setup.sh pullt ihn
# initial, updater-run.sh hält ihn bei jedem Update selbst aktuell.
UPDATER_IMAGE: git.kraemerweb.de/kraemerweb/sapb1mcp-updater:latest
volumes:
# Geteilte Ablage für update.log/update.status — so kann auch der beim
# Update NEU erstellte Admin das Ergebnis noch anzeigen.
- updates:/updates
networks:
- default
- dockerproxy
ports:
# Admin-UI inkl. /audit liegt hinter dem SAP-Login-Gate (nur /login + /healthz frei).
# Trotzdem nur intern binden: Default loopback; ADMIN_BIND in .env auf die interne
# Host-IP setzen, die der Reverse Proxy erreicht — nie 0.0.0.0.
- "${ADMIN_BIND:-127.0.0.1}:${ADMIN_HOST_PORT:-8082}:8082"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8082/healthz"]
interval: 10s
timeout: 3s
retries: 6
volumes:
pgdata:
# Staging-Volume der Update-Funktion (Name auf dem Host: sapb1mcp_updates —
# muss zur UPDATES_VOLUME-Env des Admin passen, der Updater mountet es per Name).
updates:
networks:
# Standard-Netz für alle Dienste (Postgres, Hydra, login, mcp, admin).
default:
# Isoliertes, nach außen abgeschottetes Netz nur für admin ↔ docker-socket-proxy.
dockerproxy:
internal: true
secrets:
creds_key:
file: ./secrets/creds_key
+63
View File
@@ -0,0 +1,63 @@
# Ory Hydra v2.3 — OAuth2-AS für den SAP-MCP-Stack (C#-Portierung).
# URLs/DSN/Secrets kommen aus dem Environment (compose):
# DSN, SECRETS_SYSTEM, URLS_SELF_ISSUER, URLS_LOGIN, URLS_CONSENT
serve:
cookies:
same_site_mode: Lax
tls:
# TLS terminiert der zentrale Reverse Proxy (kas-proxy/Caddy); interne Netze
# dürfen X-Forwarded-Proto setzen.
allow_termination_from:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
public:
cors:
# claude.ai macht Discovery/DCR/Token-Calls teils aus dem Browser —
# ohne CORS meldet der Connector "Client-Registrierung nicht unterstützt".
enabled: true
allowed_origins:
- https://claude.ai
- https://claude.com
- https://chatgpt.com
- https://chat.openai.com
allowed_methods:
- GET
- POST
- OPTIONS
allowed_headers:
- Authorization
- Content-Type
exposed_headers:
- Content-Type
strategies:
access_token: jwt
oidc:
dynamic_client_registration:
# Self-Service-Onboarding: MCP-Clients registrieren sich anonym
# über POST /oauth2/register (RFC 7591).
enabled: true
# Clients, die bei der Registrierung KEINE Scopes angeben (z. B. ChatGPT),
# bekommen diese Defaults — sonst scheitert ihr Authorize-Request mit
# invalid_scope. ChatGPT fordert zusätzlich OIDC-Scopes (openid/profile/
# email) an; die geben wir frei, auch wenn wir keine Profil-Claims führen.
default_scope:
- mcp
- offline_access
- openid
- profile
- email
oauth2:
pkce:
enforced_for_public_clients: true
ttl:
access_token: 1h
refresh_token: 720h
log:
level: info
leak_sensitive_values: false
Executable
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# EasyMCP / SapB1Mcp — Setup auf dem Docker-Host (Pull-Deployment).
#
# Die einzige Datei, die ein Kunde braucht. Bezug:
# curl -fsSLO https://git.kraemerweb.de/kraemerweb/easymcp-release/raw/branch/main/setup.sh
#
# Aufruf:
# bash setup.sh # neueste Version (latest)
# bash setup.sh <version> # bestimmte Version (Tag im Release-Repo)
#
# Was das Skript tut:
# 1. Holt die Deployment-Dateien (docker-compose.yml, hydra.yml, .env.example)
# anonym aus dem öffentlichen Release-Repo — passend zur gewählten Version.
# 2. Erzeugt secrets/creds_key (32 Byte base64, Verschlüsselung der SAP-Credentials).
# 3. Legt .env aus .env.example an und generiert PG_PASSWORD + SECRETS_SYSTEM;
# setzt APP_VERSION (= gewählte Version bzw. latest) und PROJECT_DIR.
# 4. Pullt das Updater-Image (für die Update-Funktion des Admin-UI).
#
# Idempotent: eine vorhandene .env und ein vorhandener creds_key werden NIE
# überschrieben — nur APP_VERSION und PROJECT_DIR werden nachgezogen. Die
# Deployment-Dateien werden immer auf den Stand der gewählten Version gebracht.
set -euo pipefail
cd "$(dirname "$0")"
RELEASE_BASE="${RELEASE_BASE:-https://git.kraemerweb.de/kraemerweb/easymcp-release}"
REGISTRY_IMAGE_BASE="${REGISTRY_IMAGE_BASE:-git.kraemerweb.de/kraemerweb}"
VERSION="${1:-}"
if [ -n "$VERSION" ]; then
REF="tag/${VERSION}"
APP_VERSION="$VERSION"
else
REF="branch/main"
APP_VERSION="latest"
fi
info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m ✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; }
fail() { printf '\033[1;31mFEHLER:\033[0m %s\n' "$*" >&2; exit 1; }
# --- Zufallswerte (openssl bevorzugt, /dev/urandom als Fallback) -------------
rand_hex() { # $1 = Byte-Anzahl; Ausgabe hex (URL-sicher, auch für DSN-URLs)
if command -v openssl >/dev/null 2>&1; then
openssl rand -hex "$1" | tr -d ' \r\n'
else
od -An -N"$1" -tx1 /dev/urandom | tr -d ' \r\n'
fi
}
rand_b64() { # $1 = Byte-Anzahl; Ausgabe base64 (eine Zeile)
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 "$1" | tr -d '\r\n'
else
head -c "$1" /dev/urandom | base64 | tr -d '\r\n'
fi
}
set_env() { # $1 = Name, $2 = Wert — setzt bzw. ergänzt NAME=WERT in .env
if grep -q "^$1=" .env; then
sed -i "s|^$1=.*|$1=$2|" .env
else
printf '%s=%s\n' "$1" "$2" >> .env
fi
}
fetch() { # $1 = Datei im Release-Repo, $2 = Zielname — atomar ersetzen
curl -fsSL "$RELEASE_BASE/raw/$REF/$1" -o "$2.new" \
|| fail "Download fehlgeschlagen: $RELEASE_BASE/raw/$REF/$1 (Version/Netzwerk prüfen)"
mv "$2.new" "$2"
}
# --- Vorbedingungen -----------------------------------------------------------
command -v curl >/dev/null 2>&1 || fail "curl nicht gefunden — bitte installieren."
command -v docker >/dev/null 2>&1 || fail "docker nicht gefunden — bitte Docker Engine installieren."
docker info >/dev/null 2>&1 || fail "Docker-Daemon nicht erreichbar (läuft der Dienst? Rechte? Ggf. sudo verwenden)."
if docker compose version >/dev/null 2>&1; then
COMPOSE="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE="docker-compose"
else
fail "Weder 'docker compose' (Plugin v2) noch 'docker-compose' gefunden."
fi
# --- 1. Deployment-Dateien holen -----------------------------------------------
info "Hole Deployment-Dateien (${VERSION:-latest}) aus $RELEASE_BASE ..."
fetch docker-compose.yml docker-compose.yml
fetch hydra.yml hydra.yml
fetch .env.example .env.example
ok "docker-compose.yml, hydra.yml, .env.example aktualisiert."
# --- 2. creds_key --------------------------------------------------------------
info "Prüfe secrets/creds_key ..."
mkdir -p secrets
if [ -f secrets/creds_key ]; then
ok "secrets/creds_key existiert bereits — bleibt unverändert."
else
( umask 077; rand_b64 32 > secrets/creds_key )
ok "secrets/creds_key erzeugt (chmod 600)."
echo " WICHTIG: Diese Datei SICHERN. Ohne sie sind die gespeicherten SAP-Zugangsdaten unlesbar."
fi
# --- 3. .env -------------------------------------------------------------------
info "Prüfe .env ..."
if [ -f .env ]; then
ok ".env existiert bereits — Secrets/Werte bleiben unverändert."
current="$(sed -n 's/^APP_VERSION=//p' .env | head -n1)"
set_env APP_VERSION "$APP_VERSION"
if [ -n "$current" ] && [ "$current" != "$APP_VERSION" ]; then
ok "Update: APP_VERSION ${current} -> ${APP_VERSION} (wirkt nach '${COMPOSE} pull' + '${COMPOSE} up -d')."
fi
else
cp .env.example .env
chmod 600 .env
PG_PASSWORD="$(rand_hex 24)" # nur [0-9a-f]: sicher für Hydras DSN-URL
SECRETS_SYSTEM="$(rand_hex 32)" # Hydra-Systemschlüssel — nach Inbetriebnahme NIE mehr ändern
set_env PG_PASSWORD "$PG_PASSWORD"
set_env SECRETS_SYSTEM "$SECRETS_SYSTEM"
set_env APP_VERSION "$APP_VERSION"
ok ".env angelegt, PG_PASSWORD + SECRETS_SYSTEM generiert (chmod 600)."
fi
# Host-Pfad des Deployments — braucht die Update-Funktion des Admin-UI (der
# Einweg-Updater mountet das Projektverzeichnis pfad-identisch). Bei jedem Lauf
# aktualisieren, falls das Verzeichnis umgezogen ist.
set_env PROJECT_DIR "$(pwd)"
# --- 4. Updater-Image für die Admin-UI-Update-Funktion --------------------------
info "Pulle Updater-Image ..."
if docker pull "$REGISTRY_IMAGE_BASE/sapb1mcp-updater:latest" >/dev/null; then
ok "Updater-Image bereit."
else
warn "Updater-Image konnte nicht gepullt werden — Registry-Zugriff prüfen; das Admin-UI-Update funktioniert sonst nicht."
fi
# --- 5. Nächste Schritte ---------------------------------------------------------
cat <<EOF
============================================================================
Setup abgeschlossen. Bitte jetzt in der .env die Umgebungswerte anpassen:
PUBLIC_ISSUER öffentliche HTTPS-URL des OAuth-Issuers (Reverse Proxy -> Port 4444)
LOGIN_URL öffentliche HTTPS-URL des Login-Dienstes (Reverse Proxy -> Port 8081)
MCP_RESOURCE_URL öffentliche HTTPS-URL des MCP-Servers (Reverse Proxy -> Port 8080)
SL_BASE_URL SAP Service Layer, z. B. https://sap-host:50000/b1s/ (trailing /b1s/ ist Pflicht)
SL_COMPANYDB SAP-Firmendatenbank
ADMIN_SAP_USER SAP-Benutzer, der das Admin-UI öffnen darf
MCP_ALLOWED_USERS kommagetrennte SAP-Benutzer mit MCP-Dauerzugriff
Optional: SL_INSECURE (self-signed SAP-Zertifikat), BIND_ADDR/ADMIN_BIND und
*_HOST_PORT, falls der Reverse Proxy auf einem anderen Host läuft.
Danach den Stack starten:
${COMPOSE} pull
${COMPOSE} up -d
Status prüfen: ${COMPOSE} ps (alle Dauerläufer müssen "healthy" werden)
Updates später: im Admin-UI unter „Update" — oder erneut 'bash setup.sh [version]'
gefolgt von '${COMPOSE} pull' + '${COMPOSE} up -d'. Details: INSTALL.md
============================================================================
EOF