Podman-Testumgebung

Lokale Testumgebung mit Podman: DataBridge-Container, Azurite, wahlweise MSSQL, PostgreSQL, MySQL und Oracle, HTTP-Mocks für Business Central, NAV und OpenAPI sowie ein Service-Bus-Emulator — inklusive Beispieldaten, Connections, Jobs und Mappings.

Die Testumgebung unter dev/ startet eine vollständig vorkonfigurierte DataBridge auf dem eigenen Rechner — ohne Azure-Ressourcen und ohne Kosten. Sie ist gedacht, um Connectoren gegen echte Datenbanken und gegen HTTP-Mocks (Business Central, NAV, OpenAPI) zu testen, ohne vorher Mappings und Jobs von Hand anzulegen.

Was gestartet wird

flowchart LR
    subgraph db["Datenbanken (wählbar)"]
        mssql[(MSSQL)]
        pg[(PostgreSQL)]
        my[(MySQL)]
        ora[(Oracle)]
    end
    init["&lt;db&gt;-init<br/>Schema + Testdaten"] --> db
    seed["seed<br/>connections, jobs, mappings"] --> azurite[(Azurite<br/>Table Storage)]
    azurite -- "Konfiguration" --> bridge["databridge<br/>Function App :7062"]
    db -- "SQL-Connectoren" --> bridge
    wiremock["wiremock :8089 (wählbar)<br/>BC / NAV / OpenAPI"] -- "HTTP-Connectoren" --> bridge
    subgraph sb["Service Bus (wählbar: servicebus/getchanges/process)"]
        sbdb[(servicebus-db)] --> emulator["servicebus<br/>Emulator :5672 / :5300"]
        emulator --> explorer["servicebus-explorer<br/>Web-UI :8090 (wählbar)"]
    end
    bridge -- "GetChanges legt Nachrichten ab" --> emulator
    emulator -- "Process*QueueItems liest" --> bridge

Die Pfeile zeigen, wer wen versorgt: Init und Seed befüllen Datenbanken und Azurite. Die DataBridge liest ihre Konfiguration aus Azurite und greift über ihre Connectoren auf die Datenbanken und auf WireMock zu. Ist das Profil servicebus aktiv, legt GetChanges erkannte Änderungen auf die Queues des Emulators, Process*QueueItems liest sie von dort. Die Connectoren lesen und schreiben.

ContainerAufgabeLäuft
mssql, postgres, mysql, oracleDatenbankserver, nur die gewähltendauerhaft
<db>-initLegt je Datenbank Schema und Testdaten aneinmalig pro Start
azuriteEmulator für Azure Table/Blob/Queue Storagedauerhaft
wiremockHTTP-Mock für Business Central, NAV und eine OpenAPI-Beispiel-API, nur die gewählten (siehe HTTP-Mocks)dauerhaft, nur wenn mindestens ein Mock gewählt ist
servicebusService-Bus-Emulator, nur wenn servicebus/getchanges/process gewählt sinddauerhaft, nur wenn gewählt
servicebus-dbEigener SQL Server als Speicher des Emulators (nicht die Dev-Datenbank mssql)dauerhaft, nur wenn servicebus gewählt ist
servicebus-explorerGrafische Web-Oberfläche für den Emulator (Service Bus einsehen)dauerhaft, nur wenn servicebus gewählt ist
seedSchreibt connections, jobs und mappings nach Azuriteeinmalig pro Start
databridgeDie DataBridge selbst, gebaut aus dem aktuellen Quellcode (src/Dockerfile). Fehlt im Host-Modus (--host)dauerhaft, außer im Host-Modus

Die DataBridge startet erst, wenn alle Init-Container und der Seed erfolgreich durchgelaufen sind. Timer- und Queue-Trigger (GetChanges, Process*QueueItems) sind standardmäßig deaktiviert, lassen sich aber gezielt zuschalten (siehe Verarbeitung schalten). HTTP-Funktionen und der MCP-Server laufen immer normal.

Voraussetzungen

  • Podman 5 oder neuer.
  • Compose-Provider für podman compose: docker-compose 2.20 oder neuer (empfohlen) bzw. ein aktuelles podman-compose. Benötigt werden depends_on mit condition und required: false.
  • Windows/macOS: eine eingerichtete podman machine. up.ps1 und down.ps1 -Reset starten sie bei Bedarf selbst. Die PowerShell-Skripte laufen unter PowerShell 7 und Windows PowerShell 5.1.
  • Arbeitsspeicher: abhängig von der Auswahl, siehe Speicherbedarf.
  • Freie Ports: 7062 (DataBridge — im Host-Modus stattdessen vom lokalen Prozess belegt), 10000–10002 (Azurite) sowie die Ports der gewählten Datenbanken (siehe Datenbanken). Mit servicebus/getchanges/process zusätzlich 5672 (AMQP) und 5300 (Management) für den Service-Bus-Emulator — diese beiden Ports sind fest und nicht über dev/.env änderbar — sowie SERVICEBUS_EXPLORER_PORT (Standard 8090) für dessen Web-Oberfläche.

Speicherbedarf

Gemessene Werte im laufenden Betrieb (podman stats, gerundet):

ContainerRAMLäuft
databridge + azurite~0,3 GBimmer
postgres~0,1 GBwenn gewählt
mysql~0,5 GBwenn gewählt
mssql~1,5 GBwenn gewählt
oracle~2,5 GBwenn gewählt
wiremock (alle Mocks in einem Container)~0,2 GBwenn ein Mock gewählt ist
servicebus + servicebus-db~0,5 GB + ~1,5 GBwenn servicebus/getchanges/process gewählt sind (siehe Service Bus)
servicebus-explorer~0,2 GB (ungefähr, .NET-Web-App)wenn servicebus/getchanges/process gewählt sind

Die Init-Container und seed beenden sich nach dem Start und belegen danach keinen Speicher.

up all (ohne Service Bus) kommt damit auf rund 5 GB, mit getchanges/process/servicebus zusätzlich rund 2 GB für den Emulator und seine SQL-DB — macOS/Windows-Leser bitte beachten: servicebus-db läuft zusätzlich zu mssql, wenn beide gewählt sind (getrennte Container, siehe Service Bus). Unter Windows und macOS laufen alle Container in der VM der podman machine, die zusätzlich Kernel und Dateicache belegt und Speicher nur zögerlich an das Host-System zurückgibt. Der Prozess vmmem bzw. die VM wächst daher deutlich darüber hinaus und kann den Rechner spürbar ausbremsen. Unter Linux laufen die Container ohne VM und der Stack merklich flüssiger.

Im Normalfall nur das starten, was man gerade testet, also die Datenbank bzw. den Mock des Connectors, an dem man arbeitet (z. B. dev\up.ps1 postgres oder dev\up.ps1 mssql bc). Die Auswahl lässt sich dauerhaft über DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev/.env festlegen. all ist für den gelegentlichen Gesamttest gedacht und braucht einen Rechner mit 16 GB oder mehr.

Unter Windows ist der größte Posten meist nicht der Container selbst, sondern der Dateicache der VM (Images, Datenbankdateien, Volumes). Linux zählt ihn als verfügbar, Windows sieht ihn aber als belegt, weil WSL ihn nicht von selbst zurückgibt. vmmemWSL kann so auf 13 GB und mehr wachsen, obwohl die Container nur rund 5 GB brauchen. Prüfen lässt sich das mit podman machine ssh free -h (Spalte buff/cache).

Cache sofort freigeben (laufende Container bleiben unberührt):

podman machine ssh "sync; echo 3 | sudo tee /proc/sys/vm/drop_caches"

Dauerhaft in %USERPROFILE%\.wslconfig die VM begrenzen und ungenutzten Cache automatisch an Windows zurückgeben lassen (danach wsl --shutdown):

[wsl2]
memory=8GB

[experimental]
autoMemoryReclaim=gradual

Beim ersten Start lädt Podman die Images (mehrere GB, vor allem Oracle und MSSQL) und baut die DataBridge. Das dauert einige Minuten; folgende Starts sind deutlich schneller.

Starten und beenden

Alle Befehle werden aus dem Repository-Stammverzeichnis aufgerufen.

dev\up.ps1                          # Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev\.env
dev\up.ps1 mssql oracle              # nur die genannten Datenbanken, keine Mocks
dev\up.ps1 mssql bc openapi          # Datenbanken und Mocks gemischt
dev\up.ps1 bc                        # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev\.env
dev\up.ps1 all                       # alle vier Datenbanken und alle drei Mocks
dev\up.ps1 mssql process             # Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)
dev\up.ps1 mssql getchanges          # Datenbank + Timer-Trigger GetChanges an
dev\up.ps1 servicebus                # nur den Emulator dazu, ohne Verarbeitung
dev\up.ps1 -HostMode mssql process   # Host-Modus: kein databridge-Container, Debugger auf Port 7062

dev\down.ps1                         # alles stoppen, Daten bleiben erhalten (VM aus: nichts zu tun)
dev\down.ps1 -Reset                  # alles stoppen und alle Daten löschen (startet die VM bei Bedarf)
dev/up.sh                    # Auswahl aus DATABRIDGE_DBS / DATABRIDGE_MOCKS in dev/.env
dev/up.sh mssql oracle       # nur die genannten Datenbanken, keine Mocks
dev/up.sh mssql bc openapi   # Datenbanken und Mocks gemischt
dev/up.sh bc                 # nur Mocks – Datenbanken dann aus DATABRIDGE_DBS in dev/.env
dev/up.sh all                # alle vier Datenbanken und alle drei Mocks
dev/up.sh mssql process      # Datenbank + Verarbeitung an (Service-Bus-Emulator automatisch mit)
dev/up.sh mssql getchanges   # Datenbank + Timer-Trigger GetChanges an
dev/up.sh servicebus         # nur den Emulator dazu, ohne Verarbeitung
dev/up.sh --host mssql process  # Host-Modus: kein databridge-Container, Debugger auf Port 7062

dev/down.sh                  # alles stoppen, Daten bleiben erhalten
dev/down.sh -v               # alles stoppen und alle Daten löschen (kompletter Reset)

Auswahlregeln:

  • Argumente gehen vor dev/.env. Datenbanken (mssql, postgres, mysql, oracle), Mocks (bc, nav, openapi) und Toggles (getchanges, process, servicebus) dürfen in beliebiger Reihenfolge gemischt werden.
  • Wer Argumente angibt, bekommt nur die genannten Mocks bzw. Toggles. dev/up.sh mssql startet also ohne WireMock und ohne Verarbeitung, auch wenn DATABRIDGE_MOCKS/ DATABRIDGE_GETCHANGES/DATABRIDGE_PROCESS in dev/.env gesetzt sind.
  • Mindestens eine Datenbank läuft immer mit, weil Seed und Jobs eine brauchen. Nennt man nur Mocks oder Toggles, kommen die Datenbanken aus DATABRIDGE_DBS.
  • getchanges oder process aktivieren automatisch das Profil servicebus. servicebus allein ist erlaubt, um die Queues z. B. per MCP zu inspizieren, ohne dass etwas verarbeitet wird.
  • Unbekannte Namen brechen mit einer Fehlermeldung ab.

Was up im Einzelnen tut

  1. Prüft, ob podman und podman compose verfügbar sind (unter Windows/macOS auch die podman machine). Unter Windows räumt up.ps1 vor dem Start der podman machine eine liegengebliebene Socket-Datei weg (siehe Fehlersuche) und wartet, bis podman compose die VM erreicht.
  2. Legt dev/.env aus dev/.env.example an, falls sie fehlt.
  3. Stoppt Datenbank-Container, die nicht gewählt sind, WireMock, wenn kein Mock gewählt ist, den Service-Bus-Emulator, wenn weder servicebus noch getchanges/process gewählt sind, und im Host-Modus (--host) den databridge-Container. Daten-Volumes bleiben erhalten.
  4. Baut die DataBridge aus dem aktuellen Quellcode und startet alle gewählten Container.
  5. Läuft der Service-Bus-Emulator mit, wartet up zusätzlich, bis dessen Management-API antwortet (kein Docker-Healthcheck möglich, siehe Service Bus).
  6. Ohne Host-Modus: startet die DataBridge nach dem Seed neu, weil sie ihre Konfiguration nur beim Start liest. Im Host-Modus: generiert stattdessen src/DataBridge/local.settings.json (siehe Debuggen) und beendet sich, ohne auf einen Container zu warten.
  7. Ohne Host-Modus: wartet, bis GetInfo antwortet, und gibt MCP-Endpoint und Zugangsdaten aus.

up kann beliebig oft aufgerufen werden. Nach einer Code-Änderung genügt ein erneutes up: Das Image wird neu gebaut, unveränderte Schichten kommen aus dem Cache.

Konfiguration (dev/.env)

VariableStandardBedeutung
DATABRIDGE_DBSmssqlDatenbanken, wenn up ohne Argument aufgerufen wird (kommagetrennt)
DEV_DB_PASSWORDDataBridge_Dev_2026Ein Passwort für alle Datenbanken (SA, App-User, Root)
DATABRIDGE_MOCKSleerMocks, wenn up ohne Argument aufgerufen wird (kommagetrennt: bc,nav,openapi). Leer heißt: kein WireMock, keine Mock-Connections und -Jobs
DATABRIDGE_GETCHANGESoffan/off — Timer-Trigger GetChanges, wenn up ohne Argument aufgerufen wird (siehe Verarbeitung schalten)
DATABRIDGE_PROCESSoffan/off — die drei Process*QueueItems-Funktionen, wenn up ohne Argument aufgerufen wird
DATABRIDGE_PORT7062Port der DataBridge
WIREMOCK_PORT8089Host-Port von WireMock (9090 ist unter Fedora oft von Cockpit belegt)
SERVICEBUS_EXPLORER_PORT8090Host-Port der Service-Bus-Explorer-Weboberfläche (siehe Service Bus einsehen)
MSSQL_PORT, POSTGRES_PORT, MYSQL_PORT, ORACLE_PORT1433, 5432, 3306, 1521Host-Ports der Datenbanken

Das Passwort enthält bewusst keine Sonderzeichen außer _, damit es in allen Connection-Strings und Kommandozeilen-Clients ohne Quoting funktioniert. Es muss die Komplexitätsregeln von SQL Server erfüllen (mindestens 8 Zeichen, 3 von 4 Zeichenklassen). dev/.env ist nicht eingecheckt.

Datenbanken

ArgumentConnectionImageHost-PortBenutzerDatenbank / Schema
mssqlmssql-dev (MSSQL)mssql/server:2022-latest1433saDataBridgeTest, Schema dbo
postgrespostgres-dev (PGSQL)postgres:175432databridgedatabridgetest, Schema public
mysqlmysql-dev (MYSQL)mysql:8.43306databridgedatabridgetest
oracleoracle-dev (ORACLE)gvenzl/oracle-free:23-slim-faststart1521databridgeService FREEPDB1, Schema DATABRIDGE

Mit den Host-Ports kann man sich auch mit eigenen Werkzeugen verbinden (SSMS, Azure Data Studio, DBeaver, psql, SQL Developer …). Azurite ist im Azure Storage Explorer über UseDevelopmentStorage=true erreichbar — Details siehe Azurite einsehen.

Hinweise je Datenbank:

  • PostgreSQL legt unquotierte Bezeichner in Kleinschreibung an. Tabellen und Spalten heißen dort account, accountid usw. Die DataBridge vergleicht Namen ohne Rücksicht auf Groß-/Kleinschreibung, Account funktioniert also genauso.
  • MySQL unterscheidet unter Linux bei Tabellennamen Groß-/Kleinschreibung. Die Tabellen heißen exakt Account, Opportunity und OpportunityProduct.
  • Oracle legt unquotierte Bezeichner in Großschreibung an (ACCOUNT, ACCOUNTID). Es wird Oracle 23ai Free verwendet, weil erst diese Version BOOLEAN, JSON und CREATE TABLE IF NOT EXISTS kennt.

Beispieldaten

In jeder gewählten Datenbank werden dieselben drei Tabellen mit denselben Daten angelegt. Die Skripte liegen unter dev/<db>/init/:

  • 01_schema.sql legt die Tabellen an, nur wenn sie fehlen.
  • 02_data.sql löscht die Testdaten und fügt sie neu ein, bei jedem Start. Wer in einer Testdatenbank Daten verändert hat, bekommt mit dem nächsten up den Ausgangszustand zurück.

Schemaänderungen an den Skripten wirken erst nach einem Reset (down -v), weil bestehende Tabellen nicht verändert werden.

Tabellen

erDiagram
    Account ||--o{ Opportunity : "AccountId"
    Opportunity ||--o{ OpportunityProduct : "OpportunityId"

Die Spalten decken bewusst möglichst viele Datentypen ab. Jede Datenbank verwendet dafür ihren nativen Typ:

Account — Firmen (3 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
AccountId (PK)uniqueidentifieruuidchar(36)varchar2(36)
AccountNumber (eindeutig)varchar(20)varchar(20)varchar(20)varchar2(20)
Namenvarchar(160)varchar(160)varchar(160)nvarchar2(160)
Descriptionnvarchar(max)texttextnclob
CountryCodechar(2)char(2)char(2)char(2)
NumberOfEmployeesintintegerintnumber(10)
IndustryCodesmallintsmallintsmallintnumber(5)
Ratingtinyintsmallinttinyintnumber(3)
Revenuemoneynumeric(19,4)decimal(19,4)number(19,4)
CreditLimitdecimal(18,2)numeric(18,2)decimal(18,2)number(18,2)
Latitudefloatdouble precisiondoublebinary_double
Longituderealrealfloatbinary_float
IsActivebitbooleanbooleanboolean
FoundedOndatedatedatedate
OpeningTimetime(0)time(0)timeinterval day to second
LastContactAtdatetime2(3)timestamp(3)datetime(3)timestamp(3)
CreatedOnUtcdatetimeoffset(3)timestamptz(3)timestamp(3)timestamp(3) with time zone
Logovarbinary(max)byteablobblob
Attributes (JSON)nvarchar(max)jsonbjsonjson
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

Opportunity — Verkaufschancen (4 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
OpportunityId (PK)uniqueidentifieruuidchar(36)varchar2(36)
AccountId (FK)uniqueidentifieruuidchar(36)varchar2(36)
Namenvarchar(200)varchar(200)varchar(200)nvarchar2(200)
EstimatedValuedecimal(19,4)numeric(19,4)decimal(19,4)number(19,4)
Probabilitytinyintsmallinttinyintnumber(3)
EstimatedCloseDatedatedatedatedate
StatusCodeintintegerintnumber(10)
IsWonbitbooleanbooleanboolean
ActualCloseAtdatetime2(0)timestamp(0)datetimetimestamp(0)
ExternalRefbigintbigintbigintnumber(19)
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

OpportunityProduct — Positionen der Verkaufschancen (6 Datensätze)

SpalteMSSQLPostgreSQLMySQLOracle
OpportunityProductId (PK, Identity)bigint identitybigint identitybigint auto_incrementnumber(19) identity
OpportunityId (FK)uniqueidentifieruuidchar(36)varchar2(36)
LineNumberintintegerintnumber(10)
ProductNamenvarchar(200)varchar(200)varchar(200)nvarchar2(200)
Quantitydecimal(18,4)numeric(18,4)decimal(18,4)number(18,4)
PricePerUnitdecimal(19,4)numeric(19,4)decimal(19,4)number(19,4)
DiscountPercentfloatdouble precisiondoublebinary_double
TaxRaterealrealfloatbinary_float
IsOptionalbitbooleanbooleanboolean
DeliveryDatedatedatedatedate
ModifiedOn (Watermark)datetime2(3)timestamp(3)datetime(3)timestamp(3)

Datensätze

Die Werte sind in allen Datenbanken identisch, einschließlich der Schlüssel. So lassen sich Ergebnisse zwischen den Datenbanken direkt vergleichen.

AccountNummerLandBesonderheit
Müller GmbHACC-1000DEAlle Felder gefüllt, inkl. Logo (Binärdaten) und JSON
Schmidt & Söhne KGACC-1001ATUmlaute und & im Namen, keine Beschreibung
Weber AGACC-1002CHInaktiv, viele leere Felder (NULL)
VerkaufschanceAccountStatusPositionen
Neue FertigungslinieMüller GmbHoffen3 (davon 1 optional)
Wartungsvertrag 2027Müller GmbHgewonnen1
Ersatzteile Q4Schmidt & Söhne KGoffen1
RahmenvertragWeber AGverloren1

Die Schlüssel sind feste GUIDs (a0000000-…-000000000001 für Accounts, b0000000-…-000000000001 für Verkaufschancen) bzw. die Positionsnummern 1–6. Die Identity-Spalte von OpportunityProduct ist so gesetzt, dass neue Zeilen ab 7 weiterzählen.

Connections, Jobs und Mappings

Der Container seed schreibt die Konfiguration nach Azurite — passend zu den gewählten Datenbanken. Die Vorlagen liegen unter dev/seed/:

Azurite einsehen

Anders als der Service-Bus-Emulator bringt Azurite keine eigene Web-Oberfläche mit. Der übliche Weg ist der kostenlose Microsoft Azure Storage Explorer (Windows/macOS/Linux):

  1. Links im Baum unter „Local & Attached“ → „Storage Accounts“ — Azurite ist dort meist automatisch als „(Emulator - Default Ports) (Key)“ gelistet.
  2. Falls nicht: Rechtsklick auf „Storage Accounts“ → „Connect to Azure Storage“ → „Connection string“ und den Connection-String einfügen, den up.sh/up.ps1 beim Start ausgeben (Zeile Azurite) — er verwendet den öffentlich dokumentierten Azurite-Well-known-Key, ist also kein Geheimnis.
  3. Unter „Tables“ liegen connections, jobs, mappings und watermark — Zeilen lassen sich dort auch bearbeiten, z. B. um einen Job per Hand auf IsActive: true zu setzen (siehe Verarbeitung schalten), ganz ohne MCP.

Alternativen: die VS-Code-Erweiterung „Azure Storage“ (derselbe Explorer-Baum direkt in der IDE) oder az storage … --connection-string "…" für Skripte.

DateiInhalt
connections.jsonEine Connection je Datenbank (Name, Typ, Connection-String mit ${DEV_DB_PASSWORD})
tables.jsonDie drei Tabellen mit Schlüssel, Watermark-Feld und den gemappten Feldern
seed.pyErzeugt daraus die Einträge und gleicht die Azure Tables ab

Einträge, die nicht mehr zur Auswahl passen, entfernt der Seed. Wer von all auf mssql wechselt, hat danach also auch nur noch die MSSQL-Connection und ihre Jobs.

Connections

Je gewählter Datenbank eine Connection: mssql-dev, postgres-dev, mysql-dev, oracle-dev (siehe Datenbanken). Je gewähltem Mock kommt bc-dev, nav-dev bzw. openapi-dev hinzu (siehe HTTP-Mocks).

Jobs

Für jede Kombination aus Quell- und Zieldatenbank entsteht ein Job je Tabelle. Die Namen folgen dem Muster <quelle>-to-<ziel>-<tabelle>:

Gewählte DatenbankenKombinationenJobsBeispiel
1 (z. B. mssql)Datenbank auf sich selbst3mssql-to-mssql-account
226mssql-to-postgres-opportunity
3618mysql-to-oracle-opportunityproduct
4 (all)1236oracle-to-mssql-account

Alle Jobs haben diese Einstellungen:

FeldWert
Source / TargetTabellenname, z. B. Account
Mappingdev-<tabelle>, z. B. dev-account
WatermarkType / WatermarkFielddatetime / ModifiedOn
PriorityStandard
IsActivefalse

Mappings

Weil Tabellen- und Spaltennamen in allen Datenbanken gleich sind, gibt es nur ein Mapping je Tabelle, das alle Jobs gemeinsam nutzen. Jedes Feld wird auf das gleichnamige Feld abgebildet:

MappingTabelleFelderSchlüssel (TargetKey)
dev-accountAccount19 (alle Spalten außer ModifiedOn)AccountId (PrimaryKey)
dev-opportunityOpportunity10OpportunityId (PrimaryKey)
dev-opportunityproductOpportunityProduct10OpportunityProductId (PrimaryKey)

ModifiedOn wird nicht gemappt, weil es das Watermark-Feld ist und die Zieldatenbank es selbst setzt.

HTTP-Mocks (WireMock)

Der Container wiremock spielt bis zu drei HTTP-Systeme gleichzeitig. Die DataBridge merkt keinen Unterschied, sie bekommt nur eine andere URL in der Connection. Die Regeln (Stubs) sind so gebaut, dass der jeweilige Connector vollständig durchläuft: Verbindungstest, Metadaten, Lesen mit Paging, Upsert (Lookup → PATCH oder POST) und Delete.

ConnectionTypTabelleAuthMetadaten
bc-devD365BCv2.0/customersOAuth2 Client Credentials gegen /oauth/token (Mock)EDMX wiremock/__files/bc/metadata.xml
nav-devD365NAVCustomersBasic (dev/dev)EDMX wiremock/__files/nav/metadata.xml
openapi-devOPENAPIcustomersAPI-Key-Header X-API-Key: dev-api-keySpec wiremock/__files/openapi/spec.json

Wie ein Container drei Systeme spielt

WireMock ist ein einzelner HTTP-Server mit einer Liste von Regeln (Stubs). Zu jeder Anfrage sucht er die passende Regel und schickt deren Antwort. Welches System gespielt wird, entscheidet allein das Pfad-Präfix der URL aus der Connection:

PräfixSystemConnection-URL (aus Sicht der DataBridge)
/bc/...Business Centralhttp://wiremock:8080/bc/v2.0/sandbox
/nav/...NAVhttp://wiremock:8080/nav/ODataV4/Company('CRONUS')/
/openapi/...OpenAPIhttp://wiremock:8080/openapi
/oauth/tokenToken-Endpoint (gemeinsam)tokenUrl von bc-dev

Innerhalb des Compose-Netzes heißt der Container wiremock (Port 8080). Vom eigenen Rechner aus erreicht man ihn unter localhost:8089.

Nur die gewählten Mocks werden geladen. Die Regeln liegen je System in einem eigenen Ordner:

dev/wiremock/
├── mappings/            Regeln
│   ├── common/          immer geladen (OAuth-Token)
│   ├── bc/  nav/  openapi/
└── __files/             Antwortdateien, auf die Regeln per bodyFileName verweisen
    ├── bc/metadata.xml  nav/metadata.xml  openapi/spec.json

Der Ordner ist schreibgeschützt nach /mocks gemountet. Beim Start kopiert der Container common und die Ordner der Mocks aus DATABRIDGE_MOCKS in sein Arbeitsverzeichnis und startet erst dann WireMock. Ein nicht gewählter Mock antwortet deshalb mit 404. Der Seed legt Connections, Jobs und Mappings nur für die gewählten Mocks an und entfernt die der anderen. Ändert sich die Auswahl, baut up den Container neu auf.

Aufbau einer Regel: request beschreibt, worauf sie passt, response, was sie antwortet.

{
  "name": "BC customer by key - existing record (update path)",
  "priority": 1,
  "request":  { "method": "GET", "urlPath": "/bc/.../customers(a0000000-0000-0000-0000-000000000001)" },
  "response": { "status": 200, "jsonBody": { "id": "a0000000-…", "number": "ACC-1000" } }
}
  • Treffer: über urlPath (exakt), urlPathPattern (Regex), queryParameters (z. B. $skiptoken fehlt → Seite 1) und headers (z. B. If-Match: *, X-API-Key).
  • Priorität: Passen mehrere Regeln, gewinnt die kleinste priority. So liefert der bekannte Kunde einen Datensatz, alle anderen Schlüssel fallen auf die allgemeine 404-Regel zurück. Daraus ergibt sich „vorhanden → PATCH, unbekannt → POST“, ohne dass WireMock sich etwas merkt.
  • Templates ("transformers": ["response-template"]): {{{request.body}}} gibt den Body zurück (POST, PATCH). {{request.baseUrl}} baut den absoluten Link auf die nächste Seite, passend zur aufgerufenen Adresse (wiremock:8080 oder localhost:8089).

Datenbestand, Jobs und Mappings

Jeder Mock bildet alle drei Beispieltabellen ab — Account (Kunden, acht Felder: Guid, String, Decimal, Int32, Boolean, Date, Zeitstempel), Opportunity und OpportunityProduct (jeweils eigene Entität, z. B. bei BC opportunities/opportunityLines). Paging ist bewusst aktiv (maxPageSize: 2, zwei Seiten mit drei Datensätzen): BC/NAV über @odata.nextLink, OpenAPI über meta.next_page.

Fester Datenbestand für Upsert-Tests: Zu jeder Tabelle existiert ein bekannter Datensatz mit fester ID (z. B. AccountId a0000000-…-0001 bzw. AccountNumber ACC-1000 für Account). Ein Lookup liefert ihn, danach folgt ein PATCH. Jeder andere Schlüssel liefert 404, danach folgt ein POST. POST und PATCH geben den Request-Body unverändert zurück. Das reicht für Response-Jobs.

Ausnahme OpportunityProduct: OpportunityProductId ist in allen Datenbanken eine Ganzzahl (Identity), BC (id), NAV (SystemId) und OpenAPI (id) erwarten dort aber eine Guid. Das Feld wird deshalb auf ein zusätzliches, nur im Mock vorhandenes Textfeld gemappt (BC externalLineNo, NAV External_Line_No, OpenAPI external_line_no). Die Jobs nach BC und OpenAPI haben keinen Ziel-Schlüssel und legen jede Position per POST neu an, der NAV-Job sucht weiterhin über Line_No.

Jobs und Mappings: Je gewählter Datenbank, Mock und Tabelle (sofern der Mock sie kennt) entstehen zwei inaktive Jobs:

JobMappingRichtung
<db>-to-<mock>-<tabelle>dev-<tabelle>-to-<mock>SQL-Tabelle → Mock-Entität (Schreibpfad des Connectors)
<mock>-to-<db>-<tabelle>dev-<mock>-to-<tabelle>Mock-Entität → SQL-Tabelle (Lesepfad, mit Watermark-Filter)

Für Account lauten die Jobnamen unverändert <db>-to-<mock>-account (Kompatibilität zu bestehenden Notizen/Screenshots). Die Feldzuordnung je Tabelle steht in dev/seed/mocks.json unter entities.<Account|Opportunity|OpportunityProduct>.fields, z. B. für Account.Name → displayName (BC), Name (NAV), name (OpenAPI).

Was hat die DataBridge wirklich geschickt? Das Anfragen-Protokoll von WireMock zeigt jeden Request samt Body:

(Invoke-RestMethod localhost:8089/__admin/requests).requests |
  Select-Object @{n='m';e={$_.request.method}}, @{n='url';e={$_.request.url}}, @{n='body';e={$_.request.body}}, @{n='status';e={$_.response.status}}
Invoke-RestMethod localhost:8089/__admin/requests/unmatched   # Requests, für die es keinen Stub gibt
Invoke-RestMethod -Method Delete localhost:8089/__admin/requests   # Protokoll leeren
curl -s localhost:8089/__admin/requests | jq '.requests[] | {m: .request.method, url: .request.url, body: .request.body, status: .response.status}'
curl -s localhost:8089/__admin/requests/unmatched     # Requests, für die es keinen Stub gibt
curl -s -X DELETE localhost:8089/__admin/requests     # Protokoll leeren

Damit lässt sich ein Mapping inhaltlich prüfen: Man vergleicht den PATCH- oder POST-Body mit den Beispieldaten aus 02_data.sql.

Eigene Stubs: JSON-Dateien unter dev/wiremock/mappings/<system>/ ablegen und WireMock neu starten (podman compose -f dev/compose.dev.yml restart wiremock). Beim Neustart werden die Dateien neu kopiert. Worauf die Connectoren bestehen, steht als Kommentar im name der Stubs. Die wichtigsten Punkte:

  • BC/NAV: POST muss genau 201 liefern, PATCH genau 200 (nicht 204).
  • BC/NAV: Jede Antwort braucht einen JSON-Body, auch ein 404.
  • nextLink muss absolut sein.
  • Jeder gelieferte Datensatz braucht das Watermark-Feld als Zeitstempel.

Typische Abläufe

Nach einer Code-Änderung testen: erneut dev/up.sh (bzw. mit denselben Datenbanken wie zuvor). Das Image wird neu gebaut, die DataBridge neu gestartet.

Seed-Vorlagen geändert (dev/seed/*.json): erneut dev/up.sh. Der Seed läuft bei jedem Start und gleicht die Tabellen ab.

Schema-Skripte geändert (dev/<db>/init/01_schema.sql): dev/down.sh -v, dann dev/up.sh.

Mock-Regeln geändert (dev/wiremock/**): podman compose -f dev/compose.dev.yml restart wiremock. Ein neues up ist nicht nötig, weil die DataBridge die Mocks erst bei Bedarf anfragt. Ausnahme: Metadaten und die OpenAPI-Spec cacht sie. Nach einer Änderung an __files deshalb auch databridge neu starten.

Einen Connector gegen den Mock prüfen:

dev\up.ps1 mssql bc
Invoke-RestMethod -Method Delete localhost:8089/__admin/requests   # Protokoll leeren
# ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...
(Invoke-RestMethod localhost:8089/__admin/requests).requests |
  Select-Object @{n='m';e={$_.request.method}}, @{n='url';e={$_.request.url}}, @{n='status';e={$_.response.status}}
Invoke-RestMethod localhost:8089/__admin/requests/unmatched   # Anfragen ohne passende Regel
dev/up.sh mssql bc
curl -s -X DELETE localhost:8089/__admin/requests          # Protokoll leeren
# ... Aktion auslösen, z. B. per MCP get_connector_table_schema auf bc-dev ...
curl -s localhost:8089/__admin/requests | jq '.requests[] | {m: .request.method, url: .request.url, status: .response.status}'
curl -s localhost:8089/__admin/requests/unmatched         # Anfragen ohne passende Regel

Logging

Die Testumgebung setzt kein APPLICATIONINSIGHTS_CONNECTION_STRING — es gibt also keine Application Insights, an die etwas gesendet werden könnte. Das gepufferte Sampling aus Betrieb → Logging (Flush-Regeln, TELEMETRY_INFO_SAMPLE_RATE, TELEMETRY_CONTROL) betrifft ausschließlich diesen Versand und greift hier nicht: alles, was die DataBridge loggt, landet unverändert und sofort im Container-Log (stdout/stderr).

src/DataBridge/host.json setzt für die Kategorien DataBridge und Function den Log-Level Trace — lokal ist also die ausführlichste Stufe aktiv, inklusive der Einträge, die produktiv nie über die Konsole hinauskommen (siehe „Trace-Level erreicht Application Insights nie“).

podman compose -f dev/compose.dev.yml logs -f databridge          # laufend mitlesen
podman compose -f dev/compose.dev.yml logs seed mssql-init        # abgeschlossene Init-Container
podman compose -f dev/compose.dev.yml logs -f databridge | Select-String "mssql-to-postgres-account"
podman compose -f dev/compose.dev.yml logs -f databridge          # laufend mitlesen
podman compose -f dev/compose.dev.yml logs seed mssql-init        # abgeschlossene Init-Container
podman compose -f dev/compose.dev.yml logs -f databridge | grep "mssql-to-postgres-account"

Ohne -f gibt der Befehl das bisherige Log aus und beendet sich; mit -f bleibt er offen und zeigt neue Zeilen laufend an (Strg+C zum Beenden). Weil jede Funktionsausführung ihren Job- bzw. Verbindungsnamen loggt, lässt sich damit gezielt nach einem Job filtern, ohne operation_Id oder Application Insights zu brauchen.

Beim Debuggen im Host-Modus läuft die DataBridge nicht im Container — dort erscheinen dieselben Log-Zeilen stattdessen direkt in der IDE (Rider-Run-Fenster, Visual-Studio-Ausgabefenster bzw. das Terminal von func start), siehe Debuggen → Gegen Podman.

Service Bus

Profil servicebus startet einen Azure Service Bus Emulator mit eigenem SQL Server als Speicher (servicebus-db, nicht die Dev-Datenbank mssql). Damit laufen Jobs erstmals lokal von Anfang bis Ende: GetChanges erkennt Änderungen, legt sie auf die passende Queue, Process*QueueItems verarbeitet sie.

ContainerPortsZweck
servicebus5672 (AMQP), 5300 (Management/Health)Der Emulator selbst
servicebus-dbinternSQL Server als Speicher des Emulators
servicebus-explorerSERVICEBUS_EXPLORER_PORT (Standard 8090)Grafische Web-Oberfläche, siehe Service Bus einsehen

Die sieben Queues (sbqexport, sbqhighexport, sbqlowexport, sbqignored, sbqtransitivehigh, sbqtransitivestandart, sbqtransitivelow) sind in dev/servicebus/config.json mit denselben Eigenschaften wie in Azure vorkonfiguriert (LockDuration PT30S, DeadLetteringOnMessageExpiration true, MaxDeliveryCount 3 bzw. 6 für die transitiven Queues) — mit einer Ausnahme: DefaultMessageTimeToLive steht auf PT1H (1 Stunde) statt der in Azure üblichen P14D, weil der Emulator maximal 1 Stunde unterstützt und mit einem längeren Wert gar nicht erst startet.

Aktivieren: dev/up.sh servicebus (nur der Emulator) oder automatisch über getchanges/process (siehe Verarbeitung schalten). Die DataBridge bekommt dann den Connection-String Endpoint=sb://servicebus;...;UseDevelopmentEmulator=true; statt des Dummy-Strings. Mit dem Profil läuft automatisch auch servicebus-explorer mit — eine grafische Web-Oberfläche für die Queues, siehe Service Bus einsehen.

Grenzen des Emulators: kein AMQP over WebSockets, keine Entra-ID-Authentifizierung. Beides nutzt die DataBridge lokal nicht.

Verarbeitung schalten

Standardmäßig sind GetChanges (Timer) und die drei Process*QueueItems-Funktionen deaktiviert — die Umgebung startet, ohne dass etwas verarbeitet wird. Zwei unabhängige Schalter:

Variable (dev/.env)ArgumentWirkung
DATABRIDGE_GETCHANGESgetchangesAzureWebJobs.GetChanges.Disabled
DATABRIDGE_PROCESSprocessAzureWebJobs.Process{Standard,High,Low}PriorityQueueItems.Disabled

Werte an/off. Beide aktivieren automatisch das Profil servicebus (siehe oben). Die Durable-Functions-Kette (StartGetChangesOrchestration, GetChangesOrchestrator, GetChangesActivity, JobState) bleibt in der Testumgebung immer deaktiviert.

Einen Job manuell ausführen (ExecuteJob)

Der schnellste Weg, einen Job Ende-zu-Ende zu testen, ohne auf den Timer zu warten: die HTTP-Function ExecuteJob löst für einen Job genau das aus, was GetChanges sonst für alle aktiven Jobs im Timer-Takt tut — Änderungen erkennen und auf die passende Service-Bus-Queue legen. Ist process an, greift Process*QueueItems (läuft dauerhaft im Hintergrund) die Nachricht Sekunden später automatisch ab und schreibt ins Ziel.

  1. dev/up.sh mssql process (Windows: dev\up.ps1 mssql process) — Datenbank, Verarbeitung und Service-Bus-Emulator starten. getchanges wird dafür nicht gebraucht.
  2. Job-Namen ermitteln — Muster <quelle>-to-<ziel>-<tabelle> (siehe Jobs), z. B. mssql-to-mssql-account bei nur einer Datenbank, oder per MCP list_jobs nachsehen.
  3. Auslösen:
Invoke-RestMethod -Method Post 'http://localhost:7062/api/ExecuteJob?code=local' `
  -ContentType 'application/json' -Body '{"JobName":"mssql-to-mssql-account"}'
curl -s -X POST 'http://localhost:7062/api/ExecuteJob?code=local' \
  -H 'Content-Type: application/json' \
  -d '{"JobName":"mssql-to-mssql-account"}'

Nachschauen, ob’s geklappt hat:

  • Log (Logging) zeigt beide Schritte durch — GetChanges erkennt X Änderungen, ProcessStandardQueueItems verarbeitet die Nachricht.
  • Bei einem Mock-Job zusätzlich das WireMock-Protokoll (siehe „Was hat die DataBridge wirklich geschickt?“), ob der PATCH/POST ankam.
  • Per MCP list_queue_statuses/get_queue_status, ob die Nachricht auf der Queue schon verarbeitet ist (Anzahl zurück auf 0).

Debuggen

Für Breakpoints und Step-Through läuft die DataBridge lokal als Prozess statt im Container — sowohl gegen diesen Podman-Stack (dev/up.sh --host) als auch gegen eine echte Azure-Instanz. Eigener Artikel: Debuggen.

Fehlersuche

ProblemUrsache und Lösung
up bricht mit Port-Konflikt abEin lokaler Datenbankserver oder Azurite belegt den Port. Port in dev/.env ändern oder den anderen Dienst stoppen.
up meldet „Timeout“Das Skript zeigt die letzten Logzeilen aller Container. Meist ist ein Init-Container an einem SQL-Fehler gescheitert.
Init-Container kann /init nicht lesen (Fedora/RHEL)SELinux. Die Mounts in compose.dev.yml tragen dafür das Flag :z — bei eigenen Mounts ergänzen.
Skripte scheitern unter Windows mit merkwürdigen FehlernZeilenenden. .gitattributes erzwingt LF für dev/**/*.sh, *.sql und *.py; bei einem vorhandenen Checkout die Dateien neu auschecken.
PowerShell zeigt bei podman compose rot „Executing external compose provider …“ (NativeCommandError)Nur ein Hinweis von Podman auf stderr, kein echter Fehler. Windows PowerShell 5.1 zeigt ihn trotzdem als Fehler an. up.ps1/down.ps1 schalten ihn selbst ab und kapseln Aufrufe, bei denen podman compose seinen Fortschritt auf stderr schreibt (z. B. „Restarting“). Bei direkten Aufrufen vorher $env:PODMAN_COMPOSE_WARNING_LOGS = 'false' setzen oder dauerhaft in %APPDATA%\containers\containers.conf unter [engine] compose_warning_logs = false eintragen.
Windows: podman machine start meldet win-sshproxy.exe failed to start, danach scheitert podman compose mit „failed to connect to the docker API at npipe:////./pipe/podman-machine-default“Ohne win-sshproxy gibt es keine Docker-API-Pipe, über die docker-compose spricht. Ursache ist eine liegengebliebene Socket-Datei %TEMP%\podman\podman-machine-default-api.sock (im Windows-Ereignisprotokoll, Quelle „.NET Runtime“: bind: Only one usage of each socket address). Sie bleibt bei jedem podman machine stop liegen und lässt sich als Unix-Socket weder per Remove-Item noch per del löschen. up.ps1 und down.ps1 benennen den Ordner deshalb vor dem VM-Start in %TEMP%\podman.stale-<Zeitstempel> um; Podman legt ihn neu an. Von Hand: VM stoppen, Ordner umbenennen, VM starten. Die podman.stale-*-Ordner können jederzeit gelöscht werden.
Windows PowerShell 5.1 zeigt Umlaute verstümmelt („für“)PowerShell 5.1 liest Skripte ohne BOM als ANSI. up.ps1 und down.ps1 sind deshalb als UTF-8 mit BOM gespeichert — beim Bearbeiten so lassen. PowerShell 7 ist davon nicht betroffen.
401 beim Aufruf der APIDer Function Key fehlt. Er ist fest auf local gesetzt (?code=local).
Oracle braucht langeDer erste Start legt die Datenbank an. Der Init-Container wartet bis zu 2,5 Minuten auf den App-User.
Geänderte Daten sind nach up wegGewollt: Die Testdaten werden bei jedem Start zurückgesetzt.
WireMock startet nicht, Port-Konflikt auf 8089WIREMOCK_PORT in dev/.env ändern. 9090 ist unter Fedora oft von Cockpit belegt.
Mock antwortet mit 404 auf allesDer Mock ist nicht gewählt (dev/up.sh … bc), oder die Anfrage passt auf keine Regel. /__admin/requests/unmatched zeigt, welche Anfragen keinen Treffer hatten, mit einem Vergleich zur nächstliegenden Regel.
CheckList meldet für openapi-dev „could not find table customers“Bekannter Fehler im OpenAPI-Connector: GetTableNames liefert Spec-Pfade (/v1/customers) statt Ressourcennamen. Das liegt nicht am Mock.
up hängt bei „Warte auf Service-Bus-Emulator“Der Emulator startet erst, wenn servicebus-db (SQL Server als Speicher) healthy ist — das dauert beim ersten Start etwas. Bei echtem Timeout: podman compose -f dev/compose.dev.yml logs servicebus servicebus-db prüfen, häufig ein Fehler in dev/servicebus/config.json.
MCP-Tools list_queue_statuses/get_queue_status liefern einen VerbindungsfehlerProfil servicebus läuft nicht (kein servicebus/getchanges/process beim letzten up-Aufruf angegeben), ExportQueueServiceBus zeigt dann auf den Dummy-Connection-String.