diff --git a/.github/workflows/package-apisix-runtime-deb-openresty-1.21.yml b/.github/workflows/package-apisix-runtime-deb-openresty-1.21.yml index 99e08b017..98604f85d 100644 --- a/.github/workflows/package-apisix-runtime-deb-openresty-1.21.yml +++ b/.github/workflows/package-apisix-runtime-deb-openresty-1.21.yml @@ -35,6 +35,8 @@ jobs: sudo apt-get install -y make ruby ruby-dev rubygems build-essential - name: Build apisix-runtime deb + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | if [ "${{ matrix.platform.arch }}" == "arm64" ]; then make package type=deb app=apisix-runtime runtime_version=${BUILD_APISIX_RUNTIME_VERSION} image_base=debian image_tag=bullseye-slim arch=linux/arm64/v8 diff --git a/.github/workflows/package-apisix-runtime-deb-ubuntu20.04.yml b/.github/workflows/package-apisix-runtime-deb-ubuntu20.04.yml index f1153eaca..d9b711396 100644 --- a/.github/workflows/package-apisix-runtime-deb-ubuntu20.04.yml +++ b/.github/workflows/package-apisix-runtime-deb-ubuntu20.04.yml @@ -28,6 +28,8 @@ jobs: sudo apt-get install -y make ruby ruby-dev rubygems build-essential - name: build apisix-runtime deb + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=deb app=apisix-runtime runtime_version=${BUILD_APISIX_RUNTIME_VERSION} image_base=ubuntu image_tag=24.04 diff --git a/.github/workflows/package-apisix-runtime-rpm-el.yml b/.github/workflows/package-apisix-runtime-rpm-el.yml index d92703683..6f1f80295 100644 --- a/.github/workflows/package-apisix-runtime-rpm-el.yml +++ b/.github/workflows/package-apisix-runtime-rpm-el.yml @@ -56,6 +56,7 @@ jobs: env: # Stream inner docker build output so a failing build step surfaces in CI. BUILDKIT_PROGRESS: plain + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=rpm app=apisix-runtime runtime_version=${BUILD_APISIX_RUNTIME_VERSION} \ image_base=rockylinux image_tag=${{ matrix.dist.image_tag }} arch=linux/amd64 diff --git a/.github/workflows/package-apisix-runtime-rpm-ubi.yml b/.github/workflows/package-apisix-runtime-rpm-ubi.yml index e2ecb459f..adf9084b3 100644 --- a/.github/workflows/package-apisix-runtime-rpm-ubi.yml +++ b/.github/workflows/package-apisix-runtime-rpm-ubi.yml @@ -26,6 +26,8 @@ jobs: sudo apt-get install -y make ruby ruby-dev rubygems build-essential - name: build apisix-runtime rpm + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=rpm app=apisix-runtime runtime_version=${BUILD_APISIX_RUNTIME_VERSION} image_base=registry.access.redhat.com/ubi9/ubi image_tag=9.6 diff --git a/.github/workflows/release-apisix-runtime.yml b/.github/workflows/release-apisix-runtime.yml index 7ba6f960b..06d18510a 100644 --- a/.github/workflows/release-apisix-runtime.yml +++ b/.github/workflows/release-apisix-runtime.yml @@ -52,11 +52,27 @@ jobs: sudo apt-get update sudo apt-get install -y make ruby ruby-dev rubygems build-essential + # A released runtime must carry ngx_http_ffi_client. The build itself is + # lenient so fork PRs, which get no secrets, still pass; here the secret + # has to be there, or the release would silently ship without the module. + - name: Require the ngx_http_ffi_client token + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} + run: | + if [ -z "${NGX_HTTP_FFI_CLIENT_TOKEN}" ]; then + echo "NGX_HTTP_FFI_CLIENT_TOKEN is not set; a release build must carry ngx_http_ffi_client" >&2 + exit 1 + fi + - name: Build apisix-runtime deb + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=deb app=apisix-runtime runtime_version="${VERSION}" image_base=debian image_tag=bookworm-slim arch=${{ matrix.platform.build_arch }} - name: Build apisix-runtime-debug deb + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=deb app=apisix-runtime runtime_version="${VERSION}" image_base=debian image_tag=bookworm-slim arch=${{ matrix.platform.build_arch }} build_latest=latest artifact=apisix-runtime-debug @@ -111,10 +127,23 @@ jobs: sudo apt-get update sudo apt-get install -y make ruby ruby-dev rubygems build-essential rpm + # A released runtime must carry ngx_http_ffi_client. The build itself is + # lenient so fork PRs, which get no secrets, still pass; here the secret + # has to be there, or the release would silently ship without the module. + - name: Require the ngx_http_ffi_client token + env: + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} + run: | + if [ -z "${NGX_HTTP_FFI_CLIENT_TOKEN}" ]; then + echo "NGX_HTTP_FFI_CLIENT_TOKEN is not set; a release build must carry ngx_http_ffi_client" >&2 + exit 1 + fi + - name: Build apisix-runtime rpm (${{ matrix.dist.el }} ${{ matrix.platform.rpm_arch }}) env: # Stream inner docker build output so a failing build step surfaces in CI. BUILDKIT_PROGRESS: plain + NGX_HTTP_FFI_CLIENT_TOKEN: ${{ secrets.NGX_HTTP_FFI_CLIENT_TOKEN }} run: | make package type=rpm app=apisix-runtime runtime_version="${VERSION}" \ image_base=rockylinux image_tag=${{ matrix.dist.image_tag }} \ diff --git a/Makefile b/Makefile index dc67e5f8a..b516567ac 100644 --- a/Makefile +++ b/Makefile @@ -39,6 +39,16 @@ endif # Set arch to linux/amd64 if it's not defined arch ?= linux/amd64 +# api7/ngx_http_ffi_client is private, so the runtime build reads a token from +# a BuildKit secret rather than a build arg, which would land in image history. +# Without the token the runtime is built without that module. +NGX_HTTP_FFI_CLIENT_TOKEN ?= +ifneq ($(NGX_HTTP_FFI_CLIENT_TOKEN),) +ffi_client_secret=--secret id=ngx_http_ffi_client_token,env=NGX_HTTP_FFI_CLIENT_TOKEN +else +ffi_client_secret= +endif + # Detect the CPU architecture CPU_ARCH := $(shell uname -m) # Map the architecture to Docker platform @@ -94,7 +104,7 @@ endif ### $(4) is code path ifneq ($(buildx), True) define build_runtime - docker build -t apache/$(1)-$(3):$(runtime_version) \ + DOCKER_BUILDKIT=1 docker build -t apache/$(1)-$(3):$(runtime_version) \ --build-arg checkout_v=$(checkout) \ --build-arg VERSION=$(version) \ --build-arg RUNTIME_VERSION=$(runtime_version) \ @@ -102,6 +112,7 @@ define build_runtime --build-arg IMAGE_TAG=$(image_tag) \ --build-arg BUILD_LATEST=$(build_latest) \ --build-arg CODE_PATH=$(4) \ + $(ffi_client_secret) \ --platform $(arch) \ -f ./dockerfiles/Dockerfile.$(2).$(3) . endef @@ -115,6 +126,7 @@ define build_runtime --build-arg IMAGE_TAG=$(image_tag) \ --build-arg BUILD_LATEST=$(build_latest) \ --build-arg CODE_PATH=$(4) \ + $(ffi_client_secret) \ --load \ --cache-from=$(cache_from) \ --cache-to=$(cache_to) \ diff --git a/README.md b/README.md index 8bb6fab99..0c7778970 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ | image_base | False | the environment for packaging, if type is `rpm` the default image_base is `centos`, if type is `deb` the default image_base is `ubuntu` | image_base=centos | | image_tag | False | the environment for packaging, it's value can be `16.04\|18.04\|20.04\|6\|7\|8`, if type is `rpm` the default image_tag is `7`, if type is `deb` the default image_tag is `20.04` | image_tag=7 | | buildx | False | if `True`, use buildx to build docker images, which may speed up GitHub Actions | buildx=True | +| NGX_HTTP_FFI_CLIENT_TOKEN | False | environment variable holding a token that can read `api7/ngx_http_ffi_client`. It is passed to the runtime build as a BuildKit secret and never as a build arg. Without it the runtime is built without that module | NGX_HTTP_FFI_CLIENT_TOKEN=ghp_xxx make package ... | ## Example @@ -106,6 +107,23 @@ ls output/ apisix-runtime_1.0.0-0~ubuntu20.04_amd64.deb ``` +### ngx_http_ffi_client + +`ngx_http_ffi_client` is the C HTTP client the AI plugins use for outbound LLM +requests. `api7/ngx_http_ffi_client` is a private repository, so the runtime +build fetches it only when `NGX_HTTP_FFI_CLIENT_TOKEN` is set, and otherwise +prints a warning and builds the runtime without it. `ai-proxy` falls back to +`lua-resty-http` on a runtime that does not carry the module, so both runtimes +work; only the outbound CPU cost differs. + +```sh +NGX_HTTP_FFI_CLIENT_TOKEN= \ + make package type=deb app=apisix-runtime version=1.0.0 +``` + +The commit is pinned by `ngx_http_ffi_client_ver` in `build-apisix-runtime.sh`, +since the repository carries no tags yet. + ## Details - `Makefile` the entrance of the packager diff --git a/build-apisix-runtime.sh b/build-apisix-runtime.sh index fa231d529..1eac48c1b 100755 --- a/build-apisix-runtime.sh +++ b/build-apisix-runtime.sh @@ -36,6 +36,24 @@ fi wasm_nginx_module_ver="0.7.0" lua_var_nginx_module_ver="v0.5.3" lua_resty_events_ver="0.2.0" +# api7/ngx_http_ffi_client is still a private repository and carries no tags, +# so it is pinned by commit and fetched with a token. A build without the token +# leaves the module out and is otherwise unchanged. +ngx_http_ffi_client_ver=${ngx_http_ffi_client_ver:-"f13fcfa4e923ad82844bf49d9d3b3d283371ef66"} +if [[ ! "$ngx_http_ffi_client_ver" =~ ^[A-Za-z0-9._/-]+$ ]]; then + echo "ERROR: invalid ngx_http_ffi_client_ver: $ngx_http_ffi_client_ver" >&2 + exit 1 +fi +# the trace stays off around the token, and only the derived yes/no reaches it +set +x +NGX_HTTP_FFI_CLIENT_TOKEN=${NGX_HTTP_FFI_CLIENT_TOKEN:-} +if [ -n "$NGX_HTTP_FFI_CLIENT_TOKEN" ]; then + ngx_http_ffi_client_have_token="yes" +else + ngx_http_ffi_client_have_token="no" +fi +set -x +ngx_http_ffi_client_dir="ngx_http_ffi_client-${ngx_http_ffi_client_ver}" install_openssl_3(){ @@ -134,6 +152,33 @@ else lua-var-nginx-module-${lua_var_nginx_module_ver} fi +if [ "$repo" == ngx_http_ffi_client ]; then + cp -r "$prev_workdir" "./$ngx_http_ffi_client_dir" +elif [ "$ngx_http_ffi_client_have_token" == "yes" ]; then + # A private repository pinned by commit, so fetch rather than clone -b. + # The token stays off the trace and out of the repository's git config. + mkdir "$ngx_http_ffi_client_dir" + ( + set +x + cd "$ngx_http_ffi_client_dir" || exit 1 + git init -q + git -c "http.extraheader=Authorization: Basic $(printf 'x-access-token:%s' \ + "$NGX_HTTP_FFI_CLIENT_TOKEN" | base64 | tr -d '\n')" \ + fetch -q --depth=1 \ + https://github.com/api7/ngx_http_ffi_client.git "$ngx_http_ffi_client_ver" + git checkout -q FETCH_HEAD + ) +else + echo "WARNING: NGX_HTTP_FFI_CLIENT_TOKEN is not set, building apisix-runtime" \ + "without ngx_http_ffi_client. ai-proxy falls back to lua-resty-http" \ + "on such a runtime." >&2 +fi + +ngx_http_ffi_client_configure_arg="" +if [ -d "$ngx_http_ffi_client_dir" ]; then + ngx_http_ffi_client_configure_arg="--add-module=../$ngx_http_ffi_client_dir" +fi + cd ngx_multi_upstream_module-${ngx_multi_upstream_module_ver} || exit 1 ./patch.sh ../openresty-${OPENRESTY_VERSION} cd .. @@ -165,6 +210,11 @@ else fi +# ngx_http_ffi_client compiles against lua-nginx-module's public API, which it +# reaches through the bundled copy rather than a separate checkout. +ngx_lua_bundle_dir=$(find bundle -maxdepth 1 -type d -name 'ngx_lua-*' | head -n 1) +export NGX_HTTP_LUA_MODULE_DIR="$PWD/$ngx_lua_bundle_dir" + ./configure --prefix="$OR_PREFIX" \ --with-cc-opt="-DAPISIX_RUNTIME_VER=$runtime_version $cc_opt" \ --with-ld-opt="-Wl,-rpath,$OR_PREFIX/wasmtime-c-api/lib $ld_opt" \ @@ -177,6 +227,7 @@ fi --add-module=../wasm-nginx-module-${wasm_nginx_module_ver} \ --add-module=../lua-var-nginx-module-${lua_var_nginx_module_ver} \ --add-module=../lua-resty-events-${lua_resty_events_ver} \ + $ngx_http_ffi_client_configure_arg \ --with-poll_module \ --with-pcre-jit \ --without-http_rds_json_module \ @@ -220,6 +271,13 @@ sudo install -d "$OR_PREFIX"/lualib/resty/events/compat/ sudo install -m 644 lualib/resty/events/compat/*.lua "$OR_PREFIX"/lualib/resty/events/compat/ cd .. +if [ -d "$ngx_http_ffi_client_dir" ]; then + # the C module needs its FFI bindings on the runtime's lua_package_path + sudo install -d "$OR_PREFIX"/lualib/resty/ + sudo install -m 644 "$ngx_http_ffi_client_dir"/lib/resty/ngx_http_ffi_client.lua \ + "$OR_PREFIX"/lualib/resty/ +fi + cd "apisix-nginx-module-${apisix_nginx_module_ver}" || exit 1 sudo OPENRESTY_PREFIX="$OR_PREFIX" make install cd .. diff --git a/dockerfiles/Dockerfile.apisix-runtime.deb b/dockerfiles/Dockerfile.apisix-runtime.deb index cda3dfe89..d24f7a955 100644 --- a/dockerfiles/Dockerfile.apisix-runtime.deb +++ b/dockerfiles/Dockerfile.apisix-runtime.deb @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 ARG IMAGE_BASE="debian" ARG IMAGE_TAG="bullseye-slim" @@ -20,7 +21,11 @@ ENV build_latest=${BUILD_LATEST:-} COPY ${CODE_PATH} ./ -RUN mv ./utils/build-common.sh ./utils/determine-dist.sh ./ \ +# the secret is optional: without it the runtime is built without +# ngx_http_ffi_client +RUN --mount=type=secret,id=ngx_http_ffi_client_token \ + export NGX_HTTP_FFI_CLIENT_TOKEN="$(cat /run/secrets/ngx_http_ffi_client_token 2>/dev/null || true)" \ + && mv ./utils/build-common.sh ./utils/determine-dist.sh ./ \ && ./build-common.sh build_apisix_runtime_deb ${build_latest} \ # determine dist and write it into /tmp/dist file && ./determine-dist.sh diff --git a/dockerfiles/Dockerfile.apisix-runtime.rpm b/dockerfiles/Dockerfile.apisix-runtime.rpm index 82e3b07ca..78dbc03ff 100644 --- a/dockerfiles/Dockerfile.apisix-runtime.rpm +++ b/dockerfiles/Dockerfile.apisix-runtime.rpm @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1 ARG IMAGE_BASE="registry.access.redhat.com/ubi9/ubi" ARG IMAGE_TAG="9.6" @@ -18,7 +19,11 @@ ENV runtime_version=${RUNTIME_VERSION} COPY ${CODE_PATH} ./ -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ +# the secret is optional: without it the runtime is built without +# ngx_http_ffi_client +RUN --mount=type=secret,id=ngx_http_ffi_client_token \ + export NGX_HTTP_FFI_CLIENT_TOKEN="$(cat /run/secrets/ngx_http_ffi_client_token 2>/dev/null || true)" \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ && source "$HOME/.cargo/env" \ && rustup install 1.69 \ && rustup default 1.69 \