Add a local Schulcloud instance modelled on the live target
A Docker Compose stack that runs the deployed images (quay.io/schulcloudverbund/*, thr theme, tag 33.40 — the versions schulcloud-thueringen.de reports) rather than a rebuild of main, so what we develop against is the deployed artefact. It exists to produce the states we can otherwise only observe read-only: log in as the teacher, grade, then read it back the way the MCP server does. Faithful where it matters and honest where it isn't: - Feature flags in env/api.env are a replay of GET /api/v3/config/public from the live instance, not a hand-picked set; instance identity mirrors the thr group_vars from dof_app_deploy. - The proxy is generated from the deployment's own ingress table (scripts/gen-proxy-conf.py) so the legacy-client / SPA / API path split matches production; getting it wrong tests a different application. - Valkey runs in `single` mode so the JWT whitelist expires sessions the way production does, rather than the in-memory shortcut that hides it. - No external OAuth / Schulportal login (excluded by request and not reproducible locally), no BigBlueButton; each divergence is marked at the line it affects. Everything binds to 127.0.0.1 and uses the upstream dev credentials, which are public. Profiles keep the heavy pieces opt-in: `tools` adds Etherpad/H5P/tldraw/ Collabora, `av` adds ClamAV, `preview` adds thumbnailing. seed.sh loads the upstream demo school (the same call the deployment's init job makes) and registers MinIO as the legacy storage provider, which has no seed data on purpose. The demo data already contains the grading states that are hard to obtain from the real account — a feedback-only grade and a 100% one — which is what surfaced the past-due submitted-text scrape gap. One config finding baked in: file-storage and h5p validate a token's issuer/audience against JWT_DOMAIN (default "localhost"), while the API stamps SC_DOMAIN; without keeping them equal, the homework page's file lookups 401. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
204
local-instance/README.md
Normal file
204
local-instance/README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# A local Schulcloud
|
||||
|
||||
A Docker Compose stack that runs a real Schulcloud instance on this machine,
|
||||
modelled on **schulcloud-thueringen.de** — the instance this MCP server reads.
|
||||
|
||||
It exists so that the parts of the product we can only observe from a student's
|
||||
account can also be *produced*: log in as the teacher, grade a submission, then
|
||||
look at it through `list_submissions` and see whether what we render matches
|
||||
what the student is shown.
|
||||
|
||||
> Everything here is a throwaway development instance. The credentials are the
|
||||
> upstream development defaults, published in the upstream repositories. Bind
|
||||
> nothing beyond `127.0.0.1`.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
cd local-instance
|
||||
docker compose up -d # ~5 min on a cold cache
|
||||
./scripts/seed.sh # loads the demo school; takes another minute or two
|
||||
```
|
||||
|
||||
Then open <http://localhost:4400> and sign in as
|
||||
`demo-schueler@schul-cloud.org` / `schulcloud` (Fritz Schmidt, a student with
|
||||
graded submissions). `seed.sh` prints the full account list on completion.
|
||||
|
||||
> The demo password differs by account — an upstream quirk, not ours. The two
|
||||
> `demo-*` accounts use `schulcloud`, the built-in `admin`/`lehrer` and the
|
||||
> teacher `klara.fall` use `Schulcloud1!`, and the `*.qa` accounts use
|
||||
> `Schulcloud1qa!`.
|
||||
|
||||
To add the external tools (Etherpad, H5P, tldraw, Collabora):
|
||||
|
||||
```bash
|
||||
docker compose --profile tools up -d
|
||||
```
|
||||
|
||||
## Why the images are the interesting part
|
||||
|
||||
`quay.io/schulcloudverbund/*` are the **same images the live instance runs**,
|
||||
built for the `thr` theme, and they are public. So this is not a rebuild of
|
||||
`main` that may have drifted — it is the deployed artefact, pinned to the
|
||||
version the live instance reports:
|
||||
|
||||
```console
|
||||
$ curl -s https://schulcloud-thueringen.de/version
|
||||
{"client": {"version": "33.40.0"}, "nuxt-client": {"version": "33.40.1"},
|
||||
"server": {"version": "33.40.2"}, "dof_app_deploy": "33.40.2"}
|
||||
```
|
||||
|
||||
Hence `SC_VERSION` defaults to `33.40`. To pin something else:
|
||||
|
||||
```bash
|
||||
SC_VERSION=33.27 docker compose up -d
|
||||
```
|
||||
|
||||
`CLAUDE.md` says *live behaviour beats upstream source* — the clones in
|
||||
`vendor/` track `main` and may be ahead of what is deployed. Running the
|
||||
deployed images is how that rule is honoured here rather than worked around.
|
||||
|
||||
## What is faithful, and what is not
|
||||
|
||||
Faithful, and load-bearing for what we test:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Images** | the live instance's own, `thr` theme, tag `33.40` |
|
||||
| **Feature flags** | `env/api.env` is a replay of `GET /api/v3/config/public` from the live instance, not a hand-picked set |
|
||||
| **Instance identity** | `env/shared.env` mirrors `dof_app_deploy/ansible/group_vars/thr/instance_cfg.yml` |
|
||||
| **URL routing** | `proxy/nginx.conf` is generated from the deployment's own ingress table (see below) |
|
||||
| **Session model** | Valkey in `single` mode, so the JWT whitelist expires sessions exactly as production does |
|
||||
| **Seed data** | the upstream demo school, including graded and ungraded submissions |
|
||||
|
||||
Deliberately different:
|
||||
|
||||
- **No external OAuth / Schulportal login.** Excluded by request, and it is the
|
||||
one part that cannot be stood up locally. Local login is username + password
|
||||
against the seeded accounts.
|
||||
- **No BigBlueButton.** `FEATURE_VIDEOCONFERENCE_ENABLED` is off; upstream it is
|
||||
on. BBB is a separate product of its own scale.
|
||||
- **`FEATURE_CONSENT_NECESSARY=false`**, or every seeded user hits a consent
|
||||
wall before reaching any content.
|
||||
- **No LDAP / TSP sync, no Nextcloud, no calendar service.**
|
||||
- **Antivirus off by default** — see the `av` profile.
|
||||
|
||||
Every one of these is marked in the env files at the line it affects.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
docker-compose.yml the stack; profiles: (default) | tools | av | preview
|
||||
env/ one file per service, all values local-only
|
||||
proxy/nginx.conf GENERATED — the single origin, see scripts/gen-proxy-conf.py
|
||||
scripts/seed.sh loads the demo school via the management app
|
||||
scripts/minio-init.sh creates the S3 buckets each service expects
|
||||
etherpad/APIKEY.txt fixed Etherpad API key, matching env/api.env
|
||||
```
|
||||
|
||||
### Ports
|
||||
|
||||
| Port | |
|
||||
|---|---|
|
||||
| **4400** | **the instance** — everything a browser touches |
|
||||
| 3030 | server API, direct |
|
||||
| 3333 | management app (seeding only, not part of a running instance) |
|
||||
| 3100 / 4000 | legacy client / SPA, direct |
|
||||
| 4444 | file-storage |
|
||||
| 9900 / 9901 | MinIO S3 API / console (`miniouser` / `miniouser`) |
|
||||
| 9980 | Collabora (`tools` profile) |
|
||||
| 27019 | MongoDB |
|
||||
| 6381 | Valkey |
|
||||
| 15673 | RabbitMQ management |
|
||||
|
||||
Everything binds to `127.0.0.1`. Port numbers are shifted off their upstream
|
||||
defaults where those commonly collide (Mongo, Valkey, RabbitMQ, MinIO) — note
|
||||
`docker-compose.override.yml` in the repo root already uses 8080 and 55432.
|
||||
|
||||
## The proxy is generated, not written
|
||||
|
||||
The live instance is one origin whose paths are split across the legacy client,
|
||||
the SPA, and several APIs. That split is not cosmetic: `/rooms/courses-list` is
|
||||
the SPA, `/courses/:id` is the legacy client, and `/api/v3/file/` is a
|
||||
different service from `/api/v3/`. Get it wrong and you are testing a different
|
||||
application from the one students use.
|
||||
|
||||
The rules live in `dof_app_deploy/ansible/group_vars/all/x_ingress.yml` (46 of
|
||||
them) plus a per-path ingress inside each service repo. Transcribing that by
|
||||
hand invites drift, so `scripts/gen-proxy-conf.py` reads the deployment repo and
|
||||
emits `proxy/nginx.conf`:
|
||||
|
||||
```bash
|
||||
python3 scripts/gen-proxy-conf.py > proxy/nginx.conf
|
||||
docker compose restart proxy
|
||||
```
|
||||
|
||||
It needs the upstream clones in `../vendor` (gitignored — see the root README),
|
||||
which is why the output is committed.
|
||||
|
||||
Two details in there are worth keeping:
|
||||
|
||||
- Every `proxy_pass` goes through a **variable plus a `resolver`**, so names are
|
||||
resolved per request. With a literal upstream, nginx refuses to start
|
||||
whenever an optional profile is down — which is the normal case.
|
||||
- **Etherpad gets the deployment's own rewrite rules**, not a plain
|
||||
`proxy_pass`. It is mounted under a prefix it knows nothing about; without the
|
||||
rewrites a pad loads and then silently never syncs.
|
||||
|
||||
## Seeding
|
||||
|
||||
`scripts/seed.sh` does what the real deployment's init job does: it asks the
|
||||
management app to load `backup/setup/*.json`, which ships **inside the server
|
||||
image**. It also registers MinIO in the `storageproviders` collection, which has
|
||||
no seed data on purpose (it holds credentials) and without which legacy file
|
||||
uploads fail.
|
||||
|
||||
Re-running it is safe; collections are replaced, not appended to.
|
||||
|
||||
### What the demo data already contains
|
||||
|
||||
The seed includes 50 tasks and 24 submissions covering the grading states that
|
||||
are otherwise hard to obtain — including the two this server renders:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| `graded: true`, `grade: null`, `gradeComment` set | graded by feedback alone |
|
||||
| `graded: true`, `grade: 100`, `gradeComment` set | a percentage **and** feedback |
|
||||
| `graded: false`, `submitted: true` | handed in, not yet looked at |
|
||||
|
||||
The second row matters: no submission in the real account has ever had a
|
||||
numeric grade, so `formatGradeState`'s percentage branch had never been seen
|
||||
against real data. Here it can be.
|
||||
|
||||
## Profiles
|
||||
|
||||
| Profile | Services | Cost |
|
||||
|---|---|---|
|
||||
| *(default)* | Mongo, Valkey, RabbitMQ, MinIO, api, management, board-collaboration, admin-api, file-storage, client, nuxt, proxy | ~4 GB images |
|
||||
| `tools` | Etherpad, H5P editor + static files + library install, tldraw server + worker, Collabora | ~4 GB more, Collabora is the bulk |
|
||||
| `av` | ClamAV + Clammit | ~1.5 GB resident for the signature database |
|
||||
| `preview` | file-preview generator (thumbnails) | small |
|
||||
|
||||
`av` is opt-in for a reason beyond size: with `ENABLE_FILE_SECURITY_CHECK=true`
|
||||
but no scanner reachable, every upload stays at
|
||||
`securityCheck.status=pending` and can never be downloaded. Turn the flag in
|
||||
`env/file-storage.env` on **together with** the profile, or neither.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Everything 502s.** The proxy is up before the apps are. `docker compose logs
|
||||
-f api` — the server waits for Mongo to become primary.
|
||||
|
||||
**Mongo never becomes healthy.** It runs as a single-node replica set because
|
||||
the migration runner opens transactions, which Mongo refuses on a standalone.
|
||||
The healthcheck initiates the set on first boot; give it ~30 s.
|
||||
|
||||
**Login succeeds, then immediately bounces back.** The `jwt` cookie is being
|
||||
dropped. Check `COOKIE__SECURE=false` in `env/client.env` — the proxy speaks
|
||||
plain HTTP locally.
|
||||
|
||||
**A file uploads but will not download.** See the `av` note above.
|
||||
|
||||
**H5P element stays empty.** `docker compose --profile tools run --rm
|
||||
h5p-libraries` and watch it finish; the editor has nothing to offer until the
|
||||
content types are in the bucket.
|
||||
267
local-instance/docker-compose.yml
Normal file
267
local-instance/docker-compose.yml
Normal file
@@ -0,0 +1,267 @@
|
||||
# A local Schulcloud, as close to schulcloud-thueringen.de as it can be made
|
||||
# without its external identity provider.
|
||||
#
|
||||
# The application images are the *same* images the real instance runs
|
||||
# (quay.io/schulcloudverbund, thr theme, tag 33.40 — see README.md), so the
|
||||
# behaviour under test is the deployed behaviour, not a rebuild of main.
|
||||
#
|
||||
# docker compose up -d core: login, courses, boards, files
|
||||
# docker compose --profile tools up -d + etherpad, h5p, tldraw, collabora
|
||||
# docker compose --profile av up -d + virus scanning of uploads
|
||||
#
|
||||
# Everything here is a throwaway dev instance: the credentials are the upstream
|
||||
# development defaults and are published in the upstream repositories. Do not
|
||||
# expose any of it beyond localhost.
|
||||
|
||||
x-sc-version: &sc-version "${SC_VERSION:-33.40}"
|
||||
|
||||
x-server-image: &server-image
|
||||
image: quay.io/schulcloudverbund/schulcloud-server:${SC_VERSION:-33.40}
|
||||
env_file: [env/shared.env, env/jwt.env, env/api.env]
|
||||
depends_on:
|
||||
mongo: {condition: service_healthy}
|
||||
valkey: {condition: service_started}
|
||||
rabbitmq: {condition: service_healthy}
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
# ---------------------------------------------------------------- infra ---
|
||||
mongo:
|
||||
image: docker.io/mongo:7
|
||||
# Single-node replica set rather than a bare mongod: the server's migration
|
||||
# runner opens transactions, which mongo refuses outside a replica set.
|
||||
command: ["--replSet", "rs0", "--bind_ip_all"]
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
ports: ["127.0.0.1:27019:27017"]
|
||||
healthcheck:
|
||||
# Initiates the replica set on first start and reports healthy once the
|
||||
# node is actually primary, which is what every other service waits for.
|
||||
test: >-
|
||||
mongosh --quiet --eval '
|
||||
try { rs.status() } catch (e) { rs.initiate({_id:"rs0",members:[{_id:0,host:"mongo:27017"}]}) }
|
||||
quit(db.hello().isWritablePrimary ? 0 : 1)'
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
|
||||
valkey:
|
||||
# The JWT whitelist. Sessions die when their key expires, exactly as in
|
||||
# production — this is the piece that makes local session testing honest.
|
||||
image: docker.io/valkey/valkey:8-alpine
|
||||
ports: ["127.0.0.1:6381:6379"]
|
||||
restart: unless-stopped
|
||||
|
||||
rabbitmq:
|
||||
image: docker.io/rabbitmq:4-management-alpine
|
||||
ports: ["127.0.0.1:15673:15672"]
|
||||
healthcheck:
|
||||
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 20
|
||||
start_period: 20s
|
||||
restart: unless-stopped
|
||||
|
||||
minio:
|
||||
# Stands in for the S3 provider the real instance uses. Buckets are created
|
||||
# by minio-init below.
|
||||
image: quay.io/minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: miniouser
|
||||
MINIO_ROOT_PASSWORD: miniouser
|
||||
volumes:
|
||||
- minio-data:/data
|
||||
ports:
|
||||
- "127.0.0.1:9900:9000" # S3 API
|
||||
- "127.0.0.1:9901:9001" # console (miniouser / miniouser)
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
restart: unless-stopped
|
||||
|
||||
minio-init:
|
||||
image: quay.io/minio/mc:latest
|
||||
depends_on:
|
||||
minio: {condition: service_healthy}
|
||||
entrypoint: ["/bin/sh", "/init.sh"]
|
||||
volumes:
|
||||
- ./scripts/minio-init.sh:/init.sh:ro
|
||||
restart: "no"
|
||||
|
||||
# ------------------------------------------------------- schulcloud api ---
|
||||
api:
|
||||
<<: *server-image
|
||||
container_name: sc-api
|
||||
command: ["dist/apps/server/apps/server.app"]
|
||||
ports: ["127.0.0.1:3030:3030"]
|
||||
|
||||
management:
|
||||
# Not part of the running instance — it exposes the seeding and migration
|
||||
# endpoints that the real deployment's init job calls, and nothing else.
|
||||
<<: *server-image
|
||||
command: ["dist/apps/server/apps/management.app"]
|
||||
# Port and base path are hardcoded to 3333 and /api in management.app.ts;
|
||||
# PORT is not read here.
|
||||
ports: ["127.0.0.1:3333:3333"]
|
||||
|
||||
board-collaboration:
|
||||
# The websocket behind column boards. Without it a board renders once and
|
||||
# then never updates.
|
||||
<<: *server-image
|
||||
command: ["dist/apps/server/apps/board-collaboration.app"]
|
||||
environment:
|
||||
PORT: "4450"
|
||||
|
||||
admin-api:
|
||||
<<: *server-image
|
||||
command: ["dist/apps/server/apps/admin-api-server.app"]
|
||||
environment:
|
||||
PORT: "4030"
|
||||
|
||||
file-storage:
|
||||
image: quay.io/schulcloudverbund/file-storage:${SC_VERSION:-33.40}
|
||||
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
|
||||
depends_on:
|
||||
mongo: {condition: service_healthy}
|
||||
rabbitmq: {condition: service_healthy}
|
||||
minio: {condition: service_healthy}
|
||||
ports: ["127.0.0.1:4444:4444"]
|
||||
restart: unless-stopped
|
||||
|
||||
file-preview:
|
||||
# Generates thumbnails via ImageMagick, driven off RabbitMQ. Optional: with
|
||||
# it absent, files still upload and download, they just have no preview.
|
||||
image: quay.io/schulcloudverbund/file-storage:file-preview-${SC_VERSION:-33.40}
|
||||
profiles: ["preview"]
|
||||
env_file: [env/shared.env, env/jwt.env, env/file-storage.env]
|
||||
depends_on:
|
||||
rabbitmq: {condition: service_healthy}
|
||||
minio: {condition: service_healthy}
|
||||
restart: unless-stopped
|
||||
|
||||
# -------------------------------------------------------------- clients ---
|
||||
client:
|
||||
# The legacy UI. Still owns "/" and much of the course view.
|
||||
image: quay.io/schulcloudverbund/schulcloud-client-thr:${SC_VERSION:-33.40}
|
||||
env_file: [env/shared.env, env/jwt.env, env/client.env]
|
||||
depends_on: [api]
|
||||
ports: ["127.0.0.1:3100:3100"]
|
||||
restart: unless-stopped
|
||||
|
||||
nuxt:
|
||||
# The Vue SPA, built for the thr theme. The image is an nginx that
|
||||
# templates env vars into its config at start.
|
||||
image: quay.io/schulcloudverbund/schulcloud-frontend-thr:${SC_VERSION:-33.40}
|
||||
env_file: [env/nuxt.env]
|
||||
ports: ["127.0.0.1:4000:4000"]
|
||||
restart: unless-stopped
|
||||
|
||||
proxy:
|
||||
# The single origin. Everything a browser touches goes through here, so the
|
||||
# app sees one host the way it does in production.
|
||||
image: docker.io/nginx:1.29-alpine
|
||||
volumes:
|
||||
- ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
ports: ["127.0.0.1:4400:4400"]
|
||||
depends_on: [api, client, nuxt]
|
||||
restart: unless-stopped
|
||||
|
||||
# -------------------------------------------------- external tools -------
|
||||
etherpad:
|
||||
# The collaborative text editor element on column boards.
|
||||
image: docker.io/etherpad/etherpad:3.3.3
|
||||
profiles: ["tools"]
|
||||
env_file: [env/etherpad.env]
|
||||
volumes:
|
||||
- ./etherpad/APIKEY.txt:/opt/etherpad-lite/APIKEY.txt:ro
|
||||
depends_on:
|
||||
mongo: {condition: service_healthy}
|
||||
restart: unless-stopped
|
||||
|
||||
h5p-editor:
|
||||
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
|
||||
profiles: ["tools"]
|
||||
command: ["dist/apps/h5p-editor.app"]
|
||||
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
|
||||
environment:
|
||||
PORT: "4448"
|
||||
depends_on:
|
||||
mongo: {condition: service_healthy}
|
||||
minio: {condition: service_healthy}
|
||||
restart: unless-stopped
|
||||
|
||||
h5p-staticfiles:
|
||||
image: quay.io/schulcloudverbund/h5p-server:static-files-${SC_VERSION:-33.40}
|
||||
profiles: ["tools"]
|
||||
restart: unless-stopped
|
||||
|
||||
h5p-libraries:
|
||||
# One-shot: installs the H5P content types listed in env/h5p.env into the
|
||||
# library bucket. Exits when done; re-run it after changing that list.
|
||||
image: quay.io/schulcloudverbund/h5p-server:${SC_VERSION:-33.40}
|
||||
profiles: ["tools"]
|
||||
command: ["dist/apps/h5p-library-management.app"]
|
||||
env_file: [env/shared.env, env/jwt.env, env/h5p.env]
|
||||
depends_on:
|
||||
mongo: {condition: service_healthy}
|
||||
minio-init: {condition: service_completed_successfully}
|
||||
restart: "no"
|
||||
|
||||
tldraw-server:
|
||||
# The whiteboard element.
|
||||
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
|
||||
profiles: ["tools"]
|
||||
command: ["dist/apps/tldraw-server.app.js"]
|
||||
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
|
||||
depends_on: [valkey, minio]
|
||||
restart: unless-stopped
|
||||
|
||||
tldraw-worker:
|
||||
image: quay.io/schulcloudverbund/tldraw-server:${SC_VERSION:-33.40}
|
||||
profiles: ["tools"]
|
||||
command: ["dist/apps/tldraw-worker.app.js"]
|
||||
env_file: [env/shared.env, env/jwt.env, env/tldraw.env]
|
||||
depends_on: [valkey, minio]
|
||||
restart: unless-stopped
|
||||
|
||||
collabora:
|
||||
# Office document editing. Reached by the browser directly on :9980, the
|
||||
# way the real deployment puts it on its own hostname.
|
||||
image: docker.io/collabora/code:latest
|
||||
profiles: ["tools"]
|
||||
environment:
|
||||
extra_params: --o:ssl.enable=false --o:ssl.termination=false
|
||||
domain: ".*"
|
||||
aliasgroup1: "http://localhost:4400"
|
||||
ports: ["127.0.0.1:9980:9980"]
|
||||
cap_add: ["MKNOD"]
|
||||
restart: unless-stopped
|
||||
|
||||
# ------------------------------------------------------------ antivirus ---
|
||||
clamav:
|
||||
# ~1.5 GB resident once the signature database loads, hence its own profile.
|
||||
image: docker.io/clamav/clamav:1.5.3
|
||||
profiles: ["av"]
|
||||
volumes:
|
||||
- clamav-db:/var/lib/clamav
|
||||
restart: unless-stopped
|
||||
|
||||
clammit:
|
||||
image: ghcr.io/dbildungsplattform/clammit:0.9.1
|
||||
profiles: ["av"]
|
||||
environment:
|
||||
CLAMMIT_CLAMD_URL: tcp://clamav:3310
|
||||
CLAMMIT_LISTEN: 0.0.0.0:8438
|
||||
depends_on: [clamav]
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mongo-data:
|
||||
minio-data:
|
||||
clamav-db:
|
||||
95
local-instance/env/api.env
vendored
Normal file
95
local-instance/env/api.env
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
# The schulcloud-server API app (apps/server.app).
|
||||
#
|
||||
# The FEATURE_* block below is not hand-picked: it is a replay of
|
||||
# GET /api/v3/config/public from the live schulcloud-thueringen.de, so the local
|
||||
# instance exposes the same surface to students that the real one does.
|
||||
# Regenerate with ./scripts/sync-feature-flags.sh.
|
||||
|
||||
PORT=3030
|
||||
|
||||
# Internal service addresses (compose DNS), not the browser-facing origin.
|
||||
API_HOST=http://api:3030/api
|
||||
FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444
|
||||
LICENSE_SUMMARY_URL=http://api:3030/api/licenses/summary
|
||||
ROOM_MEMBER_INFO_URL=http://api:3030/api/rooms/member-info
|
||||
|
||||
ALERT_STATUS_URL=https://status.schulcloud-thueringen.de/
|
||||
CALENDAR_SERVICE_ENABLED=false
|
||||
NEST_LOG_LEVEL=info
|
||||
|
||||
# Etherpad (see ../etherpad/APIKEY.txt)
|
||||
ETHERPAD__URI=http://etherpad:9001/api/1
|
||||
ETHERPAD__PAD_URI=http://localhost:4400/etherpad/p
|
||||
ETHERPAD__API_KEY=381d67e6347d235ac9446da3ea10a82efd6f8ae09fa2e90efeda80f82feeb4fd
|
||||
|
||||
# tldraw + admin API
|
||||
TLDRAW__WEBSOCKET_URL=ws://localhost:4400/tldraw-server
|
||||
TLDRAW_ADMIN_API_CLIENT__BASE_URL=http://tldraw-server:3349
|
||||
TLDRAW_ADMIN_API_CLIENT__API_KEY=tldraw-admin-key
|
||||
ADMIN_API__ALLOWED_API_KEYS=thisisasupersecureapikeythatisabsolutelysave
|
||||
|
||||
# Teacher/student visibility, as configured for thr.
|
||||
TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE=false
|
||||
TEACHER_STUDENT_VISIBILITY__IS_ENABLED_BY_DEFAULT=true
|
||||
TEACHER_STUDENT_VISIBILITY__IS_VISIBLE=false
|
||||
|
||||
# --- feature flags, mirrored from the live instance ----------------------
|
||||
FEATURE_ADMINISTRATE_ROOMS_ENABLED=true
|
||||
FEATURE_AI_TUTOR_ENABLED=false
|
||||
FEATURE_ALLOW_INSECURE_LDAP_URL_ENABLED=false
|
||||
FEATURE_BOARD_LAYOUT_ENABLED=true
|
||||
FEATURE_BOARD_READERS_CAN_EDIT_TOGGLE=true
|
||||
FEATURE_COLUMN_BOARD_COLLABORATIVE_TEXT_EDITOR_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_COLLABORA_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_FILE_FOLDER_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_H5P_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED=true
|
||||
FEATURE_COLUMN_BOARD_SHARE=true
|
||||
FEATURE_COLUMN_BOARD_SOCKET_ENABLED=true
|
||||
# local override (live: true) — needs infrastructure we do not run
|
||||
FEATURE_COLUMN_BOARD_VIDEOCONFERENCE_ENABLED=false
|
||||
FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED=false
|
||||
FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_ENABLED=false
|
||||
FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE=1073741824
|
||||
# local override (live: true) — otherwise every seeded user hits a consent wall
|
||||
FEATURE_CONSENT_NECESSARY=false
|
||||
FEATURE_COPY_SERVICE_ENABLED=true
|
||||
FEATURE_COURSE_SHARE=true
|
||||
FEATURE_CTL_TOOLS_COPY_ENABLED=true
|
||||
FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION=false
|
||||
FEATURE_EXTERNAL_PERSON_REGISTRATION_ENABLED=false
|
||||
FEATURE_EXTERNAL_SYSTEM_LOGOUT_ENABLED=false
|
||||
FEATURE_FWU_CONTENT_ENABLED=false
|
||||
FEATURE_LESSON_SHARE=true
|
||||
FEATURE_LOGIN_LINK_ENABLED=false
|
||||
FEATURE_MEDIA_SHELF_ENABLED=true
|
||||
FEATURE_NOTIFICATIONS_ENABLED=false
|
||||
FEATURE_PREFERRED_CTL_TOOLS_ENABLED=true
|
||||
FEATURE_ROOM_ADD_EXTERNAL_PERSONS_ENABLED=false
|
||||
FEATURE_ROOM_COPY_ENABLED=true
|
||||
FEATURE_ROOM_LINK_INVITATION_EXTERNAL_PERSONS_ENABLED=false
|
||||
FEATURE_ROOM_REGISTER_EXTERNAL_PERSONS_ENABLED=false
|
||||
FEATURE_ROOM_SHARE=true
|
||||
FEATURE_SCHOOL_POLICY_ENABLED_NEW=true
|
||||
FEATURE_SCHOOL_TERMS_OF_USE_ENABLED=true
|
||||
FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED=false
|
||||
FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED=false
|
||||
FEATURE_SHOW_MIGRATION_WIZARD=false
|
||||
FEATURE_SHOW_OUTDATED_USERS=false
|
||||
FEATURE_TASK_SHARE=true
|
||||
FEATURE_TEAMS_ENABLED=true
|
||||
FEATURE_TEAM_CREATE_ROOM_ENABLED=true
|
||||
FEATURE_TLDRAW_ENABLED=true
|
||||
FEATURE_USER_LOGIN_MIGRATION_ENABLED=false
|
||||
FEATURE_USER_MIGRATION_ENABLED=false
|
||||
# local override (live: true) — needs infrastructure we do not run
|
||||
FEATURE_VIDEOCONFERENCE_ENABLED=false
|
||||
FEATURE_VIDIS_MEDIA_ACTIVATIONS_ENABLED=false
|
||||
|
||||
# --- required-but-unused endpoints ---------------------------------------
|
||||
# The config classes validate these as present strings even when the feature
|
||||
# is off, so they get a placeholder rather than a real service. Hydra is the
|
||||
# OAuth2 provider behind external tool launches; we do not run it.
|
||||
HYDRA_URI=http://hydra.invalid:4444
|
||||
31
local-instance/env/client.env
vendored
Normal file
31
local-instance/env/client.env
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# schulcloud-client, the legacy Express/Handlebars UI. Still serves "/" and
|
||||
# large parts of the course view, so it is not optional.
|
||||
|
||||
PORT=3100
|
||||
# HOST is what the client puts into redirects, so it must be the browser-facing
|
||||
# origin (the proxy), not this container's own address.
|
||||
HOST=http://localhost:4400
|
||||
API_HOST=http://api:3030/api
|
||||
PUBLIC_BACKEND_URL=http://localhost:4400/api
|
||||
|
||||
FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444
|
||||
|
||||
ETHERPAD__PAD_URI=http://localhost:4400/etherpad/p
|
||||
ETHERPAD__PAD_PATH=/etherpad/p
|
||||
ETHERPAD__DOMAIN=localhost
|
||||
FEATURE_ETHERPAD_ENABLED=true
|
||||
|
||||
SESSION_VALKEY__MODE=single
|
||||
SESSION_VALKEY__URI=redis://valkey:6379
|
||||
SESSION_COOKIE_SAME_SITE=lax
|
||||
|
||||
# The proxy terminates plain HTTP locally; without this the client marks the
|
||||
# jwt cookie Secure and the browser silently drops it.
|
||||
COOKIE__SECURE=false
|
||||
COOKIE__SAME_SITE=lax
|
||||
COOKIE__HTTP_ONLY=false
|
||||
|
||||
# Signs the session cookie. Local-only value; the app refuses to start without it.
|
||||
COOKIE_SECRET=local-instance-cookie-secret-not-a-real-secret
|
||||
|
||||
LOG_LEVEL=info
|
||||
11
local-instance/env/etherpad.env
vendored
Normal file
11
local-instance/env/etherpad.env
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# Etherpad, the collaborative text editor element on column boards.
|
||||
# Settings taken from docs/topics/etherpad/Local setup.md.
|
||||
|
||||
REQUIRE_SESSION=true
|
||||
PAD_OPTIONS_SHOW_CHAT=true
|
||||
DISABLE_IP_LOGGING=true
|
||||
DEFAULT_PAD_TEXT=Schreib etwas!
|
||||
DB_TYPE=mongodb
|
||||
DB_URL=mongodb://mongo:27017/etherpad
|
||||
AUTHENTICATION_METHOD=apikey
|
||||
TRUST_PROXY=true
|
||||
30
local-instance/env/file-storage.env
vendored
Normal file
30
local-instance/env/file-storage.env
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# file-storage service (repo hpi-schul-cloud/file-storage), the /api/v3/file/* API.
|
||||
|
||||
FILES_STORAGE__SERVICE_BASE_URL=http://file-storage:4444
|
||||
FILE_STORAGE_SERVICE_URL=http://file-storage:4444
|
||||
AUTHORIZATION_API_URL=http://api:3030/api/v3
|
||||
|
||||
FILES_STORAGE_S3_ENDPOINT=http://minio:9000/
|
||||
FILES_STORAGE_S3_BUCKET=schulcloud
|
||||
FILES_STORAGE_S3_REGION=eu-central-1
|
||||
FILES_STORAGE_S3_ACCESS_KEY_ID=miniouser
|
||||
FILES_STORAGE_S3_SECRET_ACCESS_KEY=miniouser
|
||||
|
||||
# Antivirus. Off by default: ClamAV wants ~1.5 GB of RAM for its signature DB.
|
||||
# Turn it on together with the `av` profile — with the scanner absent but the
|
||||
# check enabled, every upload stays stuck in securityCheck.status=pending and
|
||||
# can never be downloaded.
|
||||
ENABLE_FILE_SECURITY_CHECK=false
|
||||
ANTIVIRUS_SERVICE_HOSTNAME=clamav
|
||||
ANTIVIRUS_SERVICE_PORT=3310
|
||||
FILES_STORAGE_USE_STREAM_TO_ANTIVIRUS=false
|
||||
|
||||
PREVIEW_PRODUCER_INCOMING_REQUEST_TIMEOUT=10000
|
||||
|
||||
# Collabora must be reachable by the *browser*, so this one is a host URL.
|
||||
COLLABORA_ONLINE_URL=http://localhost:9980
|
||||
WOPI_URL=http://localhost:4400/api/v3/wopi/files
|
||||
WOPI_POST_MESSAGE_ORIGIN=http://localhost:4400
|
||||
FEATURE_COLUMN_BOARD_COLLABORA_ENABLED=true
|
||||
|
||||
LOGGER_LOG_LEVEL=info
|
||||
18
local-instance/env/h5p.env
vendored
Normal file
18
local-instance/env/h5p.env
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# h5p-server: the h5p-editor app plus the one-shot library-management job.
|
||||
|
||||
H5P_EDITOR__S3_ENDPOINT=http://minio:9000
|
||||
H5P_EDITOR__S3_REGION=eu-central-1
|
||||
H5P_EDITOR__S3_ACCESS_KEY_ID=miniouser
|
||||
H5P_EDITOR__S3_SECRET_ACCESS_KEY=miniouser
|
||||
H5P_EDITOR__S3_BUCKET_CONTENT=h5p-content-bucket
|
||||
H5P_EDITOR__S3_BUCKET_LIBRARIES=h5p-library-bucket
|
||||
H5P_EDITOR__LIBRARIES_S3_ACCESS_KEY_ID=miniouser
|
||||
H5P_EDITOR__LIBRARIES_S3_SECRET_ACCESS_KEY=miniouser
|
||||
|
||||
# A short list keeps the one-off library install to a couple of minutes; the
|
||||
# upstream default installs ~40 libraries and takes far longer.
|
||||
H5P_EDITOR__LIBRARY_LIST=H5P.ArithmeticQuiz,H5P.Chart,H5P.MultiChoice,H5P.Blanks
|
||||
|
||||
API_HOST=http://api:3030/api
|
||||
CORE_INCOMING_REQUEST_TIMEOUT_MS=8000
|
||||
LOGGER_LOG_LEVEL=info
|
||||
5
local-instance/env/jwt.env
vendored
Normal file
5
local-instance/env/jwt.env
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# Development RSA keypair, copied verbatim from schulcloud-server/.env.development
|
||||
# (upstream, public, committed in their repo). It exists so every local service
|
||||
# validates the same tokens. NEVER use these keys anywhere reachable from outside.
|
||||
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n"
|
||||
JWT_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEA0/oW2sIZWvVt0AEgQ8PS80/udJzfWXu6t2QWjUcQA2THGvDSXXMH\n6YMMY2czyBgf6L7hHV/9p1Trfpe7YgxYhOoGsxhXG1keAYQ4+mdveaUAa3uiACdE\nodsB0OFjVUdgOHCyUIXFfhSsp2p2tmZeFi/bE2v/05kYO+ExgQuzUDbB8bCr1sc7\ngMS/2dC2iE/BVw/I0F14oZkZn0fshojg4qoaLbLVKB7Iw53IXF2878zXp81Jdnnv\nHdwVbGWqoII6sHZFQs8ob5S/WGMl4QnBHN98x0KmORUFyTv5kK4cdcC8LJ1HpoVW\nNC6js84iF9yFRhYXY2RHqh7BwaZZ4XZym/MetTdQTBDaSvhXe0A3WdahNG+DGrie\nhd6doWk98Adb49InaodH64ZRkurxiX61GEtzjMRq9EfGS5R/IfcWyPQbiir6ymKX\nfOUtywRjcm3FZzmT7j3c0UHzQVEH0NBfTMj+QKz5NILNP230j0DcjNImDbHHcVH1\nquSb6e0WXjKANTkf4gaTOw7jdQDFw0Ou3aEmwPg+Xk1cwCwSHOOmPSSssZwgjpzG\nodPO3vsMGfRYTwcGbzgdQFFj0qTmvgnM5MHtEy8qCyvM4OsAPnE0zQWn48p7PVdJ\nm6j0H/1BYgVw1KxecIVk/HryoTOkgS9lhLu8iEIyrpAlWascIK7Uw58CAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"
|
||||
10
local-instance/env/nuxt.env
vendored
Normal file
10
local-instance/env/nuxt.env
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# schulcloud-frontend (nuxt-client). The image is an nginx serving the built
|
||||
# SPA; these values are substituted into its config template at container start
|
||||
# (see nuxt-client/config/docker/nginx.conf.template).
|
||||
|
||||
PUBLIC_BACKEND_URL=http://localhost:4400/api
|
||||
LEGACY_CLIENT_URL=http://client:3100
|
||||
COLLABORA_OFFICE_URL=http://localhost:9980
|
||||
LICENSE_SUMMARY_URL_FOR_CSP=http://localhost:4400
|
||||
H5P_SCRIPT_SRC_URLS=http://localhost:4400
|
||||
H5P_IMG_SRC_URLS=http://localhost:4400
|
||||
46
local-instance/env/shared.env
vendored
Normal file
46
local-instance/env/shared.env
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# Shared by every Schulcloud app in the stack.
|
||||
# Everything in this directory is local-only and deliberately non-secret.
|
||||
|
||||
NODE_ENV=production
|
||||
NO_COLOR=true
|
||||
TZ=Europe/Berlin
|
||||
|
||||
# --- identity of this instance -------------------------------------------
|
||||
# Mirrors dof_app_deploy/ansible/group_vars/thr/instance_cfg.yml so the local
|
||||
# instance looks and behaves like schulcloud-thueringen.de.
|
||||
SC_THEME=thr
|
||||
SC_SHORTNAME=thr
|
||||
SC_TITLE=Thüringer Schulcloud
|
||||
SC_PRODUCTNAME=Thüringer Schulcloud
|
||||
SC_NAV_TITLE=Thüringer Schulcloud
|
||||
SC_CONTACT_EMAIL=schulcloud-support@thillm.de
|
||||
ACCESSIBILITY_REPORT_EMAIL=institut@thillm.de
|
||||
|
||||
# The single origin the browser talks to (the nginx in ./proxy).
|
||||
SC_DOMAIN=localhost:4400
|
||||
HOST=http://localhost:4400
|
||||
# The API stamps SC_DOMAIN into every JWT as both issuer and audience. The
|
||||
# satellite services (file-storage, h5p) validate iss/aud against JWT_DOMAIN,
|
||||
# which defaults to a bare "localhost" — so without this they reject every
|
||||
# token the API issued and the homework page's file lookups 401. Keep the two
|
||||
# in lockstep.
|
||||
JWT_DOMAIN=localhost:4400
|
||||
PUBLIC_BACKEND_URL=http://localhost:4400/api
|
||||
CTL_TOOLS_BACKEND_URL=http://localhost:4400/api
|
||||
|
||||
# --- infrastructure ------------------------------------------------------
|
||||
DB_URL=mongodb://mongo:27017/schulcloud
|
||||
DB_ENSURE_INDEXES=true
|
||||
RABBITMQ_URI=amqp://guest:guest@rabbitmq:5672
|
||||
|
||||
# The JWT whitelist. `single` reproduces production: every authenticated
|
||||
# request re-sets a Valkey key with a JWT_TIMEOUT_SECONDS TTL, and losing that
|
||||
# key logs the session out. `in-memory` would hide that behaviour entirely,
|
||||
# which is exactly the behaviour this instance exists to test.
|
||||
SESSION_VALKEY__MODE=single
|
||||
SESSION_VALKEY__URI=redis://valkey:6379
|
||||
JWT_TIMEOUT_SECONDS=7200
|
||||
JWT_SHOW_TIMEOUT_WARNING_SECONDS=3600
|
||||
|
||||
AES_KEY=randomStringWithAtLeast16Chars;
|
||||
S3_KEY=abcdefghijklmnop
|
||||
17
local-instance/env/tldraw.env
vendored
Normal file
17
local-instance/env/tldraw.env
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# tldraw-server: the whiteboard element on column boards. Server + worker.
|
||||
|
||||
REDIS_URL=redis://valkey:6379
|
||||
AUTHORIZATION_API_HOST=http://api:3030
|
||||
|
||||
S3_ACCESS_KEY=miniouser
|
||||
S3_SECRET_KEY=miniouser
|
||||
S3_BUCKET=ydocs
|
||||
S3_ENDPOINT=minio
|
||||
S3_PORT=9000
|
||||
S3_SSL=false
|
||||
|
||||
FEATURE_TLDRAW_ENABLED=true
|
||||
TLDRAW_WEBSOCKET_URL=ws://localhost:4400/tldraw-server
|
||||
X_API_ALLOWED_KEYS=tldraw-admin-key
|
||||
NOT_AUTHENTICATED_REDIRECT_URL=http://localhost:4400/login
|
||||
LOGGER_LOG_LEVEL=info
|
||||
1
local-instance/etherpad/APIKEY.txt
Normal file
1
local-instance/etherpad/APIKEY.txt
Normal file
@@ -0,0 +1 @@
|
||||
381d67e6347d235ac9446da3ea10a82efd6f8ae09fa2e90efeda80f82feeb4fd
|
||||
585
local-instance/proxy/nginx.conf
Normal file
585
local-instance/proxy/nginx.conf
Normal file
@@ -0,0 +1,585 @@
|
||||
# GENERATED by scripts/gen-proxy-conf.py — do not edit by hand.
|
||||
#
|
||||
# One origin in front of the whole stack, the way the real instance is fronted
|
||||
# by its Kubernetes ingress. The path split between the legacy client and the
|
||||
# new SPA is not cosmetic: get it wrong and you are testing a different
|
||||
# application from the one the students use.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 4400;
|
||||
server_name localhost;
|
||||
|
||||
# Docker's embedded DNS. Every proxy_pass below goes through a variable so
|
||||
# that names resolve per request rather than at startup — otherwise this
|
||||
# container refuses to boot whenever an optional profile (tools, av) is
|
||||
# down, which is the normal case.
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Course files and H5P uploads are large; the ingress allows the same.
|
||||
client_max_body_size 2600m;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
# version-aggregator-svc upstream; /serverversion and /nuxtversion are the
|
||||
# real per-app endpoints and are routed below.
|
||||
location = /version {
|
||||
default_type application/json;
|
||||
return 200 '{"local-instance":true,"see":["/serverversion","/nuxtversion"]}';
|
||||
}
|
||||
|
||||
# --- service-owned ingresses and websockets ---
|
||||
|
||||
location /api/v3/file/ {
|
||||
set $up_api_v3_file file-storage:4444;
|
||||
proxy_pass http://$up_api_v3_file;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/v3/wopi/ {
|
||||
set $up_api_v3_wopi file-storage:4444;
|
||||
proxy_pass http://$up_api_v3_wopi;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/v3/h5p-editor/h5pstatics/ {
|
||||
set $up_api_v3_h5p_editor_h5pstatics h5p-staticfiles:8080;
|
||||
rewrite ^/api/v3/h5p-editor/h5pstatics/(.*)$ /h5pstatics/$1 break;
|
||||
proxy_pass http://$up_api_v3_h5p_editor_h5pstatics;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/v3/h5p-editor/ {
|
||||
set $up_api_v3_h5p_editor h5p-editor:4448;
|
||||
proxy_pass http://$up_api_v3_h5p_editor;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api/v3/ {
|
||||
set $up_api_v3 api:3030;
|
||||
proxy_pass http://$up_api_v3;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /admin/api/v1 {
|
||||
set $up_admin_api_v1 admin-api:4030;
|
||||
proxy_pass http://$up_admin_api_v1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /board-collaboration {
|
||||
set $up_board_collaboration board-collaboration:4450;
|
||||
proxy_pass http://$up_board_collaboration;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /tldraw-server {
|
||||
set $up_tldraw_server tldraw-server:3345;
|
||||
proxy_pass http://$up_tldraw_server;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /api/tldraw {
|
||||
set $up_api_tldraw tldraw-server:3345;
|
||||
proxy_pass http://$up_api_tldraw;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# --- from dof_app_deploy x_ingress.yml ---
|
||||
|
||||
# default
|
||||
location / {
|
||||
set $up_root client:3100;
|
||||
proxy_pass http://$up_root;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# themes
|
||||
location /favicon.png {
|
||||
set $up_favicon_png nuxt:4000;
|
||||
proxy_pass http://$up_favicon_png;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# bbb_presentation_pdf
|
||||
location /bbb-presentation.pdf {
|
||||
set $up_bbb_presentation_pdf nuxt:4000;
|
||||
proxy_pass http://$up_bbb_presentation_pdf;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# runtime
|
||||
location /runtime.config.json {
|
||||
set $up_runtime_config_json nuxt:4000;
|
||||
proxy_pass http://$up_runtime_config_json;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# metrics
|
||||
location /metrics {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# login
|
||||
location /login {
|
||||
set $up_login client:3100;
|
||||
proxy_pass http://$up_login;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# error
|
||||
location /error {
|
||||
set $up_error nuxt:4000;
|
||||
proxy_pass http://$up_error;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# imprint
|
||||
location /imprint {
|
||||
set $up_imprint nuxt:4000;
|
||||
proxy_pass http://$up_imprint;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# mint-ec
|
||||
location /mint-ec {
|
||||
set $up_mint_ec nuxt:4000;
|
||||
proxy_pass http://$up_mint_ec;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# news
|
||||
location /news {
|
||||
set $up_news nuxt:4000;
|
||||
proxy_pass http://$up_news;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# tasks
|
||||
location /tasks {
|
||||
set $up_tasks nuxt:4000;
|
||||
proxy_pass http://$up_tasks;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# nuxtversion
|
||||
location /nuxtversion {
|
||||
set $up_nuxtversion nuxt:4000;
|
||||
proxy_pass http://$up_nuxtversion;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# content
|
||||
location /content {
|
||||
set $up_content nuxt:4000;
|
||||
proxy_pass http://$up_content;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# _nuxt
|
||||
location /_nuxt {
|
||||
set $up__nuxt nuxt:4000;
|
||||
proxy_pass http://$up__nuxt;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_ldap
|
||||
location /administration/ldap {
|
||||
set $up_administration_ldap nuxt:4000;
|
||||
proxy_pass http://$up_administration_ldap;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_migration
|
||||
location /administration/migration {
|
||||
set $up_administration_migration nuxt:4000;
|
||||
proxy_pass http://$up_administration_migration;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_school-settings
|
||||
location /administration/school-settings {
|
||||
set $up_administration_school_settings nuxt:4000;
|
||||
proxy_pass http://$up_administration_school_settings;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_students
|
||||
location /administration/students {
|
||||
set $up_administration_students nuxt:4000;
|
||||
proxy_pass http://$up_administration_students;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_teachers
|
||||
location /administration/teachers {
|
||||
set $up_administration_teachers nuxt:4000;
|
||||
proxy_pass http://$up_administration_teachers;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_rooms
|
||||
location /administration/rooms/manage {
|
||||
set $up_administration_rooms_manage nuxt:4000;
|
||||
proxy_pass http://$up_administration_rooms_manage;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_groups_classes
|
||||
location /administration/groups/classes {
|
||||
set $up_administration_groups_classes nuxt:4000;
|
||||
proxy_pass http://$up_administration_groups_classes;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# boards
|
||||
location /boards {
|
||||
set $up_boards nuxt:4000;
|
||||
proxy_pass http://$up_boards;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# administration_rooms_new
|
||||
location /administration/rooms/new {
|
||||
set $up_administration_rooms_new nuxt:4000;
|
||||
proxy_pass http://$up_administration_rooms_new;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# rooms-overview
|
||||
location /rooms-overview {
|
||||
set $up_rooms_overview nuxt:4000;
|
||||
proxy_pass http://$up_rooms_overview;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# dashboard
|
||||
location /dashboard {
|
||||
set $up_dashboard nuxt:4000;
|
||||
proxy_pass http://$up_dashboard;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# rooms
|
||||
location /rooms {
|
||||
set $up_rooms nuxt:4000;
|
||||
proxy_pass http://$up_rooms;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# folder
|
||||
location /folder {
|
||||
set $up_folder nuxt:4000;
|
||||
proxy_pass http://$up_folder;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# collabora
|
||||
location /collabora {
|
||||
set $up_collabora nuxt:4000;
|
||||
proxy_pass http://$up_collabora;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# h5p-player
|
||||
location /h5p/player {
|
||||
set $up_h5p_player nuxt:4000;
|
||||
proxy_pass http://$up_h5p_player;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# h5p-editor
|
||||
location /h5p/editor {
|
||||
set $up_h5p_editor nuxt:4000;
|
||||
proxy_pass http://$up_h5p_editor;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# migration
|
||||
location /migration {
|
||||
set $up_migration nuxt:4000;
|
||||
proxy_pass http://$up_migration;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# tools_context
|
||||
location /tools/context {
|
||||
set $up_tools_context nuxt:4000;
|
||||
proxy_pass http://$up_tools_context;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# media_shelf
|
||||
location /media-shelf {
|
||||
set $up_media_shelf nuxt:4000;
|
||||
proxy_pass http://$up_media_shelf;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# licenses
|
||||
location /licenses {
|
||||
set $up_licenses nuxt:4000;
|
||||
proxy_pass http://$up_licenses;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# registration-external-members
|
||||
location /registration-external-members {
|
||||
set $up_registration_external_members nuxt:4000;
|
||||
proxy_pass http://$up_registration_external_members;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# security
|
||||
location /system/security {
|
||||
set $up_system_security nuxt:4000;
|
||||
proxy_pass http://$up_system_security;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api
|
||||
location /api {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# api_v1_roster
|
||||
location /api/v1/roster {
|
||||
return 404;
|
||||
}
|
||||
|
||||
# api_v1_consentVersions
|
||||
location /api/v1/consentVersions {
|
||||
set $up_api_v1_consentVersions api:3030;
|
||||
proxy_pass http://$up_api_v1_consentVersions;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api_v1_ldap-config
|
||||
location /api/v1/ldap-config {
|
||||
set $up_api_v1_ldap_config api:3030;
|
||||
proxy_pass http://$up_api_v1_ldap_config;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api_version
|
||||
location /serverversion {
|
||||
set $up_serverversion api:3030;
|
||||
proxy_pass http://$up_serverversion;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api_v1_courses
|
||||
location /api/v1/courses {
|
||||
set $up_api_v1_courses api:3030;
|
||||
proxy_pass http://$up_api_v1_courses;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api_v1_users
|
||||
location /api/v1/users {
|
||||
set $up_api_v1_users api:3030;
|
||||
proxy_pass http://$up_api_v1_users;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# api_v1_classes
|
||||
location /api/v1/classes {
|
||||
set $up_api_v1_classes api:3030;
|
||||
proxy_pass http://$up_api_v1_classes;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ^~ /etherpad/admin { return 404; }
|
||||
location ^~ /etherpad/stats { return 404; }
|
||||
|
||||
location /etherpad/socket.io {
|
||||
set $up_etherpad etherpad:9001;
|
||||
rewrite /etherpad/socket.io/(.*) /socket.io/$1 break;
|
||||
proxy_pass http://$up_etherpad;
|
||||
proxy_redirect / /etherpad/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /etherpad {
|
||||
set $up_etherpad etherpad:9001;
|
||||
rewrite ^/etherpad$ /etherpad/ permanent;
|
||||
rewrite /etherpad/(.*) /$1 break;
|
||||
proxy_pass http://$up_etherpad;
|
||||
proxy_pass_header Server;
|
||||
proxy_redirect / /etherpad/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# version_aggregator
|
||||
location /version {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
190
local-instance/scripts/gen-proxy-conf.py
Normal file
190
local-instance/scripts/gen-proxy-conf.py
Normal file
@@ -0,0 +1,190 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regenerate proxy/nginx.conf from the real deployment's ingress table.
|
||||
|
||||
The live instance is a Kubernetes ingress that splits one origin across the
|
||||
legacy client, the new SPA and several APIs. Which path goes where is not
|
||||
documented in prose — it is the table in
|
||||
dof_app_deploy/ansible/group_vars/all/x_ingress.yml plus a per-path ingress in
|
||||
each service repo. Transcribing 46 rules by hand invites exactly the drift that
|
||||
would make this instance lie about the real one, so we generate them.
|
||||
|
||||
Usage: python3 scripts/gen-proxy-conf.py > proxy/nginx.conf
|
||||
Needs the upstream clones in ../vendor (see README.md).
|
||||
"""
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
sys.exit('needs PyYAML: pip install pyyaml')
|
||||
|
||||
VENDOR = pathlib.Path(__file__).resolve().parents[2] / 'vendor'
|
||||
INGRESS = VENDOR / 'dof_app_deploy/ansible/group_vars/all/x_ingress.yml'
|
||||
|
||||
# Kubernetes service name -> compose upstream. None means the real deployment
|
||||
# deliberately 404s that path.
|
||||
UPSTREAM = {
|
||||
'client-svc': 'client:3100',
|
||||
'nuxtclient-svc': 'nuxt:4000',
|
||||
'api-svc': 'api:3030',
|
||||
'default-backend-404-svc': None,
|
||||
'version-aggregator-svc': None, # replaced by our own /version below
|
||||
None: None,
|
||||
}
|
||||
|
||||
# Routes that live in the individual service repos' own ingress templates
|
||||
# rather than the shared table, plus the two websocket endpoints.
|
||||
# (path, upstream, websocket, rewrite-or-None)
|
||||
EXTRA = [
|
||||
('/api/v3/file/', 'file-storage:4444', False, None),
|
||||
('/api/v3/wopi/', 'file-storage:4444', False, None),
|
||||
('/api/v3/h5p-editor/h5pstatics/', 'h5p-staticfiles:8080', False,
|
||||
'^/api/v3/h5p-editor/h5pstatics/(.*)$ /h5pstatics/$1'),
|
||||
('/api/v3/h5p-editor/', 'h5p-editor:4448', False, None),
|
||||
('/api/v3/', 'api:3030', False, None),
|
||||
('/admin/api/v1', 'admin-api:4030', False, None),
|
||||
('/board-collaboration', 'board-collaboration:4450', True, None),
|
||||
('/tldraw-server', 'tldraw-server:3345', True, None),
|
||||
('/api/tldraw', 'tldraw-server:3345', False, None),
|
||||
]
|
||||
|
||||
PREAMBLE = '''# GENERATED by scripts/gen-proxy-conf.py — do not edit by hand.
|
||||
#
|
||||
# One origin in front of the whole stack, the way the real instance is fronted
|
||||
# by its Kubernetes ingress. The path split between the legacy client and the
|
||||
# new SPA is not cosmetic: get it wrong and you are testing a different
|
||||
# application from the one the students use.
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 4400;
|
||||
server_name localhost;
|
||||
|
||||
# Docker's embedded DNS. Every proxy_pass below goes through a variable so
|
||||
# that names resolve per request rather than at startup — otherwise this
|
||||
# container refuses to boot whenever an optional profile (tools, av) is
|
||||
# down, which is the normal case.
|
||||
resolver 127.0.0.11 valid=10s ipv6=off;
|
||||
|
||||
# Course files and H5P uploads are large; the ingress allows the same.
|
||||
client_max_body_size 2600m;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
# version-aggregator-svc upstream; /serverversion and /nuxtversion are the
|
||||
# real per-app endpoints and are routed below.
|
||||
location = /version {
|
||||
default_type application/json;
|
||||
return 200 '{"local-instance":true,"see":["/serverversion","/nuxtversion"]}';
|
||||
}
|
||||
'''
|
||||
|
||||
COMMON = ''' proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
'''
|
||||
|
||||
WS = ''' proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
'''
|
||||
|
||||
GONE = '''
|
||||
location {path} {{
|
||||
return 404;
|
||||
}}
|
||||
'''
|
||||
|
||||
# Etherpad is mounted under a prefix it knows nothing about, so the deployment
|
||||
# runs a dedicated nginx in front of it that rewrites the prefix away and
|
||||
# proxies socket.io separately. Copied from
|
||||
# dof_app_deploy/ansible/roles/dof_etherpad/templates/nginx-configmap-files.yml.j2
|
||||
# — a plain proxy_pass gets you a pad that loads and then never syncs.
|
||||
ETHERPAD = '''
|
||||
location ^~ /etherpad/admin { return 404; }
|
||||
location ^~ /etherpad/stats { return 404; }
|
||||
|
||||
location /etherpad/socket.io {
|
||||
set $up_etherpad etherpad:9001;
|
||||
rewrite /etherpad/socket.io/(.*) /socket.io/$1 break;
|
||||
proxy_pass http://$up_etherpad;
|
||||
proxy_redirect / /etherpad/;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
|
||||
location /etherpad {
|
||||
set $up_etherpad etherpad:9001;
|
||||
rewrite ^/etherpad$ /etherpad/ permanent;
|
||||
rewrite /etherpad/(.*) /$1 break;
|
||||
proxy_pass http://$up_etherpad;
|
||||
proxy_pass_header Server;
|
||||
proxy_redirect / /etherpad/;
|
||||
proxy_set_header Host $host;
|
||||
proxy_buffering off;
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def block(path: str, upstream: str, ws: bool, rewrite: str | None, var: str) -> str:
|
||||
out = [f'\n\tlocation {path} {{\n', f'\t\tset ${var} {upstream};\n']
|
||||
if rewrite:
|
||||
out.append(f'\t\trewrite {rewrite} break;\n')
|
||||
out.append(f'\t\tproxy_pass http://${var};\n')
|
||||
out.append(COMMON)
|
||||
if ws:
|
||||
out.append(WS)
|
||||
out.append('\t}\n')
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def varname(path: str) -> str:
|
||||
safe = ''.join(c if c.isalnum() else '_' for c in path.strip('/')) or 'root'
|
||||
return f'up_{safe}'
|
||||
|
||||
|
||||
def main() -> None:
|
||||
table = yaml.safe_load(INGRESS.read_text())['default_ingress']
|
||||
|
||||
seen: set[str] = set()
|
||||
out = [PREAMBLE]
|
||||
|
||||
out.append('\n\t# --- service-owned ingresses and websockets ---\n')
|
||||
for path, upstream, ws, rewrite in EXTRA:
|
||||
seen.add(path)
|
||||
out.append(block(path, upstream, ws, rewrite, varname(path)))
|
||||
|
||||
out.append('\n\t# --- from dof_app_deploy x_ingress.yml ---\n')
|
||||
for name, entry in table.items():
|
||||
path = entry.get('path')
|
||||
if path is None or path in seen:
|
||||
continue
|
||||
seen.add(path)
|
||||
if path == '/etherpad':
|
||||
out.append(ETHERPAD)
|
||||
continue
|
||||
upstream = UPSTREAM.get(entry.get('serviceName'), 'MISSING')
|
||||
if upstream == 'MISSING':
|
||||
sys.exit(f'unknown serviceName for {name}: {entry.get("serviceName")}')
|
||||
out.append(f'\n\t# {name}')
|
||||
out.append(GONE.format(path=path) if upstream is None
|
||||
else block(path, upstream, False, None, varname(path)))
|
||||
|
||||
out.append('}\n')
|
||||
sys.stdout.write(''.join(out))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
19
local-instance/scripts/minio-init.sh
Executable file
19
local-instance/scripts/minio-init.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# Creates the buckets each service expects. MinIO does not create them on
|
||||
# demand: file-storage reports a generic 500 on upload if its bucket is
|
||||
# missing, and the h5p library job fails halfway through.
|
||||
set -eu
|
||||
|
||||
mc alias set local http://minio:9000 miniouser miniouser
|
||||
|
||||
for bucket in \
|
||||
schulcloud ` # files-storage (the /api/v3/file API)` \
|
||||
h5p-content-bucket ` # h5p-editor content` \
|
||||
h5p-library-bucket ` # h5p content types` \
|
||||
ydocs ` # tldraw whiteboard documents` \
|
||||
fwu-content # FWU media, unused but cheap to create
|
||||
do
|
||||
mc mb --ignore-existing "local/$bucket"
|
||||
done
|
||||
|
||||
mc ls local
|
||||
69
local-instance/scripts/seed.sh
Executable file
69
local-instance/scripts/seed.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# Seed the local instance with the upstream demo school.
|
||||
#
|
||||
# This is what the real deployment's init job does (dof_app_deploy
|
||||
# .../schulcloud-server-init/templates/configmap_file_init.yml.j2): it asks the
|
||||
# management app to load backup/setup/*.json, which ships inside the server
|
||||
# image. Safe to re-run — collections are replaced, not appended to.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
MGMT=http://127.0.0.1:3333/api/management/database
|
||||
COMPOSE=(docker compose)
|
||||
|
||||
echo "==> waiting for the management app"
|
||||
for _ in $(seq 1 60); do
|
||||
if curl -fsS -o /dev/null "$MGMT/../../docs" 2>/dev/null || curl -fsS -o /dev/null -X POST "$MGMT/sync-indexes" 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "==> seeding collections (this takes a minute or two)"
|
||||
curl -fsS --retry 30 --retry-all-errors --retry-delay 10 \
|
||||
-X POST "$MGMT/seed?with-indexes=true" >/dev/null
|
||||
echo " done"
|
||||
|
||||
# The legacy file service (course/topic attachments, as opposed to the newer
|
||||
# /api/v3/file API) reads its S3 credentials from a storageproviders document
|
||||
# rather than from the environment, and there is deliberately no seed data for
|
||||
# it. Without this, legacy uploads fail with a provider-not-found error.
|
||||
echo "==> registering MinIO as the legacy storage provider"
|
||||
S3_KEY=$(grep -E '^S3_KEY=' env/shared.env | cut -d= -f2-)
|
||||
SECRET=$(curl -fsS -X POST "$MGMT/encrypt-plain-text" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "$(printf '{"plainText":"miniouser","key":"%s"}' "$S3_KEY")")
|
||||
|
||||
"${COMPOSE[@]}" exec -T mongo mongosh schulcloud --quiet --eval "
|
||||
const id = ObjectId('62949a4003839b6162aa566b');
|
||||
db.storageproviders.replaceOne({ _id: id }, {
|
||||
_id: id, isShared: true, region: 'eu-central-1', type: 'S3',
|
||||
endpointUrl: 'http://minio:9000',
|
||||
accessKeyId: 'miniouser',
|
||||
secretAccessKey: '$SECRET',
|
||||
maxBuckets: 150, freeBuckets: 138,
|
||||
createdAt: new Date(), updatedAt: new Date(), __v: 0,
|
||||
}, { upsert: true });
|
||||
const r = db.schools.updateMany({}, { \$set: { storageProvider: id } });
|
||||
print(' schools linked to the provider: ' + r.modifiedCount);
|
||||
"
|
||||
|
||||
cat <<'ACCOUNTS'
|
||||
|
||||
==> ready — http://localhost:4400
|
||||
|
||||
Seeded accounts (the demo password differs by account — upstream quirk):
|
||||
|
||||
demo-schueler@schul-cloud.org student Fritz Schmidt schulcloud
|
||||
^ has graded submissions, incl. a feedback-only and a 100% one
|
||||
demo-lehrer@schul-cloud.org teacher Erika Meier schulcloud
|
||||
klara.fall@schul-cloud.org teacher Klara Fall Schulcloud1!
|
||||
^ owns Fritz's graded Biologie submissions
|
||||
lehrer@schul-cloud.org teacher Cord Carl Schulcloud1!
|
||||
admin@schul-cloud.org admin Thorsten Test Schulcloud1!
|
||||
*.qa@schul-cloud.org various Schulcloud1qa!
|
||||
|
||||
Sign in as the teacher to grade, as the student to see what grading looks
|
||||
like from the side our MCP server reads.
|
||||
ACCOUNTS
|
||||
Reference in New Issue
Block a user