Skip to content

fix(demo): zet de FSC-beheeromgeving achter de inlogmuur #318

fix(demo): zet de FSC-beheeromgeving achter de inlogmuur

fix(demo): zet de FSC-beheeromgeving achter de inlogmuur #318

# Bewaakt dat de compose-overlays van de lokale FSC-harnessen blijven passen op hun basis.
#
# De hostnet-overlay spiegelt elke service uit docker-compose.yaml: `network_mode: host`,
# `extra_hosts` en een botsvrije poort op 127.0.0.1. Drie dingen kunnen daar stil misgaan:
#
# 1. Iemand voegt een service aan de basis toe en vergeet de overlay. Die service vraagt dan het
# default-netwerk terug, en omdat de overlay dat netwerk met `!reset` weghaalt, valt de hele
# stack om — pas bij `up`, op de machine van de volgende gebruiker.
# 2. Iemand voegt een listener toe die niet op loopback bindt. In een gedeelde netns staat die
# daarmee op élke interface van de machine, inclusief de componenten die zonder authenticatie
# draaien (controller met AUTHN_TYPE=none, postgres met harness-credentials).
# 3. Iemand voegt een service toe die `/pki` leest onder een vaste UID, zonder keep-id. Die faalt
# onder rootless podman met `permission denied` op de privékey.
#
# De federatie-opstelling (demo/environment/federatie/) legt daar een VIERDE laag op die alle peers
# in één netns zet. Die laag is de laatste en dus beslissend; wat daar geïntroduceerd wordt, is in
# de drie-bestands-merge onzichtbaar. Vandaar twee jobs die dezelfde checks draaien — via
# .github/scripts/merge-guard.sh, zodat hun dekking niet uiteen kan lopen — plus, in de tweede,
# de eis die alleen in een federatie bestaat: de peers mogen elkaars poorten niet claimen.
#
# Beide jobs renderen alleen de merge (geen containers). `config -q` alleen zou een halve overlay
# zonder morren accepteren.
name: FSC-harness overlays
on:
pull_request:
types: [opened, synchronize, reopened]
# De workflow staat in zijn eigen pad-filter: anders komt een verzwakking van de guard binnen
# zonder dat de guard zelf ooit gedraaid heeft.
paths: ['demo/environment/**', 'compose.yaml', 'compose.podman*.yaml', 'demo/*.sh', 'demo/genereer-magazijnen.py', 'toxiproxy/proxies.json', '.github/workflows/fsc-harness-overlays.yml', '.github/scripts/merge-guard.sh']
push:
branches: [main]
paths: ['demo/environment/**', 'compose.yaml', 'compose.podman*.yaml', 'demo/*.sh', 'demo/genereer-magazijnen.py', 'toxiproxy/proxies.json', '.github/workflows/fsc-harness-overlays.yml', '.github/scripts/merge-guard.sh']
# LET OP: `pull_request`, niet `pull_request_target`, en read-only zonder secrets. Beide jobs
# `source`n demo/environment/federatie/peers.env respectievelijk renderen compose-bestanden uit de
# PR — dat is PR-gestuurde uitvoering, net als de bash-unittests en de docker build/run in
# `harness-scripts`. Dat is alleen aanvaardbaar zolang deze workflow geen schrijfrechten, geen
# secrets en geen cache heeft. Wijzig geen van drieën.
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
hostnet-overlay-past-op-de-basis:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Elke service op de gedeelde netns, elke listener op loopback
run: |
set -euo pipefail
fail=0
peers=0
overlays=0
for peer in demo/environment/*/deploy/local; do
[ -f "$peer/docker-compose.yaml" ] || continue
peers=$((peers + 1))
if [ ! -f "$peer/docker-compose.podman-hostnet.yaml" ]; then
echo "FOUT: $peer heeft geen docker-compose.podman-hostnet.yaml — verwijderd of hernoemd?"
fail=1
continue
fi
overlays=$((overlays + 1))
# PKI_DIR/HOST_UID komen normaal uit .env (gitignored). De waarden doen er niet toe:
# `config` rendert alleen, het mount niets en controleert geen paden.
if ! (cd "$peer" && PKI_DIR=../../pki HOST_UID=1000 HOST_GID=1000 \
docker compose -f docker-compose.yaml \
-f docker-compose.podman.yaml \
-f docker-compose.podman-hostnet.yaml config --format json) \
| "$GITHUB_WORKSPACE/.github/scripts/merge-guard.sh" "$peer (hostnet-merge)" --postgres; then
fail=1
fi
done
if [ "$peers" -eq 0 ] || [ "$overlays" -ne "$peers" ]; then
echo "FOUT: $overlays van $peers harnessen hebben een hostnet-overlay — guard meet niet wat hij hoort te meten."
fail=1
fi
# Servicenamen en paden komen uit de PR en gaan daarom niet in een ::error-annotatie:
# een `%0A` daarin zou een tweede workflow-commando starten. De details staan hierboven.
if [ "$fail" -ne 0 ]; then
echo "::error::De hostnet-overlays passen niet meer op hun basis. Zie de log hierboven."
fi
exit "$fail"
federatie-overlay-past-op-de-peers:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Peer-blokken botsen niet en elke listener staat op loopback
run: |
set -euo pipefail
FED=demo/environment/federatie
GUARD="$GITHUB_WORKSPACE/.github/scripts/merge-guard.sh"
[ -d "$FED/compose" ] || { echo "FOUT: $FED/compose ontbreekt."; exit 1; }
# shellcheck source=/dev/null
. "$FED/peers.env"
fail=0
alle=""
aantal_peers=0
for peer in $GASTHEER $GASTEN; do
lokaal="demo/environment/$peer/deploy/local"
overlay="$FED/compose/$peer.yaml"
aantal_peers=$((aantal_peers + 1))
if [ ! -f "$overlay" ]; then
echo "FOUT: peer '$peer' staat in peers.env maar heeft geen $overlay."
fail=1
continue
fi
merged=$(cd "$lokaal" && PKI_DIR=../../pki HOST_UID=1000 HOST_GID=1000 \
docker compose -f docker-compose.yaml \
-f docker-compose.podman.yaml \
-f docker-compose.podman-hostnet.yaml \
-f "../../../federatie/compose/$peer.yaml" config --format json)
# De gastheer draait de gedeelde postgres; bij de gasten staat die in een inactief
# profiel en rendert hij niet mee, dus daar mag de eis niet gelden.
if [ "$peer" = "$GASTHEER" ]; then
printf '%s' "$merged" | "$GUARD" "$peer (federatie-merge)" --postgres || fail=1
else
printf '%s' "$merged" | "$GUARD" "$peer (federatie-merge)" || fail=1
fi
# De adressen die deze peer in de federatie claimt, uit env én uit `command`
# (stub-upstream draagt zijn adres in een `-listen=`-vlag). Elke component heeft een
# eigen adres binnen het federatie-prefix en houdt zijn standaardpoort; het adres is
# dus wat de peers onderscheidt, niet de poort.
binds=$(jq -r --arg pre "$FED_PREFIX" '.services | to_entries[] | . as $s
| ( ($s.value.environment // {}) | to_entries[] | select(.value != null) | (.value | tostring) ),
( (($s.value.command // []) + ($s.value.entrypoint // []))[]? | select(type == "string") )
| select(test("(^|=)" + ($pre | gsub("\\."; "\\.")) + "\\.[0-9]+\\.[0-9]+:[0-9]+$"))
| sub("^.*="; "")' <<<"$merged" | sort -u)
# Een listener die buiten het federatie-prefix valt, is een service die zijn
# federatie-overlay mist: die valt terug op het adres uit de hostnet-overlay
# (127.0.0.1) en botst daar met de gelijknamige component van elke andere peer. Dat
# moet hier gevangen worden en niet door de smoke: uit de gedeclareerde adressen is
# zo'n service juist VERDWENEN, dus daar valt hij niet op.
buiten_prefix=$(jq -r --arg pre "$FED_PREFIX" '.services | to_entries[] | . as $s
| ($s.value.environment // {}) | to_entries[]
| select(.key | test("^(LISTEN_ADDRESS|MONITORING_ADDRESS)"))
| select(.value != null and (.value | tostring) != "")
| select(((.value | tostring) | startswith($pre + ".")) | not)
| "\($s.key): \(.key)=\(.value)"' <<<"$merged")
if [ -n "$buiten_prefix" ]; then
echo "FOUT: $peer — listener buiten $FED_PREFIX. (mist zijn federatie-overlay):"
printf '%s\n' "$buiten_prefix" | sed 's/^/ /'
fail=1
fi
net_var="NET_$(printf '%s' "$peer" | tr '-' '_')"
net="${!net_var:-}"
if [ -z "$net" ]; then
echo "FOUT: geen $net_var in peers.env."
fail=1
continue
fi
in_net=0
for b in $binds; do
adres="${b%:*}"
# Federatie-infra draait op de GASTHEER namens iedereen, op <prefix>.0.x, en hoort bij
# geen peer-/24. Voor een gast is datzelfde adres per definitie een tweede router of
# postgres, dus daar geldt de uitzondering niet.
case "$adres" in
"$FED_PREFIX".0.*)
if [ "$peer" != "$GASTHEER" ]; then
echo "FOUT: $peer is gast maar claimt infra-adres $adres."
fail=1
fi
continue
;;
esac
case "$adres" in
"$net".*) in_net=$((in_net + 1)) ;;
*) echo "FOUT: $peer — $b valt buiten zijn eigen $net.0/24."; fail=1 ;;
esac
alle="$alle $peer:$adres"
done
# Nul adressen betekent niet "schoon" maar "de extractie matcht niets meer" — een
# opmaakwijziging in de overlay zou de guard anders stilzwijgend uitschakelen.
if [ "$in_net" -eq 0 ]; then
echo "FOUT: $peer — 0 adressen uit de merge; de extractie matcht niets meer."
fail=1
else
echo "$peer: $in_net binds in $net.0/24."
fi
done
# Disjunctheid over de peers heen: het punt van de adresindeling. Twee peers op hetzelfde
# adres botsen alsnog op hun standaardpoort.
#
# `sort -u` eerst: één peer claimt zijn eigen adres meermaals, want een component heeft
# meerdere listeners (manager: extern, intern, intern-unauth, monitoring). Zonder de
# deduplicatie leest elke component met twee poorten als een botsing met zichzelf.
# shellcheck disable=SC2086 # bewuste word splitting: `alle` is een lijst `peer:adres`.
botsingen=$(printf '%s\n' $alle | sort -u | awk -F: 'NF==2 {tel[$2]=tel[$2]" "$1} END {for (p in tel) if (split(tel[p],a," ")>1) print p": "tel[p]}')
if [ -n "$botsingen" ]; then
echo "FOUT: hetzelfde adres wordt door meer dan één peer geclaimd:"
printf '%s\n' "$botsingen" | sed 's/^/ /'
fail=1
fi
# Disjunctheid over één peer is inhoudsloos; de guard hoort te meten wat hij belooft.
if [ "$aantal_peers" -lt 2 ]; then
echo "FOUT: peers.env levert $aantal_peers peer(s) — te weinig om botsingen op te meten."
fail=1
fi
# Spiegeling: een overlay die niet in peers.env staat wordt nergens gerenderd en zou dus
# ongecontroleerd meeliften zodra iemand hem handmatig meegeeft aan `docker compose`.
for overlay in "$FED"/compose/*.yaml; do
naam="$(basename "$overlay" .yaml)"
case " $GASTHEER $GASTEN " in
*" $naam "*) ;;
*) echo "FOUT: $overlay hoort bij geen enkele peer uit peers.env."; fail=1 ;;
esac
done
if [ "$fail" -ne 0 ]; then
echo "::error::De federatie-overlays passen niet meer op hun peers. Zie de log hierboven."
fi
exit "$fail"
# De demo-stack uit compose.yaml draait in dezelfde netns-modus als de harnessen, en dus onder
# dezelfde eis: geen enkele listener buiten loopback. Zonder deze job stond die stack op élke
# interface van de machine — Redis zonder wachtwoord en twee PostgreSQL-instanties met
# demo-credentials — en botste een wildcard-bind bovendien met elke specifieke bind van een
# federatie in dezelfde netns.
demo-stack-op-loopback:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Elke demo-service op de gedeelde netns, elke listener op loopback
run: |
set -euo pipefail
# `--profile demo` erbij: zonder dat profiel rendert compose de vier Quarkus-services en
# toxiproxy niet, en meet deze job precies de helft van de stack.
DEMO_HOST=localhost DEMO_MAGAZIJNEN=98 docker compose --profile demo \
-f compose.yaml \
-f compose.podman.yaml \
-f compose.podman-hostnet.yaml config --format json \
| "$GITHUB_WORKSPACE/.github/scripts/merge-guard.sh" "demo-stack (hostnet-merge)"
- name: De console stuurt Toxiproxy in deze modus ook naar loopback
# De demo-console maakt de proxies zelf aan en zet ze elke reconcile-ronde terug. Staan haar
# adressen niet op de gedeelde netns ingesteld, dan overschrijft ze de geladen definities met
# container-DNS-namen die daar niet resolven — en dat is aan niets te zien: Toxiproxy blijft
# gezond, de zes namen kloppen nog, alleen sterft elk verbinding erdoorheen. Deze guard eist
# daarom een override per proxy uit toxiproxy/proxies.json, allemaal op 127.0.0.1.
run: |
set -euo pipefail
DEMO_HOST=localhost DEMO_MAGAZIJNEN=98 docker compose --profile demo \
-f compose.yaml \
-f compose.podman.yaml \
-f compose.podman-hostnet.yaml config --format json > /tmp/hostnet-merge.json
python3 - /tmp/hostnet-merge.json toxiproxy/proxies.json <<'PY'
import json, sys
merge, bron = (json.load(open(pad)) for pad in sys.argv[1:3])
env = merge["services"]["demo-console"].get("environment") or {}
fouten = []
for proxy in bron:
# `magazijn-a` → MAGAZIJN_A_; de env-namen in application.properties volgen die vorm.
stam = "TOXIPROXY_" + proxy["name"].upper().replace("-", "_")
for soort in ("LISTEN", "UPSTREAM"):
sleutel = f"{stam}_{soort}"
waarde = env.get(sleutel)
if waarde is None:
fouten.append(f"{sleutel} ontbreekt: de console valt terug op haar container-DNS-default")
elif not str(waarde).startswith("127.0.0.1:"):
fouten.append(f"{sleutel}={waarde} staat niet op 127.0.0.1")
if fouten:
print("::error::compose.podman-hostnet.yaml stuurt de console niet volledig naar loopback")
print("\n".join(" " + f for f in fouten))
sys.exit(1)
print(f"{len(bron)} proxies: listen en upstream van de console staan op loopback.")
PY
harness-scripts:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: shellcheck + bash-unittests
# De harness is bash, en de bash-tests die er zijn (demo/environment/lib/test-*.sh) draaiden
# tot nu toe nergens automatisch. Zonder deze stap rot elke test die eraan wordt toegevoegd.
#
# `deploy/zad/*.sh` valt bewust buiten de globs: die scripts dragen bestaande
# SC2034-bevindingen die losstaan van de lokale harness. Ze erbij nemen vergt eerst die
# opruiming; anders zou deze stap rood staan op iets wat hij niet bewaakt.
run: |
set -euo pipefail
fail=0
echo "== shellcheck =="
# -x volgt `source`-directives, zodat de gedeelde lib meegecontroleerd wordt. De
# ci-scripts.yml shellcheckt alles onder .github/scripts/; merge-guard.sh staat hier
# bewust óók, zodat een wijziging aan de guard niet ongecontroleerd blijft wanneer
# alleen deze overlay-workflow draait.
if ! shellcheck -x -S warning \
demo/environment/lib/*.sh \
demo/environment/federatie/*.sh \
demo/environment/federatie/contracts/*.sh \
demo/environment/*/deploy/local/*.sh \
demo/environment/*/pki/*.sh \
demo/environment/zad-demo/*.sh \
demo/*.sh \
.github/scripts/merge-guard.sh; then
fail=1
fi
echo "== bash-unittests =="
gedraaid=0
for t in demo/environment/lib/test-*.sh; do
[ -f "$t" ] || continue
gedraaid=$((gedraaid + 1))
echo "-- $t"
bash "$t" || fail=1
done
if [ "$gedraaid" -eq 0 ]; then
echo "FOUT: geen enkele test-*.sh gevonden — deze stap meet niets."
fail=1
fi
if [ "$fail" -ne 0 ]; then
echo "::error::shellcheck of een bash-unittest faalde. Zie de log hierboven."
fi
exit "$fail"
- name: Contract-bootstrap-image bouwt en draait onder een willekeurige UID
# Het image draait op ZAD als component; zonder deze stap blijkt een verkeerd COPY-pad, een
# ontbrekend pakket of een rechtenprobleem pas daar. OpenShift start containers onder een
# willekeurige UID in groep 0 — dat is de aanname waar de hele opstelling op rust, dus die
# wordt hier expliciet uitgeoefend en niet alleen in de Dockerfile opgeschreven.
run: |
set -euo pipefail
docker build -f demo/environment/federatie/contracts/Dockerfile \
-t fbs-fsc-contract-bootstrap:ci demo/environment
echo "== gereedschap en leesbaarheid onder UID 12345, groep 0 =="
docker run --rm -u 12345:0 --entrypoint sh fbs-fsc-contract-bootstrap:ci -c '
set -eu
command -v bash curl jq openssl >/dev/null
test -r /opt/fsc/lib/fsc-contract.sh
test -r /opt/fsc/lib/fsc-harness.sh
test -x /opt/fsc/contracts/zad-lus.sh
test -x /opt/fsc/contracts/bootstrap-consumer.sh
test -x /opt/fsc/contracts/bootstrap-provider.sh'
# De twee scripts die met béíde managers praten horen niet in het image: op ZAD kan dat
# niet en een operator die ze daar aantreft, gaat ze gebruiken.
echo "== bootstrap.sh en fbs-contracten.sh zitten er NIET in =="
docker run --rm -u 12345:0 --entrypoint sh fbs-fsc-contract-bootstrap:ci -c '
! test -e /opt/fsc/contracts/bootstrap.sh && ! test -e /opt/fsc/contracts/fbs-contracten.sh'
# Exit 2 = "configuratie deugt niet". Daar stopt de lus op, en dat maakt er een zichtbare
# crashloop van in plaats van een pod die draait en niets doet.
echo "== configuratiefouten eindigen op 2 =="
for geval in "" "FSC_ROL=onzin"; do
rc=0
if [ -n "$geval" ]; then
docker run --rm -u 12345:0 -e "$geval" fbs-fsc-contract-bootstrap:ci || rc=$?
else
docker run --rm -u 12345:0 fbs-fsc-contract-bootstrap:ci || rc=$?
fi
if [ "$rc" -ne 2 ]; then
echo "::error::verwachtte exit 2 voor '${geval:-<geen env>}', kreeg ${rc}"
exit 1
fi
done