Podman-Testumgebung
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.
Die Umgebung nutzt feste Passwörter und einen festen Function Key (local). Sie ist nicht für
den Betrieb gedacht und darf nicht aus dem Internet erreichbar sein.
Was gestartet wird
flowchart LR
subgraph db["Datenbanken (wählbar)"]
mssql[(MSSQL)]
pg[(PostgreSQL)]
my[(MySQL)]
ora[(Oracle)]
end
init["<db>-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" --> bridgeDie 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.
| Container | Aufgabe | Läuft |
|---|---|---|
mssql, postgres, mysql, oracle | Datenbankserver, nur die gewählten | dauerhaft |
<db>-init | Legt je Datenbank Schema und Testdaten an | einmalig pro Start |
azurite | Emulator für Azure Table/Blob/Queue Storage | dauerhaft |
wiremock | HTTP-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 |
servicebus | Service-Bus-Emulator, nur wenn servicebus/getchanges/process gewählt sind | dauerhaft, nur wenn gewählt |
servicebus-db | Eigener SQL Server als Speicher des Emulators (nicht die Dev-Datenbank mssql) | dauerhaft, nur wenn servicebus gewählt ist |
servicebus-explorer | Grafische Web-Oberfläche für den Emulator (Service Bus einsehen) | dauerhaft, nur wenn servicebus gewählt ist |
seed | Schreibt connections, jobs und mappings nach Azurite | einmalig pro Start |
databridge | Die 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-compose2.20 oder neuer (empfohlen) bzw. ein aktuellespodman-compose. Benötigt werdendepends_onmitconditionundrequired: false. - Windows/macOS: eine eingerichtete
podman machine.up.ps1unddown.ps1 -Resetstarten 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/processzusätzlich 5672 (AMQP) und 5300 (Management) für den Service-Bus-Emulator — diese beiden Ports sind fest und nicht überdev/.envänderbar — sowieSERVICEBUS_EXPLORER_PORT(Standard8090) für dessen Web-Oberfläche.
Speicherbedarf
Gemessene Werte im laufenden Betrieb (podman stats, gerundet):
| Container | RAM | Läuft |
|---|---|---|
databridge + azurite | ~0,3 GB | immer |
postgres | ~0,1 GB | wenn gewählt |
mysql | ~0,5 GB | wenn gewählt |
mssql | ~1,5 GB | wenn gewählt |
oracle | ~2,5 GB | wenn gewählt |
wiremock (alle Mocks in einem Container) | ~0,2 GB | wenn ein Mock gewählt ist |
servicebus + servicebus-db | ~0,5 GB + ~1,5 GB | wenn 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 mssqlstartet also ohne WireMock und ohne Verarbeitung, auch wennDATABRIDGE_MOCKS/DATABRIDGE_GETCHANGES/DATABRIDGE_PROCESSindev/.envgesetzt 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. getchangesoderprocessaktivieren automatisch das Profilservicebus.servicebusallein 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
- Prüft, ob
podmanundpodman composeverfügbar sind (unter Windows/macOS auch diepodman machine). Unter Windows räumtup.ps1vor dem Start derpodman machineeine liegengebliebene Socket-Datei weg (siehe Fehlersuche) und wartet, bispodman composedie VM erreicht. - Legt
dev/.envausdev/.env.examplean, falls sie fehlt. - Stoppt Datenbank-Container, die nicht gewählt sind, WireMock, wenn kein Mock gewählt ist,
den Service-Bus-Emulator, wenn weder
servicebusnochgetchanges/processgewählt sind, und im Host-Modus (--host) dendatabridge-Container. Daten-Volumes bleiben erhalten. - Baut die DataBridge aus dem aktuellen Quellcode und startet alle gewählten Container.
- Läuft der Service-Bus-Emulator mit, wartet
upzusätzlich, bis dessen Management-API antwortet (kein Docker-Healthcheck möglich, siehe Service Bus). - 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. - Ohne Host-Modus: wartet, bis
GetInfoantwortet, 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)
| Variable | Standard | Bedeutung |
|---|---|---|
DATABRIDGE_DBS | mssql | Datenbanken, wenn up ohne Argument aufgerufen wird (kommagetrennt) |
DEV_DB_PASSWORD | DataBridge_Dev_2026 | Ein Passwort für alle Datenbanken (SA, App-User, Root) |
DATABRIDGE_MOCKS | leer | Mocks, wenn up ohne Argument aufgerufen wird (kommagetrennt: bc,nav,openapi). Leer heißt: kein WireMock, keine Mock-Connections und -Jobs |
DATABRIDGE_GETCHANGES | off | an/off — Timer-Trigger GetChanges, wenn up ohne Argument aufgerufen wird (siehe Verarbeitung schalten) |
DATABRIDGE_PROCESS | off | an/off — die drei Process*QueueItems-Funktionen, wenn up ohne Argument aufgerufen wird |
DATABRIDGE_PORT | 7062 | Port der DataBridge |
WIREMOCK_PORT | 8089 | Host-Port von WireMock (9090 ist unter Fedora oft von Cockpit belegt) |
SERVICEBUS_EXPLORER_PORT | 8090 | Host-Port der Service-Bus-Explorer-Weboberfläche (siehe Service Bus einsehen) |
MSSQL_PORT, POSTGRES_PORT, MYSQL_PORT, ORACLE_PORT | 1433, 5432, 3306, 1521 | Host-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
| Argument | Connection | Image | Host-Port | Benutzer | Datenbank / Schema |
|---|---|---|---|---|---|
mssql | mssql-dev (MSSQL) | mssql/server:2022-latest | 1433 | sa | DataBridgeTest, Schema dbo |
postgres | postgres-dev (PGSQL) | postgres:17 | 5432 | databridge | databridgetest, Schema public |
mysql | mysql-dev (MYSQL) | mysql:8.4 | 3306 | databridge | databridgetest |
oracle | oracle-dev (ORACLE) | gvenzl/oracle-free:23-slim-faststart | 1521 | databridge | Service 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,accountidusw. Die DataBridge vergleicht Namen ohne Rücksicht auf Groß-/Kleinschreibung,Accountfunktioniert also genauso. - MySQL unterscheidet unter Linux bei Tabellennamen Groß-/Kleinschreibung. Die Tabellen heißen
exakt
Account,OpportunityundOpportunityProduct. - Oracle legt unquotierte Bezeichner in Großschreibung an (
ACCOUNT,ACCOUNTID). Es wird Oracle 23ai Free verwendet, weil erst diese VersionBOOLEAN,JSONundCREATE TABLE IF NOT EXISTSkennt.
Beispieldaten
In jeder gewählten Datenbank werden dieselben drei Tabellen mit denselben Daten angelegt. Die
Skripte liegen unter dev/<db>/init/:
01_schema.sqllegt die Tabellen an, nur wenn sie fehlen.02_data.sqllöscht die Testdaten und fügt sie neu ein, bei jedem Start. Wer in einer Testdatenbank Daten verändert hat, bekommt mit dem nächstenupden 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)
| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
AccountId (PK) | uniqueidentifier | uuid | char(36) | varchar2(36) |
AccountNumber (eindeutig) | varchar(20) | varchar(20) | varchar(20) | varchar2(20) |
Name | nvarchar(160) | varchar(160) | varchar(160) | nvarchar2(160) |
Description | nvarchar(max) | text | text | nclob |
CountryCode | char(2) | char(2) | char(2) | char(2) |
NumberOfEmployees | int | integer | int | number(10) |
IndustryCode | smallint | smallint | smallint | number(5) |
Rating | tinyint | smallint | tinyint | number(3) |
Revenue | money | numeric(19,4) | decimal(19,4) | number(19,4) |
CreditLimit | decimal(18,2) | numeric(18,2) | decimal(18,2) | number(18,2) |
Latitude | float | double precision | double | binary_double |
Longitude | real | real | float | binary_float |
IsActive | bit | boolean | boolean | boolean |
FoundedOn | date | date | date | date |
OpeningTime | time(0) | time(0) | time | interval day to second |
LastContactAt | datetime2(3) | timestamp(3) | datetime(3) | timestamp(3) |
CreatedOnUtc | datetimeoffset(3) | timestamptz(3) | timestamp(3) | timestamp(3) with time zone |
Logo | varbinary(max) | bytea | blob | blob |
Attributes (JSON) | nvarchar(max) | jsonb | json | json |
ModifiedOn (Watermark) | datetime2(3) | timestamp(3) | datetime(3) | timestamp(3) |
Opportunity — Verkaufschancen (4 Datensätze)
| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
OpportunityId (PK) | uniqueidentifier | uuid | char(36) | varchar2(36) |
AccountId (FK) | uniqueidentifier | uuid | char(36) | varchar2(36) |
Name | nvarchar(200) | varchar(200) | varchar(200) | nvarchar2(200) |
EstimatedValue | decimal(19,4) | numeric(19,4) | decimal(19,4) | number(19,4) |
Probability | tinyint | smallint | tinyint | number(3) |
EstimatedCloseDate | date | date | date | date |
StatusCode | int | integer | int | number(10) |
IsWon | bit | boolean | boolean | boolean |
ActualCloseAt | datetime2(0) | timestamp(0) | datetime | timestamp(0) |
ExternalRef | bigint | bigint | bigint | number(19) |
ModifiedOn (Watermark) | datetime2(3) | timestamp(3) | datetime(3) | timestamp(3) |
OpportunityProduct — Positionen der Verkaufschancen (6 Datensätze)
| Spalte | MSSQL | PostgreSQL | MySQL | Oracle |
|---|---|---|---|---|
OpportunityProductId (PK, Identity) | bigint identity | bigint identity | bigint auto_increment | number(19) identity |
OpportunityId (FK) | uniqueidentifier | uuid | char(36) | varchar2(36) |
LineNumber | int | integer | int | number(10) |
ProductName | nvarchar(200) | varchar(200) | varchar(200) | nvarchar2(200) |
Quantity | decimal(18,4) | numeric(18,4) | decimal(18,4) | number(18,4) |
PricePerUnit | decimal(19,4) | numeric(19,4) | decimal(19,4) | number(19,4) |
DiscountPercent | float | double precision | double | binary_double |
TaxRate | real | real | float | binary_float |
IsOptional | bit | boolean | boolean | boolean |
DeliveryDate | date | date | date | date |
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.
| Account | Nummer | Land | Besonderheit |
|---|---|---|---|
| Müller GmbH | ACC-1000 | DE | Alle Felder gefüllt, inkl. Logo (Binärdaten) und JSON |
| Schmidt & Söhne KG | ACC-1001 | AT | Umlaute und & im Namen, keine Beschreibung |
| Weber AG | ACC-1002 | CH | Inaktiv, viele leere Felder (NULL) |
| Verkaufschance | Account | Status | Positionen |
|---|---|---|---|
| Neue Fertigungslinie | Müller GmbH | offen | 3 (davon 1 optional) |
| Wartungsvertrag 2027 | Müller GmbH | gewonnen | 1 |
| Ersatzteile Q4 | Schmidt & Söhne KG | offen | 1 |
| Rahmenvertrag | Weber AG | verloren | 1 |
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):
- Links im Baum unter „Local & Attached“ → „Storage Accounts“ — Azurite ist dort meist automatisch als „(Emulator - Default Ports) (Key)“ gelistet.
- Falls nicht: Rechtsklick auf „Storage Accounts“ → „Connect to Azure Storage“ → „Connection
string“ und den Connection-String einfügen, den
up.sh/up.ps1beim Start ausgeben (ZeileAzurite) — er verwendet den öffentlich dokumentierten Azurite-Well-known-Key, ist also kein Geheimnis. - Unter „Tables“ liegen
connections,jobs,mappingsundwatermark— Zeilen lassen sich dort auch bearbeiten, z. B. um einen Job per Hand aufIsActive: truezu 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.
| Datei | Inhalt |
|---|---|
connections.json | Eine Connection je Datenbank (Name, Typ, Connection-String mit ${DEV_DB_PASSWORD}) |
tables.json | Die drei Tabellen mit Schlüssel, Watermark-Feld und den gemappten Feldern |
seed.py | Erzeugt 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 Datenbanken | Kombinationen | Jobs | Beispiel |
|---|---|---|---|
1 (z. B. mssql) | Datenbank auf sich selbst | 3 | mssql-to-mssql-account |
| 2 | 2 | 6 | mssql-to-postgres-opportunity |
| 3 | 6 | 18 | mysql-to-oracle-opportunityproduct |
4 (all) | 12 | 36 | oracle-to-mssql-account |
Alle Jobs haben diese Einstellungen:
| Feld | Wert |
|---|---|
Source / Target | Tabellenname, z. B. Account |
Mapping | dev-<tabelle>, z. B. dev-account |
WatermarkType / WatermarkField | datetime / ModifiedOn |
Priority | Standard |
IsActive | false |
Die Jobs dienen als Testobjekte für list_jobs/get_job und als Vorlage. Sie laufen nicht, und
die DataBridge prüft inaktive Jobs beim Start nicht — deshalb steht bei ihnen isComplete: false.
Das ist kein Fehler. Aktiviert man einen Job, wird er beim nächsten Start geprüft.
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:
| Mapping | Tabelle | Felder | Schlüssel (TargetKey) |
|---|---|---|---|
dev-account | Account | 19 (alle Spalten außer ModifiedOn) | AccountId (PrimaryKey) |
dev-opportunity | Opportunity | 10 | OpportunityId (PrimaryKey) |
dev-opportunityproduct | OpportunityProduct | 10 | OpportunityProductId (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.
| Connection | Typ | Tabelle | Auth | Metadaten |
|---|---|---|---|---|
bc-dev | D365BC | v2.0/customers | OAuth2 Client Credentials gegen /oauth/token (Mock) | EDMX wiremock/__files/bc/metadata.xml |
nav-dev | D365NAV | Customers | Basic (dev/dev) | EDMX wiremock/__files/nav/metadata.xml |
openapi-dev | OPENAPI | customers | API-Key-Header X-API-Key: dev-api-key | Spec 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äfix | System | Connection-URL (aus Sicht der DataBridge) |
|---|---|---|
/bc/... | Business Central | http://wiremock:8080/bc/v2.0/sandbox |
/nav/... | NAV | http://wiremock:8080/nav/ODataV4/Company('CRONUS')/ |
/openapi/... | OpenAPI | http://wiremock:8080/openapi |
/oauth/token | Token-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.$skiptokenfehlt → Seite 1) undheaders(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:8080oderlocalhost: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:
| Job | Mapping | Richtung |
|---|---|---|
<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.
- Die Mocks merken sich nichts. Ein POST legt nichts an, ein anschließender GET liefert weiter 404.
- Filter werden nicht ausgewertet. Die Listen-Stubs liefern immer denselben Bestand.
- Dataverse (
D365CE) ist nicht dabei: DerServiceClientnutzt das SDK-Protokoll mit OAuth und lässt sich nicht sinnvoll per HTTP-Stub abbilden. - Mit den Mocks allein (ohne
servicebus/getchanges/process) laufen Jobs nicht Ende-zu-Ende. Nutzbar sind dann Verbindungstest, Metadaten (list_connector_tables,get_connector_table_schema) undCheckList. Für den vollen Ablauf siehe Service Bus und Verarbeitung schalten.
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
201liefern, PATCH genau200(nicht204). - BC/NAV: Jede Antwort braucht einen JSON-Body, auch ein 404.
nextLinkmuss 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.
| Container | Ports | Zweck |
|---|---|---|
servicebus | 5672 (AMQP), 5300 (Management/Health) | Der Emulator selbst |
servicebus-db | intern | SQL Server als Speicher des Emulators |
servicebus-explorer | SERVICEBUS_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.
Der ServiceBusAdministrationClient (Queue-Status/MCP-Diagnose) spricht mit dem Emulator über
dessen Management-API auf Port 5300, der normale ServiceBusClient (Senden/Empfangen) über den
AMQP-Port 5672. ServiceBusService hängt den Port deshalb nur beim Erzeugen des Admin-Clients an
den Endpoint an, wenn der Connection-String UseDevelopmentEmulator=true enthält und noch keinen
Port hat. In Azure passiert dort nichts, weil der volle Hostname bereits einen impliziten
Standardport hat und UseDevelopmentEmulator nicht gesetzt ist.
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) | Argument | Wirkung |
|---|---|---|
DATABRIDGE_GETCHANGES | getchanges | AzureWebJobs.GetChanges.Disabled |
DATABRIDGE_PROCESS | process | AzureWebJobs.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.
dev/up.sh mssql process(Windows:dev\up.ps1 mssql process) — Datenbank, Verarbeitung und Service-Bus-Emulator starten.getchangeswird dafür nicht gebraucht.- Job-Namen ermitteln — Muster
<quelle>-to-<ziel>-<tabelle>(siehe Jobs), z. B.mssql-to-mssql-accountbei nur einer Datenbank, oder per MCPlist_jobsnachsehen. - 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"}'
Anders als der Timer prüft ExecuteJob IsActive nicht — es sucht den Job nur über den Namen
(ProcessJobsForRetrievalInOrderService.ExecuteByJobName). Die Seed-Jobs bleiben also bewusst
IsActive=false (siehe Jobs) und lassen sich trotzdem einzeln per ExecuteJob testen.
IsActive: true (per MCP update_job oder direkt in Azurite) ist erst nötig, wenn stattdessen der
Timer (getchanges) selbst anhand des Cron-Ausdrucks erkennen soll, welche Jobs fällig sind —
Verarbeitung bleibt so ein bewusster Opt-in, kein Automatismus.
Nachschauen, ob’s geklappt hat:
- Log (Logging) zeigt beide Schritte durch —
GetChangeserkennt X Änderungen,ProcessStandardQueueItemsverarbeitet 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
| Problem | Ursache und Lösung |
|---|---|
up bricht mit Port-Konflikt ab | Ein 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 Fehlern | Zeilenenden. .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 API | Der Function Key fehlt. Er ist fest auf local gesetzt (?code=local). |
| Oracle braucht lange | Der 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 weg | Gewollt: Die Testdaten werden bei jedem Start zurückgesetzt. |
| WireMock startet nicht, Port-Konflikt auf 8089 | WIREMOCK_PORT in dev/.env ändern. 9090 ist unter Fedora oft von Cockpit belegt. |
| Mock antwortet mit 404 auf alles | Der 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 Verbindungsfehler | Profil servicebus läuft nicht (kein servicebus/getchanges/process beim letzten up-Aufruf angegeben), ExportQueueServiceBus zeigt dann auf den Dummy-Connection-String. |