Skip to content
Patronage Ads Sync Docs
Esc
navigateopen⌘Jpreview
On this page

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:

  1. GET /metadata.
  2. Compare connectorImage with supportedImageVersions[provider] (or supportedImageVersions.destination) as an exact string, digest included.
  3. Compare wrapperVersion with the exact string ads-sync-wrapper-v0.3.
  4. 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 5xx response (the body is not inspected on a 5xx);
  • never retry a 200 whose fields mismatch, and never retry a 4xx;
  • 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.

Was this page helpful?