Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,27 +129,11 @@ updates:
schedule:
day: monday
interval: weekly
- directory: /containers/router
groups:
all-uv-router:
patterns:
- "*"
open-pull-requests-limit: 1
package-ecosystem: uv
schedule:
day: monday
interval: weekly
- directory: /containers/router
groups:
all-docker-router:
patterns:
- "*"
ignore:
# Stay on the Python minor version that the router supports; patch and digest updates still apply.
- dependency-name: "python"
update-types:
- version-update:semver-major
- version-update:semver-minor
open-pull-requests-limit: 1
package-ecosystem: docker
schedule:
Expand Down
11 changes: 4 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,9 @@ jobs:
with:
ref: ${{ needs.bump-version.outputs.version }}

# The image version label is the router's own __version__ (the Dockerfile
# default), not the AWF version, so VERSION is deliberately not passed.
# The image version label is the router's own version (the Dockerfile's
# ARG VERSION default, which the build checks against main.go), not the
# AWF version, so VERSION is deliberately not passed.
# VCS_REF is the checked-out tag commit, not github.sha: bump-version
# creates a tag-only version commit on top of the dispatched commit.
- name: Compute router build arguments
Expand All @@ -323,14 +324,10 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64

- name: Install cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

# No QEMU: the builder stage cross-compiles and the scratch runtime stage runs nothing.
- name: Build and push Router image
id: build_router
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
Expand Down
123 changes: 15 additions & 108 deletions .github/workflows/test-router.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ on:
- 'containers/router/**'
- '.github/workflows/test-router.yml'
schedule:
# Weekly, so that pip-audit reports new advisories without a code change
# Weekly, so that govulncheck reports new advisories without a code change
- cron: '23 7 * * 1'
workflow_dispatch:

Expand All @@ -28,48 +28,6 @@ defaults:
working-directory: containers/router

jobs:
checks:
name: Lint, type-check and test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
python-version: '3.12'
working-directory: containers/router

- name: Install dependencies
run: uv sync --locked --dev

- name: Check formatting
run: uv run --locked ruff format --check .

- name: Lint
run: uv run --locked ruff check .

- name: Type-check
run: uv run --locked ty check

- name: Check the parity corpus
run: uv run --locked python tests/parity_corpus.py --check

- name: Run tests
run: uv run --locked python -m pytest

- name: Run release tests
run: uv run --locked python -m pytest --run-release -m release

go:
name: Go vet and test (${{ matrix.os }})
strategy:
Expand All @@ -78,9 +36,6 @@ jobs:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
defaults:
run:
working-directory: containers/router/go

steps:
- name: Checkout repository
Expand All @@ -91,7 +46,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: containers/router/go/go.mod
go-version-file: containers/router/go.mod
# The module has no dependencies, so there is no go.sum to key a cache on.
cache: false

Expand All @@ -114,39 +69,6 @@ jobs:
if: runner.os == 'Linux'
run: go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...

differential:
# Temporary: the image cut-over removes this job along with the Python router.
name: Compare the Python and Go CLIs
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
python-version: '3.12'
working-directory: containers/router

- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: containers/router/go/go.mod
cache: false

- name: Install dependencies
run: uv sync --locked --dev

- name: Build the Go CLI
run: go -C go build -o gh-aw-router .

- name: Compare the CLIs
run: uv run --locked python tests/differential.py --go-cli go/gh-aw-router

container:
name: Container contract tests
runs-on: ubuntu-latest
Expand All @@ -158,34 +80,19 @@ jobs:
with:
persist-credentials: false

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
python-version: '3.12'
working-directory: containers/router

- name: Install dependencies
run: uv sync --locked --dev

- name: Run Docker contract tests
run: uv run --locked python -m pytest --run-docker -m docker

advisories:
name: Dependency advisories
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
persist-credentials: false
go-version-file: containers/router/go.mod
cache: false

- name: Set up uv
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
python-version: '3.12'
working-directory: containers/router
- name: Build the image
run: |
docker build \
--build-arg OPENAPI_SHA256="$(sha256sum openapi.yaml | cut -d ' ' -f 1)" \
--tag gh-aw-router:test .

- name: Audit locked runtime dependencies
run: uvx pip-audit==2.10.1 --disable-pip --no-deps --requirement requirements.lock
- name: Run container tests
env:
GH_AW_ROUTER_TEST_IMAGE: gh-aw-router:test
run: go test -count=1 -run '^TestContainer' -v ./...
9 changes: 5 additions & 4 deletions containers/api-proxy/fixtures/routing-corpus.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
'use strict';

// Responses recorded from the router in containers/router. Refreshing means taking a
// new export with its contract exporter (see containers/router/README.md); never edit
// a case to make a test pass. A case names the routing tables it was recorded
// against, and containers/router owns those tables.
// Responses recorded from the router in containers/router. Refreshing means copying
// the matching cases from containers/router/testdata/routing-contract, whose
// responses the router's tests check; never edit a case to make a test pass. A case
// names the routing tables it was recorded against, and containers/router owns those
// tables.

const assert = require('node:assert/strict');
const { readFileSync } = require('node:fs');
Expand Down
8 changes: 4 additions & 4 deletions containers/router/.dockerignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
**
!Dockerfile
!openapi.yaml
!requirements.lock
!src/
!src/**
!go.mod
!go.sum
!*.go
*_test.go
!routing/
!routing/cost-*.json
!trust/
!trust/**
**/__pycache__/
17 changes: 3 additions & 14 deletions containers/router/.gitignore
Original file line number Diff line number Diff line change
@@ -1,16 +1,5 @@
.venv/
__pycache__/
*.py[cod]
*.egg-info/
.pytest_cache/
.ruff_cache/
.coverage
.coverage.*
htmlcov/
build/
dist/
/go/gh-aw-router
/go/gh-aw-router.exe
/gh-aw-router
/gh-aw-router.exe
.env
.env.*
!.env.example
Expand All @@ -21,4 +10,4 @@ dist/
.idea/
.vscode/
.DS_Store
Thumbs.db
Thumbs.db
91 changes: 47 additions & 44 deletions containers/router/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
# syntax=docker/dockerfile:1.7

# Alpine, like the other AWF images: Debian slim carries High CVEs that Debian won't fix,
# which the blocking supply-chain scan rejects.
FROM python:3.12.15-alpine3.24@sha256:1b668429b3511ab407d8e00648891631b0b1a4d7e15e3ca70f38ab5b91ad4ab4
ARG VERSION=0.2.0

ARG VERSION=0.1.3
# The builder runs on the build machine and cross-compiles, so multi-platform builds need no
# emulation. The runtime image has no shell, so every check runs here.
FROM --platform=$BUILDPLATFORM golang:1.27.3-alpine@sha256:f2c872bcee949b7be9c490efd02b48a8a2fbf947cdf533f1c58525ec9d8170fd AS build

ARG VERSION
ARG OPENAPI_SHA256
# Go's default. Builds behind a module mirror can override it.
ARG GOPROXY=https://proxy.golang.org,direct
ARG TARGETOS
ARG TARGETARCH
ENV GOTOOLCHAIN=local

WORKDIR /src
COPY go.* ./
RUN go mod download && go mod verify
COPY *.go ./
RUN grep -qxF "const version = \"${VERSION}\"" main.go \
|| { echo "VERSION must match the version constant in main.go" >&2; exit 1; }
RUN CGO_ENABLED=0 GOOS="$TARGETOS" GOARCH="$TARGETARCH" \
go build -trimpath -buildvcs=false -ldflags="-s -w" -o /out/usr/local/bin/gh-aw-router .

# Fails the build unless the label matches the shipped contract, so the label cannot drift from it.
COPY openapi.yaml /out/app/openapi.yaml
RUN printf '%s /out/app/openapi.yaml\n' "${OPENAPI_SHA256}" | sha256sum -c -s
COPY routing/cost-*.json /out/routing/
# AWF's CA distrust policy filters the bundle the runtime image ships.
COPY trust/ /opt/awf/trust/
RUN apk add --no-cache openssl ca-certificates \
&& sh /opt/awf/trust/install-system-trust.sh
RUN mkdir -p /out/etc/ssl/certs \
&& cp /etc/ssl/certs/ca-certificates.crt /out/etc/ssl/certs/ \
&& find /out -type d -exec chmod 0555 {} + \
&& find /out -type f -exec chmod 0444 {} + \
&& chmod 0555 /out/usr/local/bin/gh-aw-router

FROM scratch

ARG VERSION
ARG VCS_REF=unknown
ARG OPENAPI_SHA256
ARG PIP_INDEX_URL=https://pypi.org/simple
LABEL org.opencontainers.image.title="gh-aw-router HTTP sidecar" \
org.opencontainers.image.description="Stateless classification planning and model routing" \
org.opencontainers.image.source="https://github.com/github/gh-aw-firewall" \
Expand All @@ -16,50 +50,19 @@ LABEL org.opencontainers.image.title="gh-aw-router HTTP sidecar" \
org.opencontainers.image.licenses="MIT" \
io.github.gh-aw-router.openapi-sha256="${OPENAPI_SHA256}"

ENV HOME=/home/gh-aw-router \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/src \
PYTHONUNBUFFERED=1 \
GH_AW_ROUTER_ROUTING_TABLES=/routing

RUN apk upgrade --no-cache \
&& addgroup -S -g 10001 gh-aw-router \
&& adduser -S -D -u 10001 -G gh-aw-router \
-h /home/gh-aw-router -s /sbin/nologin gh-aw-router \
&& mkdir -p /app/src /routing

COPY trust/ /opt/awf/trust/
RUN apk add --no-cache openssl ca-certificates \
&& sh /opt/awf/trust/install-system-trust.sh
ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt \
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt

COPY requirements.lock /tmp/requirements.lock
RUN python -m pip install \
--disable-pip-version-check \
--index-url "${PIP_INDEX_URL}" \
--no-cache-dir \
--require-hashes \
--requirement /tmp/requirements.lock \
&& rm /tmp/requirements.lock

COPY src /app/src
RUN python -c "import sys; from gh_aw_router import __version__; sys.exit(0 if sys.argv[1] == __version__ else 'VERSION must match the package version')" "$VERSION"
COPY routing /routing
# Fails the build unless the label matches the shipped contract, so the label cannot drift from it.
COPY openapi.yaml /app/openapi.yaml
RUN printf '%s /app/openapi.yaml\n' "${OPENAPI_SHA256}" | sha256sum -c -s
COPY --from=build /out/ /

RUN find /app /routing -type d -exec chmod 0555 {} + \
&& find /app /routing -type f -exec chmod 0444 {} +
# AWF limits the container to 512 MiB and 100 processes, without a CPU quota.
ENV GH_AW_ROUTER_ROUTING_TABLES=/routing \
GOMAXPROCS=2 \
GOMEMLIMIT=384MiB

USER 10001:10001
WORKDIR /home/gh-aw-router

EXPOSE 8737
STOPSIGNAL SIGTERM
HEALTHCHECK --interval=5s --timeout=2s --start-period=2s --retries=12 \
CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8737/healthz', timeout=1).close()"]

ENTRYPOINT ["python", "-m", "gh_aw_router"]
# No HEALTHCHECK, like the other AWF images: AWF's compose config runs
# /usr/local/bin/gh-aw-router healthcheck.
ENTRYPOINT ["/usr/local/bin/gh-aw-router"]
CMD ["serve", "--bind", "0.0.0.0:8737", "--log-level", "info"]
Loading
Loading