Connector image HTTP contract
Exact HTTP surface of the Ads Sync connector images, the metadata verification consumers must do, and the build inputs that produce the images.
Every Ads Sync connector image runs one Python HTTP wrapper, airbyte-source-server.py, in front of a pinned Airbyte connector. A Qualified Run talks to that wrapper over HTTP; it never runs the connector command directly. This page is the wrapper contract for wrapper version ads-sync-wrapper-v0.3.
The build inputs live in the Ads Sync Reference Deployment at packages/ads-sync/deploy/images/. Consumers build the images themselves; the project publishes Dockerfiles, not built images.
images
| image | Dockerfile | base image (digest pinned in FROM) |
|---|---|---|
| Google Ads source | Dockerfile |
airbyte/source-google-ads:6.1.0@sha256:dea39deedba0a095f60159d808dfb47fa778e304846396d2ab2f04c951b480ed |
| Google Search Console source | Dockerfile.gsc |
airbyte/source-google-search-console:2.1.9@sha256:3ee78d227a25ec01a31b9f131b1b8d80afd6e3aaf0c0c2f1b09c7973190465b3 |
| Meta Ads source | Dockerfile.meta |
airbyte/source-facebook-marketing:5.2.11@sha256:4d6c916b29862ded4b5b94feea0b8ef75899f34c364e4884312e50414b6d447c |
| Postgres destination | Dockerfile.postgres |
airbyte/destination-postgres:3.0.13@sha256:0b310bd46ba0e006757ea3dc1d3b8ef8e3bcf51c3a96f5460a836653b5ac4f4c |
The pins equal supportedImageVersions in @patronage/ads-sync. Each Dockerfile also sets AIRBYTE_CONNECTOR_IMAGE to the same reference; /metadata reports it as connectorImage.
build command
IMAGE_REGISTRY=registry.example.com/ads-sync bash build-images.sh --push
Run build-images.sh with bash from packages/ads-sync/deploy/images/; the public tree does not preserve the executable bit. Without --push it loads the images into the local Docker daemon. IMAGE_TAG defaults to 0.3.0 and IMAGE_PLATFORM to linux/amd64.
exact-match build steps
| build argument | default | alternative | step |
|---|---|---|---|
GSC_AUTH_MODE |
refresh_token |
access_token |
patch-gsc-manifest-auth.py rewrites the pinned manifest OAuth authenticator to a BearerAuthenticator that reads authorization.access_token |
POSTGRES_SCHEMA_MODE |
airbyte_owned |
preprovisioned |
patch-postgres-preprovisioned-schema.py rewrites the connector jar constant CREATE SCHEMA IF NOT EXISTS <schema>; to SELECT 1 /* preprovisioned <schema> */; |
| always | pin-postgres-java-cacerts.sh copies the destination image CA bundle to /app/java-cacerts; Dockerfile.postgres then sets DESTINATION_POSTGRES_OPTS to that JKS trust store |
Each patch script requires the pinned upstream text to match exactly once. On any drift it exits non-zero and the image build fails. The defaults are the single-tenant mode: the operator owns the Google refresh token, and the destination writer role has database-level CREATE so Airbyte owns schema DDL. TLS is never lowered; keep sslmode=verify-full in the destination JDBC parameters.
wrapper environment
| variable | set by | effect |
|---|---|---|
AIRBYTE_CONNECTOR_COMMAND |
Dockerfile | connector entrypoint the wrapper runs |
AIRBYTE_CONNECTOR_IMAGE |
Dockerfile | value of connectorImage in /metadata |
AIRBYTE_CONNECTOR_SERVICE |
Dockerfile | value of service in /metadata |
AIRBYTE_IMAGE_BUILD_OPTIONS |
Dockerfile | value of buildOptions in /metadata, for example postgres_schema_mode=airbyte_owned |
AIRBYTE_READ_TIMEOUT_SECONDS |
Dockerfile or runtime | /read and /read-artifact wall-clock budget; default 1860, Google Ads image sets 330 |
AIRBYTE_WRITE_TIMEOUT_SECONDS |
runtime | /write wall-clock budget; default 1260 |
AIRBYTE_JAVA_TRUST_STORE |
Dockerfile.postgres |
JKS path that /network-check reports on |
AIRBYTE_NETWORK_CHECK_PORTS |
runtime | comma-separated port allowlist for /network-check; default 5432 |
AIRBYTE_WRAPPER_PORT |
runtime | listen port; default 8080 |
endpoints
The wrapper listens on port 8080 on all interfaces (0.0.0.0). Every response is application/json.
network posture
The wrapper has no authentication on any route. Under Cloudflare Containers the port is reachable only through the Durable Object binding, and that network isolation is the only access control. /write runs a destination write with a caller-supplied config, and /network-check opens a TCP connection to a caller-supplied host on an allowlisted port. Do not expose the container port on a host or a shared network. When you run an image outside Cloudflare Containers (for example under docker run for provider admission), bind the port to loopback only.
| method and path | request body | success | failure |
|---|---|---|---|
GET /metadata (also GET / and GET /health) |
none | 200 metadata object |
none |
GET /spec |
none | 200 run result |
502 run result, 504 timeout |
POST /check |
connector config object | 200 run result |
400, 502, 504 |
POST /discover |
connector config object | 200 run result |
400, 502, 504 |
POST /read |
{ config, catalog, state? } |
200 run result without output |
400, 502, 504 |
POST /read-artifact |
{ config, catalog, state? } |
200 run result with stdout and stderr |
400, 502, 504 |
POST /write |
{ config, catalog, messages, include_output? } |
200 run result |
400, 502, 504 |
POST /network-check |
{ host, port } |
200 { connected: true, trustStore } |
400, 502 |
| any other path | 404 { "error": "Not found" } |
Any POST body must be a JSON object; a missing, non-JSON, or non-object body returns 400 { "error": ... }.
/metadata
{
"buildOptions": "postgres_schema_mode=airbyte_owned",
"connectorImage": "airbyte/destination-postgres:3.0.13@sha256:0b310bd46ba0e006757ea3dc1d3b8ef8e3bcf51c3a96f5460a836653b5ac4f4c",
"service": "airbyte-postgres-destination",
"status": "ok",
"wrapperVersion": "ads-sync-wrapper-v0.3"
}
buildOptions is an empty string on images without build modes.
run result
/spec, /check, /discover, /read, /read-artifact, and /write return the same shape:
{
"args": ["read", "--config", "<config>", "--catalog", "<config>"],
"exitCode": 0,
"stderrLength": 1234,
"stdoutLength": 56789,
"success": true
}
args redacts every temporary JSON path as <config>. The status is 200 when the connector exits 0 and 502 otherwise; the body is identical in both cases. /read-artifact always adds stdout and stderr as strings; /write adds them only when include_output is true. Consumers must treat stdout and stderr as untrusted text that can contain credentials and must apply artifact bounds and redaction before storing them. The Qualified Run seam does this for every artifact body it persists.
The wrapper writes each request object to a temporary file for the connector process. It enters the cleanup block before the first write, so a failed later write (for example, disk full while writing the catalog) still deletes the config file that holds the credential.
When the connector exceeds the wall-clock budget the wrapper returns 504 { "args": [...], "error": "Connector timed out" }. The wrapper starts the connector under /usr/bin/timeout with a 30-second inner margin (60 seconds for /write) so the connector process ends before the HTTP budget does.
The connector always gets at least 30 seconds inside the budget. When AIRBYTE_READ_TIMEOUT_SECONDS or AIRBYTE_WRITE_TIMEOUT_SECONDS is smaller than the inner margin plus that floor (60 seconds for /read, 90 for /write), the wrapper does not start the connector. It returns 500 { "args": [...], "error": "Connector timeout budget is too small", "minimumTimeoutSeconds": <n>, "timeoutSeconds": <configured> }. This is an image configuration error, not a run result.
/read-artifact
config and catalog must be objects; state is optional and passed through as --state. The catalog is the configured Airbyte catalog for the Sync Connection. The wrapper writes each object to a temporary file inside the container, runs read, deletes the files, and returns the run result with stdout (Airbyte protocol messages, one per line) and stderr. Use /read when only the exit code matters.
/write
config and catalog must be objects and messages must be a string of newline-delimited Airbyte protocol messages, which the wrapper passes to the connector on stdin. The destination writes only what the compiled destination input contains; state commit is the caller’s decision after a successful write.
/network-check
host must be a non-empty hostname string and port an integer in the AIRBYTE_NETWORK_CHECK_PORTS allowlist; other values return 400. The wrapper opens one TCP connection with a 10-second timeout and does not send data or credentials.
{
"connected": true,
"trustStore": {
"destinationOptionsPresent": true,
"keytoolReadable": true,
"path": "/app/java-cacerts",
"readable": true
}
}
trustStore is null on images without AIRBYTE_JAVA_TRUST_STORE. A failed connection returns 502 { "connected": false, "errorType": "<Python exception class>" }, for example TimeoutError (timeout on the older Python in the pinned destination image) or ConnectionRefusedError. Use it as an image-level readiness probe for the destination: it proves the container can reach the Postgres host and that the pinned JVM trust store is readable before a /write spends the connector budget.
metadata verification
Before the first connector call of every Qualified Run, the consumer must:
GET /metadata.- Compare
connectorImagewithsupportedImageVersions[provider](orsupportedImageVersions.destination) as an exact string, digest included. - Compare
wrapperVersionwith the exact stringads-sync-wrapper-v0.3. - Fail the run on any mismatch. Do not retry a mismatch; a responding wrapper with the wrong image or version is a deployment error, not a transient condition.
Prefix, tag-only, or semver-range comparisons are not the contract. The pins are what the tests in packages/ads-sync/deploy/images/images.test.ts and the Qualified Run seam assert.
cold-start retry
A newly started Cloudflare Container can answer /metadata with a transient platform error, or refuse the connection, before the wrapper process listens. The Qualified Run seam performs a bounded, unavailable-only retry:
- retry only when the request did not reach a healthy wrapper: connection failure, or any
5xxresponse (the body is not inspected on a5xx); - never retry a
200whose fields mismatch, and never retry a4xx; - stop after the bounded attempt budget and fail the run.
The retry is seam behavior. A consumer that calls the wrapper directly must implement the same rule; the wrapper itself has no retry.
versioning
WRAPPER_VERSION in airbyte-source-server.py changes whenever this contract changes. Consumers pin the exact string; the seam and the internal Paitronage app pin ads-sync-wrapper-v0.3. Adding an endpoint, changing a response field, or changing an error status is a version bump.