forked from supabase/stripe-sync-engine
-
Notifications
You must be signed in to change notification settings - Fork 14
Add smokescreen HTTP CONNECT proxy support #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
911f63f
Add smokescreen HTTP CONNECT proxy support and e2e test
tonyxiao 8346850
ci: trigger CI run
tonyxiao dd93ac8
smokescreen: enforce proxy via Docker network isolation
tonyxiao 46df172
fix: run prettier to fix ci.yml quote style
tonyxiao 42f2172
review: pin smokescreen to v0.0.4, clarify NO_PROXY comment
tonyxiao 33706c7
fix: remove --branch from smokescreen git clone to avoid CI failures
tonyxiao e0333db
ci: trigger CI run after resolving stuck workflow
tonyxiao 70668e7
fix(smokescreen): update test to use X-Pipeline header and new JSON f…
tonyxiao 61d0dc0
debug: add engine container logs on health check failure
tonyxiao a687402
fix(smokescreen): use container IP instead of port mapping for engine…
tonyxiao File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| FROM golang:1.23 AS builder | ||
| WORKDIR /app | ||
| RUN git clone --depth 1 https://github.com/stripe/smokescreen.git . | ||
| RUN CGO_ENABLED=0 GOOS=linux go build -o smokescreen -ldflags="-s -w" . | ||
|
|
||
| FROM alpine:3.19 | ||
| RUN apk add --no-cache netcat-openbsd | ||
| COPY --from=builder /app/smokescreen /usr/local/bin/smokescreen | ||
| ENTRYPOINT ["smokescreen"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,165 @@ | ||
| #!/usr/bin/env bash | ||
| # Test src-stripe, dest-pg, and dest-sheets through smokescreen HTTP CONNECT proxy. | ||
| # | ||
| # Uses Docker network isolation to ENFORCE that all outbound HTTPS goes through | ||
| # smokescreen — the engine container has no direct internet access. | ||
| # Without a working proxy, Stripe and Google API calls would fail outright. | ||
| # | ||
| # Required: STRIPE_API_KEY | ||
| # Optional: ENGINE_IMAGE (skips local build — CI passes the pre-built image) | ||
| # Optional: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN, GOOGLE_SPREADSHEET_ID | ||
| set -euo pipefail | ||
|
|
||
| REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" | ||
|
|
||
| # In CI the pre-built image is passed via ENGINE_IMAGE; locally we build from source. | ||
| BUILD_ENGINE=false | ||
| if [ -z "${ENGINE_IMAGE:-}" ]; then | ||
| ENGINE_IMAGE="sync-engine:smokescreen-test" | ||
| BUILD_ENGINE=true | ||
| fi | ||
|
|
||
| SMOKESCREEN_IMAGE="sync-engine-smokescreen:test" | ||
| S="$$" # unique suffix for this run | ||
| NET="smokescreen-isolated-${S}" | ||
| SMOKESCREEN_CONTAINER="smokescreen-${S}" | ||
| ENGINE_CONTAINER="engine-smokescreen-${S}" | ||
| PG_CONTAINER="pg-smokescreen-${S}" | ||
| ENGINE_URL="" # set after container starts | ||
|
|
||
| cleanup() { | ||
| docker rm -f "$ENGINE_CONTAINER" "$SMOKESCREEN_CONTAINER" "$PG_CONTAINER" >/dev/null 2>&1 || true | ||
| docker network rm "$NET" >/dev/null 2>&1 || true | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # ── Build images ──────────────────────────────────────────────────────────── | ||
|
|
||
| echo "==> Building smokescreen image" | ||
| docker build -t "$SMOKESCREEN_IMAGE" "$REPO_ROOT/docker/smokescreen" | ||
|
|
||
| if $BUILD_ENGINE; then | ||
| echo "==> Building engine image" | ||
| docker build -t "$ENGINE_IMAGE" "$REPO_ROOT" | ||
| fi | ||
|
|
||
| # ── Isolated network ───────────────────────────────────────────────────────── | ||
| # --internal means no default gateway → containers cannot reach the internet directly. | ||
|
|
||
| echo "==> Creating isolated Docker network: $NET" | ||
| docker network create --internal "$NET" | ||
|
|
||
| # ── Postgres (on isolated network — reachable by engine, not internet-exposed) ── | ||
|
|
||
| echo "==> Starting Postgres" | ||
| docker run -d --name "$PG_CONTAINER" \ | ||
| --network "$NET" \ | ||
| -e POSTGRES_USER=postgres \ | ||
| -e POSTGRES_PASSWORD=postgres \ | ||
| -e POSTGRES_DB=postgres \ | ||
| postgres:18 | ||
| PG_URL="postgres://postgres:postgres@${PG_CONTAINER}:5432/postgres" | ||
|
|
||
| # ── Smokescreen (isolated net + bridge → has internet, proxies for engine) ─── | ||
|
|
||
| echo "==> Starting smokescreen" | ||
| docker run -d --name "$SMOKESCREEN_CONTAINER" \ | ||
| --network "$NET" \ | ||
| "$SMOKESCREEN_IMAGE" | ||
| # Connect to default bridge so smokescreen itself can reach the internet | ||
| docker network connect bridge "$SMOKESCREEN_CONTAINER" | ||
|
|
||
| for i in $(seq 1 20); do | ||
| docker exec "$SMOKESCREEN_CONTAINER" nc -z localhost 4750 >/dev/null 2>&1 && break | ||
| [ "$i" -eq 20 ] && { echo "FAIL: smokescreen health check timed out"; exit 1; } | ||
| sleep 0.5 | ||
| done | ||
| echo " Smokescreen ready" | ||
|
|
||
| # ── Engine (isolated network ONLY — HTTPS must route through smokescreen) ──── | ||
|
|
||
| echo "==> Starting engine (HTTPS_PROXY=http://${SMOKESCREEN_CONTAINER}:4750)" | ||
| # No -p port mapping: --internal networks block port publishing on Linux. | ||
| # Instead, reach the engine by its container IP on the bridge (host has a | ||
| # directly connected route to the bridge subnet even for --internal networks). | ||
| docker run -d --name "$ENGINE_CONTAINER" \ | ||
| --network "$NET" \ | ||
| -e PORT=3000 \ | ||
| -e HTTPS_PROXY="http://${SMOKESCREEN_CONTAINER}:4750" \ | ||
| "$ENGINE_IMAGE" | ||
|
|
||
| # Wait for the container to get an IP assignment | ||
| ENGINE_IP="" | ||
| for i in $(seq 1 10); do | ||
| ENGINE_IP=$(docker inspect "$ENGINE_CONTAINER" \ | ||
| --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' 2>/dev/null) | ||
| [ -n "$ENGINE_IP" ] && break | ||
| sleep 0.5 | ||
| done | ||
| [ -n "$ENGINE_IP" ] || { echo "FAIL: could not get engine container IP"; exit 1; } | ||
| ENGINE_URL="http://${ENGINE_IP}:3000" | ||
|
|
||
| for i in $(seq 1 20); do | ||
| curl -sf "${ENGINE_URL}/health" >/dev/null && break | ||
| [ "$i" -eq 20 ] && { | ||
| echo "FAIL: engine health check timed out (IP: $ENGINE_IP)" | ||
| echo "==> Engine container logs:" | ||
| docker logs "$ENGINE_CONTAINER" 2>&1 || true | ||
| echo "==> Engine container inspect (State):" | ||
| docker inspect "$ENGINE_CONTAINER" --format '{{json .State}}' 2>&1 || true | ||
| exit 1 | ||
| } | ||
| sleep 0.5 | ||
| done | ||
| echo " Engine ready at $ENGINE_URL" | ||
|
|
||
| for i in $(seq 1 20); do | ||
| docker exec "$PG_CONTAINER" pg_isready -U postgres >/dev/null 2>&1 && break | ||
| [ "$i" -eq 20 ] && { echo "FAIL: postgres health check timed out"; exit 1; } | ||
| sleep 0.5 | ||
| done | ||
| echo " Postgres ready" | ||
|
|
||
| # ── 1) Read from Stripe (HTTPS → smokescreen → api.stripe.com) ─────────────── | ||
|
|
||
| echo "==> src-stripe: read through smokescreen" | ||
| READ_PARAMS=$(printf \ | ||
| '{"source":{"name":"stripe","api_key":"%s","backfill_limit":5},"destination":{"name":"postgres","url":"postgres://unused:5432/db","schema":"stripe"},"streams":[{"name":"products"}]}' \ | ||
| "$STRIPE_API_KEY") | ||
| OUTPUT=$(curl -sf --max-time 30 -X POST "${ENGINE_URL}/read" \ | ||
| -H "X-Pipeline: $READ_PARAMS") | ||
| RECORD_COUNT=$(echo "$OUTPUT" | grep -c '"type":"record"' || true) | ||
| echo " Got $RECORD_COUNT record(s)" | ||
| [ "$RECORD_COUNT" -gt 0 ] || { echo "FAIL: no records from Stripe"; exit 1; } | ||
|
|
||
| # ── 2) Write to Postgres (direct TCP on isolated network) ───────────────────── | ||
|
|
||
| echo "==> dest-pg: setup + write" | ||
| PG_PARAMS=$(printf \ | ||
| '{"source":{"name":"stripe","api_key":"%s"},"destination":{"name":"postgres","url":"%s","schema":"stripe_smokescreen_test"}}' \ | ||
| "$STRIPE_API_KEY" "$PG_URL") | ||
| curl -sf --max-time 30 -X POST "${ENGINE_URL}/setup" \ | ||
| -H "X-Pipeline: $PG_PARAMS" && echo " setup OK" | ||
| echo "$OUTPUT" | curl -sf --max-time 60 -X POST "${ENGINE_URL}/write" \ | ||
| -H "X-Pipeline: $PG_PARAMS" \ | ||
| -H "Content-Type: application/x-ndjson" \ | ||
| --data-binary @- | head -3 || true | ||
| echo " dest-pg OK" | ||
|
|
||
| # ── 3) Write to Google Sheets (HTTPS → smokescreen → googleapis.com) ───────── | ||
|
|
||
| if [ -n "${GOOGLE_CLIENT_ID:-}" ]; then | ||
| echo "==> dest-sheets: write through smokescreen" | ||
| SHEETS_PARAMS=$(printf \ | ||
| '{"source":{"name":"stripe","api_key":"%s"},"destination":{"name":"google-sheets","client_id":"%s","client_secret":"%s","access_token":"unused","refresh_token":"%s","spreadsheet_id":"%s"}}' \ | ||
| "$STRIPE_API_KEY" "$GOOGLE_CLIENT_ID" "$GOOGLE_CLIENT_SECRET" "$GOOGLE_REFRESH_TOKEN" "$GOOGLE_SPREADSHEET_ID") | ||
| echo "$OUTPUT" | curl -sf --max-time 60 -X POST "${ENGINE_URL}/write" \ | ||
| -H "X-Pipeline: $SHEETS_PARAMS" \ | ||
| -H "Content-Type: application/x-ndjson" \ | ||
| --data-binary @- | head -3 || true | ||
| echo " dest-sheets OK" | ||
| else | ||
| echo "==> Skipping dest-sheets (GOOGLE_CLIENT_ID not set)" | ||
| fi | ||
|
|
||
| echo "==> All smokescreen tests passed" |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Patching
https.globalAgentglobally affects all HTTPS traffic in the process and does not account for common proxy controls likeNO_PROXY/no_proxy(e.g., bypassing proxy for localhost/internal services). Consider respectingNO_PROXYor scoping proxy usage to the specific HTTP clients that need it to avoid unexpected routing changes in long-runningservemode.