diff --git a/.github/labeler.yml b/.github/labeler.yml index f0a57e8ae9c..1abdd0f0ac5 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -76,7 +76,7 @@ core/templates: core/tracing: - changed-files: - - any-glob-to-any-file: ['kong/tracing/**/*', 'kong/pdk/tracing.lua'] + - any-glob-to-any-file: ['kong/observability/tracing/**/*', 'kong/pdk/tracing.lua'] core/wasm: - changed-files: diff --git a/.github/matrix-full.yml b/.github/matrix-full.yml index 376fcac72ef..e8379aa5160 100644 --- a/.github/matrix-full.yml +++ b/.github/matrix-full.yml @@ -22,10 +22,6 @@ build-packages: check-manifest-suite: ubuntu-22.04-arm64 # Debian -- label: debian-10 - image: debian:10 - package: deb - check-manifest-suite: debian-10-amd64 - label: debian-11 image: debian:11 package: deb @@ -36,12 +32,6 @@ build-packages: check-manifest-suite: debian-12-amd64 # RHEL -- label: rhel-7 - image: centos:7 - package: rpm - package-type: el7 - bazel-args: --//:wasmx_el7_workaround=true --//:brotli=False - check-manifest-suite: el7-amd64 - label: rhel-8 image: rockylinux:8 package: rpm @@ -60,10 +50,11 @@ build-packages: # Amazon Linux - label: amazonlinux-2 - image: amazonlinux:2 package: rpm package-type: aws2 check-manifest-suite: amazonlinux-2-amd64 + # simdjson doesn't compile on gcc7.3.1 (needs 7.4) + bazel-args: --platforms=//:aws2-crossbuild-x86_64 --//:simdjson=False - label: amazonlinux-2023 image: amazonlinux:2023 package: rpm @@ -140,12 +131,6 @@ release-packages: artifact: kong.arm64.deb # Debian -- label: debian-10 - package: deb - artifact-from: debian-10 - artifact-version: 10 - artifact-type: debian - artifact: kong.amd64.deb - label: debian-11 package: deb artifact-from: debian-11 @@ -160,12 +145,6 @@ release-packages: artifact: kong.amd64.deb # RHEL -- label: rhel-7 - package: rpm - artifact-from: rhel-7 - artifact-version: 7 - artifact-type: rhel - artifact: kong.el7.amd64.rpm - label: rhel-8 package: rpm artifact-from: rhel-8 diff --git a/.github/workflows/add-pongo-release.yml b/.github/workflows/add-release-pongo.yml similarity index 100% rename from .github/workflows/add-pongo-release.yml rename to .github/workflows/add-release-pongo.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index b82f9e1c6d9..12f386a2934 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Create backport pull requests - uses: korthout/backport-action@52886ff43ef0184911d99c0a489f5c1307db8fc7 # v2.4.1 + uses: korthout/backport-action@924c8170740fa1e3685f69014971f7f251633f53 # v2.4.1 id: backport with: github_token: ${{ secrets.PAT }} diff --git a/.github/workflows/changelog-requirement.yml b/.github/workflows/changelog-requirement.yml index 3c81ed06d33..d844abc6566 100644 --- a/.github/workflows/changelog-requirement.yml +++ b/.github/workflows/changelog-requirement.yml @@ -21,7 +21,7 @@ jobs: - name: Find changelog files id: changelog-list - uses: tj-actions/changed-files@03334d095e2739fa9ac4034ec16f66d5d01e9eba # 44.5.1 + uses: tj-actions/changed-files@6b2903bdce6310cfbddd87c418f253cf29b2dec9 # 44.5.6 with: files_yaml: | changelogs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c73fb2d3eaf..fc3b3ac895f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,9 +1,6 @@ name: Package & Release # The workflow to build and release official Kong packages and images. -# -# TODO: -# Do not bump the version of actions/checkout to v4 before dropping rhel7 and amazonlinux2. on: # yamllint disable-line rule:truthy pull_request: @@ -60,7 +57,7 @@ jobs: commit-sha: ${{ github.event.pull_request.head.sha || github.sha }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build Info id: build-info run: | @@ -132,15 +129,15 @@ jobs: - name: Cache Git id: cache-git - if: (matrix.package == 'rpm' || matrix.image == 'debian:10') && matrix.image != '' - uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c # v3, DO NOT BUMP, v4 BREAKS ON CENTOS7 OR AMAZONLINUX2 + if: (matrix.package == 'rpm') && matrix.image != '' + uses: actions/cache@v4 with: path: /usr/local/git key: ${{ matrix.label }}-git-2.41.0 - # el-7,8, amazonlinux-2,2023, debian-10 doesn't have git 2.18+, so we need to install it manually + # el-7,8, amazonlinux-2,2023 doesn't have git 2.18+, so we need to install it manually - name: Install newer Git - if: (matrix.package == 'rpm' || matrix.image == 'debian:10') && matrix.image != '' && steps.cache-git.outputs.cache-hit != 'true' + if: (matrix.package == 'rpm') && matrix.image != '' && steps.cache-git.outputs.cache-hit != 'true' run: | if which apt 2>/dev/null; then apt update @@ -154,30 +151,18 @@ jobs: tar xf git-2.41.0.tar.gz cd git-2.41.0 - # https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5948/diffs - if [[ ${{ matrix.image }} == "centos:7" ]]; then - echo 'CFLAGS=-std=gnu99' >> config.mak - fi - make configure ./configure --prefix=/usr/local/git make -j$(nproc) make install - name: Add Git to PATH - if: (matrix.package == 'rpm' || matrix.image == 'debian:10') && matrix.image != '' + if: (matrix.package == 'rpm') && matrix.image != '' run: | echo "/usr/local/git/bin" >> $GITHUB_PATH - - name: Debian Git dependencies - if: matrix.image == 'debian:10' - run: | - apt update - # dependencies for git - apt install -y wget libz-dev libssl-dev libcurl4-gnutls-dev libexpat1-dev sudo - - name: Checkout Kong source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Swap git with https run: git config --global url."https://github".insteadOf git://github @@ -194,7 +179,7 @@ jobs: - name: Cache Packages id: cache-deps if: env.GHA_CACHE == 'true' - uses: actions/cache@e12d46a63a90f2fae62d114769bbf2a179198b5c # v3, DO NOT BUMP, v4 BREAKS ON CENTOS7 OR AMAZONLINUX2 + uses: actions/cache@v4 with: path: bazel-bin/pkg key: ${{ steps.cache-key.outputs.cache-key }} @@ -270,7 +255,7 @@ jobs: sudo dmesg || true tail -n500 bazel-out/**/*/CMake.log || true - - name: Upload artifact + - name: Upload artifacts uses: actions/upload-artifact@v3 with: name: ${{ matrix.label }}-packages @@ -288,7 +273,7 @@ jobs: include: "${{ fromJSON(needs.metadata.outputs.matrix)['build-packages'] }}" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download artifact uses: actions/download-artifact@v3 @@ -324,7 +309,7 @@ jobs: include: "${{ fromJSON(needs.metadata.outputs.matrix)['build-images'] }}" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download artifact uses: actions/download-artifact@v3 @@ -398,7 +383,7 @@ jobs: platforms: ${{ steps.docker_platforms_arg.outputs.platforms }} build-args: | KONG_BASE_IMAGE=${{ matrix.base-image }} - KONG_ARTIFACT_PATH=bazel-bin/pkg/ + KONG_ARTIFACT_PATH=bazel-bin/pkg KONG_VERSION=${{ needs.metadata.outputs.kong-version }} RPM_PLATFORM=${{ steps.docker_rpm_platform_arg.outputs.rpm_platform }} EE_PORTS=8002 8445 8003 8446 8004 8447 @@ -601,7 +586,7 @@ jobs: - name: Get latest commit SHA on master run: | - echo "latest_sha=$(git ls-remote origin -h refs/heads/${{ github.event.inputs.default_branch }} | cut -f1)" >> $GITHUB_ENV + echo "latest_sha=$(git ls-remote origin -h refs/heads/master | cut -f1)" >> $GITHUB_ENV - name: Docker meta id: meta @@ -610,7 +595,7 @@ jobs: images: ${{ needs.metadata.outputs.docker-repository }} sep-tags: " " tags: | - type=raw,value=latest,enable=${{ matrix.label == 'ubuntu' && github.ref_name == github.event.inputs.default_branch && env.latest_sha == needs.metadata.outputs.commit-sha }} + type=raw,value=latest,enable=${{ matrix.label == 'ubuntu' && github.ref_name == 'master' && env.latest_sha == needs.metadata.outputs.commit-sha }} type=match,enable=${{ github.event_name == 'workflow_dispatch' }},pattern=\d.\d,value=${{ github.event.inputs.version }} type=match,enable=${{ github.event_name == 'workflow_dispatch' && matrix.label == 'ubuntu' }},pattern=\d.\d,value=${{ github.event.inputs.version }},suffix= type=raw,enable=${{ github.event_name == 'workflow_dispatch' }},${{ github.event.inputs.version }} diff --git a/.requirements b/.requirements index 994f2ea43c5..fd5057a57d1 100644 --- a/.requirements +++ b/.requirements @@ -4,8 +4,8 @@ OPENRESTY=1.25.3.1 OPENRESTY_SHA256=32ec1a253a5a13250355a075fe65b7d63ec45c560bbe213350f0992a57cd79df LUAROCKS=3.11.1 LUAROCKS_SHA256=c3fb3d960dffb2b2fe9de7e3cb004dc4d0b34bb3d342578af84f84325c669102 -OPENSSL=3.2.1 -OPENSSL_SHA256=83c7329fe52c850677d75e5d0b0ca245309b97e8ecbcfdc1dfdc4ab9fac35b39 +OPENSSL=3.2.2 +OPENSSL_SHA256=197149c18d9e9f292c43f0400acaba12e5f52cacfe050f3d199277ea738ec2e7 PCRE=10.43 PCRE_SHA256=889d16be5abb8d05400b33c25e151638b8d4bac0e2d9c76e9d6923118ae8a34e LIBEXPAT=2.6.2 @@ -16,6 +16,7 @@ LIBEXPAT_SHA256=d4cf38d26e21a56654ffe4acd9cd5481164619626802328506a2869afab29ab3 LUA_KONG_NGINX_MODULE=a8411f7cf4289049f0bd3e8e40088e7256389ed3 # 0.11.0 LUA_RESTY_LMDB=7d2581cbe30cde18a8482d820c227ca0845c0ded # 1.4.2 LUA_RESTY_EVENTS=2dcd1d7a256c53103c0fdbe804f419174e0ea8ba # 0.3.0 +LUA_RESTY_SIMDJSON=b861c98d50ab75b6c2fc6e875a5ea23143dc4157 # 1.0.0 LUA_RESTY_WEBSOCKET=966c69c39f03029b9b42ec0f8e55aaed7d6eebc0 # 0.4.0.1 ATC_ROUTER=ffd11db657115769bf94f0c4f915f98300bc26b6 # 1.6.2 SNAPPY=23b3286820105438c5dbb9bc22f1bb85c5812c8a # 1.2.0 diff --git a/BUILD.bazel b/BUILD.bazel index aac3609fcb9..20c265c370e 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -41,15 +41,6 @@ nfpm_pkg( visibility = ["//visibility:public"], ) -nfpm_pkg( - name = "kong_apk", - config = "//build:package/nfpm.yaml", - env = nfpm_env, - packager = "apk", - pkg_name = "kong", - visibility = ["//visibility:public"], -) - nfpm_pkg( name = "kong_el9", config = "//build:package/nfpm.yaml", @@ -68,18 +59,6 @@ nfpm_pkg( visibility = ["//visibility:public"], ) -nfpm_pkg( - name = "kong_el7", - config = "//build:package/nfpm.yaml", - env = nfpm_env, - extra_env = { - "RPM_EXTRA_DEPS": "hostname", - }, - packager = "rpm", - pkg_name = "kong.el7", - visibility = ["//visibility:public"], -) - nfpm_pkg( name = "kong_aws2", config = "//build:package/nfpm.yaml", @@ -148,6 +127,20 @@ config_setting( visibility = ["//visibility:public"], ) +# --//:simdjson=true +bool_flag( + name = "simdjson", + build_setting_default = True, +) + +config_setting( + name = "simdjson_flag", + flag_values = { + ":simdjson": "true", + }, + visibility = ["//visibility:public"], +) + # --//:licensing=false bool_flag( name = "licensing", @@ -245,20 +238,6 @@ string_flag( visibility = ["//visibility:public"], ) -# --//:wasmx_el7_workaround=false -bool_flag( - name = "wasmx_el7_workaround", - build_setting_default = False, -) - -config_setting( - name = "wasmx_el7_workaround_flag", - flag_values = { - ":wasmx_el7_workaround": "true", - }, - visibility = ["//visibility:public"], -) - # --//:skip_tools=false bool_flag( name = "skip_tools", @@ -303,38 +282,6 @@ platform( ], ) -# backward compatibility -alias( - name = "ubuntu-22.04-arm64", - actual = ":generic-crossbuild-aarch64", -) - -platform( - name = "alpine-crossbuild-x86_64", - constraint_values = [ - "@platforms//os:linux", - "@platforms//cpu:x86_64", - "//build/platforms/distro:alpine", - ":cross_build", - ], -) - -# backward compatibility -alias( - name = "alpine-x86_64", - actual = ":alpine-crossbuild-x86_64", -) - -platform( - name = "alpine-crossbuild-aarch64", - constraint_values = [ - "@platforms//os:linux", - "@platforms//cpu:aarch64", - "//build/platforms/distro:alpine", - ":cross_build", - ], -) - [ platform( name = vendor + "-crossbuild-aarch64", @@ -348,10 +295,20 @@ platform( for vendor in aarch64_glibc_distros ] +platform( + name = "aws2-crossbuild-x86_64", + constraint_values = [ + "@platforms//os:linux", + "@platforms//cpu:x86_64", + "//build/platforms/distro:aws2", + ":cross_build", + ], +) + # config_settings define a select() condition based on user-set constraint_values # see https://bazel.build/docs/configurable-attributes config_setting( - name = "aarch64-linux-anylibc-cross", + name = "aarch64-linux-glibc-cross", constraint_values = [ "@platforms//os:linux", "@platforms//cpu:aarch64", @@ -361,11 +318,10 @@ config_setting( ) config_setting( - name = "x86_64-linux-musl-cross", + name = "x86_64-linux-glibc-cross", constraint_values = [ "@platforms//os:linux", "@platforms//cpu:x86_64", - "//build/platforms/distro:alpine", ":cross_build", ], visibility = ["//visibility:public"], @@ -375,8 +331,8 @@ selects.config_setting_group( # matches all cross build platforms name = "any-cross", match_any = [ - ":aarch64-linux-anylibc-cross", - ":x86_64-linux-musl-cross", + ":aarch64-linux-glibc-cross", + ":x86_64-linux-glibc-cross", ], visibility = ["//visibility:public"], ) diff --git a/CHANGELOG-OLD.md b/CHANGELOG-OLD.md index 389ff247d5b..d19bab0d172 100644 --- a/CHANGELOG-OLD.md +++ b/CHANGELOG-OLD.md @@ -2,6 +2,10 @@ Looking for recent releases? Please see [CHANGELOG.md](CHANGELOG.md) instead. +- [2.8.5](#285) +- [2.8.4](#284) +- [2.8.3](#283) +- [2.8.2](#282) - [2.8.1](#281) - [2.8.0](#280) - [2.7.1](#271) @@ -65,6 +69,55 @@ Looking for recent releases? Please see [CHANGELOG.md](CHANGELOG.md) instead. - [0.10.0](#0100---20170307) - [0.9.9 and prior](#099---20170202) +## [2.8.5] + +### Kong + +#### Performance +##### Performance + +- Fixed an inefficiency issue in the Luajit hashing algorithm + [#13269](https://github.com/Kong/kong/issues/13269) + + +#### Fixes +##### Default + +- Added zlib1g-dev dependency to Ubuntu packages. + [#13269](https://github.com/Kong/kong/issues/13269) + + +## [2.8.4] + +### Fixes + +- Fixed a bug where internal redirects (i.e. those produced by the error_page directive) could interfere with worker process handling the request when buffered proxying is being used. + +## [2.8.3] + +### Fixes + +##### Plugins + +- **HTTP Log**: fix internal error during validating the schema if http_endpoint contains + userinfo but headers is empty [#9574](https://github.com/Kong/kong/pull/9574) +- Update the batch queues module so that queues no longer grow without bounds if + their consumers fail to process the entries. Instead, old batches are now dropped + and an error is logged. + [#10247](https://github.com/Kong/kong/pull/10247) + +##### CLI + +- Fixed a packaging problem affecting a subset of releases where the `kong version` + command was incorrect + +## [2.8.2] + +### Dependencies + +- Bumped `OpenSSL` from 1.1.1n to 1.1.1o + [#8635](https://github.com/Kong/kong/pull/8809) + ## [2.8.1] ### Dependencies diff --git a/Makefile b/Makefile index 2cfe608cdbb..402bdffd030 100644 --- a/Makefile +++ b/Makefile @@ -119,12 +119,8 @@ build-release: check-bazel package/deb: check-bazel build-release $(BAZEL) build --config release :kong_deb -package/apk: check-bazel build-release - $(BAZEL) build --config release :kong_apk - package/rpm: check-bazel build-release $(BAZEL) build --config release :kong_el8 --action_env=RPM_SIGNING_KEY_FILE --action_env=NFPM_RPM_PASSPHRASE - $(BAZEL) build --config release :kong_el7 --action_env=RPM_SIGNING_KEY_FILE --action_env=NFPM_RPM_PASSPHRASE $(BAZEL) build --config release :kong_aws2 --action_env=RPM_SIGNING_KEY_FILE --action_env=NFPM_RPM_PASSPHRASE $(BAZEL) build --config release :kong_aws2022 --action_env=RPM_SIGNING_KEY_FILE --action_env=NFPM_RPM_PASSPHRASE diff --git a/WORKSPACE b/WORKSPACE index ae97c320d94..32663c411a2 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -40,6 +40,10 @@ load("//build/nfpm:repositories.bzl", "nfpm_repositories") nfpm_repositories() +load("@simdjson_ffi//build:repos.bzl", "simdjson_ffi_repositories") + +simdjson_ffi_repositories() + load("@atc_router//build:repos.bzl", "atc_router_repositories") atc_router_repositories() diff --git a/build/BUILD.bazel b/build/BUILD.bazel index 3af4f65ac45..008a71ffce5 100644 --- a/build/BUILD.bazel +++ b/build/BUILD.bazel @@ -83,6 +83,11 @@ lualib_deps = [ "@atc_router//:lualib_srcs", ] +# TODO: merge into luaclib_deps once amazonlinux2 support is dropped +lualib_conditional_deps = [ + "@simdjson_ffi//:lualib_srcs", +] + [ kong_install( name = "install-%s-lualib" % get_workspace_name(k), @@ -95,13 +100,18 @@ lualib_deps = [ ] else "/lib" ), ) - for k in lualib_deps + for k in lualib_deps + lualib_conditional_deps ] luaclib_deps = [ "@atc_router", ] +# TODO: merge into luaclib_deps once amazonlinux2 support is dropped +luaclib_conditional_deps = [ + "@simdjson_ffi", +] + [ kong_install( name = "install-%s-luaclib" % get_workspace_name(k), @@ -109,7 +119,7 @@ luaclib_deps = [ prefix = "openresty/site/lualib", strip_path = get_workspace_name(k), ) - for k in luaclib_deps + for k in luaclib_deps + luaclib_conditional_deps ] kong_rules_group( @@ -120,7 +130,13 @@ kong_rules_group( ] + [ "install-%s-luaclib" % get_workspace_name(k) for k in luaclib_deps - ], + ] + select({ + "@kong//:simdjson_flag": [ + ":install-simdjson_ffi-lualib", + ":install-simdjson_ffi-luaclib", + ], + "//conditions:default": [], + }), ) # WasmX @@ -218,6 +234,7 @@ kong_genrule( ], outs = [ "bin/luarocks", + "bin/luarocks-admin", "etc/kong/kong.conf.default", "etc/luarocks", "lib", @@ -231,6 +248,7 @@ kong_genrule( LUAROCKS=${WORKSPACE_PATH}/$(dirname '$(location @luarocks//:luarocks_make)')/luarocks_tree cp -r ${LUAROCKS}/share ${LUAROCKS}/lib ${LUAROCKS}/etc ${BUILD_DESTDIR}/. cp ${LUAROCKS}/bin/luarocks ${BUILD_DESTDIR}/bin/. + cp ${LUAROCKS}/bin/luarocks-admin ${BUILD_DESTDIR}/bin/. chmod -R "u+rw" ${BUILD_DESTDIR}/share/lua mkdir -p ${BUILD_DESTDIR}/etc/kong/ diff --git a/build/README.md b/build/README.md index ea480c7a8e0..b3d224d1aa7 100644 --- a/build/README.md +++ b/build/README.md @@ -175,11 +175,9 @@ bazel build --config release //build:kong --verbose_failures Supported build targets for binary packages: - `:kong_deb` -- `:kong_el7` - `:kong_el8` - `:kong_aws2` - `:kong_aws2023` -- `:kong_apk` For example, to build the deb package: @@ -226,8 +224,9 @@ Cross compiling is currently only tested on Ubuntu 22.04 x86_64 with following t - **//:generic-crossbuild-aarch64** Use the system installed aarch64 toolchain. - Requires user to manually install `crossbuild-essential-arm64` on Debian/Ubuntu. -- **//:alpine-crossbuild-x86_64** Alpine Linux x86_64; bazel manages the build toolchain. -- **//:alpine-crossbuild-aarch64** Alpine Linux aarch64; bazel manages the build toolchain. +- **//:vendor_name-crossbuild-aarch64** Target to Redhat based Linux aarch64; bazel manages the build toolchain, `vendor_name` +can be any of `rhel8`, `rhel9`, `aws2` or `aws2023`. +- **//:aws2-crossbuild-x86_64** Target to AmazonLinux 2 x86_64; bazel manages the build toolchain. Make sure platforms are selected both in building Kong and packaging kong: diff --git a/build/cross_deps/README.md b/build/cross_deps/README.md new file mode 100644 index 00000000000..e2d52bf33b2 --- /dev/null +++ b/build/cross_deps/README.md @@ -0,0 +1,25 @@ +# Dependencies for cross build + +When cross building Kong (the target architecture is different from the host), +we need to build some extra dependencies to produce headers and dynamic libraries +to let compiler and linker work properly. + +Following are the dependencies: +- libxcrypt +- libyaml +- zlib + +Note that the artifacts of those dependencies are only used during build time, +they are not shipped together with our binary artifact (.deb, .rpm or docker image etc). + +We currently do cross compile on following platforms: +- Amazonlinux 2 +- Amazonlinux 2023 +- Ubuntu 18.04 (Version 3.4.x.x only) +- Ubuntu 22.04 +- RHEL 9 +- Debian 12 + +As we do not use different versions in different distros just for simplicity, the version +of those dependencies should remain the lowest among all distros originally shipped, to +allow the produced artifacts has lowest ABI/API to be compatible across all distros. \ No newline at end of file diff --git a/build/cross_deps/libxcrypt/BUILD.libxcrypt.bazel b/build/cross_deps/libxcrypt/BUILD.libxcrypt.bazel index 933172eec78..f1edd9a56d3 100644 --- a/build/cross_deps/libxcrypt/BUILD.libxcrypt.bazel +++ b/build/cross_deps/libxcrypt/BUILD.libxcrypt.bazel @@ -26,11 +26,11 @@ configure_make( configure_command = "configure", configure_in_place = True, configure_options = select({ - "@kong//:aarch64-linux-anylibc-cross": [ - "--host=aarch64-linux", + "@kong//:aarch64-linux-glibc-cross": [ + "--host=aarch64-unknown-linux-gnu", ], - "@kong//:x86_64-linux-musl-cross": [ - "--host=x86_64-linux-musl", + "@kong//:x86_64-linux-glibc-cross": [ + "--host=x86_64-unknown-linux-gnu", ], "//conditions:default": [], }) + select({ diff --git a/build/cross_deps/libxcrypt/repositories.bzl b/build/cross_deps/libxcrypt/repositories.bzl index f6c28d02244..ec6d450ba46 100644 --- a/build/cross_deps/libxcrypt/repositories.bzl +++ b/build/cross_deps/libxcrypt/repositories.bzl @@ -9,6 +9,7 @@ def libxcrypt_repositories(): # thus crypt.h and libcrypt.so.1 are missing from cross tool chain # ubuntu2004: 4.4.10 # ubuntu2204: 4.4.27 + # NOTE: do not bump the following version, see build/cross_deps/README.md for detail. http_archive( name = "cross_deps_libxcrypt", url = "https://github.com/besser82/libxcrypt/releases/download/v4.4.27/libxcrypt-4.4.27.tar.xz", diff --git a/build/cross_deps/libyaml/BUILD.libyaml.bazel b/build/cross_deps/libyaml/BUILD.libyaml.bazel index ad4e48560df..f192f0788a7 100644 --- a/build/cross_deps/libyaml/BUILD.libyaml.bazel +++ b/build/cross_deps/libyaml/BUILD.libyaml.bazel @@ -14,11 +14,11 @@ configure_make( configure_command = "configure", configure_in_place = True, configure_options = select({ - "@kong//:aarch64-linux-anylibc-cross": [ - "--host=aarch64-linux", + "@kong//:aarch64-linux-glibc-cross": [ + "--host=aarch64-unknown-linux-gnu", ], - "@kong//:x86_64-linux-musl-cross": [ - "--host=x86_64-linux-musl", + "@kong//:x86_64-linux-glibc-cross": [ + "--host=x86_64-unknown-linux-gnu", ], "//conditions:default": [], }), diff --git a/build/cross_deps/libyaml/repositories.bzl b/build/cross_deps/libyaml/repositories.bzl index b7b2800cf96..dffd0798cde 100644 --- a/build/cross_deps/libyaml/repositories.bzl +++ b/build/cross_deps/libyaml/repositories.bzl @@ -6,6 +6,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") def libyaml_repositories(): """Defines the libyaml repository""" + # NOTE: do not bump the following version, see build/cross_deps/README.md for detail. http_archive( name = "cross_deps_libyaml", url = "https://pyyaml.org/download/libyaml/yaml-0.2.5.tar.gz", diff --git a/build/cross_deps/zlib/repositories.bzl b/build/cross_deps/zlib/repositories.bzl index 3185b65222a..325c23a5ac3 100644 --- a/build/cross_deps/zlib/repositories.bzl +++ b/build/cross_deps/zlib/repositories.bzl @@ -6,6 +6,7 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") def zlib_repositories(): """Defines the zlib repository""" + # NOTE: do not bump the following version, see build/cross_deps/README.md for detail. http_archive( name = "cross_deps_zlib", urls = [ diff --git a/build/dockerfiles/apk.Dockerfile b/build/dockerfiles/apk.Dockerfile deleted file mode 100644 index fb3901a62d3..00000000000 --- a/build/dockerfiles/apk.Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -ARG KONG_BASE_IMAGE=alpine:3.16 -FROM --platform=$TARGETPLATFORM $KONG_BASE_IMAGE - -LABEL maintainer="Kong Docker Maintainers (@team-gateway-bot)" - -ARG KONG_VERSION -ENV KONG_VERSION $KONG_VERSION - -ARG KONG_PREFIX=/usr/local/kong -ENV KONG_PREFIX $KONG_PREFIX - -ARG EE_PORTS - -ARG TARGETARCH - -ARG KONG_ARTIFACT=kong.${TARGETARCH}.apk.tar.gz -ARG KONG_ARTIFACT_PATH= -COPY ${KONG_ARTIFACT_PATH}${KONG_ARTIFACT} /tmp/kong.apk.tar.gz - -RUN apk upgrade --update-cache \ - && apk add --virtual .build-deps tar gzip \ - && tar -C / -xzf /tmp/kong.apk.tar.gz \ - && apk add --no-cache libstdc++ libgcc perl tzdata libcap zlib zlib-dev bash yaml \ - && adduser -S kong \ - && addgroup -S kong \ - && mkdir -p "${KONG_PREFIX}" \ - && chown -R kong:0 ${KONG_PREFIX} \ - && chown kong:0 /usr/local/bin/kong \ - && chmod -R g=u ${KONG_PREFIX} \ - && rm -rf /tmp/kong.apk.tar.gz \ - && ln -sf /usr/local/openresty/bin/resty /usr/local/bin/resty \ - && ln -sf /usr/local/openresty/luajit/bin/luajit /usr/local/bin/luajit \ - && ln -sf /usr/local/openresty/luajit/bin/luajit /usr/local/bin/lua \ - && ln -sf /usr/local/openresty/nginx/sbin/nginx /usr/local/bin/nginx \ - && apk del .build-deps \ - && kong version - -COPY build/dockerfiles/entrypoint.sh /entrypoint.sh - -USER kong - -ENTRYPOINT ["/entrypoint.sh"] - -EXPOSE 8000 8443 8001 8444 $EE_PORTS - -STOPSIGNAL SIGQUIT - -HEALTHCHECK --interval=60s --timeout=10s --retries=10 CMD kong-health - -CMD ["kong", "docker-start"] diff --git a/build/dockerfiles/deb.Dockerfile b/build/dockerfiles/deb.Dockerfile index c25cbadd5d5..ab81d5f0498 100644 --- a/build/dockerfiles/deb.Dockerfile +++ b/build/dockerfiles/deb.Dockerfile @@ -14,16 +14,15 @@ ARG EE_PORTS ARG TARGETARCH ARG KONG_ARTIFACT=kong.${TARGETARCH}.deb -ARG KONG_ARTIFACT_PATH= -COPY ${KONG_ARTIFACT_PATH}${KONG_ARTIFACT} /tmp/kong.deb +ARG KONG_ARTIFACT_PATH -RUN apt-get update \ +RUN --mount=type=bind,source=${KONG_ARTIFACT_PATH},target=/tmp/pkg \ + apt-get update \ && apt-get -y upgrade \ && apt-get -y autoremove \ && DEBIAN_FRONTEND=noninteractive apt-get install -y tzdata \ - && apt-get install -y --no-install-recommends /tmp/kong.deb \ + && apt-get install -y --no-install-recommends /tmp/pkg/${KONG_ARTIFACT} \ && rm -rf /var/lib/apt/lists/* \ - && rm -rf /tmp/kong.deb \ && chown kong:0 /usr/local/bin/kong \ && chown -R kong:0 ${KONG_PREFIX} \ && ln -sf /usr/local/openresty/bin/resty /usr/local/bin/resty \ diff --git a/build/dockerfiles/entrypoint.sh b/build/dockerfiles/entrypoint.sh index 4aa71dedaa1..11a2e530ece 100755 --- a/build/dockerfiles/entrypoint.sh +++ b/build/dockerfiles/entrypoint.sh @@ -46,11 +46,12 @@ if [[ "$1" == "kong" ]]; then # remove all dangling sockets in $PREFIX dir before starting Kong LOGGED_SOCKET_WARNING=0 - for localfile in "$PREFIX"/*; do + socket_path=$PREFIX/sockets + for localfile in "$socket_path"/*; do if [ -S "$localfile" ]; then if (( LOGGED_SOCKET_WARNING == 0 )); then printf >&2 'WARN: found dangling unix sockets in the prefix directory ' - printf >&2 '(%q) ' "$PREFIX" + printf >&2 '(%q) ' "$socket_path" printf >&2 'while preparing to start Kong. This may be a sign that Kong ' printf >&2 'was previously shut down uncleanly or is in an unknown state ' printf >&2 'and could require further investigation.\n' diff --git a/build/dockerfiles/rpm.Dockerfile b/build/dockerfiles/rpm.Dockerfile index 958140c9830..f05102afa2a 100644 --- a/build/dockerfiles/rpm.Dockerfile +++ b/build/dockerfiles/rpm.Dockerfile @@ -28,13 +28,12 @@ ARG EE_PORTS ARG TARGETARCH ARG KONG_ARTIFACT=kong.${RPM_PLATFORM}.${TARGETARCH}.rpm -ARG KONG_ARTIFACT_PATH= -COPY ${KONG_ARTIFACT_PATH}${KONG_ARTIFACT} /tmp/kong.rpm +ARG KONG_ARTIFACT_PATH # hadolint ignore=DL3015 -RUN yum update -y \ - && yum install -y /tmp/kong.rpm \ - && rm /tmp/kong.rpm \ +RUN --mount=type=bind,source=${KONG_ARTIFACT_PATH},target=/tmp/pkg \ + yum update -y \ + && yum install -y /tmp/pkg/${KONG_ARTIFACT} \ && chown kong:0 /usr/local/bin/kong \ && chown -R kong:0 /usr/local/kong \ && ln -sf /usr/local/openresty/bin/resty /usr/local/bin/resty \ diff --git a/build/libexpat/BUILD.libexpat.bazel b/build/libexpat/BUILD.libexpat.bazel index c6e14a17ef3..ac3da072c8c 100644 --- a/build/libexpat/BUILD.libexpat.bazel +++ b/build/libexpat/BUILD.libexpat.bazel @@ -24,11 +24,11 @@ configure_make( "--without-examples", "--without-docbook", ] + select({ - "@kong//:aarch64-linux-anylibc-cross": [ - "--host=aarch64-linux", + "@kong//:aarch64-linux-glibc-cross": [ + "--host=aarch64-unknown-linux-gnu", ], - "@kong//:x86_64-linux-musl-cross": [ - "--host=x86_64-linux-musl", + "@kong//:x86_64-linux-glibc-cross": [ + "--host=x86_64-unknown-linux-gnu", ], "//conditions:default": [], }), diff --git a/build/luarocks/templates/luarocks_exec.sh b/build/luarocks/templates/luarocks_exec.sh index ad146f24013..2451635b89c 100644 --- a/build/luarocks/templates/luarocks_exec.sh +++ b/build/luarocks/templates/luarocks_exec.sh @@ -73,7 +73,6 @@ ROCKS_CONFIG=$ROCKS_CONFIG export LUAROCKS_CONFIG=$ROCKS_CONFIG export CC=$CC export LD=$LD -export EXT_BUILD_ROOT=$root_path # for musl # no idea why PATH is not preserved in ctx.actions.run_shell export PATH=$PATH diff --git a/build/nfpm/rules.bzl b/build/nfpm/rules.bzl index d6f5bb94f46..cb5999bd874 100644 --- a/build/nfpm/rules.bzl +++ b/build/nfpm/rules.bzl @@ -21,8 +21,6 @@ def _nfpm_pkg_impl(ctx): env["OPENRESTY_PATCHES"] = "" pkg_ext = ctx.attr.packager - if pkg_ext == "apk": - pkg_ext = "apk.tar.gz" # create like kong.amd64.deb out = ctx.actions.declare_file("%s/%s.%s.%s" % ( diff --git a/build/openresty/BUILD.openresty.bazel b/build/openresty/BUILD.openresty.bazel index 4b6aa4292b1..700ebe22d7f 100644 --- a/build/openresty/BUILD.openresty.bazel +++ b/build/openresty/BUILD.openresty.bazel @@ -172,7 +172,7 @@ CONFIGURE_OPTIONS = [ "--add-module=$$EXT_BUILD_ROOT$$/external/lua-resty-lmdb", "--add-module=$$EXT_BUILD_ROOT$$/external/lua-resty-events", ] + select({ - "@kong//:aarch64-linux-anylibc-cross": [ + "@kong//:aarch64-linux-glibc-cross": [ "--crossbuild=Linux:aarch64", "--with-endian=little", "--with-int=4", @@ -185,7 +185,7 @@ CONFIGURE_OPTIONS = [ "--with-time-t=8", "--with-sys-nerr=132", ], - "@kong//:x86_64-linux-musl-cross": [ + "@kong//:x86_64-linux-glibc-cross": [ "--crossbuild=Linux:x86_64", "--with-endian=little", "--with-int=4", @@ -206,8 +206,6 @@ CONFIGURE_OPTIONS = [ ], "//conditions:default": [], }) + select({ - # any cross build that migrated to use libxcrypt needs those flags - # alpine uses different libc so doesn't need it ":needs-xcrypt2": [ "--with-cc-opt=\"-I$$EXT_BUILD_DEPS$$/libxcrypt/include\"", "--with-ld-opt=\"-L$$EXT_BUILD_DEPS$$/libxcrypt/lib\"", @@ -268,8 +266,12 @@ configure_make( "@lua-kong-nginx-module//:all_srcs", "@lua-resty-events//:all_srcs", "@lua-resty-lmdb//:all_srcs", - "@ngx_brotli//:all_srcs", ] + select({ + "@kong//:brotli_flag": [ + "@ngx_brotli//:all_srcs", + ], + "//conditions:default": [], + }) + select({ "@kong//:wasmx_flag": [ "@ngx_wasmx_module//:all_srcs", # wasm_runtime has to be a "data" (target) instead of "build_data" (exec) @@ -319,7 +321,6 @@ configure_make( "//conditions:default": [], }) + select({ # any cross build that migrated to use libxcrypt needs those flags - # alpine uses different libc so doesn't need it ":needs-xcrypt2": [ "@cross_deps_libxcrypt//:libxcrypt", ], diff --git a/build/openresty/openssl/openssl.bzl b/build/openresty/openssl/openssl.bzl index a9bf1a8de4d..d17582b5ba5 100644 --- a/build/openresty/openssl/openssl.bzl +++ b/build/openresty/openssl/openssl.bzl @@ -11,10 +11,13 @@ load("@kong_bindings//:variables.bzl", "KONG_VAR") # Read https://wiki.openssl.org/index.php/Compilation_and_Installation CONFIGURE_OPTIONS = select({ - "@kong//:aarch64-linux-anylibc-cross": [ + "@kong//:aarch64-linux-glibc-cross": [ "linux-aarch64", ], - # no extra args needed for "@kong//:x86_64-linux-musl-cross" or non-cross builds + "@kong//:x86_64-linux-glibc-cross": [ + "linux-x86_64", + ], + # no extra args needed for non-cross builds "//conditions:default": [], }) + [ "-g", diff --git a/build/openresty/openssl/openssl_repositories.bzl b/build/openresty/openssl/openssl_repositories.bzl index 2d2171fc66f..7cb96f59a84 100644 --- a/build/openresty/openssl/openssl_repositories.bzl +++ b/build/openresty/openssl/openssl_repositories.bzl @@ -7,6 +7,11 @@ load("@kong_bindings//:variables.bzl", "KONG_VAR") def openssl_repositories(): version = KONG_VAR["OPENSSL"] + openssl_verion_uri = version + if version.startswith("3"): + # for 3.x only use the first two digits + openssl_verion_uri = ".".join(version.split(".")[:2]) + maybe( http_archive, name = "openssl", @@ -14,7 +19,6 @@ def openssl_repositories(): sha256 = KONG_VAR["OPENSSL_SHA256"], strip_prefix = "openssl-" + version, urls = [ - "https://www.openssl.org/source/openssl-" + version + ".tar.gz", "https://github.com/openssl/openssl/releases/download/openssl-" + version + "/openssl-" + version + ".tar.gz", ], ) diff --git a/build/openresty/patches/nginx-1.25.3_07-fix-lua-context-clean-by-send-header.patch b/build/openresty/patches/nginx-1.25.3_07-fix-lua-context-clean-by-send-header.patch new file mode 100644 index 00000000000..4db81ee59cb --- /dev/null +++ b/build/openresty/patches/nginx-1.25.3_07-fix-lua-context-clean-by-send-header.patch @@ -0,0 +1,45 @@ +diff --git a/bundle/ngx_lua-0.10.26/src/ngx_http_lua_util.c b/bundle/ngx_lua-0.10.26/src/ngx_http_lua_util.c +index 8fd2656..b2fdb6c 100644 +--- a/bundle/ngx_lua-0.10.26/src/ngx_http_lua_util.c ++++ b/bundle/ngx_lua-0.10.26/src/ngx_http_lua_util.c +@@ -549,6 +549,10 @@ ngx_http_lua_send_header_if_needed(ngx_http_request_t *r, + if (!ctx->buffering) { + dd("sending headers"); + rc = ngx_http_send_header(r); ++ if (r->filter_finalize) { ++ ngx_http_set_ctx(r, ctx, ngx_http_lua_module); ++ } ++ + ctx->header_sent = 1; + return rc; + } +diff --git a/bundle/ngx_lua-0.10.26/t/002-content.t b/bundle/ngx_lua-0.10.26/t/002-content.t +index 54de40e..eb9d587 100644 +--- a/bundle/ngx_lua-0.10.26/t/002-content.t ++++ b/bundle/ngx_lua-0.10.26/t/002-content.t +@@ -1098,3 +1098,25 @@ failed to load inlined Lua code: content_by_lua(...45678901234567890123456789012 + GET /lua + --- response_body_like: 503 Service Temporarily Unavailable + --- error_code: 503 ++ ++ ++ ++=== TEST 52: send_header trigger filter finalize does not clear the ctx ++--- config ++ location /lua { ++ content_by_lua_block { ++ ngx.header["Last-Modified"] = ngx.http_time(ngx.time()) ++ ngx.send_headers() ++ local phase = ngx.get_phase() ++ } ++ header_filter_by_lua_block { ++ ngx.header["X-Hello-World"] = "Hello World" ++ } ++ } ++--- request ++GET /lua ++--- more_headers ++If-Unmodified-Since: Wed, 01 Jan 2020 07:28:00 GMT ++--- error_code: 412 ++--- no_error_log ++unknown phase: 0 diff --git a/build/openresty/repositories.bzl b/build/openresty/repositories.bzl index 0d992fd3fd9..3b01aa23901 100644 --- a/build/openresty/repositories.bzl +++ b/build/openresty/repositories.bzl @@ -6,6 +6,7 @@ load("//build:build_system.bzl", "git_or_local_repository") load("@kong_bindings//:variables.bzl", "KONG_VAR") load("//build/openresty/pcre:pcre_repositories.bzl", "pcre_repositories") load("//build/openresty/openssl:openssl_repositories.bzl", "openssl_repositories") +load("//build/openresty/simdjson_ffi:simdjson_ffi_repositories.bzl", "simdjson_ffi_repositories") load("//build/openresty/atc_router:atc_router_repositories.bzl", "atc_router_repositories") load("//build/openresty/wasmx:wasmx_repositories.bzl", "wasmx_repositories") load("//build/openresty/wasmx/filters:repositories.bzl", "wasm_filters_repositories") @@ -30,6 +31,7 @@ filegroup( def openresty_repositories(): pcre_repositories() openssl_repositories() + simdjson_ffi_repositories() atc_router_repositories() wasmx_repositories() wasm_filters_repositories() diff --git a/build/openresty/simdjson_ffi/BUILD.bazel b/build/openresty/simdjson_ffi/BUILD.bazel new file mode 100644 index 00000000000..e69de29bb2d diff --git a/build/openresty/simdjson_ffi/simdjson_ffi_repositories.bzl b/build/openresty/simdjson_ffi/simdjson_ffi_repositories.bzl new file mode 100644 index 00000000000..0d9649c9560 --- /dev/null +++ b/build/openresty/simdjson_ffi/simdjson_ffi_repositories.bzl @@ -0,0 +1,11 @@ +"""A module defining the dependency lua-resty-simdjson""" + +load("//build:build_system.bzl", "git_or_local_repository") +load("@kong_bindings//:variables.bzl", "KONG_VAR") + +def simdjson_ffi_repositories(): + git_or_local_repository( + name = "simdjson_ffi", + branch = KONG_VAR["LUA_RESTY_SIMDJSON"], + remote = "https://github.com/Kong/lua-resty-simdjson", + ) diff --git a/build/openresty/wasmx/rules.bzl b/build/openresty/wasmx/rules.bzl index 97865a7d78b..912a8b1e314 100644 --- a/build/openresty/wasmx/rules.bzl +++ b/build/openresty/wasmx/rules.bzl @@ -1,15 +1,6 @@ load("//build/openresty/wasmx:wasmx_repositories.bzl", "wasm_runtimes") wasmx_configure_options = select({ - "@kong//:wasmx_el7_workaround_flag": [ - # bypass "multiple definitions of 'assertions'" linker error from wasm.h: - # https://github.com/WebAssembly/wasm-c-api/blob/master/include/wasm.h#L29 - # and ensure a more recent libstdc++ is found - # https://github.com/Kong/ngx_wasm_module/blob/main/assets/release/Dockerfiles/Dockerfile.amd64.centos7#L28-L31 - "--with-ld-opt=\"-Wl,--allow-multiple-definition -L/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8\"", - ], - "//conditions:default": [], -}) + select({ "@kong//:wasmx_flag": [ "--with-cc-opt=\"-DNGX_WASM_HOST_PROPERTY_NAMESPACE=kong\"", ], diff --git a/build/package/nfpm.yaml b/build/package/nfpm.yaml index 2206a99946b..559baa286f8 100644 --- a/build/package/nfpm.yaml +++ b/build/package/nfpm.yaml @@ -32,8 +32,8 @@ contents: - src: nfpm-prefix/share dst: /usr/local/share type: tree -- src: nfpm-prefix/etc/kong - dst: /etc/kong +- dst: /etc/kong + type: dir - src: bin/kong dst: /usr/local/bin/kong - src: bin/kong-health @@ -46,8 +46,12 @@ contents: dst: /usr/local/kong/filters/proxy_wasm_key_auth.meta.json - src: build/package/kong.logrotate dst: /etc/kong/kong.logrotate + type: config|noreplace file_info: mode: 0644 +- src: nfpm-prefix/etc/kong/kong.conf.default + dst: /etc/kong/kong.conf.default + type: config - src: /usr/local/openresty/bin/resty dst: /usr/local/bin/resty type: symlink @@ -82,11 +86,10 @@ overrides: - ${RPM_EXTRA_DEPS} - ${RPM_EXTRA_DEPS_2} - ${RPM_EXTRA_DEPS_3} - apk: - depends: - - ca-certificates rpm: + prefixes: + - /usr/local signature: # PGP secret key (can also be ASCII-armored), the passphrase is taken # from the environment variable $NFPM_RPM_PASSPHRASE with a fallback diff --git a/build/platforms/distro/BUILD b/build/platforms/distro/BUILD index 4816ca427a8..773ee534dd4 100644 --- a/build/platforms/distro/BUILD +++ b/build/platforms/distro/BUILD @@ -6,12 +6,6 @@ constraint_value( visibility = ["//visibility:public"], ) -constraint_value( - name = "alpine", - constraint_setting = ":distro", - visibility = ["//visibility:public"], -) - constraint_value( name = "rhel9", constraint_setting = ":distro", diff --git a/build/toolchain/BUILD b/build/toolchain/BUILD index be5811521f1..cfa7cc4ab9b 100644 --- a/build/toolchain/BUILD +++ b/build/toolchain/BUILD @@ -49,18 +49,10 @@ cc_toolchain( define_managed_toolchain( arch = "x86_64", - gcc_version = "11", - libc = "musl", - target_compatible_with = ["//build/platforms/distro:alpine"], - vendor = "alpine", -) - -define_managed_toolchain( - arch = "aarch64", - gcc_version = "11", - libc = "musl", - target_compatible_with = ["//build/platforms/distro:alpine"], - vendor = "alpine", + gcc_version = aarch64_glibc_distros["aws2"], + libc = "gnu", + target_compatible_with = ["//build/platforms/distro:aws2"], + vendor = "aws2", ) [ diff --git a/build/toolchain/cc_toolchain_config.bzl b/build/toolchain/cc_toolchain_config.bzl index f72fb3f4b33..60b7e12c88a 100644 --- a/build/toolchain/cc_toolchain_config.bzl +++ b/build/toolchain/cc_toolchain_config.bzl @@ -70,7 +70,14 @@ def _cc_toolchain_config_impl(ctx): # file is something like external/aarch64-rhel9-linux-gnu-gcc-11/aarch64-rhel9-linux-gnu/bin/ar # we will take aarch64-rhel9-linux-gnu-gcc-11/aarch64-rhel9-linux-gnu - toolchain_path_prefix = INTERNAL_ROOT + "/" + "/".join(ctx.files.src[0].path.split("/")[1:3]) + ar_path = None + for f in ctx.files.src: + if f.path.endswith("bin/ar"): + ar_path = f.path + break + if not ar_path: + fail("Cannot find ar in the toolchain") + toolchain_path_prefix = INTERNAL_ROOT + "/" + "/".join(ar_path.split("/")[1:3]) _tools_root_dir = INTERNAL_ROOT + "/" + ctx.files.src[0].path.split("/")[1] tools_prefix = _tools_root_dir + "/bin/" + tools_prefix diff --git a/build/toolchain/managed_toolchain.bzl b/build/toolchain/managed_toolchain.bzl index 33c9001fc5a..7d603ad22b9 100644 --- a/build/toolchain/managed_toolchain.bzl +++ b/build/toolchain/managed_toolchain.bzl @@ -78,16 +78,9 @@ def register_all_toolchains(name = None): register_managed_toolchain( arch = "x86_64", - gcc_version = "11", - libc = "musl", - vendor = "alpine", - ) - - register_managed_toolchain( - arch = "aarch64", - gcc_version = "11", - libc = "musl", - vendor = "alpine", + gcc_version = "7", + libc = "gnu", + vendor = "aws2", ) for vendor in aarch64_glibc_distros: diff --git a/build/toolchain/repositories.bzl b/build/toolchain/repositories.bzl index 19e7e2510ee..a8a7c0a1b9a 100644 --- a/build/toolchain/repositories.bzl +++ b/build/toolchain/repositories.bzl @@ -21,50 +21,42 @@ filegroup( """ def toolchain_repositories(): - http_archive( - name = "x86_64-alpine-linux-musl-gcc-11", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.4.0/x86_64-alpine-linux-musl-gcc-11.tar.gz", - sha256 = "4fbc9a48f1f7ace6d2a19a1feeac1f69cf86ce8ece40b101e351d1f703b3560c", - strip_prefix = "x86_64-alpine-linux-musl", - build_file_content = build_file_content, - ) - - http_archive( - name = "aarch64-alpine-linux-musl-gcc-11", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.4.0/aarch64-alpine-linux-musl-gcc-11.tar.gz", - sha256 = "abd7003fc4aa6d533c5aad97a5726040137f580026b1db78d3a8059a69c3d45b", - strip_prefix = "aarch64-alpine-linux-musl", - build_file_content = build_file_content, - ) - http_archive( name = "aarch64-rhel9-linux-gnu-gcc-11", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.5.0/aarch64-rhel9-linux-gnu-glibc-2.34-gcc-11.tar.gz", - sha256 = "40fcf85e8315869621573512499aa3e2884283e0054dfefc2bad3bbf21b954c0", + url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.7.0/aarch64-rhel9-linux-gnu-glibc-2.34-gcc-11.tar.gz", + sha256 = "8db520adb98f43dfe3da5d51e09679b85956e3a11362d7cba37a85065e87fcf7", strip_prefix = "aarch64-rhel9-linux-gnu", build_file_content = build_file_content, ) http_archive( name = "aarch64-rhel8-linux-gnu-gcc-8", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.5.0/aarch64-rhel8-linux-gnu-glibc-2.28-gcc-8.tar.gz", - sha256 = "7a9a28ccab6d3b068ad49b2618276707e0a31b437ad010c8969ba8660ddf63fb", + url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.7.0/aarch64-rhel8-linux-gnu-glibc-2.28-gcc-8.tar.gz", + sha256 = "de41ca31b6a056bddd770b4cb50fe8e8c31e8faa9ce857771ab7410a954d1cbe", strip_prefix = "aarch64-rhel8-linux-gnu", build_file_content = build_file_content, ) http_archive( name = "aarch64-aws2023-linux-gnu-gcc-11", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.5.0/aarch64-aws2023-linux-gnu-glibc-2.34-gcc-11.tar.gz", - sha256 = "01498b49c20255dd3d5da733fa5d60b5dad4b1cdd55e50552d8f2867f3d82e98", + url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.7.0/aarch64-aws2023-linux-gnu-glibc-2.34-gcc-11.tar.gz", + sha256 = "c0333ba0934b32f59ab9c3076c47785c94413aae264cc2ee78d6d5fd46171a9d", strip_prefix = "aarch64-aws2023-linux-gnu", build_file_content = build_file_content, ) http_archive( name = "aarch64-aws2-linux-gnu-gcc-7", - url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.5.0/aarch64-aws2-linux-gnu-glibc-2.26-gcc-7.tar.gz", - sha256 = "9a8d0bb84c3eea7b662192bf44aaf33a76c9c68848a68a544a91ab90cd8cba60", + url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.7.0/aarch64-aws2-linux-gnu-glibc-2.26-gcc-7.tar.gz", + sha256 = "de365a366b5de93b0f6d851746e7ced06946b083b390500d4c1b4a8360702331", strip_prefix = "aarch64-aws2-linux-gnu", build_file_content = build_file_content, ) + + http_archive( + name = "x86_64-aws2-linux-gnu-gcc-7", + url = "https://github.com/Kong/crosstool-ng-actions/releases/download/0.7.0/x86_64-aws2-linux-gnu-glibc-2.26-gcc-7.tar.gz", + sha256 = "645c242d13bf456ca59a7e9701e9d2f53336fd0497ccaff2b151da9921469985", + strip_prefix = "x86_64-aws2-linux-gnu", + build_file_content = build_file_content, + ) diff --git a/changelog/unreleased/kong/acl-always-use-authenticated-groups.yml b/changelog/unreleased/kong/acl-always-use-authenticated-groups.yml new file mode 100644 index 00000000000..a2da56e2fc6 --- /dev/null +++ b/changelog/unreleased/kong/acl-always-use-authenticated-groups.yml @@ -0,0 +1,3 @@ +message: "**acl:** Added a new config `always_use_authenticated_groups` to support using authenticated groups even when an authenticated consumer already exists." +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/add-ai-data-latency.yml b/changelog/unreleased/kong/add-ai-data-latency.yml new file mode 100644 index 00000000000..2f3c58fb05e --- /dev/null +++ b/changelog/unreleased/kong/add-ai-data-latency.yml @@ -0,0 +1,3 @@ +message: "AI plugins: retrieved latency data and pushed it to logs and metrics." +type: feature +scope: "Plugin" diff --git a/changelog/unreleased/kong/admin-api-map-brackets-syntax.yml b/changelog/unreleased/kong/admin-api-map-brackets-syntax.yml new file mode 100644 index 00000000000..74e0419aad4 --- /dev/null +++ b/changelog/unreleased/kong/admin-api-map-brackets-syntax.yml @@ -0,0 +1,3 @@ +message: "Added support for brackets syntax for map fields configuration via the Admin API" +type: feature +scope: Admin API diff --git a/changelog/unreleased/kong/ai-plugin-read-file.yml b/changelog/unreleased/kong/ai-plugin-read-file.yml new file mode 100644 index 00000000000..d10f38c021d --- /dev/null +++ b/changelog/unreleased/kong/ai-plugin-read-file.yml @@ -0,0 +1,3 @@ +message: "allow AI plugin to read request from buffered file" +type: feature +scope: "Plugin" diff --git a/changelog/unreleased/kong/ai-proxy-aws-bedrock.yml b/changelog/unreleased/kong/ai-proxy-aws-bedrock.yml new file mode 100644 index 00000000000..adc608b92b0 --- /dev/null +++ b/changelog/unreleased/kong/ai-proxy-aws-bedrock.yml @@ -0,0 +1,5 @@ +message: | + Kong AI Gateway (AI Proxy and associated plugin family) now supports + all AWS Bedrock "Converse API" models. +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/ai-proxy-google-gemini.yml b/changelog/unreleased/kong/ai-proxy-google-gemini.yml new file mode 100644 index 00000000000..bc4fb06b21c --- /dev/null +++ b/changelog/unreleased/kong/ai-proxy-google-gemini.yml @@ -0,0 +1,5 @@ +message: | + Kong AI Gateway (AI Proxy and associated plugin family) now supports + the Google Gemini "chat" (generateContent) interface. +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/bump-lua-resty-aws.yml b/changelog/unreleased/kong/bump-lua-resty-aws.yml new file mode 100644 index 00000000000..5c84bdf2075 --- /dev/null +++ b/changelog/unreleased/kong/bump-lua-resty-aws.yml @@ -0,0 +1,3 @@ +message: "Bumped lua-resty-aws to 1.5.3 to fix a bug related to STS regional endpoint." +type: dependency +scope: Core diff --git a/changelog/unreleased/kong/bump-lua-resty-openssl.yml b/changelog/unreleased/kong/bump-lua-resty-openssl.yml new file mode 100644 index 00000000000..d29ceaeb0a0 --- /dev/null +++ b/changelog/unreleased/kong/bump-lua-resty-openssl.yml @@ -0,0 +1,2 @@ +message: "Bumped lua-resty-openssl from 1.4.0 to 1.5.0" +type: dependency diff --git a/changelog/unreleased/kong/bump-ngx-wasm-module.yml b/changelog/unreleased/kong/bump-ngx-wasm-module.yml index 35d788bab6c..ca50eb2686f 100644 --- a/changelog/unreleased/kong/bump-ngx-wasm-module.yml +++ b/changelog/unreleased/kong/bump-ngx-wasm-module.yml @@ -1,2 +1,2 @@ -message: "Bumped `ngx_wasm_module` to `ae7b61150de7c14eb901307daed403b67f29e962`" +message: "Bumped `ngx_wasm_module` to `9c95991472ec80cdb1681af203eba091607b5753`" type: dependency diff --git a/changelog/unreleased/kong/bump-wasmtime.yml b/changelog/unreleased/kong/bump-wasmtime.yml new file mode 100644 index 00000000000..4f3adfb3a4d --- /dev/null +++ b/changelog/unreleased/kong/bump-wasmtime.yml @@ -0,0 +1,2 @@ +message: "Bumped `Wasmtime` version to `22.0.0`" +type: dependency diff --git a/changelog/unreleased/kong/bump_openssl.yml b/changelog/unreleased/kong/bump_openssl.yml new file mode 100644 index 00000000000..5f6167d2275 --- /dev/null +++ b/changelog/unreleased/kong/bump_openssl.yml @@ -0,0 +1,2 @@ +message: "Bumped OpenSSL to 3.2.2, to fix unbounded memory growth with session handling in TLSv1.3" +type: dependency diff --git a/changelog/unreleased/kong/certificates_schema_validate.yml b/changelog/unreleased/kong/certificates_schema_validate.yml new file mode 100644 index 00000000000..83cce82fb7d --- /dev/null +++ b/changelog/unreleased/kong/certificates_schema_validate.yml @@ -0,0 +1,3 @@ +message: "Fixed an issue where validation of the certificate schema failed if the `snis` field was present in the request body." +scope: Admin API +type: bugfix \ No newline at end of file diff --git a/changelog/unreleased/kong/cp-luarocks-admin-to-bin.yml b/changelog/unreleased/kong/cp-luarocks-admin-to-bin.yml new file mode 100644 index 00000000000..4563f041b64 --- /dev/null +++ b/changelog/unreleased/kong/cp-luarocks-admin-to-bin.yml @@ -0,0 +1,3 @@ +message: "Fixed an issue where luarocks-admin was not available in /usr/local/bin." +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/feat-ai-prompt-guard-all-roles.yml b/changelog/unreleased/kong/feat-ai-prompt-guard-all-roles.yml new file mode 100644 index 00000000000..5a1d9ca0cee --- /dev/null +++ b/changelog/unreleased/kong/feat-ai-prompt-guard-all-roles.yml @@ -0,0 +1,3 @@ +message: "**AI-Prompt-Guard**: add `match_all_roles` option to allow match all roles in addition to `user`." +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/feat-aws-lambda-configurable-sts-endpoint.yml b/changelog/unreleased/kong/feat-aws-lambda-configurable-sts-endpoint.yml new file mode 100644 index 00000000000..a39a7324102 --- /dev/null +++ b/changelog/unreleased/kong/feat-aws-lambda-configurable-sts-endpoint.yml @@ -0,0 +1,4 @@ +message: > + "**AWS-Lambda**: Added support for a configurable STS endpoint with the new configuration field `aws_sts_endpoint_url`. +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/feat-pdk-unlimited-body-size.yml b/changelog/unreleased/kong/feat-pdk-unlimited-body-size.yml new file mode 100644 index 00000000000..5cb3f291d15 --- /dev/null +++ b/changelog/unreleased/kong/feat-pdk-unlimited-body-size.yml @@ -0,0 +1,3 @@ +message: Added `0` to support unlimited body size. When parameter `max_allowed_file_size` is `0`, `get_raw_body` will return the entire body, but the size of this body will still be limited by Nginx's `client_max_body_size`. +type: feature +scope: PDK diff --git a/changelog/unreleased/kong/feat-via.yml b/changelog/unreleased/kong/feat-via.yml new file mode 100644 index 00000000000..a34263a906d --- /dev/null +++ b/changelog/unreleased/kong/feat-via.yml @@ -0,0 +1,6 @@ +message: | + Append gateway info to upstream `Via` header like `1.1 kong/3.8.0`, and optionally to + response `Via` header if it is present in the `headers` config of "kong.conf", like `2 kong/3.8.0`, + according to `RFC7230` and `RFC9110`. +type: feature +scope: Core diff --git a/changelog/unreleased/kong/fix-age-header.yml b/changelog/unreleased/kong/fix-age-header.yml deleted file mode 100644 index f3db9eba9be..00000000000 --- a/changelog/unreleased/kong/fix-age-header.yml +++ /dev/null @@ -1,3 +0,0 @@ -message: "proxy-cache response_headers age schema parameter changed from age to Age." -type: bugfix -scope: "Plugin" diff --git a/changelog/unreleased/kong/fix-ai-metrics-prometheus-compat.yml b/changelog/unreleased/kong/fix-ai-metrics-prometheus-compat.yml new file mode 100644 index 00000000000..b09c39e9931 --- /dev/null +++ b/changelog/unreleased/kong/fix-ai-metrics-prometheus-compat.yml @@ -0,0 +1,4 @@ +message: > + "**Prometheus**: Fixed an issue where CP/DP compatibility check was missing for the new configuration field `ai_metrics`. +type: bugfix +scope: Plugin diff --git a/changelog/unreleased/kong/fix-ai-prompt-guard-order.yml b/changelog/unreleased/kong/fix-ai-prompt-guard-order.yml new file mode 100644 index 00000000000..a6bfdfab9ae --- /dev/null +++ b/changelog/unreleased/kong/fix-ai-prompt-guard-order.yml @@ -0,0 +1,3 @@ +message: "**AI-Prompt-Guard**: Fixed an issue when `allow_all_conversation_history` is set to false, the first user request is selected instead of the last one." +type: bugfix +scope: Plugin diff --git a/changelog/unreleased/kong/fix-aws-lambda-empty-array-mutli-value.yml b/changelog/unreleased/kong/fix-aws-lambda-empty-array-mutli-value.yml new file mode 100644 index 00000000000..47f72e5b19d --- /dev/null +++ b/changelog/unreleased/kong/fix-aws-lambda-empty-array-mutli-value.yml @@ -0,0 +1,3 @@ +message: "**AWS-Lambda**: Fixed an issue that the plugin does not work with multiValueHeaders defined in proxy integration and legacy empty_arrays_mode." +type: bugfix +scope: Plugin diff --git a/changelog/unreleased/kong/fix-cors-wildcard.yml b/changelog/unreleased/kong/fix-cors-wildcard.yml new file mode 100644 index 00000000000..78676aec0f9 --- /dev/null +++ b/changelog/unreleased/kong/fix-cors-wildcard.yml @@ -0,0 +1,3 @@ +message: "**CORS**: Fixed an issue where the `Access-Control-Allow-Origin` header was not sent when `conf.origins` has multiple entries but includes `*`." +type: bugfix +scope: Plugin diff --git a/changelog/unreleased/kong/fix-filter-finalize-in-send-header-clear-context.yml b/changelog/unreleased/kong/fix-filter-finalize-in-send-header-clear-context.yml new file mode 100644 index 00000000000..cac4566c7b4 --- /dev/null +++ b/changelog/unreleased/kong/fix-filter-finalize-in-send-header-clear-context.yml @@ -0,0 +1,3 @@ +message: Fixed an issue where `lua-nginx-module` context was cleared when `ngx.send_header()` triggered `filter_finalize` [openresty/lua-nginx-module#2323](https://github.com/openresty/lua-nginx-module/pull/2323). +type: bugfix +scope: Core \ No newline at end of file diff --git a/changelog/unreleased/kong/fix-otel-migrations-exception.yml b/changelog/unreleased/kong/fix-otel-migrations-exception.yml new file mode 100644 index 00000000000..08ae5efec75 --- /dev/null +++ b/changelog/unreleased/kong/fix-otel-migrations-exception.yml @@ -0,0 +1,3 @@ +message: "**OpenTelemetry:** Fixed an issue where migration fails when upgrading from below version 3.3 to 3.7." +type: bugfix +scope: Plugin diff --git a/changelog/unreleased/kong/fix-reports-uninitialized-variable-in-400.yml b/changelog/unreleased/kong/fix-reports-uninitialized-variable-in-400.yml new file mode 100644 index 00000000000..398af4beb46 --- /dev/null +++ b/changelog/unreleased/kong/fix-reports-uninitialized-variable-in-400.yml @@ -0,0 +1,4 @@ +message: | + Fixed an issue where unnecessary uninitialized variable error log is reported when 400 bad requests were received. +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/fix-sni-cache-invalidate.yml b/changelog/unreleased/kong/fix-sni-cache-invalidate.yml new file mode 100644 index 00000000000..a898826b275 --- /dev/null +++ b/changelog/unreleased/kong/fix-sni-cache-invalidate.yml @@ -0,0 +1,4 @@ +message: | + Fixed an issue where the sni cache isn't invalidated when a sni is updated. +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/fix-type-of-logrotate.yml b/changelog/unreleased/kong/fix-type-of-logrotate.yml new file mode 100644 index 00000000000..62a2968e541 --- /dev/null +++ b/changelog/unreleased/kong/fix-type-of-logrotate.yml @@ -0,0 +1,5 @@ +message: | + The kong.logrotate configuration file will no longer be overwritten during upgrade. + When upgrading, set the environment variable `DEBIAN_FRONTEND=noninteractive` on Debian/Ubuntu to avoid any interactive prompts and enable fully automatic upgrades. +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/fix-wasm-enable-pwm-lua-resolver.yml b/changelog/unreleased/kong/fix-wasm-enable-pwm-lua-resolver.yml new file mode 100644 index 00000000000..8099909ba91 --- /dev/null +++ b/changelog/unreleased/kong/fix-wasm-enable-pwm-lua-resolver.yml @@ -0,0 +1,4 @@ +message: | + Re-enabled the Lua DNS resolver from proxy-wasm by default. +type: bugfix +scope: Configuration diff --git a/changelog/unreleased/kong/move-sockets-to-subdir.yml b/changelog/unreleased/kong/move-sockets-to-subdir.yml new file mode 100644 index 00000000000..37fdd5d10a8 --- /dev/null +++ b/changelog/unreleased/kong/move-sockets-to-subdir.yml @@ -0,0 +1,3 @@ +message: Moved internal Unix sockets to a subdirectory (`sockets`) of the Kong prefix. +type: bugfix +scope: Core diff --git a/changelog/unreleased/kong/otel-formatted-logs.yml b/changelog/unreleased/kong/otel-formatted-logs.yml new file mode 100644 index 00000000000..3212e09b4ff --- /dev/null +++ b/changelog/unreleased/kong/otel-formatted-logs.yml @@ -0,0 +1,3 @@ +message: "**OpenTelemetry:** Added support for OpenTelemetry formatted logs." +type: feature +scope: Plugin diff --git a/changelog/unreleased/kong/pdk-log-error.yml b/changelog/unreleased/kong/pdk-log-error.yml new file mode 100644 index 00000000000..988d10831bd --- /dev/null +++ b/changelog/unreleased/kong/pdk-log-error.yml @@ -0,0 +1,3 @@ +message: Fixed an issue that pdk.log.serialize() will throw an error when JSON entity set by serialize_value contains json.null +type: bugfix +scope: PDK diff --git a/changelog/unreleased/kong/pdk-read-file.yml b/changelog/unreleased/kong/pdk-read-file.yml new file mode 100644 index 00000000000..fbf87187acf --- /dev/null +++ b/changelog/unreleased/kong/pdk-read-file.yml @@ -0,0 +1,3 @@ +message: "extend kong.request.get_body and kong.request.get_raw_body to read from buffered file" +type: feature +scope: "PDK" diff --git a/changelog/unreleased/kong/pdk-telemetry-log.yml b/changelog/unreleased/kong/pdk-telemetry-log.yml new file mode 100644 index 00000000000..3de258d3f6e --- /dev/null +++ b/changelog/unreleased/kong/pdk-telemetry-log.yml @@ -0,0 +1,5 @@ +message: | + Added a new PDK module `kong.telemetry` and function: `kong.telemetry.log` + to generate log entries to be reported via the OpenTelemetry plugin. +type: feature +scope: PDK diff --git a/changelog/unreleased/kong/proxy-cache-fix-age-header.yml b/changelog/unreleased/kong/proxy-cache-fix-age-header.yml new file mode 100644 index 00000000000..b4d94e0c8ae --- /dev/null +++ b/changelog/unreleased/kong/proxy-cache-fix-age-header.yml @@ -0,0 +1,4 @@ +message: | + **proxy-cache**: Fixed an issue where the Age header was not being updated correctly when serving cached responses. +scope: Plugin +type: bugfix diff --git a/changelog/unreleased/kong/refactor_dns_client.yml b/changelog/unreleased/kong/refactor_dns_client.yml new file mode 100644 index 00000000000..da5cd40f65c --- /dev/null +++ b/changelog/unreleased/kong/refactor_dns_client.yml @@ -0,0 +1,9 @@ +message: > + Starting from this version, a new DNS client library has been implemented and added into Kong. The new DNS client library has the following changes + - Introduced global caching for DNS records across workers, significantly reducing the query load on DNS servers. + - Introduced observable statistics for the new DNS client, and a new Admin API `/status/dns` to retrieve them. + - Deprecated the `dns_no_sync` option. Multiple DNS queries for the same name will always be synchronized (even across workers). This remains functional with the legacy DNS client library. + - Deprecated the `dns_not_found_ttl` option. It uses the `dns_error_ttl` option for all error responses. This option remains functional with the legacy DNS client library. + - Deprecated the `dns_order` option. By default, SRV, A, and AAAA are supported. Only names in the SRV format (`_service._proto.name`) enable resolving of DNS SRV records. +type: feature +scope: Core diff --git a/changelog/unreleased/kong/resty-simdjson.yml b/changelog/unreleased/kong/resty-simdjson.yml new file mode 100644 index 00000000000..2da90247be2 --- /dev/null +++ b/changelog/unreleased/kong/resty-simdjson.yml @@ -0,0 +1,5 @@ +message: | + Introduced a yieldable JSON library `lua-resty-simdjson`, + which would improve the latency significantly. +type: dependency +scope: Core diff --git a/changelog/unreleased/kong/traditional_router_header_validation.yml b/changelog/unreleased/kong/traditional_router_header_validation.yml deleted file mode 100644 index 124181b7e01..00000000000 --- a/changelog/unreleased/kong/traditional_router_header_validation.yml +++ /dev/null @@ -1,3 +0,0 @@ -message: Fixed an issue where the `route` entity would accept an invalid regex pattern if the `router_flavor` is `traditional` or `traditional_compatible`. Now, the invalid regex pattern for matching the value of request headers will not be accepted when creating the `route` entity. -type: bugfix -scope: Core diff --git a/changelog/unreleased/kong/yield-in-gzip.yml b/changelog/unreleased/kong/yield-in-gzip.yml new file mode 100644 index 00000000000..59e7cedf5f3 --- /dev/null +++ b/changelog/unreleased/kong/yield-in-gzip.yml @@ -0,0 +1,3 @@ +message: Improved latency performance when gzipping/gunzipping large data (such as CP/DP config data). +type: performance +scope: Core diff --git a/kong-3.8.0-0.rockspec b/kong-3.8.0-0.rockspec index 03479573b53..bab08660e7d 100644 --- a/kong-3.8.0-0.rockspec +++ b/kong-3.8.0-0.rockspec @@ -33,8 +33,9 @@ dependencies = { "lua-protobuf == 0.5.1", "lua-resty-healthcheck == 3.1.0", "lua-messagepack == 0.5.4", - "lua-resty-aws == 1.5.0", - "lua-resty-openssl == 1.4.0", + "lua-resty-aws == 1.5.3", + "lua-resty-openssl == 1.5.0", + "lua-resty-gcp == 0.0.13", "lua-resty-counter == 0.2.1", "lua-resty-ipmatcher == 0.6.1", "lua-resty-acme == 0.14.0", @@ -114,6 +115,11 @@ build = { ["kong.resty.dns.client"] = "kong/resty/dns/client.lua", ["kong.resty.dns.utils"] = "kong/resty/dns/utils.lua", + + ["kong.dns.client"] = "kong/dns/client.lua", + ["kong.dns.stats"] = "kong/dns/stats.lua", + ["kong.dns.utils"] = "kong/dns/utils.lua", + ["kong.resty.ctx"] = "kong/resty/ctx.lua", ["kong.resty.mlcache"] = "kong/resty/mlcache/init.lua", @@ -147,6 +153,7 @@ build = { ["kong.api"] = "kong/api/init.lua", ["kong.api.api_helpers"] = "kong/api/api_helpers.lua", ["kong.api.arguments"] = "kong/api/arguments.lua", + ["kong.api.arguments_decoder"] = "kong/api/arguments_decoder.lua", ["kong.api.endpoints"] = "kong/api/endpoints.lua", ["kong.api.routes.cache"] = "kong/api/routes/cache.lua", ["kong.api.routes.certificates"] = "kong/api/routes/certificates.lua", @@ -197,6 +204,7 @@ build = { ["kong.tools.cjson"] = "kong/tools/cjson.lua", ["kong.tools.emmy_debugger"] = "kong/tools/emmy_debugger.lua", ["kong.tools.redis.schema"] = "kong/tools/redis/schema.lua", + ["kong.tools.aws_stream"] = "kong/tools/aws_stream.lua", ["kong.runloop.handler"] = "kong/runloop/handler.lua", ["kong.runloop.events"] = "kong/runloop/events.lua", @@ -337,6 +345,7 @@ build = { ["kong.pdk.vault"] = "kong/pdk/vault.lua", ["kong.pdk.tracing"] = "kong/pdk/tracing.lua", ["kong.pdk.plugin"] = "kong/pdk/plugin.lua", + ["kong.pdk.telemetry"] = "kong/pdk/telemetry.lua", ["kong.plugins.basic-auth.migrations"] = "kong/plugins/basic-auth/migrations/init.lua", ["kong.plugins.basic-auth.migrations.000_base_basic_auth"] = "kong/plugins/basic-auth/migrations/000_base_basic_auth.lua", @@ -541,7 +550,6 @@ build = { ["kong.plugins.proxy-cache.api"] = "kong/plugins/proxy-cache/api.lua", ["kong.plugins.proxy-cache.strategies"] = "kong/plugins/proxy-cache/strategies/init.lua", ["kong.plugins.proxy-cache.strategies.memory"] = "kong/plugins/proxy-cache/strategies/memory.lua", - ["kong.plugins.proxy-cache.clustering.compat.response_headers_translation"] = "kong/plugins/proxy-cache/clustering/compat/response_headers_translation.lua", ["kong.plugins.grpc-web.deco"] = "kong/plugins/grpc-web/deco.lua", ["kong.plugins.grpc-web.handler"] = "kong/plugins/grpc-web/handler.lua", @@ -582,6 +590,9 @@ build = { ["kong.plugins.opentelemetry.schema"] = "kong/plugins/opentelemetry/schema.lua", ["kong.plugins.opentelemetry.proto"] = "kong/plugins/opentelemetry/proto.lua", ["kong.plugins.opentelemetry.otlp"] = "kong/plugins/opentelemetry/otlp.lua", + ["kong.plugins.opentelemetry.traces"] = "kong/plugins/opentelemetry/traces.lua", + ["kong.plugins.opentelemetry.logs"] = "kong/plugins/opentelemetry/logs.lua", + ["kong.plugins.opentelemetry.utils"] = "kong/plugins/opentelemetry/utils.lua", ["kong.plugins.ai-proxy.handler"] = "kong/plugins/ai-proxy/handler.lua", ["kong.plugins.ai-proxy.schema"] = "kong/plugins/ai-proxy/schema.lua", @@ -603,6 +614,10 @@ build = { ["kong.llm.drivers.anthropic"] = "kong/llm/drivers/anthropic.lua", ["kong.llm.drivers.mistral"] = "kong/llm/drivers/mistral.lua", ["kong.llm.drivers.llama2"] = "kong/llm/drivers/llama2.lua", + ["kong.llm.drivers.gemini"] = "kong/llm/drivers/gemini.lua", + ["kong.llm.drivers.bedrock"] = "kong/llm/drivers/bedrock.lua", + + ["kong.llm.proxy.handler"] = "kong/llm/proxy/handler.lua", ["kong.plugins.ai-prompt-decorator.handler"] = "kong/plugins/ai-prompt-decorator/handler.lua", ["kong.plugins.ai-prompt-decorator.schema"] = "kong/plugins/ai-prompt-decorator/schema.lua", @@ -621,29 +636,31 @@ build = { ["kong.vaults.env"] = "kong/vaults/env/init.lua", ["kong.vaults.env.schema"] = "kong/vaults/env/schema.lua", - ["kong.tracing.instrumentation"] = "kong/tracing/instrumentation.lua", - ["kong.tracing.propagation"] = "kong/tracing/propagation/init.lua", - ["kong.tracing.propagation.schema"] = "kong/tracing/propagation/schema.lua", - ["kong.tracing.propagation.utils"] = "kong/tracing/propagation/utils.lua", - ["kong.tracing.propagation.extractors._base"] = "kong/tracing/propagation/extractors/_base.lua", - ["kong.tracing.propagation.extractors.w3c"] = "kong/tracing/propagation/extractors/w3c.lua", - ["kong.tracing.propagation.extractors.b3"] = "kong/tracing/propagation/extractors/b3.lua", - ["kong.tracing.propagation.extractors.jaeger"] = "kong/tracing/propagation/extractors/jaeger.lua", - ["kong.tracing.propagation.extractors.ot"] = "kong/tracing/propagation/extractors/ot.lua", - ["kong.tracing.propagation.extractors.gcp"] = "kong/tracing/propagation/extractors/gcp.lua", - ["kong.tracing.propagation.extractors.aws"] = "kong/tracing/propagation/extractors/aws.lua", - ["kong.tracing.propagation.extractors.datadog"] = "kong/tracing/propagation/extractors/datadog.lua", - ["kong.tracing.propagation.injectors._base"] = "kong/tracing/propagation/injectors/_base.lua", - ["kong.tracing.propagation.injectors.w3c"] = "kong/tracing/propagation/injectors/w3c.lua", - ["kong.tracing.propagation.injectors.b3"] = "kong/tracing/propagation/injectors/b3.lua", - ["kong.tracing.propagation.injectors.b3-single"] = "kong/tracing/propagation/injectors/b3-single.lua", - ["kong.tracing.propagation.injectors.jaeger"] = "kong/tracing/propagation/injectors/jaeger.lua", - ["kong.tracing.propagation.injectors.ot"] = "kong/tracing/propagation/injectors/ot.lua", - ["kong.tracing.propagation.injectors.gcp"] = "kong/tracing/propagation/injectors/gcp.lua", - ["kong.tracing.propagation.injectors.aws"] = "kong/tracing/propagation/injectors/aws.lua", - ["kong.tracing.propagation.injectors.datadog"] = "kong/tracing/propagation/injectors/datadog.lua", - ["kong.tracing.request_id"] = "kong/tracing/request_id.lua", - ["kong.tracing.tracing_context"] = "kong/tracing/tracing_context.lua", + ["kong.observability.tracing.instrumentation"] = "kong/observability/tracing/instrumentation.lua", + ["kong.observability.tracing.propagation"] = "kong/observability/tracing/propagation/init.lua", + ["kong.observability.tracing.propagation.schema"] = "kong/observability/tracing/propagation/schema.lua", + ["kong.observability.tracing.propagation.utils"] = "kong/observability/tracing/propagation/utils.lua", + ["kong.observability.tracing.propagation.extractors._base"] = "kong/observability/tracing/propagation/extractors/_base.lua", + ["kong.observability.tracing.propagation.extractors.w3c"] = "kong/observability/tracing/propagation/extractors/w3c.lua", + ["kong.observability.tracing.propagation.extractors.b3"] = "kong/observability/tracing/propagation/extractors/b3.lua", + ["kong.observability.tracing.propagation.extractors.jaeger"] = "kong/observability/tracing/propagation/extractors/jaeger.lua", + ["kong.observability.tracing.propagation.extractors.ot"] = "kong/observability/tracing/propagation/extractors/ot.lua", + ["kong.observability.tracing.propagation.extractors.gcp"] = "kong/observability/tracing/propagation/extractors/gcp.lua", + ["kong.observability.tracing.propagation.extractors.aws"] = "kong/observability/tracing/propagation/extractors/aws.lua", + ["kong.observability.tracing.propagation.extractors.datadog"] = "kong/observability/tracing/propagation/extractors/datadog.lua", + ["kong.observability.tracing.propagation.injectors._base"] = "kong/observability/tracing/propagation/injectors/_base.lua", + ["kong.observability.tracing.propagation.injectors.w3c"] = "kong/observability/tracing/propagation/injectors/w3c.lua", + ["kong.observability.tracing.propagation.injectors.b3"] = "kong/observability/tracing/propagation/injectors/b3.lua", + ["kong.observability.tracing.propagation.injectors.b3-single"] = "kong/observability/tracing/propagation/injectors/b3-single.lua", + ["kong.observability.tracing.propagation.injectors.jaeger"] = "kong/observability/tracing/propagation/injectors/jaeger.lua", + ["kong.observability.tracing.propagation.injectors.ot"] = "kong/observability/tracing/propagation/injectors/ot.lua", + ["kong.observability.tracing.propagation.injectors.gcp"] = "kong/observability/tracing/propagation/injectors/gcp.lua", + ["kong.observability.tracing.propagation.injectors.aws"] = "kong/observability/tracing/propagation/injectors/aws.lua", + ["kong.observability.tracing.propagation.injectors.datadog"] = "kong/observability/tracing/propagation/injectors/datadog.lua", + ["kong.observability.tracing.request_id"] = "kong/observability/tracing/request_id.lua", + ["kong.observability.tracing.tracing_context"] = "kong/observability/tracing/tracing_context.lua", + + ["kong.observability.logs"] = "kong/observability/logs.lua", ["kong.timing"] = "kong/timing/init.lua", ["kong.timing.context"] = "kong/timing/context.lua", diff --git a/kong.conf.default b/kong.conf.default index 24aafdad915..87c9b12cf96 100644 --- a/kong.conf.default +++ b/kong.conf.default @@ -43,17 +43,12 @@ # `prefix` location. -#proxy_error_log = logs/error.log # Path for proxy port request error - # logs. The granularity of these logs - # is adjusted by the `log_level` - # property. - -#proxy_stream_access_log = logs/access.log basic # Path for tcp streams proxy port access - # logs. Set this value to `off` to - # disable logging proxy requests. - # If this value is a relative path, - # it will be placed under the - # `prefix` location. +#proxy_error_log = logs/error.log # Path for proxy port request error logs. + # The granularity of these logs is adjusted by the `log_level` property. + +#proxy_stream_access_log = logs/access.log basic # Path for TCP streams proxy port access logs. + # Set to `off` to disable logging proxy requests. + # If this value is a relative path, it will be placed under the `prefix` location. # `basic` is defined as `'$remote_addr [$time_local] ' # '$protocol $status $bytes_sent $bytes_received ' # '$session_time'` @@ -63,124 +58,95 @@ # is adjusted by the `log_level` # property. -#admin_access_log = logs/admin_access.log # Path for Admin API request access - # logs. If Hybrid Mode is enabled - # and the current node is set to be - # the Control Plane, then the - # connection requests from Data Planes - # are also written to this file with +#admin_access_log = logs/admin_access.log # Path for Admin API request access logs. + # If hybrid mode is enabled and the current node is set + # to be the control plane, then the connection requests + # from data planes are also written to this file with # server name "kong_cluster_listener". # - # Set this value to `off` to - # disable logging Admin API requests. - # If this value is a relative path, - # it will be placed under the - # `prefix` location. + # Set this value to `off` to disable logging Admin API requests. + # If this value is a relative path, it will be placed under the `prefix` location. -#admin_error_log = logs/error.log # Path for Admin API request error - # logs. The granularity of these logs - # is adjusted by the `log_level` - # property. -#status_access_log = off # Path for Status API request access - # logs. The default value of `off` - # implies that logging for this API +#admin_error_log = logs/error.log # Path for Admin API request error logs. + # The granularity of these logs is adjusted by the `log_level` property. + +#status_access_log = off # Path for Status API request access logs. + # The default value of `off` implies that logging for this API # is disabled by default. - # If this value is a relative path, - # it will be placed under the - # `prefix` location. + # If this value is a relative path, it will be placed under the `prefix` location. -#status_error_log = logs/status_error.log # Path for Status API request error - # logs. The granularity of these logs - # is adjusted by the `log_level` - # property. +#status_error_log = logs/status_error.log # Path for Status API request error logs. + # The granularity of these logs is adjusted by the `log_level` property. -#vaults = bundled # Comma-separated list of vaults this node - # should load. By default, all the bundled - # vaults are enabled. +#vaults = bundled # Comma-separated list of vaults this node should load. + # By default, all the bundled vaults are enabled. # # The specified name(s) will be substituted as # such in the Lua namespace: # `kong.vaults.{name}.*`. -#opentelemetry_tracing = off # Deprecated: use tracing_instrumentations instead +#opentelemetry_tracing = off # Deprecated: use `tracing_instrumentations` instead. -#tracing_instrumentations = off # Comma-separated list of tracing instrumentations - # this node should load. By default, no instrumentations - # are enabled. +#tracing_instrumentations = off # Comma-separated list of tracing instrumentations this node should load. + # By default, no instrumentations are enabled. # - # Valid values to this setting are: + # Valid values for this setting are: # # - `off`: do not enable instrumentations. # - `request`: only enable request-level instrumentations. # - `all`: enable all the following instrumentations. - # - `db_query`: trace database query - # - `dns_query`: trace DNS query. - # - `router`: trace router execution, including - # router rebuilding. + # - `db_query`: trace database queries. + # - `dns_query`: trace DNS queries. + # - `router`: trace router execution, including router rebuilding. # - `http_client`: trace OpenResty HTTP client requests. # - `balancer`: trace balancer retries. - # - `plugin_rewrite`: trace plugins iterator - # execution with rewrite phase. - # - `plugin_access`: trace plugins iterator - # execution with access phase. - # - `plugin_header_filter`: trace plugins iterator - # execution with header_filter phase. + # - `plugin_rewrite`: trace plugin iterator execution with rewrite phase. + # - `plugin_access`: trace plugin iterator execution with access phase. + # - `plugin_header_filter`: trace plugin iterator execution with header_filter phase. # - # **Note:** In the current implementation, - # tracing instrumentations are not enabled in - # stream mode. + # **Note:** In the current implementation, tracing instrumentations are not enabled in stream mode. -#opentelemetry_tracing_sampling_rate = 1.0 # Deprecated: use tracing_sampling_rate instead +#opentelemetry_tracing_sampling_rate = 1.0 # Deprecated: use `tracing_sampling_rate` instead. #tracing_sampling_rate = 0.01 # Tracing instrumentation sampling rate. # Tracer samples a fixed percentage of all spans # following the sampling rate. # - # Example: `0.25`, this should account for 25% of all traces. + # Example: `0.25`, this accounts for 25% of all traces. -#plugins = bundled # Comma-separated list of plugins this node - # should load. By default, only plugins - # bundled in official distributions are - # loaded via the `bundled` keyword. +#plugins = bundled # Comma-separated list of plugins this node should load. + # By default, only plugins bundled in official distributions + # are loaded via the `bundled` keyword. # - # Loading a plugin does not enable it by - # default, but only instructs Kong to load its - # source code, and allows to configure the - # plugin via the various related Admin API - # endpoints. + # Loading a plugin does not enable it by default, but only + # instructs Kong to load its source code and allows + # configuration via the various related Admin API endpoints. # - # The specified name(s) will be substituted as - # such in the Lua namespace: - # `kong.plugins.{name}.*`. + # The specified name(s) will be substituted as such in the + # Lua namespace: `kong.plugins.{name}.*`. # - # When the `off` keyword is specified as the - # only value, no plugins will be loaded. + # When the `off` keyword is specified as the only value, + # no plugins will be loaded. # - # `bundled` and plugin names can be mixed - # together, as the following examples suggest: + # `bundled` and plugin names can be mixed together, as the + # following examples suggest: # # - `plugins = bundled,custom-auth,custom-log` - # will include the bundled plugins plus two - # custom ones + # will include the bundled plugins plus two custom ones. # - `plugins = custom-auth,custom-log` will - # *only* include the `custom-auth` and - # `custom-log` plugins. - # - `plugins = off` will not include any - # plugins - # - # **Note:** Kong will not start if some - # plugins were previously configured (i.e. - # have rows in the database) and are not - # specified in this list. Before disabling a - # plugin, ensure all instances of it are - # removed before restarting Kong. - # - # **Note:** Limiting the amount of available - # plugins can improve P99 latency when - # experiencing LRU churning in the database - # cache (i.e. when the configured - # `mem_cache_size`) is full. + # *only* include the `custom-auth` and `custom-log` plugins. + # - `plugins = off` will not include any plugins. + # + # **Note:** Kong will not start if some plugins were previously + # configured (i.e. have rows in the database) and are not + # specified in this list. Before disabling a plugin, ensure + # all instances of it are removed before restarting Kong. + # + # **Note:** Limiting the amount of available plugins can + # improve P99 latency when experiencing LRU churning in the + # database cache (i.e. when the configured `mem_cache_size`) is full. + #dedicated_config_processing = on # Enables or disables a special worker # process for configuration processing. This process @@ -192,7 +158,7 @@ # Currently this has effect only on data planes. #pluginserver_names = # Comma-separated list of names for pluginserver - # processes. The actual names are used for + # processes. The actual names are used for # log messages and to relate the actual settings. #pluginserver_XXX_socket = /.socket # Path to the unix socket @@ -207,7 +173,7 @@ # manages #port_maps = # With this configuration parameter, you can - # let the Kong to know about the port from + # let Kong Gateway know the port from # which the packets are forwarded to it. This # is fairly common when running Kong in a # containerized or virtualized environment. @@ -234,8 +200,8 @@ #proxy_server = # Proxy server defined as a URL. Kong will only use this - # option if any component is explicitly configured - # to use proxy. + # option if a component is explicitly configured + # to use a proxy. #proxy_server_ssl_verify = off # Toggles server certificate verification if @@ -295,16 +261,16 @@ # HYBRID MODE #------------------------------------------------------------------------------ -#role = traditional # Use this setting to enable Hybrid Mode, +#role = traditional # Use this setting to enable hybrid mode, # This allows running some Kong nodes in a # control plane role with a database and # have them deliver configuration updates # to other nodes running to DB-less running in - # a Data Plane role. + # a data plane role. # - # Valid values to this setting are: + # Valid values for this setting are: # - # - `traditional`: do not use Hybrid Mode. + # - `traditional`: do not use hybrid mode. # - `control_plane`: this node runs in a # control plane role. It can use a database # and will deliver configuration updates @@ -313,23 +279,21 @@ # It runs DB-less and receives configuration # updates from a control plane node. -#cluster_mtls = shared # Sets the verification between nodes of the - # cluster. - # - # Valid values to this setting are: - # - # - `shared`: use a shared certificate/key - # pair specified with the `cluster_cert` - # and `cluster_cert_key` settings. - # Note that CP and DP nodes have to present - # the same certificate to establish mTLS - # connections. - # - `pki`: use `cluster_ca_cert`, - # `cluster_server_name` and `cluster_cert` - # for verification. - # These are different certificates for each - # DP node, but issued by a cluster-wide +#cluster_mtls = shared # Sets the verification method between nodes of the cluster. + # + # Valid values for this setting are: + # + # - `shared`: use a shared certificate/key pair specified with + # the `cluster_cert` and `cluster_cert_key` settings. + # Note that CP and DP nodes must present the same certificate + # to establish mTLS connections. + # - `pki`: use `cluster_ca_cert`, `cluster_server_name`, and + # `cluster_cert` for verification. These are different + # certificates for each DP node, but issued by a cluster-wide # common CA certificate: `cluster_ca_cert`. + # - `pki_check_cn`: similar to `pki` but additionally checks + # for the common name of the data plane certificate specified + # in `cluster_allowed_common_names`. #cluster_cert = # Cluster certificate to use # when establishing secure communication @@ -359,29 +323,24 @@ # # The certificate key can be configured on this # property with either of the following values: - # * absolute path to the certificate key - # * certificate key content - # * base64 encoded certificate key content - -#cluster_ca_cert = # The trusted CA certificate file in PEM - # format used for Control Plane to verify - # Data Plane's certificate and Data Plane - # to verify Control Plane's certificate. - # Required on data plane if `cluster_mtls` - # is set to `pki`. - # If Control Plane certificate is issued - # by a well known CA, user can set - # `lua_ssl_trusted_certificate=system` - # on Data Plane and leave this field empty. - # - # This field is ignored if `cluster_mtls` is - # set to `shared`. - # - # The certificate can be configured on this property - # with either of the following values: - # * absolute path to the certificate - # * certificate content - # * base64 encoded certificate content + # - absolute path to the certificate key + # - certificate key content + # - base64 encoded certificate key content + +#cluster_ca_cert = # The trusted CA certificate file in PEM format used for: + # - Control plane to verify data plane's certificate + # - Data plane to verify control plane's certificate + # + # Required on data plane if `cluster_mtls` is set to `pki`. + # If the control plane certificate is issued by a well-known CA, + # set `lua_ssl_trusted_certificate=system` on the data plane and leave this field empty. + # + # This field is ignored if `cluster_mtls` is set to `shared`. + # + # The certificate can be configured on this property with any of the following values: + # - absolute path to the certificate + # - certificate content + # - base64 encoded certificate content #------------------------------------------------------------------------------ # HYBRID MODE DATA PLANE @@ -397,8 +356,8 @@ # `kong_clustering` is used. #cluster_control_plane = # To be used by data plane nodes only: - # address of the control plane node from - # which configuration updates will be fetched, + # address of the control plane node from which + # configuration updates will be fetched, # in `host:port` format. #cluster_max_payload = 16777216 @@ -406,15 +365,15 @@ # to be sent across from CP to DP in Hybrid mode # Default is 16MB - 16 * 1024 * 1024. -#cluster_dp_labels = # Comma separated list of Labels for the data plane. +#cluster_dp_labels = # Comma-separated list of labels for the data plane. # Labels are key-value pairs that provide additional # context information for each DP. # Each label must be configured as a string in the # format `key:value`. # # Labels are only compatible with hybrid mode - # deployments with Kong Konnect (SaaS), - # this configuration doesn't work with + # deployments with Kong Konnect (SaaS). + # This configuration doesn't work with # self-hosted deployments. # # Keys and values follow the AIP standards: @@ -439,7 +398,7 @@ # This setting has no effect if `role` is not set to # `control_plane`. # - # Connection made to this endpoint are logged + # Connections made to this endpoint are logged # to the same location as Admin API access logs. # See `admin_access_log` config description for more # information. @@ -452,7 +411,7 @@ # # This is to prevent the cluster data plane table from # growing indefinitely. The default is set to - # 14 days. That is, if CP haven't heard from a DP for + # 14 days. That is, if the CP hasn't heard from a DP for # 14 days, its entry will be removed. #cluster_ocsp = off @@ -466,7 +425,7 @@ # OCSP checks are only performed on CP nodes, it has no # effect on DP nodes. # - # Valid values to this setting are: + # Valid values for this setting are: # # - `on`: OCSP revocation check is enabled and DP # must pass the check in order to establish @@ -474,13 +433,14 @@ # - `off`: OCSP revocation check is disabled. # - `optional`: OCSP revocation check will be attempted, # however, if the required extension is not - # found inside DP provided certificate + # found inside DP-provided certificate # or communication with the OCSP responder # failed, then DP is still allowed through. + #cluster_use_proxy = off # Whether to turn on HTTP CONNECT proxy support for # hybrid mode connections. `proxy_server` will be used - # for Hybrid mode connections if this option is turned on. + # for hybrid mode connections if this option is turned on. #------------------------------------------------------------------------------ # NGINX #------------------------------------------------------------------------------ @@ -504,42 +464,42 @@ # - `proxy_protocol` will enable usage of the # PROXY protocol for a given address/port. # - `deferred` instructs to use a deferred accept on - # Linux (the TCP_DEFER_ACCEPT socket option). + # Linux (the `TCP_DEFER_ACCEPT` socket option). # - `bind` instructs to make a separate bind() call # for a given address:port pair. # - `reuseport` instructs to create an individual - # listening socket for each worker process - # allowing the Kernel to better distribute incoming - # connections between worker processes + # listening socket for each worker process, + # allowing the kernel to better distribute incoming + # connections between worker processes. # - `backlog=N` sets the maximum length for the queue # of pending TCP connections. This number should - # not be too small in order to prevent clients - # seeing "Connection refused" error connecting to + # not be too small to prevent clients + # seeing "Connection refused" errors when connecting to # a busy Kong instance. - # **Note:** on Linux, this value is limited by the - # setting of `net.core.somaxconn` Kernel parameter. + # **Note:** On Linux, this value is limited by the + # setting of the `net.core.somaxconn` kernel parameter. # In order for the larger `backlog` set here to take - # effect it is necessary to raise + # effect, it is necessary to raise # `net.core.somaxconn` at the same time to match or # exceed the `backlog` number set. - # - `ipv6only=on|off` whether an IPv6 socket listening + # - `ipv6only=on|off` specifies whether an IPv6 socket listening # on a wildcard address [::] will accept only IPv6 - # connections or both IPv6 and IPv4 connections - # - so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt] - # configures the "TCP keepalive" behavior for the listening - # socket. If this parameter is omitted then the operating + # connections or both IPv6 and IPv4 connections. + # - `so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]` + # configures the `TCP keepalive` behavior for the listening + # socket. If this parameter is omitted, the operating # system’s settings will be in effect for the socket. If it - # is set to the value "on", the SO_KEEPALIVE option is turned - # on for the socket. If it is set to the value "off", the - # SO_KEEPALIVE option is turned off for the socket. Some + # is set to the value `on`, the `SO_KEEPALIVE` option is turned + # on for the socket. If it is set to the value `off`, the + # `SO_KEEPALIVE` option is turned off for the socket. Some # operating systems support setting of TCP keepalive parameters - # on a per-socket basis using the TCP_KEEPIDLE, TCP_KEEPINTVL, - # and TCP_KEEPCNT socket options. + # on a per-socket basis using the `TCP_KEEPIDLE`,` TCP_KEEPINTVL`, + # and `TCP_KEEPCNT` socket options. # # This value can be set to `off`, thus disabling # the HTTP/HTTPS proxy port for this node. - # If stream_listen is also set to `off`, this enables - # 'control-plane' mode for this node + # If `stream_listen` is also set to `off`, this enables + # control plane mode for this node # (in which all traffic proxying capabilities are # disabled). This node can then be used only to # configure a cluster of Kong @@ -573,33 +533,33 @@ # - `bind` instructs to make a separate bind() call # for a given address:port pair. # - `reuseport` instructs to create an individual - # listening socket for each worker process - # allowing the Kernel to better distribute incoming - # connections between worker processes + # listening socket for each worker process, + # allowing the kernel to better distribute incoming + # connections between worker processes. # - `backlog=N` sets the maximum length for the queue # of pending TCP connections. This number should - # not be too small in order to prevent clients - # seeing "Connection refused" error connecting to + # not be too small to prevent clients + # seeing "Connection refused" errors when connecting to # a busy Kong instance. - # **Note:** on Linux, this value is limited by the - # setting of `net.core.somaxconn` Kernel parameter. + # **Note:** On Linux, this value is limited by the + # setting of the `net.core.somaxconn` kernel parameter. # In order for the larger `backlog` set here to take - # effect it is necessary to raise + # effect, it is necessary to raise # `net.core.somaxconn` at the same time to match or # exceed the `backlog` number set. - # - `ipv6only=on|off` whether an IPv6 socket listening + # - `ipv6only=on|off` specifies whether an IPv6 socket listening # on a wildcard address [::] will accept only IPv6 - # connections or both IPv6 and IPv4 connections - # - so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt] - # configures the "TCP keepalive" behavior for the listening - # socket. If this parameter is omitted then the operating + # connections or both IPv6 and IPv4 connections. + # - `so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]` + # configures the `TCP keepalive` behavior for the listening + # socket. If this parameter is omitted, the operating # system’s settings will be in effect for the socket. If it - # is set to the value "on", the SO_KEEPALIVE option is turned - # on for the socket. If it is set to the value "off", the - # SO_KEEPALIVE option is turned off for the socket. Some + # is set to the value `on`, the `SO_KEEPALIVE` option is turned + # on for the socket. If it is set to the value `off`, the + # `SO_KEEPALIVE` option is turned off for the socket. Some # operating systems support setting of TCP keepalive parameters - # on a per-socket basis using the TCP_KEEPIDLE, TCP_KEEPINTVL, - # and TCP_KEEPCNT socket options. + # on a per-socket basis using the` TCP_KEEPIDLE`, `TCP_KEEPINTVL`, + # and `TCP_KEEPCNT` socket options. # # Examples: # @@ -609,7 +569,7 @@ # stream_listen = [::1]:1234 backlog=16384 # ``` # - # By default this value is set to `off`, thus + # By default, this value is set to `off`, thus # disabling the stream proxy port for this node. # See http://nginx.org/en/docs/stream/ngx_stream_core_module.html#listen @@ -625,10 +585,10 @@ # IPv4, IPv6, and hostnames. # # It is highly recommended to avoid exposing the Admin API to public - # interface(s), by using values such as 0.0.0.0:8001 + # interfaces, by using values such as `0.0.0.0:8001` # # See https://docs.konghq.com/gateway/latest/production/running-kong/secure-admin-api/ - # for more information about how to secure your Admin API + # for more information about how to secure your Admin API. # # Some suffixes can be specified for each pair: # @@ -640,41 +600,41 @@ # - `proxy_protocol` will enable usage of the # PROXY protocol for a given address/port. # - `deferred` instructs to use a deferred accept on - # Linux (the TCP_DEFER_ACCEPT socket option). + # Linux (the `TCP_DEFER_ACCEPT` socket option). # - `bind` instructs to make a separate bind() call # for a given address:port pair. # - `reuseport` instructs to create an individual - # listening socket for each worker process + # listening socket for each worker process, # allowing the Kernel to better distribute incoming - # connections between worker processes + # connections between worker processes. # - `backlog=N` sets the maximum length for the queue # of pending TCP connections. This number should - # not be too small in order to prevent clients - # seeing "Connection refused" error connecting to + # not be too small to prevent clients + # seeing "Connection refused" errors when connecting to # a busy Kong instance. - # **Note:** on Linux, this value is limited by the - # setting of `net.core.somaxconn` Kernel parameter. + # **Note:** On Linux, this value is limited by the + # setting of the `net.core.somaxconn` kernel parameter. # In order for the larger `backlog` set here to take - # effect it is necessary to raise + # effect, it is necessary to raise # `net.core.somaxconn` at the same time to match or # exceed the `backlog` number set. - # - `ipv6only=on|off` whether an IPv6 socket listening + # - `ipv6only=on|off` specifies whether an IPv6 socket listening # on a wildcard address [::] will accept only IPv6 - # connections or both IPv6 and IPv4 connections - # - so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt] - # configures the "TCP keepalive" behavior for the listening - # socket. If this parameter is omitted then the operating + # connections or both IPv6 and IPv4 connections. + # - `so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]` + # configures the “TCP keepalive” behavior for the listening + # socket. If this parameter is omitted, the operating # system’s settings will be in effect for the socket. If it - # is set to the value "on", the SO_KEEPALIVE option is turned - # on for the socket. If it is set to the value "off", the - # SO_KEEPALIVE option is turned off for the socket. Some + # is set to the value `on`, the `SO_KEEPALIVE` option is turned + # on for the socket. If it is set to the value `off`, the + # `SO_KEEPALIVE` option is turned off for the socket. Some # operating systems support setting of TCP keepalive parameters - # on a per-socket basis using the TCP_KEEPIDLE, TCP_KEEPINTVL, - # and TCP_KEEPCNT socket options. + # on a per-socket basis using the `TCP_KEEPIDLE`, `TCP_KEEPINTVL`, + # and `TCP_KEEPCNT` socket options. # # This value can be set to `off`, thus disabling # the Admin interface for this node, enabling a - # 'data-plane' mode (without configuration + # data plane mode (without configuration # capabilities) pulling its configuration changes # from the database. # @@ -694,7 +654,7 @@ # through a particular address/port be made with TLS # enabled. # - `http2` will allow for clients to open HTTP/2 - # connections to Kong's proxy server. + # connections to Kong's Status API server. # - `proxy_protocol` will enable usage of the PROXY protocol. # # This value can be set to `off`, disabling @@ -743,7 +703,7 @@ # uses to cache entities might be double this value. # The created zones are shared by all worker # processes and do not become larger when more - # worker is used. + # workers are used. #ssl_cipher_suite = intermediate # Defines the TLS ciphers served by Nginx. # Accepted values are `modern`, @@ -790,7 +750,7 @@ # # This value is ignored if `ssl_cipher_suite` # is `modern` or `intermediate`. The reason is - # that `modern` has no ciphers that needs this, + # that `modern` has no ciphers that need this, # and `intermediate` uses `ffdhe2048`. # # See http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam @@ -811,27 +771,27 @@ # # See http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout -#ssl_session_cache_size = 10m # Sets the size of the caches that store session parameters +#ssl_session_cache_size = 10m # Sets the size of the caches that store session parameters. # # See https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache #ssl_cert = # Comma-separated list of certificates for `proxy_listen` values with TLS enabled. # - # If more than one certificates are specified, it can be used to provide - # alternate type of certificate (for example, ECC certificate) that will be served - # to clients that supports them. Note to properly serve using ECC certificates, + # If more than one certificate is specified, it can be used to provide + # alternate types of certificates (for example, ECC certificates) that will be served + # to clients that support them. Note that to properly serve using ECC certificates, # it is recommended to also set `ssl_cipher_suite` to # `modern` or `intermediate`. # # Unless this option is explicitly set, Kong will auto-generate - # a pair of default certificates (RSA + ECC) first time it starts up and use - # it for serving TLS requests. + # a pair of default certificates (RSA + ECC) the first time it starts up and use + # them for serving TLS requests. # - # Certificates can be configured on this property with either of the following + # Certificates can be configured on this property with any of the following # values: - # * absolute path to the certificate - # * certificate content - # * base64 encoded certificate content + # - absolute path to the certificate + # - certificate content + # - base64 encoded certificate content #ssl_cert_key = # Comma-separated list of keys for `proxy_listen` values with TLS enabled. # @@ -840,14 +800,14 @@ # provided in the same order. # # Unless this option is explicitly set, Kong will auto-generate - # a pair of default private keys (RSA + ECC) first time it starts up and use - # it for serving TLS requests. + # a pair of default private keys (RSA + ECC) the first time it starts up and use + # them for serving TLS requests. # - # Keys can be configured on this property with either of the following + # Keys can be configured on this property with any of the following # values: - # * absolute path to the certificate key - # * certificate key content - # * base64 encoded certificate key content + # - absolute path to the certificate key + # - certificate key content + # - base64 encoded certificate key content #client_ssl = off # Determines if Nginx should attempt to send client-side # TLS certificates and perform Mutual TLS Authentication @@ -859,11 +819,11 @@ # This value can be overwritten dynamically with the `client_certificate` # attribute of the `Service` object. # - # The certificate can be configured on this property with either of the following + # The certificate can be configured on this property with any of the following # values: - # * absolute path to the certificate - # * certificate content - # * base64 encoded certificate content + # - absolute path to the certificate + # - certificate content + # - base64 encoded certificate content #client_ssl_cert_key = # If `client_ssl` is enabled, the client TLS key # for the `proxy_ssl_certificate_key` directive. @@ -871,11 +831,11 @@ # This value can be overwritten dynamically with the `client_certificate` # attribute of the `Service` object. # - # The certificate key can be configured on this property with either of the following + # The certificate key can be configured on this property with any of the following # values: - # * absolute path to the certificate key - # * certificate key content - # * base64 encoded certificate key content + # - absolute path to the certificate key + # - certificate key content + # - base64 encoded certificate key content #admin_ssl_cert = # Comma-separated list of certificates for `admin_listen` values with TLS enabled. # @@ -893,13 +853,21 @@ # # See docs for `ssl_cert_key` for detailed usage. +#debug_ssl_cert = # Comma-separated list of certificates for `debug_listen` values with TLS enabled. + # + # See docs for `ssl_cert` for detailed usage. + +#debug_ssl_cert_key = # Comma-separated list of keys for `debug_listen` values with TLS enabled. + # + # See docs for `ssl_cert_key` for detailed usage. + #headers = server_tokens, latency_tokens, X-Kong-Request-Id # Comma-separated list of headers Kong should # inject in client responses. # # Accepted values are: # - `Server`: Injects `Server: kong/x.y.z` - # on Kong-produced response (e.g. Admin + # on Kong-produced responses (e.g., Admin # API, rejected requests from auth plugin). # - `Via`: Injects `Via: kong/x.y.z` for # successfully proxied requests. @@ -907,11 +875,11 @@ # (in milliseconds) by Kong to process # a request and run all plugins before # proxying the request upstream. - # - `X-Kong-Response-Latency`: time taken - # (in millisecond) by Kong to produce - # a response in case of e.g. plugin + # - `X-Kong-Response-Latency`: Time taken + # (in milliseconds) by Kong to produce + # a response in case of, e.g., a plugin # short-circuiting the request, or in - # in case of an error. + # case of an error. # - `X-Kong-Upstream-Latency`: Time taken # (in milliseconds) by the upstream # service to send response headers. @@ -930,10 +898,10 @@ # - `latency_tokens`: Same as specifying # `X-Kong-Proxy-Latency`, # `X-Kong-Response-Latency`, - # `X-Kong-Admin-Latency` and - # `X-Kong-Upstream-Latency` + # `X-Kong-Admin-Latency`, and + # `X-Kong-Upstream-Latency`. # - # In addition to those, this value can be set + # In addition to these, this value can be set # to `off`, which prevents Kong from injecting # any of the above headers. Note that this # does not prevent plugins from injecting @@ -941,6 +909,7 @@ # # Example: `headers = via, latency_tokens` + #headers_upstream = X-Kong-Request-Id # Comma-separated list of headers Kong should # inject in requests to upstream. @@ -955,7 +924,7 @@ # does not prevent plugins from injecting # headers of their own. -#trusted_ips = # Defines trusted IP addresses blocks that are +#trusted_ips = # Defines trusted IP address blocks that are # known to send correct `X-Forwarded-*` # headers. # Requests from trusted IPs make Kong forward @@ -969,7 +938,7 @@ # values (CIDR blocks) but as a # comma-separated list. # - # To trust *all* /!\ IPs, set this value to + # To trust *all* IPs, set this value to # `0.0.0.0/0,::/0`. # # If the special value `unix:` is specified, @@ -1021,7 +990,7 @@ # connection. #upstream_keepalive_max_requests = 10000 # Sets the default maximum number of - # requests than can be proxied upstream + # requests that can be proxied upstream # through one keepalive connection. # After the maximum number of requests # is reached, the connection will be @@ -1043,9 +1012,9 @@ # indefinitely. #allow_debug_header = off # Enable the `Kong-Debug` header function. - # if it is `on`, kong will add - # `Kong-Route-Id` `Kong-Route-Name` `Kong-Service-Id` - # `Kong-Service-Name` debug headers to response when + # If it is `on`, Kong will add + # `Kong-Route-Id`, `Kong-Route-Name`, `Kong-Service-Id`, + # and `Kong-Service-Name` debug headers to the response when # the client request header `Kong-Debug: 1` is present. #------------------------------------------------------------------------------ @@ -1055,7 +1024,7 @@ # Nginx directives can be dynamically injected in the runtime nginx.conf file # without requiring a custom Nginx configuration template. # -# All configuration properties respecting the naming scheme +# All configuration properties following the naming scheme # `nginx__` will result in `` being injected in # the Nginx configuration block corresponding to the property's ``. # Example: @@ -1070,18 +1039,21 @@ # - `nginx_main_`: Injects `` in Kong's configuration # `main` context. # - `nginx_events_`: Injects `` in Kong's `events {}` -# block. +# block. # - `nginx_http_`: Injects `` in Kong's `http {}` block. # - `nginx_proxy_`: Injects `` in Kong's proxy # `server {}` block. # - `nginx_location_`: Injects `` in Kong's proxy `/` -# location block (nested under Kong's proxy server {} block). +# location block (nested under Kong's proxy `server {}` block). # - `nginx_upstream_`: Injects `` in Kong's proxy # `upstream {}` block. # - `nginx_admin_`: Injects `` in Kong's Admin API # `server {}` block. # - `nginx_status_`: Injects `` in Kong's Status API -# `server {}` block (only effective if `status_listen` is enabled). +# `server {}` block (only effective if `status_listen` is enabled). +# - `nginx_debug_`: Injects `` in Kong's Debug API +# `server{}` block (only effective if `debug_listen` or `debug_listen_local` +# is enabled). # - `nginx_stream_`: Injects `` in Kong's stream module # `stream {}` block (only effective if `stream_listen` is enabled). # - `nginx_sproxy_`: Injects `` in Kong's stream module @@ -1100,7 +1072,7 @@ # # If different sets of protocols are desired between the proxy and Admin API # server, you may specify `nginx_proxy_ssl_protocols` and/or -# `nginx_admin_ssl_protocols`, both of which taking precedence over the +# `nginx_admin_ssl_protocols`, both of which take precedence over the # `http {}` block. #nginx_main_worker_rlimit_nofile = auto @@ -1131,8 +1103,8 @@ #nginx_http_large_client_header_buffers = 4 8k # Sets the maximum number and # size of buffers used for - # reading large clients - # requests headers. + # reading large client + # request headers. # See http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers #nginx_http_client_max_body_size = 0 # Defines the maximum request body size @@ -1148,13 +1120,13 @@ #nginx_admin_client_max_body_size = 10m # Defines the maximum request body size for # Admin API. -#nginx_http_charset = UTF-8 # Adds the specified charset to the "Content-Type" +#nginx_http_charset = UTF-8 # Adds the specified charset to the “Content-Type” # response header field. If this charset is different - # from the charset specified in the source_charset + # from the charset specified in the `source_charset` # directive, a conversion is performed. # # The parameter `off` cancels the addition of - # charset to the "Content-Type" response header field. + # charset to the “Content-Type” response header field. # See http://nginx.org/en/docs/http/ngx_http_charset_module.html#charset #nginx_http_client_body_buffer_size = 8k # Defines the buffer size for reading @@ -1188,7 +1160,7 @@ # in the worker process level PCRE JIT compiled regex cache. # It is recommended to set it to at least (number of regex paths * 2) # to avoid high CPU usages if you manually specified `router_flavor` to - # `traditional`. `expressions` and `traditional_compat` router does + # `traditional`. `expressions` and `traditional_compat` router do # not make use of the PCRE library and their behavior # is unaffected by this setting. @@ -1196,12 +1168,11 @@ # keep-alive connection. After the maximum number of requests are made, # the connection is closed. # Closing connections periodically is necessary to free per-connection - # memory allocations. Therefore, using too high maximum number of requests - # could result in excessive memory usage and not recommended. + # memory allocations. Therefore, using too high a maximum number of requests + # could result in excessive memory usage and is not recommended. # See: https://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_requests - #------------------------------------------------------------------------------ # DATASTORE #------------------------------------------------------------------------------ @@ -1211,9 +1182,8 @@ # independently in memory. # # When using a database, Kong will store data for all its entities (such as -# Routes, Services, Consumers, and Plugins) in PostgreSQL, -# and all Kong nodes belonging to the same cluster must connect themselves -# to the same database. +# routes, services, consumers, and plugins) in PostgreSQL, +# and all Kong nodes belonging to the same cluster must connect to the same database. # # Kong supports PostgreSQL versions 9.5 and above. # @@ -1230,13 +1200,13 @@ # reduce the latency jitter if the Kong proxy node's latency to the main # Postgres instance is high. # -# The read-only Postgres instance only serves read queries and write -# queries still goes to the main connection. The read-only Postgres instance +# The read-only Postgres instance only serves read queries, and write +# queries still go to the main connection. The read-only Postgres instance # can be eventually consistent while replicating changes from the main # instance. # # At least the `pg_ro_host` config is needed to enable this feature. -# By default, all other database config for the read-only connection are +# By default, all other database config for the read-only connection is # inherited from the corresponding main connection config described above but # may be optionally overwritten explicitly using the `pg_ro_*` config below. @@ -1371,7 +1341,7 @@ #declarative_config = # The path to the declarative configuration # file which holds the specification of all - # entities (Routes, Services, Consumers, etc.) + # entities (routes, services, consumers, etc.) # to be used when the `database` is set to # `off`. # @@ -1380,32 +1350,32 @@ # allocated to it via the `lmdb_map_size` # property. # - # If the Hybrid mode `role` is set to `data_plane` + # If the hybrid mode `role` is set to `data_plane` # and there's no configuration cache file, # this configuration is used before connecting - # to the Control Plane node as a user-controlled + # to the control plane node as a user-controlled # fallback. #declarative_config_string = # The declarative configuration as a string #lmdb_environment_path = dbless.lmdb # Directory where the LMDB database files used by - # DB-less and Hybrid mode to store Kong + # DB-less and hybrid mode to store Kong # configurations reside. # # This path is relative under the Kong `prefix`. #lmdb_map_size = 2048m # Maximum size of the LMDB memory map, used to store the - # DB-less and Hybird mode configurations. Default is 2048m. + # DB-less and hybrid mode configurations. Default is 2048m. # - # This config defines the limit of LMDB file size, the + # This config defines the limit of LMDB file size; the # actual file size growth will be on-demand and # proportional to the actual config size. # - # Note this value can be set very large, say a couple of GBs + # Note this value can be set very large, say a couple of GBs, # to accommodate future database growth and - # Multi Version Concurrency Control (MVCC) headroom needs. + # Multi-Version Concurrency Control (MVCC) headroom needs. # The file size of the LMDB database file should stabilize - # after a few config reload/Hybrid mode syncs and the actual + # after a few config reloads/hybrid mode syncs, and the actual # memory used by the LMDB database will be smaller than # the file size due to dynamic swapping of database pages by # the OS. @@ -1415,12 +1385,11 @@ #------------------------------------------------------------------------------ # In order to avoid unnecessary communication with the datastore, Kong caches -# entities (such as APIs, Consumers, Credentials...) for a configurable period +# entities (such as APIs, consumers, credentials...) for a configurable period # of time. It also handles invalidations if such an entity is updated. # # This section allows for configuring the behavior of Kong regarding the # caching of such configuration entities. - #db_update_frequency = 5 # Frequency (in seconds) at which to check for # updated entities with the datastore. # @@ -1442,7 +1411,7 @@ # servers should suffer no such delays, and # this value can be safely set to 0. # Postgres setups with read replicas should - # set this value to maximum expected replication + # set this value to the maximum expected replication # lag between the writer and reader instances. #db_cache_ttl = 0 # Time-to-live (in seconds) of an entity from @@ -1464,7 +1433,7 @@ # If set to 0, misses will never expire. #db_resurrect_ttl = 30 # Time (in seconds) for which stale entities - # from the datastore should be resurrected for + # from the datastore should be resurrected # when they cannot be refreshed (e.g., the # datastore is unreachable). When this TTL # expires, a new attempt to refresh the stale @@ -1498,22 +1467,22 @@ # # Kong will resolve hostnames as either `SRV` or `A` records (in that order, and # `CNAME` records will be dereferenced in the process). -# In case a name was resolved as an `SRV` record it will also override any given -# port number by the `port` field contents received from the DNS server. +# In case a name is resolved as an `SRV` record, it will also override any given +# port number with the `port` field contents received from the DNS server. # # The DNS options `SEARCH` and `NDOTS` (from the `/etc/resolv.conf` file) will # be used to expand short names to fully qualified ones. So it will first try # the entire `SEARCH` list for the `SRV` type, if that fails it will try the # `SEARCH` list for `A`, etc. # -# For the duration of the `ttl`, the internal DNS resolver will loadbalance each -# request it gets over the entries in the DNS record. For `SRV` records the +# For the duration of the `ttl`, the internal DNS resolver will load balance each +# request it gets over the entries in the DNS record. For `SRV` records, the # `weight` fields will be honored, but it will only use the lowest `priority` # field entries in the record. -#dns_resolver = # Comma separated list of nameservers, each +#dns_resolver = # Comma-separated list of nameservers, each # entry in `ip[:port]` format to be used by - # Kong. If not specified the nameservers in + # Kong. If not specified, the nameservers in # the local `resolv.conf` file will be used. # Port defaults to 53 if omitted. Accepts # both IPv4 and IPv6 addresses. @@ -1527,7 +1496,7 @@ # record types. The `LAST` type means the # type of the last successful lookup (for the # specified name). The format is a (case - # insensitive) comma separated list. + # insensitive) comma-separated list. #dns_valid_ttl = # By default, DNS records are cached using # the TTL value of a response. If this @@ -1549,7 +1518,7 @@ # DNS records stored in memory cache. # Least recently used DNS records are discarded # from cache if it is full. Both errors and - # data are cached, therefore a single name query + # data are cached; therefore, a single name query # can easily take up 10-15 slots. #dns_not_found_ttl = 30 # TTL in seconds for empty DNS responses and @@ -1558,9 +1527,9 @@ #dns_error_ttl = 1 # TTL in seconds for error responses. #dns_no_sync = off # If enabled, then upon a cache-miss every - # request will trigger its own dns query. - # When disabled multiple requests for the - # same name/type will be synchronised to a + # request will trigger its own DNS query. + # When disabled, multiple requests for the + # same name/type will be synchronized to a # single query. #------------------------------------------------------------------------------ @@ -1598,8 +1567,8 @@ # Defines whether this node should rebuild its # state synchronously or asynchronously (the # balancers and the router are rebuilt on - # updates that affects them, e.g., updates to - # Routes, Services or Upstreams, via the Admin + # updates that affect them, e.g., updates to + # routes, services, or upstreams via the admin # API or loading a declarative configuration # file). (This option is deprecated and will be # removed in future releases. The new default @@ -1619,18 +1588,18 @@ # # Note that `strict` ensures that all workers # of a given node will always proxy requests - # with an identical router, but that increased - # long tail latency can be observed if - # frequent Routes and Services updates are + # with an identical router, but increased + # long-tail latency can be observed if + # frequent routes and services updates are # expected. - # Using `eventual` will help preventing long - # tail latency issues in such cases, but may + # Using `eventual` will help prevent long-tail + # latency issues in such cases, but may # cause workers to route requests differently - # for a short period of time after Routes and - # Services updates. + # for a short period of time after routes and + # services updates. #worker_state_update_frequency = 5 - # Defines (in seconds) how often the worker state changes are + # Defines how often the worker state changes are # checked with a background job. When a change # is detected, a new router or balancer will be # built, as needed. Raising this value will @@ -1644,40 +1613,40 @@ # performing request routing. Incremental router # rebuild is available when the flavor is set # to either `expressions` or - # `traditional_compatible` which could - # significantly shorten rebuild time for large + # `traditional_compatible`, which could + # significantly shorten rebuild time for a large # number of routes. # # Accepted values are: # - # - `traditional_compatible`: the DSL based expression - # router engine will be used under the hood. However + # - `traditional_compatible`: the DSL-based expression + # router engine will be used under the hood. However, # the router config interface will be the same - # as `traditional` and expressions are + # as `traditional`, and expressions are # automatically generated at router build time. - # The `expression` field on the `Route` object + # The `expression` field on the `route` object # is not visible. - # - `expressions`: the DSL based expression router engine - # will be used under the hood. Traditional router - # config interface is still visible, and you could also write - # Router Expression manually and provide them in the - # `expression` field on the `Route` object. - # - `traditional`: the pre-3.0 Router engine will be - # used. Config interface will be the same as - # pre-3.0 Kong and the `expression` field on the - # `Route` object is not visible. + # - `expressions`: the DSL-based expression router engine + # will be used under the hood. The traditional router + # config interface is still visible, and you can also write + # router Expressions manually and provide them in the + # `expression` field on the `route` object. + # - `traditional`: the pre-3.0 router engine will be + # used. The config interface will be the same as + # pre-3.0 Kong, and the `expression` field on the + # `route` object is not visible. # # Deprecation warning: In Kong 3.0, `traditional` - # mode should be avoided and only be used in case - # `traditional_compatible` did not work as expected. - # This flavor of router will be removed in the next + # mode should be avoided and only be used if + # `traditional_compatible` does not work as expected. + # This flavor of the router will be removed in the next # major release of Kong. #lua_max_req_headers = 100 # Maximum number of request headers to parse by default. # # This argument can be set to an integer between 1 and 1000. # - # When proxying the Kong sends all the request headers + # When proxying, Kong sends all the request headers, # and this setting does not have any effect. It is used # to limit Kong and its plugins from reading too many # request headers. @@ -1686,18 +1655,18 @@ # # This argument can be set to an integer between 1 and 1000. # - # When proxying, Kong returns all the response headers + # When proxying, Kong returns all the response headers, # and this setting does not have any effect. It is used # to limit Kong and its plugins from reading too many # response headers. -#lua_max_uri_args = 100 # Maximum number of request uri arguments to parse by +#lua_max_uri_args = 100 # Maximum number of request URI arguments to parse by # default. # # This argument can be set to an integer between 1 and 1000. # # When proxying, Kong sends all the request query - # arguments and this setting does not have any effect. + # arguments, and this setting does not have any effect. # It is used to limit Kong and its plugins from reading # too many query arguments. @@ -1707,7 +1676,7 @@ # This argument can be set to an integer between 1 and 1000. # # When proxying, Kong sends all the request post - # arguments and this setting does not have any effect. + # arguments, and this setting does not have any effect. # It is used to limit Kong and its plugins from reading # too many post arguments. @@ -1728,29 +1697,29 @@ # The special value `system` attempts to search for the # "usual default" provided by each distro, according # to an arbitrary heuristic. In the current implementation, - # The following pathnames will be tested in order, + # the following pathnames will be tested in order, # and the first one found will be used: # - # - /etc/ssl/certs/ca-certificates.crt (Debian/Ubuntu/Gentoo) - # - /etc/pki/tls/certs/ca-bundle.crt (Fedora/RHEL 6) - # - /etc/ssl/ca-bundle.pem (OpenSUSE) - # - /etc/pki/tls/cacert.pem (OpenELEC) - # - /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem (CentOS/RHEL 7) - # - /etc/ssl/cert.pem (OpenBSD, Alpine) + # - `/etc/ssl/certs/ca-certificates.crt` (Debian/Ubuntu/Gentoo) + # - `/etc/pki/tls/certs/ca-bundle.crt` (Fedora/RHEL 6) + # - `/etc/ssl/ca-bundle.pem` (OpenSUSE) + # - `/etc/pki/tls/cacert.pem` (OpenELEC) + # - `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem` (CentOS/RHEL 7) + # - `/etc/ssl/cert.pem` (OpenBSD, Alpine) # # `system` can be used by itself or in conjunction with other - # CA filepaths. + # CA file paths. # # When `pg_ssl_verify` is enabled, these # certificate authority files will be # used for verifying Kong's database connections. # # Certificates can be configured on this property - # with either of the following values: - # * `system` - # * absolute path to the certificate - # * certificate content - # * base64 encoded certificate content + # with any of the following values: + # - `system` + # - absolute path to the certificate + # - certificate content + # - base64 encoded certificate content # # See https://github.com/openresty/lua-nginx-module#lua_ssl_trusted_certificate @@ -1794,7 +1763,6 @@ # server. # # See https://github.com/openresty/lua-nginx-module#lua_socket_pool_size - #untrusted_lua = sandbox # Controls loading of Lua functions from admin-supplied # sources such as the Admin API. LuaJIT bytecode @@ -1911,9 +1879,9 @@ #------------------------------------------------------------------------------ # KONG MANAGER #------------------------------------------------------------------------------ - -# The Admin GUI for Kong Gateway. - +# +# The Admin GUI for Kong Enterprise. +# #admin_gui_listen = 0.0.0.0:8002, 0.0.0.0:8445 ssl # Kong Manager Listeners # @@ -1925,14 +1893,10 @@ # Suffixes can be specified for each pair, similarly to # the `admin_listen` directive. -#admin_gui_url = - # Kong Manager URL +#admin_gui_url = # Kong Manager URL # # The lookup, or balancer, address for Kong Manager. # - # When set, the CORS headers in the Admin API response - # will also change to the corresponding origin - # # Accepted format (items in parentheses are optional): # # `://(:)` @@ -1940,16 +1904,19 @@ # Examples: # # - `http://127.0.0.1:8003` - # - `https://kong-manager.test` + # - `https://kong-admin.test` # - `http://dev-machine` + # + # By default, Kong Manager will use the window request + # host and append the resolved listener port depending + # on the requested protocol. -#admin_gui_path = / - # Kong Manager base path +#admin_gui_path = / # Kong Manager base path # # This configuration parameter allows the user to customize # the path prefix where Kong Manager is served. When updating - # this parameter, it's recommended to update the path in - # `admin_gui_url` as well. + # this parameter, it's recommended to update the path in `admin_gui_url` + # as well. # # Accepted format: # @@ -1966,37 +1933,34 @@ # - `/kong-manager` # - `/kong/manager` -#admin_gui_api_url = - # Hierarchical part of a URL which is composed +#admin_gui_api_url = # Hierarchical part of a URI which is composed # optionally of a host, port, and path at which the # Admin API accepts HTTP or HTTPS traffic. When - # this config is not provided, Kong Manager will + # this config is disabled, Kong Manager will # use the window protocol + host and append the # resolved admin_listen HTTP/HTTPS port. -#admin_gui_ssl_cert = - # The SSL certificate for `admin_gui_listen` values +#admin_gui_ssl_cert = # The SSL certificate for `admin_gui_listen` values # with SSL enabled. # # values: - # * absolute path to the certificate - # * certificate content - # * base64 encoded certificate content - -#admin_gui_ssl_cert_key = - # The SSL key for `admin_gui_listen` values with SSL - # enabled. - # - # values: - # * absolute path to the certificate key - # * certificate key content - # * base64 encoded certificate key content + # - absolute path to the certificate + # - certificate content + # - base64 encoded certificate content + +#admin_gui_ssl_cert_key = # The SSL key for `admin_gui_listen` values with SSL + # enabled. + # + # values: + # - absolute path to the certificate key + # - certificate key content + # - base64 encoded certificate key content #admin_gui_access_log = logs/admin_gui_access.log # Kong Manager Access Logs # - # Here you can set an absolute or relative path for - # Kong Manager access logs. When the path is relative, + # Here you can set an absolute or relative path for Kong + # Manager access logs. When the path is relative, # logs are placed in the `prefix` location. # # Setting this value to `off` disables access logs @@ -2006,8 +1970,8 @@ #admin_gui_error_log = logs/admin_gui_error.log # Kong Manager Error Logs # - # Here you can set an absolute or relative path for - # Kong Manager access logs. When the path is relative, + # Here you can set an absolute or relative path for Kong + # Manager access logs. When the path is relative, # logs are placed in the `prefix` location. # # Setting this value to `off` disables error logs for @@ -2018,7 +1982,7 @@ #------------------------------------------------------------------------------ -# WASM +# WEBASSEMBLY (WASM) #------------------------------------------------------------------------------ #wasm = off # Enable/disable wasm support. This must be enabled in @@ -2044,14 +2008,14 @@ # The resulting filter modules available for use in Kong # will be: # - # * `my_module` - # * `my_other_module` + # - `my_module` + # - `my_other_module` # # Notes: # - # * No recursion is performed. Only .wasm files at the - # top level are registered - # * This path _may_ be a symlink to a directory. + # - No recursion is performed. Only .wasm files at the + # top level are registered. + # - This path _may_ be a symlink to a directory. #wasm_filters = bundled,user # Comma-separated list of Wasm filters to be made # available for use in filter chains. @@ -2073,7 +2037,7 @@ # filters # - `wasm_filters = filter-a,filter-b` enables _only_ # filters named `filter-a` or `filter-b` (whether - # bundled _or_ user-suppplied) + # bundled _or_ user-supplied) # # If a conflict occurs where a bundled filter and a # user-supplied filter share the same name, a warning @@ -2084,7 +2048,7 @@ # WASM injected directives #------------------------------------------------------------------------------ -# The Nginx Wasm module (i.e. ngx_wasm_module) has its own settings, which can +# The Nginx Wasm module (i.e., ngx_wasm_module) has its own settings, which can # be tuned via `wasm_*` directives in the Nginx configuration file. Kong # supports configuration of these directives via its Nginx directive injection # mechanism. @@ -2102,7 +2066,7 @@ # separate namespaces in the `"/"` format. # For using these functions with non-namespaced keys, the Nginx template needs # a `shm_kv *` entry, which can be defined using `nginx_wasm_shm_kv`. -# - `nginx_wasm_wasmtime_`: Injects `flag ` into the `wasmtime {}` +# - `nginx_wasm_wasmtime_`: Injects `flag ` into the `wasmtime {}` # block, allowing various Wasmtime-specific flags to be set. # - `nginx__`: Injects `` into the # `http {}` or `server {}` blocks, as specified in the Nginx injected directives @@ -2138,16 +2102,16 @@ # `nginx_wasm_tls_trusted_certificate` directive. # - `lua_ssl_verify_depth`: when set (to a value greater than zero), several # TLS-related `nginx_wasm_*` settings are enabled: -# * `nginx_wasm_tls_verify_cert` -# * `nginx_wasm_tls_verify_host` -# * `nginx_wasm_tls_no_verify_warn` +# - `nginx_wasm_tls_verify_cert` +# - `nginx_wasm_tls_verify_host` +# - `nginx_wasm_tls_no_verify_warn` # # Like other `kong.conf` fields, all injected Nginx directives documented here # can be set via environment variable. For instance, setting: # # `KONG_NGINX_WASM_TLS_VERIFY_CERT=` # -# Will inject the following in to the `wasm {}` block: +# Will inject the following into the `wasm {}` block: # # `tls_verify_cert ;` # @@ -2166,13 +2130,13 @@ #------------------------------------------------------------------------------- # REQUEST DEBUGGING #------------------------------------------------------------------------------- -# Request debugging is a mechanism that allows admin to collect the timing of -# proxy path request in the response header (X-Kong-Request-Debug-Output) +# Request debugging is a mechanism that allows admins to collect the timing of +# proxy path requests in the response header (X-Kong-Request-Debug-Output) # and optionally, the error log. # # This feature provides insights into the time spent within various components of Kong, # such as plugins, DNS resolution, load balancing, and more. It also provides contextual -# information such as domain name tried during these processes. +# information such as domain names tried during these processes. # #request_debug = on # When enabled, Kong will provide detailed timing information # for its components to the client and the error log @@ -2180,8 +2144,19 @@ # - `X-Kong-Request-Debug`: # If the value is set to `*`, # timing information will be collected and exported for the current request. - # If this header is not present or contains unknown value, + # If this header is not present or contains an unknown value, # timing information will not be collected for the current request. + # You can also specify a list of filters, separated by commas, + # to filter the scope of the time information that is collected. + # The following filters are supported for `X-Kong-Request-Debug`: + # - `rewrite`: Collect timing information from the `rewrite` phase. + # - `access`: Collect timing information from the `access` phase. + # - `balancer`: Collect timing information from the `balancer` phase. + # - `response`: Collect timing information from the `response` phase. + # - `header_filter`: Collect timing information from the `header_filter` phase. + # - `body_filter`: Collect timing information from the `body_filter` phase. + # - `log`: Collect timing information from the `log` phase. + # - `upstream`: Collect timing information from the `upstream` phase. # # - `X-Kong-Request-Debug-Log`: # If set to `true`, timing information will also be logged diff --git a/kong/api/arguments.lua b/kong/api/arguments.lua index e58c042a7da..d0c12d7a9a7 100644 --- a/kong/api/arguments.lua +++ b/kong/api/arguments.lua @@ -1,25 +1,21 @@ local cjson = require "cjson.safe" local upload = require "resty.upload" +local decoder = require "kong.api.arguments_decoder" local setmetatable = setmetatable local getmetatable = getmetatable -local tonumber = tonumber local rawget = rawget local concat = table.concat local insert = table.insert -local ipairs = ipairs local pairs = pairs local lower = string.lower local find = string.find local sub = string.sub -local next = next local type = type local ngx = ngx local req = ngx.req local log = ngx.log -local re_match = ngx.re.match -local re_gmatch = ngx.re.gmatch local req_read_body = req.read_body local get_uri_args = req.get_uri_args local get_body_data = req.get_body_data @@ -31,25 +27,9 @@ local kong = kong local NOTICE = ngx.NOTICE -local multipart_mt = {} local arguments_mt = {} -function multipart_mt:__tostring() - return self.data -end - - -function multipart_mt:__index(name) - local json = rawget(self, "json") - if json then - return json[name] - end - - return nil -end - - function arguments_mt:__index(name) return rawget(self, "post")[name] or rawget(self, "uri")[name] @@ -129,7 +109,7 @@ end local function combine_arg(to, arg) - if type(arg) ~= "table" or getmetatable(arg) == multipart_mt then + if type(arg) ~= "table" or getmetatable(arg) == decoder.multipart_mt then insert(to, #to + 1, arg) else @@ -140,7 +120,7 @@ local function combine_arg(to, arg) to[k] = v else - if type(t) == "table" and getmetatable(t) ~= multipart_mt then + if type(t) == "table" and getmetatable(t) ~= decoder.multipart_mt then combine_arg(t, v) else @@ -279,140 +259,6 @@ infer = function(args, schema) end -local function decode_array_arg(name, value, container) - container = container or {} - - if type(name) ~= "string" then - container[name] = value - return container[name] - end - - local indexes = {} - local count = 0 - local search = name - - while true do - local captures, err = re_match(search, [[(.+)\[(\d*)\]$]], "ajos") - if captures then - search = captures[1] - count = count + 1 - indexes[count] = tonumber(captures[2]) - - elseif err then - log(NOTICE, err) - break - - else - break - end - end - - if count == 0 then - container[name] = value - return container[name] - end - - container[search] = {} - container = container[search] - - for i = count, 1, -1 do - local index = indexes[i] - - if i == 1 then - if index then - insert(container, index, value) - return container[index] - end - - if type(value) == "table" and getmetatable(value) ~= multipart_mt then - for j, v in ipairs(value) do - insert(container, j, v) - end - - else - container[#container + 1] = value - end - - return container - - else - if not container[index or 1] then - container[index or 1] = {} - container = container[index or 1] - end - end - end -end - - -local function decode_arg(name, value) - if type(name) ~= "string" or re_match(name, [[^\.+|\.$]], "jos") then - return { name = value } - end - - local iterator, err = re_gmatch(name, [[[^.]+]], "jos") - if not iterator then - if err then - log(NOTICE, err) - end - - return decode_array_arg(name, value) - end - - local names = {} - local count = 0 - - while true do - local captures, err = iterator() - if captures then - count = count + 1 - names[count] = captures[0] - - elseif err then - log(NOTICE, err) - break - - else - break - end - end - - if count == 0 then - return decode_array_arg(name, value) - end - - local container = {} - local bucket = container - - for i = 1, count do - if i == count then - decode_array_arg(names[i], value, bucket) - return container - - else - bucket = decode_array_arg(names[i], {}, bucket) - end - end -end - - -local function decode(args, schema) - local i = 0 - local r = {} - - if type(args) ~= "table" then - return r - end - - for name, value in pairs(args) do - i = i + 1 - r[i] = decode_arg(name, value) - end - - return infer(combine(r), schema) -end - - local function parse_multipart_header(header, results) local name local value @@ -531,7 +377,7 @@ local function parse_multipart_stream(options, boundary) data = { n = 0, } - }, multipart_mt) + }, decoder.multipart_mt) headers = nil end @@ -582,7 +428,7 @@ local function parse_multipart_stream(options, boundary) if part_name then local enclosure = part_args[part_name] if enclosure then - if type(enclosure) == "table" and getmetatable(enclosure) ~= multipart_mt then + if type(enclosure) == "table" and getmetatable(enclosure) ~= decoder.multipart_mt then enclosure[#enclosure + 1] = part else @@ -624,6 +470,14 @@ local function parse_multipart(options, content_type) end +-- decodes and infers the arguments +-- the name "decode" is kept for backwards compatibility +local function decode(args, schema) + local decoded = decoder.decode(args) + return infer(combine(decoded), schema) +end + + local function load(opts) local options = setmetatable(opts or {}, defaults) @@ -730,11 +584,9 @@ end return { - load = load, - decode = decode, - decode_arg = decode_arg, - infer = infer, - infer_value = infer_value, - combine = combine, - multipart_mt = multipart_mt, + load = load, + infer_value = infer_value, + decode = decode, + _infer = infer, + _combine = combine, } diff --git a/kong/api/arguments_decoder.lua b/kong/api/arguments_decoder.lua new file mode 100644 index 00000000000..b8b2839c301 --- /dev/null +++ b/kong/api/arguments_decoder.lua @@ -0,0 +1,287 @@ +local getmetatable = getmetatable +local tonumber = tonumber +local rawget = rawget +local insert = table.insert +local unpack = table.unpack -- luacheck: ignore table +local ipairs = ipairs +local pairs = pairs +local type = type +local ngx = ngx +local req = ngx.req +local log = ngx.log +local re_match = ngx.re.match +local re_gmatch = ngx.re.gmatch +local re_gsub = ngx.re.gsub +local get_method = req.get_method + + +local NOTICE = ngx.NOTICE + +local ENC_LEFT_SQUARE_BRACKET = "%5B" +local ENC_RIGHT_SQUARE_BRACKET = "%5D" + + +local multipart_mt = {} + + +function multipart_mt:__tostring() + return self.data +end + + +function multipart_mt:__index(name) + local json = rawget(self, "json") + if json then + return json[name] + end + + return nil +end + + +-- Extracts keys from a string representing a nested table +-- e.g. [foo][bar][21].key => ["foo", "bar", 21, "key"]. +-- is_map is meant to label patterns that use the bracket map syntax. +local function extract_param_keys(keys_string) + local is_map = false + + -- iterate through keys (split by dots or square brackets) + local iterator, err = re_gmatch(keys_string, [=[\.([^\[\.]+)|\[([^\]]*)\]]=], "jos") + if not iterator then + return nil, err + end + + local keys = {} + for captures, it_err in iterator do + if it_err then + log(NOTICE, it_err) + + else + local key_name + if captures[1] then + -- The first capture: `\.([^\[\.]+)` matches dot-separated keys + key_name = captures[1] + + else + -- The second capture: \[([^\]]*)\] matches bracket-separated keys + key_name = captures[2] + + -- If a bracket-separated key is non-empty and non-numeric, set + -- is_map to true: foo[test] is a map, foo[] and bar[42] are arrays + local map_key_found = key_name ~= "" and tonumber(key_name) == nil + if map_key_found then + is_map = true + end + end + + insert(keys, key_name) + end + end + + return keys, nil, is_map +end + + +-- Extracts the parameter name and keys from a string +-- e.g. myparam[foo][bar][21].key => myparam, ["foo", "bar", 21, "key"] +local function get_param_name_and_keys(name_and_keys) + -- key delimiter must appear after the first character + -- e.g. for `[5][foo][bar].key`, `[5]` is the parameter name + local first_key_delimiter = name_and_keys:find('[%[%.]', 2) + if not first_key_delimiter then + return nil, "keys not found" + end + + local param_name = name_and_keys:sub(1, first_key_delimiter - 1) + local keys_string = name_and_keys:sub(first_key_delimiter) + + local keys, err, is_map = extract_param_keys(keys_string) + if not keys then + return nil, err + end + + return param_name, nil, keys, is_map +end + + +-- Nests the provided path into container +-- e.g. nest_path({}, {"foo", "bar", 21, "key"}, 42) => { foo = { bar = { [21] = { key = 42 } } } } +local function nest_path(container, path, value) + container = container or {} + + if type(path) ~= "table" then + return nil, "path must be a table" + end + + for i = 1, #path do + local segment = path[i] + + local arr_index = tonumber(segment) + -- if it looks like: foo[] or bar[42], it's an array + local isarray = segment == "" or arr_index ~= nil + + if isarray then + if i == #path then + + if arr_index then + insert(container, arr_index, value) + return container[arr_index] + end + + if type(value) == "table" and getmetatable(value) ~= multipart_mt then + for j, v in ipairs(value) do + insert(container, j, v) + end + + return container + end + + container[#container + 1] = value + return container + + else + local position = arr_index or 1 + if not container[position] then + container[position] = {} + container = container[position] + end + end + + else -- it's a map + if i == #path then + container[segment] = value + return container[segment] + + elseif not container[segment] then + container[segment] = {} + container = container[segment] + end + end + end +end + + +-- Decodes a complex argument (map, array or mixed), into a nested table +-- e.g. foo[bar][21].key, 42 => { foo = { bar = { [21] = { key = 42 } } } } +local function decode_map_array_arg(name, value, container) + local param_name, err, keys, is_map = get_param_name_and_keys(name) + if not param_name or not keys or #keys == 0 then + return nil, err or "not a map or array" + end + + -- the meaning of square brackets varies depending on the http method. + -- It is considered a map when a non numeric value exists between brackets + -- if the method is POST, PUT, or PATCH, otherwise it is interpreted as LHS + -- brackets used for search capabilities (only in EE). + if is_map then + local method = get_method() + if method ~= "POST" and method ~= "PUT" and method ~= "PATCH" then + return nil, "map not supported for this method" + end + end + + local path = {param_name, unpack(keys)} + return nest_path(container, path, value) +end + + +local function decode_complex_arg(name, value, container) + container = container or {} + + if type(name) ~= "string" then + container[name] = value + return container[name] + end + + local decoded = decode_map_array_arg(name, value, container) + if not decoded then + container[name] = value + return container[name] + end + + return decoded +end + + +local function decode_arg(raw_name, value) + if type(raw_name) ~= "string" or re_match(raw_name, [[^\.+|\.$]], "jos") then + return { name = value } + end + + -- unescape `[` and `]` characters when the array / map syntax is detected + local array_map_pattern = ENC_LEFT_SQUARE_BRACKET .. "(.*?)" .. ENC_RIGHT_SQUARE_BRACKET + local name = re_gsub(raw_name, array_map_pattern, "[$1]", "josi") + + -- treat test[foo.bar] as a single match instead of splitting on the dot + local iterator, err = re_gmatch(name, [=[([^.](?:\[[^\]]*\])*)+]=], "jos") + if not iterator then + if err then + log(NOTICE, err) + end + + return decode_complex_arg(name, value) + end + + local names = {} + local count = 0 + + while true do + local captures, err = iterator() + if captures then + count = count + 1 + names[count] = captures[0] + + elseif err then + log(NOTICE, err) + break + + else + break + end + end + + if count == 0 then + return decode_complex_arg(name, value) + end + + local container = {} + local bucket = container + + for i = 1, count do + if i == count then + decode_complex_arg(names[i], value, bucket) + return container + + else + bucket = decode_complex_arg(names[i], {}, bucket) + end + end +end + + +local function decode(args) + local i = 0 + local r = {} + + if type(args) ~= "table" then + return r + end + + for name, value in pairs(args) do + i = i + 1 + r[i] = decode_arg(name, value) + end + + return r +end + + +return { + decode = decode, + multipart_mt = multipart_mt, + _decode_arg = decode_arg, + _extract_param_keys = extract_param_keys, + _get_param_name_and_keys = get_param_name_and_keys, + _nest_path = nest_path, + _decode_map_array_arg = decode_map_array_arg, +} diff --git a/kong/api/routes/kong.lua b/kong/api/routes/kong.lua index d2fa8a59443..633083a6d5f 100644 --- a/kong/api/routes/kong.lua +++ b/kong/api/routes/kong.lua @@ -269,5 +269,20 @@ return { } return kong.response.exit(200, body) end - } + }, + ["/status/dns"] = { + GET = function (self, db, helpers) + if kong.configuration.legacy_dns_client then + return kong.response.exit(501, { message = "not implemented with the legacy DNS client" }) + end + + return kong.response.exit(200, { + worker = { + id = ngx.worker.id() or -1, + count = ngx.worker.count(), + }, + stats = kong.dns.stats(), + }) + end + }, } diff --git a/kong/clustering/compat/checkers.lua b/kong/clustering/compat/checkers.lua index 3dd083fd7eb..55dcbbc2bd4 100644 --- a/kong/clustering/compat/checkers.lua +++ b/kong/clustering/compat/checkers.lua @@ -2,7 +2,7 @@ local ipairs = ipairs local type = type -local log_warn_message +local log_warn_message, _AI_PROVIDER_INCOMPATIBLE do local ngx_log = ngx.log local ngx_WARN = ngx.WARN @@ -19,21 +19,81 @@ do KONG_VERSION, hint, dp_version, action) ngx_log(ngx_WARN, _log_prefix, msg, log_suffix) end -end + local _AI_PROVIDERS_ADDED = { + [3008000000] = { + "gemini", + "bedrock", + }, + } + + _AI_PROVIDER_INCOMPATIBLE = function(provider, ver) + for _, v in ipairs(_AI_PROVIDERS_ADDED[ver]) do + if v == provider then + return true + end + end + + return false + end +end local compatible_checkers = { { 3008000000, --[[ 3.8.0.0 ]] function (config_table, dp_version, log_suffix) local has_update - local adapter = require("kong.plugins.proxy-cache.clustering.compat.response_headers_translation").adapter for _, plugin in ipairs(config_table.plugins or {}) do - if plugin.name == 'proxy-cache' then - has_update = adapter(plugin.config) - if has_update then - log_warn_message('adapts ' .. plugin.name .. ' plugin response_headers configuration to older version', - 'revert to older schema', - dp_version, log_suffix) + if plugin.name == 'aws-lambda' then + local config = plugin.config + if config.aws_sts_endpoint_url ~= nil then + config.aws_sts_endpoint_url = nil + has_update = true + log_warn_message('configures ' .. plugin.name .. ' plugin with aws_sts_endpoint_url', + 'will be removed.', + dp_version, log_suffix) + end + end + + if plugin.name == 'ai-proxy' then + local config = plugin.config + if _AI_PROVIDER_INCOMPATIBLE(config.model.provider, 3008000000) then + log_warn_message('configures ' .. plugin.name .. ' plugin with' .. + ' "openai preserve mode", because ' .. config.model.provider .. ' provider ' .. + ' is not supported in this release', + dp_version, log_suffix) + + config.model.provider = "openai" + config.route_type = "preserve" + + has_update = true + end + end + + if plugin.name == 'ai-request-transformer' then + local config = plugin.config + if _AI_PROVIDER_INCOMPATIBLE(config.llm.model.provider, 3008000000) then + log_warn_message('configures ' .. plugin.name .. ' plugin with' .. + ' "openai preserve mode", because ' .. config.llm.model.provider .. ' provider ' .. + ' is not supported in this release', + dp_version, log_suffix) + + config.llm.model.provider = "openai" + + has_update = true + end + end + + if plugin.name == 'ai-response-transformer' then + local config = plugin.config + if _AI_PROVIDER_INCOMPATIBLE(config.llm.model.provider, 3008000000) then + log_warn_message('configures ' .. plugin.name .. ' plugin with' .. + ' "openai preserve mode", because ' .. config.llm.model.provider .. ' provider ' .. + ' is not supported in this release', + dp_version, log_suffix) + + config.llm.model.provider = "openai" + + has_update = true end end end diff --git a/kong/clustering/compat/init.lua b/kong/clustering/compat/init.lua index 54d8f357382..85bcf072d8d 100644 --- a/kong/clustering/compat/init.lua +++ b/kong/clustering/compat/init.lua @@ -303,6 +303,14 @@ local function invalidate_keys_from_config(config_plugins, keys, log_suffix, dp_ end end + -- Any dataplane older than 3.8.0 + if dp_version_num < 3008000000 then + -- OSS + if name == "opentelemetry" then + has_update = rename_field(config, "traces_endpoint", "endpoint", has_update) + end + end + for _, key in ipairs(keys[name]) do if delete_at(config, key) then ngx_log(ngx_WARN, _log_prefix, name, " plugin contains configuration '", key, diff --git a/kong/clustering/compat/removed_fields.lua b/kong/clustering/compat/removed_fields.lua index 96561e6d5b0..5c1b7404fe8 100644 --- a/kong/clustering/compat/removed_fields.lua +++ b/kong/clustering/compat/removed_fields.lua @@ -163,5 +163,52 @@ return { oauth2 = { "realm", }, + opentelemetry = { + "traces_endpoint", + "logs_endpoint", + }, + ai_proxy = { + "max_request_body_size", + "model.options.gemini", + "auth.gcp_use_service_account", + "auth.gcp_service_account_json", + "model.options.bedrock", + "auth.aws_access_key_id", + "auth.aws_secret_access_key", + }, + ai_prompt_decorator = { + "max_request_body_size", + }, + ai_prompt_guard = { + "match_all_roles", + "max_request_body_size", + }, + ai_prompt_template = { + "max_request_body_size", + }, + ai_request_transformer = { + "max_request_body_size", + "llm.model.options.gemini", + "llm.auth.gcp_use_service_account", + "llm.auth.gcp_service_account_json", + "llm.model.options.bedrock", + "llm.auth.aws_access_key_id", + "llm.auth.aws_secret_access_key", + }, + ai_response_transformer = { + "max_request_body_size", + "llm.model.options.gemini", + "llm.auth.gcp_use_service_account", + "llm.auth.gcp_service_account_json", + "llm.model.options.bedrock", + "llm.auth.aws_access_key_id", + "llm.auth.aws_secret_access_key", + }, + prometheus = { + "ai_metrics", + }, + acl = { + "always_use_authenticated_groups", + }, }, } diff --git a/kong/clustering/control_plane.lua b/kong/clustering/control_plane.lua index 6bdfb24e192..990eb5ec346 100644 --- a/kong/clustering/control_plane.lua +++ b/kong/clustering/control_plane.lua @@ -3,7 +3,6 @@ local _MT = { __index = _M, } local semaphore = require("ngx.semaphore") -local cjson = require("cjson.safe") local declarative = require("kong.db.declarative") local clustering_utils = require("kong.clustering.utils") local compat = require("kong.clustering.compat") @@ -20,8 +19,8 @@ local pairs = pairs local ngx = ngx local ngx_log = ngx.log local timer_at = ngx.timer.at -local cjson_decode = cjson.decode -local cjson_encode = cjson.encode +local json_decode = clustering_utils.json_decode +local json_encode = clustering_utils.json_encode local kong = kong local ngx_exit = ngx.exit local exiting = ngx.worker.exiting @@ -121,7 +120,7 @@ function _M:export_deflated_reconfigure_payload() -- store serialized plugins map for troubleshooting purposes local shm_key_name = "clustering:cp_plugins_configured:worker_" .. (worker_id() or -1) - kong_dict:set(shm_key_name, cjson_encode(self.plugins_configured)) + kong_dict:set(shm_key_name, json_encode(self.plugins_configured)) ngx_log(ngx_DEBUG, "plugin configuration map key: ", shm_key_name, " configuration: ", kong_dict:get(shm_key_name)) local config_hash, hashes = calculate_config_hash(config_table) @@ -136,7 +135,7 @@ function _M:export_deflated_reconfigure_payload() self.reconfigure_payload = payload - payload, err = cjson_encode(payload) + payload, err = json_encode(payload) if not payload then return nil, err end @@ -207,7 +206,7 @@ function _M:handle_cp_websocket(cert) err = "failed to receive websocket basic info data" else - data, err = cjson_decode(data) + data, err = json_decode(data) if type(data) ~= "table" then err = "failed to decode websocket basic info data" .. (err and ": " .. err or "") diff --git a/kong/clustering/data_plane.lua b/kong/clustering/data_plane.lua index f6621c2a234..35ae9161f27 100644 --- a/kong/clustering/data_plane.lua +++ b/kong/clustering/data_plane.lua @@ -3,7 +3,6 @@ local _MT = { __index = _M, } local semaphore = require("ngx.semaphore") -local cjson = require("cjson.safe") local config_helper = require("kong.clustering.config_helper") local clustering_utils = require("kong.clustering.utils") local declarative = require("kong.db.declarative") @@ -18,8 +17,8 @@ local sub = string.sub local ngx = ngx local ngx_log = ngx.log local ngx_sleep = ngx.sleep -local cjson_decode = cjson.decode -local cjson_encode = cjson.encode +local json_decode = clustering_utils.json_decode +local json_encode = clustering_utils.json_encode local exiting = ngx.worker.exiting local ngx_time = ngx.time local inflate_gzip = require("kong.tools.gzip").inflate_gzip @@ -111,7 +110,7 @@ end ---@param err_t kong.clustering.config_helper.update.err_t ---@param log_suffix? string local function send_error(c, err_t, log_suffix) - local payload, json_err = cjson_encode({ + local payload, json_err = json_encode({ type = "error", error = err_t, }) @@ -121,7 +120,7 @@ local function send_error(c, err_t, log_suffix) ngx_log(ngx_ERR, _log_prefix, "failed to JSON-encode error payload for ", "control plane: ", json_err, ", payload: ", inspect(err_t), log_suffix) - payload = assert(cjson_encode({ + payload = assert(json_encode({ type = "error", error = { name = constants.CLUSTERING_DATA_PLANE_ERROR.GENERIC, @@ -180,11 +179,11 @@ function _M:communicate(premature) -- The CP will make the decision on whether sync will be allowed -- based on the received information local _ - _, err = c:send_binary(cjson_encode({ type = "basic_info", - plugins = self.plugins_list, - process_conf = configuration, - filters = self.filters, - labels = labels, })) + _, err = c:send_binary(json_encode({ type = "basic_info", + plugins = self.plugins_list, + process_conf = configuration, + filters = self.filters, + labels = labels, })) if err then ngx_log(ngx_ERR, _log_prefix, "unable to send basic information to control plane: ", uri, " err: ", err, " (retrying after ", reconnection_delay, " seconds)", log_suffix) @@ -238,7 +237,7 @@ function _M:communicate(premature) local msg = assert(inflate_gzip(data)) yield() - msg = assert(cjson_decode(msg)) + msg = assert(json_decode(msg)) yield() if msg.type ~= "reconfigure" then diff --git a/kong/clustering/utils.lua b/kong/clustering/utils.lua index 0ac9c8e6926..8598ff3e51c 100644 --- a/kong/clustering/utils.lua +++ b/kong/clustering/utils.lua @@ -3,6 +3,7 @@ local ws_client = require("resty.websocket.client") local ws_server = require("resty.websocket.server") local parse_url = require("socket.url").parse local process_type = require("ngx.process").type +local cjson = require("cjson.safe") local type = type local table_insert = table.insert @@ -24,8 +25,8 @@ local _log_prefix = "[clustering] " local KONG_VERSION = kong.version -local prefix = kong.configuration.prefix or require("pl.path").abspath(ngx.config.prefix()) -local CLUSTER_PROXY_SSL_TERMINATOR_SOCK = fmt("unix:%s/cluster_proxy_ssl_terminator.sock", prefix) +local CLUSTER_PROXY_SSL_TERMINATOR_SOCK = fmt("unix:%s/cluster_proxy_ssl_terminator.sock", + kong.configuration.socket_path) local _M = {} @@ -155,6 +156,7 @@ function _M.connect_dp(dp_id, dp_hostname, dp_ip, dp_version) return wb, log_suffix end + function _M.is_dp_worker_process() if kong.configuration.role == "data_plane" and kong.configuration.dedicated_config_processing == true then @@ -164,4 +166,34 @@ function _M.is_dp_worker_process() return worker_id() == 0 end + +-- encode/decode json with cjson or simdjson +local ok, simdjson_dec = pcall(require, "resty.simdjson.decoder") +if not ok or kong.configuration.cluster_cjson then + _M.json_decode = cjson.decode + _M.json_encode = cjson.encode + +else + _M.json_decode = function(str) + -- enable yield and not reentrant for decode + local dec = simdjson_dec.new(true) + + local res, err = dec:process(str) + dec:destroy() + + return res, err + end + + _M.json_encode = cjson.encode + --[[ TODO: make simdjson encoding more compatible with cjson + -- enable yield and reentrant for encode + local enc = require("resty.simdjson.encoder").new(true) + + _M.json_encode = function(obj) + return enc:process(obj) + end + --]] +end + + return _M diff --git a/kong/cmd/start.lua b/kong/cmd/start.lua index 75c0c7b0af8..6bc2dd97b41 100644 --- a/kong/cmd/start.lua +++ b/kong/cmd/start.lua @@ -13,11 +13,11 @@ local function is_socket(path) return lfs.attributes(path, "mode") == "socket" end -local function cleanup_dangling_unix_sockets(prefix) +local function cleanup_dangling_unix_sockets(socket_path) local found = {} - for child in lfs.dir(prefix) do - local path = prefix .. "/" .. child + for child in lfs.dir(socket_path) do + local path = socket_path .. "/" .. child if is_socket(path) then table.insert(found, path) end @@ -31,7 +31,7 @@ local function cleanup_dangling_unix_sockets(prefix) "preparing to start Kong. This may be a sign that Kong was " .. "previously shut down uncleanly or is in an unknown state and " .. "could require further investigation.", - prefix) + socket_path) log.warn("Attempting to remove dangling sockets before starting Kong...") @@ -59,7 +59,7 @@ local function execute(args) assert(prefix_handler.prepare_prefix(conf, args.nginx_conf, nil, nil, args.nginx_conf_flags)) - cleanup_dangling_unix_sockets(conf.prefix) + cleanup_dangling_unix_sockets(conf.socket_path) _G.kong = kong_global.new() kong_global.init_pdk(_G.kong, conf) diff --git a/kong/cmd/utils/prefix_handler.lua b/kong/cmd/utils/prefix_handler.lua index b1e6557f4ac..74268b139bb 100644 --- a/kong/cmd/utils/prefix_handler.lua +++ b/kong/cmd/utils/prefix_handler.lua @@ -481,6 +481,13 @@ local function prepare_prefix(kong_config, nginx_custom_template_path, skip_writ return nil, kong_config.prefix .. " is not a directory" end + if not exists(kong_config.socket_path) then + local ok, err = makepath(kong_config.socket_path) + if not ok then + return nil, err + end + end + -- create directories in prefix for _, dir in ipairs {"logs", "pids"} do local ok, err = makepath(join(kong_config.prefix, dir)) diff --git a/kong/conf_loader/constants.lua b/kong/conf_loader/constants.lua index cda8a9a9ccd..59bd482cce6 100644 --- a/kong/conf_loader/constants.lua +++ b/kong/conf_loader/constants.lua @@ -370,6 +370,7 @@ local CONF_PARSERS = { dns_not_found_ttl = { typ = "number" }, dns_error_ttl = { typ = "number" }, dns_no_sync = { typ = "boolean" }, + legacy_dns_client = { typ = "boolean" }, privileged_worker = { typ = "boolean", deprecated = { @@ -498,6 +499,7 @@ local CONF_PARSERS = { cluster_use_proxy = { typ = "boolean" }, cluster_dp_labels = { typ = "array" }, cluster_rpc = { typ = "boolean" }, + cluster_cjson = { typ = "boolean" }, kic = { typ = "boolean" }, pluginserver_names = { typ = "array" }, diff --git a/kong/conf_loader/init.lua b/kong/conf_loader/init.lua index 13b908dc4c3..ac11ecb27b8 100644 --- a/kong/conf_loader/init.lua +++ b/kong/conf_loader/init.lua @@ -17,6 +17,7 @@ local pl_path = require "pl.path" local tablex = require "pl.tablex" local log = require "kong.cmd.utils.log" local env = require "kong.cmd.utils.env" +local constants = require "kong.constants" local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy @@ -482,6 +483,10 @@ local function load(path, custom_conf, opts) -- load absolute paths conf.prefix = abspath(conf.prefix) + -- The socket path is where we store listening unix sockets for IPC and private APIs. + -- It is derived from the prefix and is NOT intended to be user-configurable + conf.socket_path = pl_path.join(conf.prefix, constants.SOCKET_DIRECTORY) + if conf.lua_ssl_trusted_certificate and #conf.lua_ssl_trusted_certificate > 0 then @@ -640,8 +645,7 @@ local function load(path, custom_conf, opts) -- set it as such in kong_defaults, because it can only be used if wasm is -- _also_ enabled. We inject it here if the user has not opted to set it -- themselves. - -- TODO: as a temporary compatibility fix, we are forcing it to 'off'. - add_wasm_directive("nginx_http_proxy_wasm_lua_resolver", "off") + add_wasm_directive("nginx_http_proxy_wasm_lua_resolver", "on") -- configure wasmtime module cache if conf.role == "traditional" or conf.role == "data_plane" then diff --git a/kong/conf_loader/parse.lua b/kong/conf_loader/parse.lua index 5be3cadb2ca..5a585fdde03 100644 --- a/kong/conf_loader/parse.lua +++ b/kong/conf_loader/parse.lua @@ -784,7 +784,7 @@ local function check_and_parse(conf, opts) end if conf.tracing_instrumentations and #conf.tracing_instrumentations > 0 then - local instrumentation = require "kong.tracing.instrumentation" + local instrumentation = require "kong.observability.tracing.instrumentation" local available_types_map = cycle_aware_deep_copy(instrumentation.available_types) available_types_map["all"] = true available_types_map["off"] = true diff --git a/kong/constants.lua b/kong/constants.lua index 63df1f2f5a4..33b8dcaa0aa 100644 --- a/kong/constants.lua +++ b/kong/constants.lua @@ -280,6 +280,8 @@ local constants = { service = "upstream", } }, + + SOCKET_DIRECTORY = "sockets", } for _, v in ipairs(constants.CLUSTERING_SYNC_STATUS) do diff --git a/kong/db/dao/certificates.lua b/kong/db/dao/certificates.lua index f202b7d3400..b44475c565e 100644 --- a/kong/db/dao/certificates.lua +++ b/kong/db/dao/certificates.lua @@ -60,7 +60,6 @@ function _Certificates:insert(cert, options) end end - cert.snis = nil cert, err, err_t = self.super.insert(self, cert, options) if not cert then return nil, err, err_t @@ -99,7 +98,6 @@ function _Certificates:update(cert_pk, cert, options) end end - cert.snis = nil cert, err, err_t = self.super.update(self, cert_pk, cert, options) if err then return nil, err, err_t @@ -137,7 +135,6 @@ function _Certificates:upsert(cert_pk, cert, options) end end - cert.snis = nil cert, err, err_t = self.super.upsert(self, cert_pk, cert, options) if err then return nil, err, err_t diff --git a/kong/db/schema/entities/certificates.lua b/kong/db/schema/entities/certificates.lua index 9e9127a2010..6063afc1006 100644 --- a/kong/db/schema/entities/certificates.lua +++ b/kong/db/schema/entities/certificates.lua @@ -21,6 +21,7 @@ return { { cert_alt = typedefs.certificate { required = false, referenceable = true }, }, { key_alt = typedefs.key { required = false, referenceable = true, encrypted = true }, }, { tags = typedefs.tags }, + { snis = { type = "array", elements = typedefs.wildcard_host, required = false, transient = true }, }, }, entity_checks = { diff --git a/kong/db/schema/entities/routes.lua b/kong/db/schema/entities/routes.lua index 313f996b04a..47063e169fa 100644 --- a/kong/db/schema/entities/routes.lua +++ b/kong/db/schema/entities/routes.lua @@ -196,10 +196,6 @@ local routes = { }, }, }, - values = { - type = "array", - elements = typedefs.regex_or_plain_pattern, - } } }, { regex_priority = { description = "A number used to choose which route resolves a given request when several routes match it using regexes simultaneously.", type = "integer", default = 0 }, }, diff --git a/kong/db/schema/metaschema.lua b/kong/db/schema/metaschema.lua index deef4f5852a..504893d567e 100644 --- a/kong/db/schema/metaschema.lua +++ b/kong/db/schema/metaschema.lua @@ -192,6 +192,8 @@ local field_schema = { { encrypted = { type = "boolean" }, }, { referenceable = { type = "boolean" }, }, { json_schema = json_metaschema }, + -- Transient attribute: used to mark a field as a non-db column + { transient = { type = "boolean" }, }, -- Deprecation attribute: used to mark a field as deprecated -- Results in `message` and `removal_in_version` to be printed in a warning -- (via kong.deprecation) when the field is used. @@ -490,6 +492,14 @@ local attribute_types = { json_schema = { ["json"] = true, }, + transient = { + ["string"] = true, + ["number"] = true, + ["integer"] = true, + ["array"] = true, + ["set"] = true, + ["boolean"] = true, + }, } diff --git a/kong/db/schema/typedefs.lua b/kong/db/schema/typedefs.lua index 2d05a30fcee..e786871c45a 100644 --- a/kong/db/schema/typedefs.lua +++ b/kong/db/schema/typedefs.lua @@ -1,7 +1,7 @@ --- A library of ready-to-use type synonyms to use in schema definitions. -- @module kong.db.schema.typedefs local queue_schema = require "kong.tools.queue_schema" -local propagation_schema = require "kong.tracing.propagation.schema" +local propagation_schema = require "kong.observability.tracing.propagation.schema" local openssl_pkey = require "resty.openssl.pkey" local openssl_x509 = require "resty.openssl.x509" local Schema = require "kong.db.schema" @@ -497,54 +497,36 @@ local function validate_host_with_wildcards(host) return typedefs.host_with_optional_port.custom_validator(no_wildcards) end - -local function is_regex_pattern(pattern) - return pattern:sub(1, 1) == "~" -end - - -local function is_valid_regex_pattern(pattern) - local regex = pattern:sub(2) -- remove the leading "~" - -- the value will be interpreted as a regex by the router; but is it a - -- valid one? Let's dry-run it with the same options as our router. - local _, _, err = ngx.re.find("", regex, "aj") - if err then - return nil, - string.format("invalid regex: '%s' (PCRE returned: %s)", - regex, err) - end - - return true -end - - local function validate_path_with_regexes(path) + local ok, err, err_code = typedefs.path.custom_validator(path) if err_code == "percent" then return ok, err, err_code end - if is_regex_pattern(path) then - return is_valid_regex_pattern(path) - end + if path:sub(1, 1) ~= "~" then + -- prefix matching. let's check if it's normalized form + local normalized = normalize(path, true) + if path ~= normalized then + return nil, "non-normalized path, consider use '" .. normalized .. "' instead" + end - -- prefix matching. let's check if it's normalized form - local normalized = normalize(path, true) - if path ~= normalized then - return nil, "non-normalized path, consider use '" .. normalized .. "' instead" + return true end - return true -end - + path = path:sub(2) -local function validate_regex_or_plain_pattern(pattern) - if not is_regex_pattern(pattern) then - return true + -- the value will be interpreted as a regex by the router; but is it a + -- valid one? Let's dry-run it with the same options as our router. + local _, _, err = ngx.re.find("", path, "aj") + if err then + return nil, + string.format("invalid regex: '%s' (PCRE returned: %s)", + path, err) end - return is_valid_regex_pattern(pattern) + return true end @@ -646,12 +628,6 @@ typedefs.headers = Schema.define { description = "A map of header names to arrays of header values." } -typedefs.regex_or_plain_pattern = Schema.define { - type = "string", - custom_validator = validate_regex_or_plain_pattern, - description = "A string representing a regex or plain pattern." -} - typedefs.no_headers = Schema.define(typedefs.headers { eq = null, description = "A null value representing no headers." }) typedefs.semantic_version = Schema.define { diff --git a/kong/db/strategies/postgres/init.lua b/kong/db/strategies/postgres/init.lua index a19f1230dc9..1a33c5e54a2 100644 --- a/kong/db/strategies/postgres/init.lua +++ b/kong/db/strategies/postgres/init.lua @@ -844,6 +844,10 @@ function _M.new(connector, schema, errors) for field_name, field in schema:each_field() do + if field.transient then + goto continue + end + if field.type == "foreign" then local foreign_schema = field.schema local foreign_key_names = {} @@ -925,6 +929,7 @@ function _M.new(connector, schema, errors) insert(fields, prepared_field) end + ::continue:: end local primary_key_names = {} diff --git a/kong/dns/README.md b/kong/dns/README.md new file mode 100644 index 00000000000..f96055146e4 --- /dev/null +++ b/kong/dns/README.md @@ -0,0 +1,174 @@ +Name +==== + +Kong DNS client - The module is currently only used by Kong, and builds on top of the `lua-resty-dns` and `lua-resty-mlcache` libraries. + +Table of Contents +================= + +* [Name](#name) +* [APIs](#apis) + * [new](#new) + * [resolve](#resolve) + * [resolve_address](#resolve_address) +* [Performance characteristics](#performance-characteristics) + * [Memory](#memory) + +# APIs + +The following APIs are for internal development use only within Kong. In the current version, the new DNS library still needs to be compatible with the original DNS library. Therefore, the functions listed below cannot be directly invoked. For example, the `_M:resolve` function in the following APIs will be replaced to ensure compatibility with the previous DNS library API interface specifications `_M.resolve`. + +## new + +**syntax:** *c, err = dns_client.new(opts)* +**context:** any + +**Functionality:** + +Creates a dns client object. Returns `nil` and a message string on error. + +Performs a series of initialization operations: + +* parse `host` file, +* parse `resolv.conf` file (used by the underlying `lua-resty-dns` library), +* initialize multiple TTL options, +* create a mlcache object and initialize it. + +**Input parameters:** + +`@opts` It accepts a options table argument. The following options are supported: + +* TTL options: + * `valid_ttl`: (default: `nil`) + * By default, it caches answers using the TTL value of a response. This optional parameter (in seconds) allows overriding it. + * `stale_ttl`: (default: `3600`) + * the time in seconds for keeping expired DNS records. + * Stale data remains in use from when a record expires until either the background refresh query completes or until `stale_ttl` seconds have passed. This helps Kong stay resilient if the DNS server is temporarily unavailable. + * `error_ttl`: (default: `1`) + * the time in seconds for caching DNS error responses. +* `hosts`: (default: `/etc/hosts`) + * the path of `hosts` file. +* `resolv_conf`: (default: `/etc/resolv.conf`) + * the path of `resolv.conf` file, it will be parsed and passed into the underlying `lua-resty-dns` library. +* `family`: (default: `{ "SRV", "A", "AAAA" }`) + * the types of DNS records that the library should query, it is taken from `kong.conf` option `dns_family`. +* options for the underlying `lua-resty-dns` library: + * `retrans`: (default: `5`) + * the total number of times of retransmitting the DNS request when receiving a DNS response times out according to the timeout setting. When trying to retransmit the query, the next nameserver according to the round-robin algorithm will be picked up. + * If not given, it is taken from `resolv.conf` option `options attempts:`. + * `timeout`: (default: `2000`) + * the time in milliseconds for waiting for the response for a single attempt of request transmission. + * If not given, it is taken from `resolv.conf` option `options timeout:`. But note that its unit in `resolv.conf` is second. + * `random_resolver`: (default: `false`) + * a boolean flag controls whether to randomly pick the nameserver to query first. If `true`, it will always start with the random nameserver. + * If not given, it is taken from `resolv.conf` option `rotate`. + * `nameservers`: + * a list of nameservers to be used. Each nameserver entry can be either a single hostname string or a table holding both the hostname string and the port number. For example, `{"8.8.8.8", {"8.8.4.4", 53} }`. + * If not given, it is taken from `resolv.conf` option `nameserver`. +* `cache_purge`: (default: `false`) + * a boolean flag controls whether to clear the internal cache shared by other DNS client instances across workers. + +[Back to TOC](#table-of-contents) + +## resolve + +**syntax:** *answers, err, tries? = resolve(qname, qtype, cache_only, tries?)* +**context:** *rewrite_by_lua\*, access_by_lua\*, content_by_lua\*, ngx.timer.\** + +**Functionality:** + +Performs a DNS resolution. + +1. Check if the `` matches SRV format (`\_service.\_proto.name`) to determine the `` (SRV or A/AAAA), then use the key `:` to query mlcache. If cached results are found, return them directly. +2. If there are no results available in the cache, it triggers the L3 callback of `mlcache:get` to query records from the DNS servers, details are as follows: + 1. Check if `` has an IP address in the `hosts` file, return if found. + 2. Check if `` is an IP address itself, return if true. + 3. Use `mlcache:peek` to check if the expired key still exists in the shared dictionary. If it does, return it directly to mlcache and trigger an asynchronous background task to update the expired data (`start_stale_update_task`). The maximum time that expired data can be reused is `stale_ttl`, but the maximum TTL returned to mlcache cannot exceed 60s. This way, if the expired key is not successfully updated by the background task after 60s, it can still be reused by calling the `resolve` function from the upper layer to trigger the L3 callback to continue executing this logic and initiate another background task for updating. + 1. For example, with a `stale_ttl` of 3600s, if the background task fails to update the record due to network issues during this time, and the upper-level application continues to call resolve to get the domain name result, it will trigger a background task to query the DNS result for that domain name every 60s, resulting in approximately 60 background tasks being triggered (3600s/60s). + 4. Query the DNS server, with `:` combinations: + 1. The `` is extended according to settings in `resolv.conf`, such as `ndots`, `search`, and `domain`. + +**Return value:** + +* Return value `answers, err`: + * Return one array-like Lua table contains all the records. + * For example, `{{"address":"[2001:db8:3333:4444:5555:6666:7777:8888]","class":1,"name":"example.test","ttl":30,"type":28},{"address":"192.168.1.1","class":1,"name":"example.test","ttl":30,"type":1},"expire":1720765379,"ttl":30}`. + * IPv6 addresses are enclosed in brackets (`[]`). + * If the server returns a non-zero error code, it will return `nil` and a string describing the error in this record. + * For example, `nil, "dns server error: name error"`, the server returned a result with error code 3 (NXDOMAIN). + * In case of severe errors, such network error or server's malformed DNS record response, it will return `nil` and a string describing the error instead. For example: + * `nil, "dns server error: failed to send request to UDP server 10.0.0.1:53: timeout"`, there was a network issue. +* Return value and input parameter `@tries?`: + * If provided as an empty table, it will be returned as a third result. This table will be an array containing the error message for each (if any) failed try. + * For example, `[["example.test:A","dns server error: 3 name error"], ["example.test:AAAA","dns server error: 3 name error"]]`, both attempts failed due to a DNS server error with error code 3 (NXDOMAIN), indicating a name error. + +**Input parameters:** + +* `@qname`: the domain name to resolve. +* `@qtype`: (optional: `nil` or DNS TYPE value) + * specify the query type instead of `self.order` types. +* `@cache_only`: (optional: `boolean`) + * control whether to solely retrieve data from the internal cache without querying to the nameserver. +* `@tries?`: see the above section `Return value and input paramter @tries?`. + +[Back to TOC](#table-of-contents) + +## resolve_address + +**syntax:** *ip, port_or_err, tries? = resolve_address(name, port, cache_only, tries?)* +**context:** *rewrite_by_lua\*, access_by_lua\*, content_by_lua\*, ngx.timer.\** + +**Functionality:** + +Performs a DNS resolution, and return a single randomly selected address (IP and port number). + +When calling multiple times on cached records, it will apply load-balancing based on a round-robin (RR) scheme. For SRV records, this will be a _weighted_ round-robin (WRR) scheme (because of the weights it will be randomized). It will apply the round-robin schemes on each level individually. + +**Return value:** + +* Return value `ip, port_or_err`: + * Return one IP address and port number from records. + * Return `nil, err` if errors occur, with `err` containing an error message. +* Return value and input parameter `@tries?`: same as `@tries?` of `resolve` API. + +**Input parameters:** + +* `@name`: the domain name to resolve. +* `@port`: (optional: `nil` or port number) + * default port number to return if none was found in the lookup chain (only SRV records carry port information, SRV with `port=0` will be ignored). +* `@cache_only`: (optional: `boolean`) + * control whether to solely retrieve data from the internal cache without querying to the nameserver. + +[Back to TOC](#table-of-contents) + +# Performance characteristics + +## Memory + +We evaluated the capacity of DNS records using the following resources: + +* Shared memory size: + * 5 MB (by default): `lua_shared_dict kong_dns_cache 5m`. + * 10 MB: `lua_shared_dict kong_dns_cache 10m`. +* DNS response: + * Each DNS resolution response contains some number of A type records. + * Record: ~80 bytes json string, e.g., `{address = "127.0.0.1", name = , ttl = 3600, class = 1, type = 1}`. + * Domain: ~36 bytes string, e.g., `example.long.long.long.long.test`. Domain names with lengths between 10 and 36 bytes yield similar results. + +The results of evaluation are as follows: + +| shared memory size | number of records per response | number of loaded responses | +|--------------------|-------------------|----------| +| 5 MB | 1 | 20224 | +| 5 MB | 2 ~ 3 | 10081 | +| 5 MB | 4 ~ 9 | 5041 | +| 5 MB | 10 ~ 20 | 5041 | +| 5 MB | 21 ~ 32 | 1261 | +| 10 MB | 1 | 40704 | +| 10 MB | 2 ~ 3 | 20321 | +| 10 MB | 4 ~ 9 | 10161 | +| 10 MB | 10 ~ 20 | 5081 | +| 10 MB | 20 ~ 32 | 2541 | + + +[Back to TOC](#table-of-contents) diff --git a/kong/dns/client.lua b/kong/dns/client.lua new file mode 100644 index 00000000000..f4a2072397d --- /dev/null +++ b/kong/dns/client.lua @@ -0,0 +1,686 @@ +local cjson = require("cjson.safe") +local utils = require("kong.dns.utils") +local stats = require("kong.dns.stats") +local mlcache = require("kong.resty.mlcache") +local resolver = require("resty.dns.resolver") + +local now = ngx.now +local log = ngx.log +local ERR = ngx.ERR +local WARN = ngx.WARN +local NOTICE = ngx.NOTICE +local DEBUG = ngx.DEBUG +local ALERT = ngx.ALERT +local timer_at = ngx.timer.at +local worker_id = ngx.worker.id + +local pairs = pairs +local ipairs = ipairs +local tonumber = tonumber +local setmetatable = setmetatable + +local math_min = math.min +local math_floor = math.floor +local string_lower = string.lower +local table_insert = table.insert +local table_isempty = require("table.isempty") + +local is_srv = utils.is_srv +local parse_hosts = utils.parse_hosts +local ipv6_bracket = utils.ipv6_bracket +local search_names = utils.search_names +local parse_resolv_conf = utils.parse_resolv_conf +local get_next_round_robin_answer = utils.get_next_round_robin_answer +local get_next_weighted_round_robin_answer = utils.get_next_weighted_round_robin_answer + +local req_dyn_hook_run_hook = require("kong.dynamic_hook").run_hook + + +-- Constants and default values + +local PREFIX = "[dns_client] " + +local DEFAULT_ERROR_TTL = 1 -- unit: second +local DEFAULT_STALE_TTL = 3600 +-- long-lasting TTL of 10 years for hosts or static IP addresses in cache settings +local LONG_LASTING_TTL = 10 * 365 * 24 * 60 * 60 + +local DEFAULT_FAMILY = { "SRV", "A", "AAAA" } + +local TYPE_SRV = resolver.TYPE_SRV +local TYPE_A = resolver.TYPE_A +local TYPE_AAAA = resolver.TYPE_AAAA +local TYPE_A_OR_AAAA = -1 -- used to resolve IP addresses for SRV targets + +local TYPE_TO_NAME = { + [TYPE_SRV] = "SRV", + [TYPE_A] = "A", + [TYPE_AAAA] = "AAAA", + [TYPE_A_OR_AAAA] = "A/AAAA", +} + +local HIT_L3 = 3 -- L1 lru, L2 shm, L3 callback, L4 stale + +local HIT_LEVEL_TO_NAME = { + [1] = "hit_lru", + [2] = "hit_shm", + [3] = "miss", + [4] = "hit_stale", +} + +-- client specific error +local CACHE_ONLY_ERROR_CODE = 100 +local CACHE_ONLY_ERROR_MESSAGE = "cache only lookup failed" +local CACHE_ONLY_ANSWERS = { + errcode = CACHE_ONLY_ERROR_CODE, + errstr = CACHE_ONLY_ERROR_MESSAGE, +} + +local EMPTY_RECORD_ERROR_CODE = 101 +local EMPTY_RECORD_ERROR_MESSAGE = "empty record received" + + +-- APIs + +local _M = { + TYPE_SRV = TYPE_SRV, + TYPE_A = TYPE_A, + TYPE_AAAA = TYPE_AAAA, +} +local _MT = { __index = _M, } + + +local _TRIES_MT = { __tostring = cjson.encode, } + + +local init_hosts do + local function insert_answer_into_cache(cache, hosts_cache, address, name, qtype) + local answers = { + ttl = LONG_LASTING_TTL, + expire = now() + LONG_LASTING_TTL, + { + name = name, + type = qtype, + address = address, + class = 1, + ttl = LONG_LASTING_TTL, + }, + } + + hosts_cache[name .. ":" .. qtype] = answers + hosts_cache[name .. ":" .. TYPE_A_OR_AAAA] = answers + end + + -- insert hosts into cache + function init_hosts(cache, path) + local hosts = parse_hosts(path) + local hosts_cache = {} + + for name, address in pairs(hosts) do + name = string_lower(name) + + if address.ipv6 then + insert_answer_into_cache(cache, hosts_cache, address.ipv6, name, TYPE_AAAA) + end + + if address.ipv4 then + insert_answer_into_cache(cache, hosts_cache, address.ipv4, name, TYPE_A) + end + end + + return hosts, hosts_cache + end +end + + +-- distinguish the worker_events sources registered by different new() instances +local ipc_counter = 0 + +function _M.new(opts) + opts = opts or {} + + local enable_ipv4, enable_ipv6, enable_srv + + for _, typstr in ipairs(opts.family or DEFAULT_FAMILY) do + typstr = typstr:upper() + + if typstr == "A" then + enable_ipv4 = true + + elseif typstr == "AAAA" then + enable_ipv6 = true + + elseif typstr == "SRV" then + enable_srv = true + + else + return nil, "Invalid dns type in dns_family array: " .. typstr + end + end + + log(NOTICE, PREFIX, "supported types: ", enable_srv and "srv " or "", + enable_ipv4 and "ipv4 " or "", enable_ipv6 and "ipv6 " or "") + + -- parse resolv.conf + local resolv, err = parse_resolv_conf(opts.resolv_conf, opts.enable_ipv6) + if not resolv then + log(WARN, PREFIX, "Invalid resolv.conf: ", err) + resolv = { options = {} } + end + + -- init the resolver options for lua-resty-dns + local nameservers = (opts.nameservers and not table_isempty(opts.nameservers)) + and opts.nameservers + or resolv.nameservers + + if not nameservers or table_isempty(nameservers) then + log(WARN, PREFIX, "Invalid configuration, no nameservers specified") + end + + local no_random + + if opts.random_resolver == nil then + no_random = not resolv.options.rotate + else + no_random = not opts.random_resolver + end + + local r_opts = { + retrans = opts.retrans or resolv.options.attempts or 5, + timeout = opts.timeout or resolv.options.timeout or 2000, -- ms + no_random = no_random, + nameservers = nameservers, + } + + -- init the mlcache + + -- maximum timeout for the underlying r:query() operation to complete + -- socket timeout * retrans * 2 calls for send and receive + 1s extra delay + local lock_timeout = r_opts.timeout / 1000 * r_opts.retrans * 2 + 1 -- s + + local resty_lock_opts = { + timeout = lock_timeout, + exptimeout = lock_timeout + 1, + } + + -- TODO: convert the ipc a module constant, currently we need to use the + -- ipc_source to distinguish sources of different DNS client events. + ipc_counter = ipc_counter + 1 + local ipc_source = "dns_client_mlcache#" .. ipc_counter + local ipc = { + register_listeners = function(events) + -- The DNS client library will be required in globalpatches before Kong + -- initializes worker_events. + if not kong or not kong.worker_events then + return + end + + local cwid = worker_id() or -1 + for _, ev in pairs(events) do + local handler = function(data, event, source, wid) + if cwid ~= wid then -- Current worker has handled this event. + ev.handler(data) + end + end + + kong.worker_events.register(handler, ipc_source, ev.channel) + end + end, + + -- @channel: event channel name, such as "mlcache:invalidate:dns_cache" + -- @data: mlcache's key name, such as ":" + broadcast = function(channel, data) + if not kong or not kong.worker_events then + return + end + + local ok, err = kong.worker_events.post(ipc_source, channel, data) + if not ok then + log(ERR, PREFIX, "failed to post event '", ipc_source, "', '", channel, "': ", err) + end + end, + } + + local cache, err = mlcache.new("dns_cache", "kong_dns_cache", { + ipc = ipc, + neg_ttl = opts.error_ttl or DEFAULT_ERROR_TTL, + -- 10000 is a reliable and tested value from the original library. + lru_size = opts.cache_size or 10000, + shm_locks = ngx.shared.kong_locks and "kong_locks", + resty_lock_opts = resty_lock_opts, + }) + + if not cache then + return nil, "could not create mlcache: " .. err + end + + if opts.cache_purge then + cache:purge(true) + end + + -- parse hosts + local hosts, hosts_cache = init_hosts(cache, opts.hosts) + + return setmetatable({ + cache = cache, + stats = stats.new(), + hosts = hosts, + r_opts = r_opts, + resolv = opts._resolv or resolv, + valid_ttl = opts.valid_ttl, + error_ttl = opts.error_ttl or DEFAULT_ERROR_TTL, + stale_ttl = opts.stale_ttl or DEFAULT_STALE_TTL, + enable_srv = enable_srv, + enable_ipv4 = enable_ipv4, + enable_ipv6 = enable_ipv6, + hosts_cache = hosts_cache, + + -- TODO: Make the table readonly. But if `string.buffer.encode/decode` and + -- `pl.tablex.readonly` are called on it, it will become empty table. + -- + -- quickly accessible constant empty answers + EMPTY_ANSWERS = { + errcode = EMPTY_RECORD_ERROR_CODE, + errstr = EMPTY_RECORD_ERROR_MESSAGE, + ttl = opts.error_ttl or DEFAULT_ERROR_TTL, + }, + }, _MT) +end + + +local function process_answers(self, qname, qtype, answers) + local errcode = answers.errcode + if errcode then + answers.ttl = self.error_ttl + return answers + end + + local processed_answers = {} + + -- 0xffffffff for maximum TTL value + local ttl = math_min(self.valid_ttl or 0xffffffff, 0xffffffff) + + for _, answer in ipairs(answers) do + answer.name = string_lower(answer.name) + + if self.valid_ttl then + answer.ttl = self.valid_ttl + else + ttl = math_min(ttl, answer.ttl) + end + + local answer_type = answer.type + + if answer_type == qtype then + -- compatible with balancer, see https://github.com/Kong/kong/pull/3088 + if answer_type == TYPE_AAAA then + answer.address = ipv6_bracket(answer.address) + + elseif answer_type == TYPE_SRV then + answer.target = ipv6_bracket(answer.target) + end + + table_insert(processed_answers, answer) + end + end + + if table_isempty(processed_answers) then + log(DEBUG, PREFIX, "processed ans:empty") + return self.EMPTY_ANSWERS + end + + log(DEBUG, PREFIX, "processed ans:", #processed_answers) + + processed_answers.expire = now() + ttl + processed_answers.ttl = ttl + + return processed_answers +end + + +local function resolve_query(self, name, qtype, tries) + local key = name .. ":" .. qtype + + local stats = self.stats + + stats:incr(key, "query") + + local r, err = resolver:new(self.r_opts) + if not r then + return nil, "failed to instantiate the resolver: " .. err + end + + local start = now() + + local answers, err = r:query(name, { qtype = qtype }) + r:destroy() + + local duration = math_floor((now() - start) * 1000) + + stats:set(key, "query_last_time", duration) + + log(DEBUG, PREFIX, "r:query(", key, ") ans:", answers and #answers or "-", + " t:", duration, " ms") + + -- network error or malformed DNS response + if not answers then + stats:incr(key, "query_fail_nameserver") + err = "DNS server error: " .. tostring(err) .. ", took " .. duration .. " ms" + + -- TODO: make the error more structured, like: + -- { qname = name, qtype = qtype, error = err, } or something similar + table_insert(tries, { name .. ":" .. TYPE_TO_NAME[qtype], err }) + + return nil, err + end + + answers = process_answers(self, name, qtype, answers) + + stats:incr(key, answers.errstr and + "query_fail:" .. answers.errstr or + "query_succ") + + -- DNS response error + if answers.errcode then + err = ("dns %s error: %s %s"):format( + answers.errcode < CACHE_ONLY_ERROR_CODE and "server" or "client", + answers.errcode, answers.errstr) + table_insert(tries, { name .. ":" .. TYPE_TO_NAME[qtype], err }) + end + + return answers +end + + +-- resolve all `name`s and return first usable answers +local function resolve_query_names(self, names, qtype, tries) + local answers, err + + for _, qname in ipairs(names) do + answers, err = resolve_query(self, qname, qtype, tries) + + -- severe error occurred + if not answers then + return nil, err + end + + if not answers.errcode then + return answers, nil, answers.ttl + end + end + + -- not found in the search iteration + return answers, nil, answers.ttl +end + + +local function resolve_query_types(self, name, qtype, tries) + local names = search_names(name, self.resolv, self.hosts) + local answers, err, ttl + + -- the specific type + if qtype ~= TYPE_A_OR_AAAA then + return resolve_query_names(self, names, qtype, tries) + end + + -- query A or AAAA + if self.enable_ipv4 then + answers, err, ttl = resolve_query_names(self, names, TYPE_A, tries) + if not answers or not answers.errcode then + return answers, err, ttl + end + end + + if self.enable_ipv6 then + answers, err, ttl = resolve_query_names(self, names, TYPE_AAAA, tries) + end + + return answers, err, ttl +end + + +local function stale_update_task(premature, self, key, name, qtype) + if premature then + return + end + + local tries = setmetatable({}, _TRIES_MT) + local answers = resolve_query_types(self, name, qtype, tries) + if not answers or answers.errcode then + log(DEBUG, PREFIX, "failed to update stale DNS records: ", tostring(tries)) + return + end + + log(DEBUG, PREFIX, "update stale DNS records: ", #answers) + self.cache:set(key, { ttl = answers.ttl }, answers) +end + + +local function start_stale_update_task(self, key, name, qtype) + self.stats:incr(key, "stale") + + local ok, err = timer_at(0, stale_update_task, self, key, name, qtype) + if not ok then + log(ALERT, PREFIX, "failed to start a timer to update stale DNS records: ", err) + end +end + + +local function check_and_get_ip_answers(name) + -- TODO: use is_valid_ipv4 from kong/tools/ip.lua instead + if name:match("^%d+%.%d+%.%d+%.%d+$") then -- IPv4 + return { + { name = name, class = 1, type = TYPE_A, address = name }, + } + end + + if name:find(":", 1, true) then -- IPv6 + return { + { name = name, class = 1, type = TYPE_AAAA, address = ipv6_bracket(name) }, + } + end + + return nil +end + + +local function resolve_callback(self, name, qtype, cache_only, tries) + -- check if name is ip address + local answers = check_and_get_ip_answers(name) + if answers then -- domain name is IP literal + answers.ttl = LONG_LASTING_TTL + answers.expire = now() + answers.ttl + return answers, nil, answers.ttl + end + + -- check if this key exists in the hosts file (it maybe evicted from cache) + local key = name .. ":" .. qtype + local answers = self.hosts_cache[key] + if answers then + return answers, nil, answers.ttl + end + + -- `:peek(stale=true)` verifies if the expired key remains in L2 shm, then + -- initiates an asynchronous background updating task to refresh it. + local ttl, _, answers = self.cache:peek(key, true) + + if answers and not answers.errcode and self.stale_ttl and ttl then + + -- `_expire_at` means the final expiration time of stale records + if not answers._expire_at then + answers._expire_at = answers.expire + self.stale_ttl + end + + -- trigger the update task by the upper caller every 60 seconds + local remaining_stale_ttl = math_min(answers._expire_at - now(), 60) + + if remaining_stale_ttl > 0 then + log(DEBUG, PREFIX, "start stale update task ", key, + " remaining_stale_ttl:", remaining_stale_ttl) + + -- mlcache's internal lock mechanism ensures concurrent control + start_stale_update_task(self, key, name, qtype) + answers.ttl = remaining_stale_ttl + answers.expire = remaining_stale_ttl + now() + + return answers, nil, remaining_stale_ttl + end + end + + if cache_only then + return CACHE_ONLY_ANSWERS, nil, -1 + end + + log(DEBUG, PREFIX, "cache miss, try to query ", key) + + return resolve_query_types(self, name, qtype, tries) +end + + +local function resolve_all(self, name, qtype, cache_only, tries, has_timing) + name = string_lower(name) + tries = setmetatable(tries or {}, _TRIES_MT) + + if not qtype then + qtype = ((self.enable_srv and is_srv(name)) and TYPE_SRV or TYPE_A_OR_AAAA) + end + + local key = name .. ":" .. qtype + + log(DEBUG, PREFIX, "resolve_all ", key) + + local stats = self.stats + + stats:incr(key, "runs") + + local answers, err, hit_level = self.cache:get(key, nil, resolve_callback, + self, name, qtype, cache_only, + tries) + -- check for runtime errors in the callback + if err and err:sub(1, 8) == "callback" then + log(ALERT, PREFIX, err) + end + + local hit_str = hit_level and HIT_LEVEL_TO_NAME[hit_level] or "fail" + stats:incr(key, hit_str) + + log(DEBUG, PREFIX, "cache lookup ", key, " ans:", answers and #answers or "-", + " hlv:", hit_str) + + if has_timing then + req_dyn_hook_run_hook("timing", "dns:cache_lookup", + (hit_level and hit_level < HIT_L3)) + end + + if answers and answers.errcode then + err = ("dns %s error: %s %s"):format( + answers.errcode < CACHE_ONLY_ERROR_CODE and "server" or "client", + answers.errcode, answers.errstr) + return nil, err, tries + end + + return answers, err, tries +end + + +function _M:resolve(name, qtype, cache_only, tries) + return resolve_all(self, name, qtype, cache_only, tries, + ngx.ctx and ngx.ctx.has_timing) +end + + +function _M:resolve_address(name, port, cache_only, tries) + local has_timing = ngx.ctx and ngx.ctx.has_timing + + local answers, err, tries = resolve_all(self, name, nil, cache_only, tries, + has_timing) + + if answers and answers[1] and answers[1].type == TYPE_SRV then + local answer = get_next_weighted_round_robin_answer(answers) + port = answer.port ~= 0 and answer.port or port + answers, err, tries = resolve_all(self, answer.target, TYPE_A_OR_AAAA, + cache_only, tries, has_timing) + end + + if not answers then + return nil, err, tries + end + + return get_next_round_robin_answer(answers).address, port, tries +end + + +-- compatible with original DNS client library +-- These APIs will be deprecated if fully replacing the original one. +local dns_client + +function _M.init(opts) + log(DEBUG, PREFIX, "(re)configuring dns client") + + if opts then + opts.valid_ttl = opts.valid_ttl or opts.validTtl + opts.error_ttl = opts.error_ttl or opts.badTtl + opts.stale_ttl = opts.stale_ttl or opts.staleTtl + opts.cache_size = opts.cache_size or opts.cacheSize + end + + local client, err = _M.new(opts) + if not client then + return nil, err + end + + dns_client = client + return true +end + + +-- New and old libraries have the same function name. +_M._resolve = _M.resolve + +function _M.resolve(name, r_opts, cache_only, tries) + return dns_client:_resolve(name, r_opts and r_opts.qtype, cache_only, tries) +end + + +function _M.toip(name, port, cache_only, tries) + return dns_client:resolve_address(name, port, cache_only, tries) +end + + +-- "_ldap._tcp.example.com:33" -> "_ldap._tcp.example.com|SRV" +local function format_key(key) + local qname, qtype = key:match("^(.+):(%-?%d+)$") -- match "(qname):(qtype)" + return qtype and qname .. "|" .. (TYPE_TO_NAME[tonumber(qtype)] or qtype) + or key +end + + +function _M.stats() + return dns_client.stats:emit(format_key) +end + + +-- For testing + +if package.loaded.busted then + function _M.getobj() + return dns_client + end + + function _M.getcache() + return { + set = function(self, k, v, ttl) + self.cache:set(k, {ttl = ttl or 0}, v) + end, + + delete = function(self, k) + self.cache:delete(k) + end, + + cache = dns_client.cache, + } + end +end + + +return _M diff --git a/kong/dns/stats.lua b/kong/dns/stats.lua new file mode 100644 index 00000000000..ca6faa6cf36 --- /dev/null +++ b/kong/dns/stats.lua @@ -0,0 +1,63 @@ +local tb_new = require("table.new") +local tb_nkeys = require("table.nkeys") + + +local pairs = pairs +local setmetatable = setmetatable + + +local _M = {} +local _MT = { __index = _M, } + + +function _M.new() + local self = { + -- pre-allocate 4 slots + stats = tb_new(0, 4), + } + + return setmetatable(self, _MT) +end + + +function _M:_get_stats(name) + local stats = self.stats + + if not stats[name] then + -- keys will be: query/query_last_time/query_fail_nameserver + -- query_succ/query_fail/stale/runs/... + -- 6 slots may be a approprate number + stats[name] = tb_new(0, 6) + end + + return stats[name] +end + + +function _M:incr(name, key) + local stats = self:_get_stats(name) + + stats[key] = (stats[key] or 0) + 1 +end + + +function _M:set(name, key, value) + local stats = self:_get_stats(name) + + stats[key] = value +end + + +function _M:emit(fmt) + local stats = self.stats + local output = tb_new(0, tb_nkeys(stats)) + + for k, v in pairs(stats) do + output[fmt(k)] = v + end + + return output +end + + +return _M diff --git a/kong/dns/utils.lua b/kong/dns/utils.lua new file mode 100644 index 00000000000..2549693dbef --- /dev/null +++ b/kong/dns/utils.lua @@ -0,0 +1,326 @@ +local utils = require("kong.resty.dns.utils") + + +local log = ngx.log + + +local NOTICE = ngx.NOTICE + + +local type = type +local ipairs = ipairs +local tonumber = tonumber +local math_random = math.random +local table_new = require("table.new") +local table_clear = require("table.clear") +local table_insert = table.insert +local table_remove = table.remove + + +local readlines = require("pl.utils").readlines + + +local DEFAULT_HOSTS_FILE = "/etc/hosts" +local DEFAULT_RESOLV_CONF = "/etc/resolv.conf" + + +local LOCALHOST = { + ipv4 = "127.0.0.1", + ipv6 = "[::1]", +} + + +local DEFAULT_HOSTS = { localhost = LOCALHOST, } + + +-- checks the hostname type +-- @return "ipv4", "ipv6", or "domain" +local function hostname_type(name) + local remainder, colons = name:gsub(":", "") + if colons > 1 then + return "ipv6" + end + + if remainder:match("^[%d%.]+$") then + return "ipv4" + end + + return "domain" +end + + +-- parses a hostname with an optional port +-- IPv6 addresses are always returned in square brackets +-- @param name the string to check (this may contain a port number) +-- @return `name/ip` + `port (or nil)` + `type ("ipv4", "ipv6" or "domain")` +local function parse_hostname(name) + local t = hostname_type(name) + if t == "ipv4" or t == "domain" then + local ip, port = name:match("^([^:]+)%:*(%d*)$") + return ip, tonumber(port), t + end + + -- ipv6 + if name:match("%[") then -- brackets, so possibly a port + local ip, port = name:match("^%[([^%]]+)%]*%:*(%d*)$") + return "[" .. ip .. "]", tonumber(port), t + end + + return "[" .. name .. "]", nil, t -- no brackets also means no port +end + + +local function get_lines(path) + if type(path) == "table" then + return path + end + + return readlines(path) +end + + +local function parse_hosts(path, enable_ipv6) + local lines, err = get_lines(path or DEFAULT_HOSTS_FILE) + if not lines then + log(NOTICE, "Invalid hosts file: ", err) + return DEFAULT_HOSTS + end + + local hosts = {} + + for _, line in ipairs(lines) do + -- Remove leading/trailing whitespaces and split by whitespace + local parts = {} + local n = 0 + for part in line:gmatch("%S+") do + if part:sub(1, 1) == '#' then + break + end + + n = n + 1 + parts[n] = part:lower() + end + + -- Check if the line contains an IP address followed by hostnames + if n >= 2 then + local ip, _, family = parse_hostname(parts[1]) + + if family ~= "domain" then -- ipv4/ipv6 + for i = 2, n do + local host = parts[i] + local v = hosts[host] + + if not v then + v = {} + hosts[host] = v + end + + v[family] = v[family] or ip -- prefer to use the first ip + end + end + end + end + + if not hosts.localhost then + hosts.localhost = LOCALHOST + end + + return hosts +end + + +-- TODO: need to rewrite it instead of calling parseResolvConf from the old library +local function parse_resolv_conf(path, enable_ipv6) + local resolv, err = utils.parseResolvConf(path or DEFAULT_RESOLV_CONF) + if not resolv then + return nil, err + end + + resolv = utils.applyEnv(resolv) + resolv.options = resolv.options or {} + resolv.ndots = resolv.options.ndots or 1 + resolv.search = resolv.search or (resolv.domain and { resolv.domain }) + + -- check if timeout is 0s + if resolv.options.timeout and resolv.options.timeout <= 0 then + log(NOTICE, "A non-positive timeout of ", resolv.options.timeout, + "s is configured in resolv.conf. Setting it to 2000ms.") + resolv.options.timeout = 2000 -- 2000ms is lua-resty-dns default + end + + -- remove special domain like "." + if resolv.search then + for i = #resolv.search, 1, -1 do + if resolv.search[i] == "." then + table_remove(resolv.search, i) + end + end + end + + -- nameservers + if resolv.nameserver then + local n = 0 + local nameservers = {} + + for _, address in ipairs(resolv.nameserver) do + local ip, port, t = utils.parseHostname(address) + if t == "ipv4" or + (t == "ipv6" and not ip:find([[%]], nil, true) and enable_ipv6) + then + n = n + 1 + nameservers[n] = port and { ip, port } or ip + end + end + + resolv.nameservers = nameservers + end + + return resolv +end + + +local function is_fqdn(name, ndots) + if name:sub(-1) == "." then + return true + end + + local _, dot_count = name:gsub("%.", "") + + return (dot_count >= ndots) +end + + +-- check if it matchs the SRV pattern: _._. +local function is_srv(name) + return name:match("^_[^._]+%._[^._]+%.[^.]+") ~= nil +end + + +-- construct names from resolv options: search, ndots and domain +local function search_names(name, resolv, hosts) + local resolv_search = resolv.search + + if not resolv_search or is_fqdn(name, resolv.ndots) or + (hosts and hosts[name]) + then + return { name } + end + + local count = #resolv_search + local names = table_new(count + 1, 0) + + for i = 1, count do + names[i] = name .. "." .. resolv_search[i] + end + names[count + 1] = name -- append the original name at last + + return names +end + + +-- add square brackets around IPv6 addresses if a non-strict check detects them +local function ipv6_bracket(name) + if name:match("^[^[].*:") then -- not start with '[' and contains ':' + return "[" .. name .. "]" + end + + return name +end + + +-- util APIs to balance @answers + +local function get_next_round_robin_answer(answers) + answers.last = (answers.last or 0) % #answers + 1 + + return answers[answers.last] +end + + +local get_next_weighted_round_robin_answer +do + -- based on the Nginx's SWRR algorithm and lua-resty-balancer + local function swrr_next(answers) + local total = 0 + local best = nil -- best answer in answers[] + + for _, answer in ipairs(answers) do + -- 0.1 gives weight 0 record a minimal chance of being chosen (rfc 2782) + local w = (answer.weight == 0) and 0.1 or answer.weight + local cw = answer.cw + w + + answer.cw = cw + + if not best or cw > best.cw then + best = answer + end + + total = total + w + end + + best.cw = best.cw - total + + return best + end + + + local function swrr_init(answers) + for _, answer in ipairs(answers) do + answer.cw = 0 -- current weight + end + + -- random start + for _ = 1, math_random(#answers) do + swrr_next(answers) + end + end + + + -- gather records with the lowest priority in SRV record + local function filter_lowest_priority_answers(answers) + -- SRV record MUST have `priority` field + local lowest_priority = answers[1].priority + local l = {} -- lowest priority records list + + for _, answer in ipairs(answers) do + if answer.priority < lowest_priority then + lowest_priority = answer.priority + table_clear(l) + l[1] = answer + + elseif answer.priority == lowest_priority then + table_insert(l, answer) + end + end + + answers.lowest_prio_records = l + + return l + end + + + get_next_weighted_round_robin_answer = function(answers) + local l = answers.lowest_prio_records or filter_lowest_priority_answers(answers) + + -- perform round robin selection on lowest priority answers @l + if not l[1].cw then + swrr_init(l) + end + + return swrr_next(l) + end +end + + +return { + hostname_type = hostname_type, + parse_hostname = parse_hostname, + parse_hosts = parse_hosts, + parse_resolv_conf = parse_resolv_conf, + is_fqdn = is_fqdn, + is_srv = is_srv, + search_names = search_names, + ipv6_bracket = ipv6_bracket, + get_next_round_robin_answer = get_next_round_robin_answer, + get_next_weighted_round_robin_answer = get_next_weighted_round_robin_answer, +} diff --git a/kong/dynamic_hook/README.md b/kong/dynamic_hook/README.md index 281408a4cb6..cff872d8043 100644 --- a/kong/dynamic_hook/README.md +++ b/kong/dynamic_hook/README.md @@ -28,7 +28,7 @@ dynamic_hook.hook_function("my_group", _G, "print", "varargs", { }) -- Enable the hook group -dynamic_hook.always_enable("my_group") +dynamic_hook.enable_by_default("my_group") -- Call the function print("world!") -- prints "hello, world!" diff --git a/kong/dynamic_hook/init.lua b/kong/dynamic_hook/init.lua index 31b13b216af..966e59a3cbd 100644 --- a/kong/dynamic_hook/init.lua +++ b/kong/dynamic_hook/init.lua @@ -1,3 +1,5 @@ +local get_request = require "resty.core.base".get_request + local ngx = ngx local type = type local pcall = pcall @@ -233,6 +235,10 @@ function _M.is_group_enabled(group_name) return true end + if not get_request() then + return false + end + local dynamic_hook = ngx.ctx.dynamic_hook if not dynamic_hook then return false @@ -314,13 +320,24 @@ end --- Enables a hook group for all requests -- --- @function dynamic_hook:always_enable +-- @function dynamic_hook:enable_by_default -- @tparam string group_name The name of the hook group to enable -function _M.always_enable(group_name) +function _M.enable_by_default(group_name) assert(type(group_name) == "string", "group_name must be a string") ALWAYS_ENABLED_GROUPS[group_name] = true end +--- Disables a hook group that was enabled with `enable_by_default` +-- +-- @function dynamic_hook:disable_by_default +-- @tparam string group_name The name of the hook group to disable +function _M.disable_by_default(group_name) + assert(type(group_name) == "string", "group_name must be a string") + + ALWAYS_ENABLED_GROUPS[group_name] = nil +end + + return _M diff --git a/kong/error_handlers.lua b/kong/error_handlers.lua index 91db16a825c..5266a2a4ff8 100644 --- a/kong/error_handlers.lua +++ b/kong/error_handlers.lua @@ -1,7 +1,7 @@ local kong = kong local find = string.find local fmt = string.format -local request_id = require "kong.tracing.request_id" +local request_id = require "kong.observability.tracing.request_id" local tools_http = require "kong.tools.http" diff --git a/kong/global.lua b/kong/global.lua index b7a1bbc04ee..f40c0b2c589 100644 --- a/kong/global.lua +++ b/kong/global.lua @@ -168,28 +168,20 @@ function _GLOBAL.init_pdk(self, kong_config) end -function _GLOBAL.init_worker_events() +function _GLOBAL.init_worker_events(kong_config) -- Note: worker_events will not work correctly if required at the top of the file. -- It must be required right here, inside the init function local worker_events local opts - local configuration = kong.configuration - - -- `kong.configuration.prefix` is already normalized to an absolute path, - -- but `ngx.config.prefix()` is not - local prefix = configuration and - configuration.prefix or - require("pl.path").abspath(ngx.config.prefix()) - + local socket_path = kong_config.socket_path local sock = ngx.config.subsystem == "stream" and "stream_worker_events.sock" or "worker_events.sock" - local listening = "unix:" .. prefix .. "/" .. sock + local listening = "unix:" .. socket_path .. "/" .. sock - local max_payload_len = configuration and - configuration.worker_events_max_payload + local max_payload_len = kong_config.worker_events_max_payload if max_payload_len and max_payload_len > 65535 then -- default is 64KB ngx.log(ngx.WARN, @@ -203,9 +195,9 @@ function _GLOBAL.init_worker_events() listening = listening, -- unix socket for broker listening max_queue_len = 1024 * 50, -- max queue len for events buffering max_payload_len = max_payload_len, -- max payload size in bytes - enable_privileged_agent = configuration and configuration.dedicated_config_processing - and configuration.role == "data_plane" - or false + enable_privileged_agent = kong_config.dedicated_config_processing + and kong_config.role == "data_plane" + or false, } worker_events = require "resty.events.compat" diff --git a/kong/globalpatches.lua b/kong/globalpatches.lua index 4c9581f49d0..8d2a318568e 100644 --- a/kong/globalpatches.lua +++ b/kong/globalpatches.lua @@ -409,6 +409,10 @@ return function(options) local seeded = {} local randomseed = math.randomseed + if options.rbusted then + _G.math.native_randomseed = randomseed + end + _G.math.randomseed = function() local pid = ngx.worker.pid() local id @@ -534,6 +538,8 @@ return function(options) local old_tcp_connect local old_udp_setpeername + local old_ngx_log = ngx.log + -- need to do the extra check here: https://github.com/openresty/lua-nginx-module/issues/860 local function strip_nils(first, second) if second then @@ -589,6 +595,31 @@ return function(options) return sock end + -- OTel-formatted logs feature + local dynamic_hook = require "kong.dynamic_hook" + local hook_called = false + _G.ngx.log = function(...) + if hook_called then + -- detect recursive loops or yielding from the hook: + old_ngx_log(ngx.ERR, debug.traceback("concurrent execution detected for: ngx.log", 2)) + return old_ngx_log(...) + end + + -- stack level = 5: + -- 1: maybe_push + -- 2: dynamic_hook.pcall + -- 3: dynamic_hook.run_hook + -- 4: patched function + -- 5: caller + hook_called = true + dynamic_hook.run_hook("observability_logs", "push", 5, nil, ...) + hook_called = false + return old_ngx_log(...) + end + -- export native ngx.log to be used where + -- the patched code must not be executed + _G.native_ngx_log = old_ngx_log + if not options.cli and not options.rbusted then local timing = require "kong.timing" timing.register_hooks() @@ -597,7 +628,7 @@ return function(options) -- STEP 5: load code that should be using the patched versions, if any (because of dependency chain) do -- dns query patch - local instrumentation = require "kong.tracing.instrumentation" + local instrumentation = require "kong.observability.tracing.instrumentation" client.toip = instrumentation.get_wrapped_dns_query(client.toip) -- patch request_uri to record http_client spans diff --git a/kong/include/opentelemetry/proto/collector/logs/v1/logs_service.proto b/kong/include/opentelemetry/proto/collector/logs/v1/logs_service.proto new file mode 100644 index 00000000000..8260d8aaeb8 --- /dev/null +++ b/kong/include/opentelemetry/proto/collector/logs/v1/logs_service.proto @@ -0,0 +1,79 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.collector.logs.v1; + +import "opentelemetry/proto/logs/v1/logs.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Collector.Logs.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.collector.logs.v1"; +option java_outer_classname = "LogsServiceProto"; +option go_package = "go.opentelemetry.io/proto/otlp/collector/logs/v1"; + +// Service that can be used to push logs between one Application instrumented with +// OpenTelemetry and an collector, or between an collector and a central collector (in this +// case logs are sent/received to/from multiple Applications). +service LogsService { + // For performance reasons, it is recommended to keep this RPC + // alive for the entire life of the application. + rpc Export(ExportLogsServiceRequest) returns (ExportLogsServiceResponse) {} +} + +message ExportLogsServiceRequest { + // An array of ResourceLogs. + // For data coming from a single resource this array will typically contain one + // element. Intermediary nodes (such as OpenTelemetry Collector) that receive + // data from multiple origins typically batch the data before forwarding further and + // in that case this array will contain multiple elements. + repeated opentelemetry.proto.logs.v1.ResourceLogs resource_logs = 1; +} + +message ExportLogsServiceResponse { + // The details of a partially successful export request. + // + // If the request is only partially accepted + // (i.e. when the server accepts only parts of the data and rejects the rest) + // the server MUST initialize the `partial_success` field and MUST + // set the `rejected_` with the number of items it rejected. + // + // Servers MAY also make use of the `partial_success` field to convey + // warnings/suggestions to senders even when the request was fully accepted. + // In such cases, the `rejected_` MUST have a value of `0` and + // the `error_message` MUST be non-empty. + // + // A `partial_success` message with an empty value (rejected_ = 0 and + // `error_message` = "") is equivalent to it not being set/present. Senders + // SHOULD interpret it the same way as in the full success case. + ExportLogsPartialSuccess partial_success = 1; +} + +message ExportLogsPartialSuccess { + // The number of rejected log records. + // + // A `rejected_` field holding a `0` value indicates that the + // request was fully accepted. + int64 rejected_log_records = 1; + + // A developer-facing human-readable message in English. It should be used + // either to explain why the server rejected parts of the data during a partial + // success or to convey warnings/suggestions during a full success. The message + // should offer guidance on how users can address such issues. + // + // error_message is an optional field. An error_message with an empty value + // is equivalent to it not being set. + string error_message = 2; +} diff --git a/kong/include/opentelemetry/proto/logs/v1/logs.proto b/kong/include/opentelemetry/proto/logs/v1/logs.proto new file mode 100644 index 00000000000..f9b97dd7451 --- /dev/null +++ b/kong/include/opentelemetry/proto/logs/v1/logs.proto @@ -0,0 +1,211 @@ +// Copyright 2020, OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package opentelemetry.proto.logs.v1; + +import "opentelemetry/proto/common/v1/common.proto"; +import "opentelemetry/proto/resource/v1/resource.proto"; + +option csharp_namespace = "OpenTelemetry.Proto.Logs.V1"; +option java_multiple_files = true; +option java_package = "io.opentelemetry.proto.logs.v1"; +option java_outer_classname = "LogsProto"; +option go_package = "go.opentelemetry.io/proto/otlp/logs/v1"; + +// LogsData represents the logs data that can be stored in a persistent storage, +// OR can be embedded by other protocols that transfer OTLP logs data but do not +// implement the OTLP protocol. +// +// The main difference between this message and collector protocol is that +// in this message there will not be any "control" or "metadata" specific to +// OTLP protocol. +// +// When new fields are added into this message, the OTLP request MUST be updated +// as well. +message LogsData { + // An array of ResourceLogs. + // For data coming from a single resource this array will typically contain + // one element. Intermediary nodes that receive data from multiple origins + // typically batch the data before forwarding further and in that case this + // array will contain multiple elements. + repeated ResourceLogs resource_logs = 1; +} + +// A collection of ScopeLogs from a Resource. +message ResourceLogs { + reserved 1000; + + // The resource for the logs in this message. + // If this field is not set then resource info is unknown. + opentelemetry.proto.resource.v1.Resource resource = 1; + + // A list of ScopeLogs that originate from a resource. + repeated ScopeLogs scope_logs = 2; + + // The Schema URL, if known. This is the identifier of the Schema that the resource data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // This schema_url applies to the data in the "resource" field. It does not apply + // to the data in the "scope_logs" field which have their own schema_url field. + string schema_url = 3; +} + +// A collection of Logs produced by a Scope. +message ScopeLogs { + // The instrumentation scope information for the logs in this message. + // Semantically when InstrumentationScope isn't set, it is equivalent with + // an empty instrumentation scope name (unknown). + opentelemetry.proto.common.v1.InstrumentationScope scope = 1; + + // A list of log records. + repeated LogRecord log_records = 2; + + // The Schema URL, if known. This is the identifier of the Schema that the log data + // is recorded in. To learn more about Schema URL see + // https://opentelemetry.io/docs/specs/otel/schemas/#schema-url + // This schema_url applies to all logs in the "logs" field. + string schema_url = 3; +} + +// Possible values for LogRecord.SeverityNumber. +enum SeverityNumber { + // UNSPECIFIED is the default SeverityNumber, it MUST NOT be used. + SEVERITY_NUMBER_UNSPECIFIED = 0; + SEVERITY_NUMBER_TRACE = 1; + SEVERITY_NUMBER_TRACE2 = 2; + SEVERITY_NUMBER_TRACE3 = 3; + SEVERITY_NUMBER_TRACE4 = 4; + SEVERITY_NUMBER_DEBUG = 5; + SEVERITY_NUMBER_DEBUG2 = 6; + SEVERITY_NUMBER_DEBUG3 = 7; + SEVERITY_NUMBER_DEBUG4 = 8; + SEVERITY_NUMBER_INFO = 9; + SEVERITY_NUMBER_INFO2 = 10; + SEVERITY_NUMBER_INFO3 = 11; + SEVERITY_NUMBER_INFO4 = 12; + SEVERITY_NUMBER_WARN = 13; + SEVERITY_NUMBER_WARN2 = 14; + SEVERITY_NUMBER_WARN3 = 15; + SEVERITY_NUMBER_WARN4 = 16; + SEVERITY_NUMBER_ERROR = 17; + SEVERITY_NUMBER_ERROR2 = 18; + SEVERITY_NUMBER_ERROR3 = 19; + SEVERITY_NUMBER_ERROR4 = 20; + SEVERITY_NUMBER_FATAL = 21; + SEVERITY_NUMBER_FATAL2 = 22; + SEVERITY_NUMBER_FATAL3 = 23; + SEVERITY_NUMBER_FATAL4 = 24; +} + +// LogRecordFlags represents constants used to interpret the +// LogRecord.flags field, which is protobuf 'fixed32' type and is to +// be used as bit-fields. Each non-zero value defined in this enum is +// a bit-mask. To extract the bit-field, for example, use an +// expression like: +// +// (logRecord.flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK) +// +enum LogRecordFlags { + // The zero value for the enum. Should not be used for comparisons. + // Instead use bitwise "and" with the appropriate mask as shown above. + LOG_RECORD_FLAGS_DO_NOT_USE = 0; + + // Bits 0-7 are used for trace flags. + LOG_RECORD_FLAGS_TRACE_FLAGS_MASK = 0x000000FF; + + // Bits 8-31 are reserved for future use. +} + +// A log record according to OpenTelemetry Log Data Model: +// https://github.com/open-telemetry/oteps/blob/main/text/logs/0097-log-data-model.md +message LogRecord { + reserved 4; + + // time_unix_nano is the time when the event occurred. + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + fixed64 time_unix_nano = 1; + + // Time when the event was observed by the collection system. + // For events that originate in OpenTelemetry (e.g. using OpenTelemetry Logging SDK) + // this timestamp is typically set at the generation time and is equal to Timestamp. + // For events originating externally and collected by OpenTelemetry (e.g. using + // Collector) this is the time when OpenTelemetry's code observed the event measured + // by the clock of the OpenTelemetry code. This field MUST be set once the event is + // observed by OpenTelemetry. + // + // For converting OpenTelemetry log data to formats that support only one timestamp or + // when receiving OpenTelemetry log data by recipients that support only one timestamp + // internally the following logic is recommended: + // - Use time_unix_nano if it is present, otherwise use observed_time_unix_nano. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. + // Value of 0 indicates unknown or missing timestamp. + fixed64 observed_time_unix_nano = 11; + + // Numerical value of the severity, normalized to values described in Log Data Model. + // [Optional]. + SeverityNumber severity_number = 2; + + // The severity text (also known as log level). The original string representation as + // it is known at the source. [Optional]. + string severity_text = 3; + + // A value containing the body of the log record. Can be for example a human-readable + // string message (including multi-line) describing the event in a free form or it can + // be a structured data composed of arrays and maps of other values. [Optional]. + opentelemetry.proto.common.v1.AnyValue body = 5; + + // Additional attributes that describe the specific event occurrence. [Optional]. + // Attribute keys MUST be unique (it is not allowed to have more than one + // attribute with the same key). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 6; + uint32 dropped_attributes_count = 7; + + // Flags, a bit field. 8 least significant bits are the trace flags as + // defined in W3C Trace Context specification. 24 most significant bits are reserved + // and must be set to 0. Readers must not assume that 24 most significant bits + // will be zero and must correctly mask the bits when reading 8-bit trace flag (use + // flags & LOG_RECORD_FLAGS_TRACE_FLAGS_MASK). [Optional]. + fixed32 flags = 8; + + // A unique identifier for a trace. All logs from the same trace share + // the same `trace_id`. The ID is a 16-byte array. An ID with all zeroes OR + // of length other than 16 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. + // + // The receivers SHOULD assume that the log record is not associated with a + // trace if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + bytes trace_id = 9; + + // A unique identifier for a span within a trace, assigned when the span + // is created. The ID is an 8-byte array. An ID with all zeroes OR of length + // other than 8 bytes is considered invalid (empty string in OTLP/JSON + // is zero-length and thus is also invalid). + // + // This field is optional. If the sender specifies a valid span_id then it SHOULD also + // specify a valid trace_id. + // + // The receivers SHOULD assume that the log record is not associated with a + // span if any of the following is true: + // - the field is not present, + // - the field contains an invalid value. + bytes span_id = 10; +} diff --git a/kong/init.lua b/kong/init.lua index 24a6c6b1071..2a68f2acadf 100644 --- a/kong/init.lua +++ b/kong/init.lua @@ -84,7 +84,7 @@ local balancer = require "kong.runloop.balancer" local kong_error_handlers = require "kong.error_handlers" local plugin_servers = require "kong.runloop.plugin_servers" local lmdb_txn = require "resty.lmdb.transaction" -local instrumentation = require "kong.tracing.instrumentation" +local instrumentation = require "kong.observability.tracing.instrumentation" local process = require "ngx.process" local tablepool = require "tablepool" local table_new = require "table.new" @@ -837,7 +837,7 @@ function Kong.init_worker() schema_state = nil - local worker_events, err = kong_global.init_worker_events() + local worker_events, err = kong_global.init_worker_events(kong.configuration) if not worker_events then stash_init_worker_error("failed to instantiate 'kong.worker_events' " .. "module: " .. err) diff --git a/kong/llm/drivers/anthropic.lua b/kong/llm/drivers/anthropic.lua index fcc6419d33b..77c9f363f9b 100644 --- a/kong/llm/drivers/anthropic.lua +++ b/kong/llm/drivers/anthropic.lua @@ -225,7 +225,7 @@ local function handle_stream_event(event_t, model_info, route_type) return delta_to_event(event_data, model_info) elseif event_id == "message_stop" then - return "[DONE]", nil, nil + return ai_shared._CONST.SSE_TERMINATOR, nil, nil elseif event_id == "ping" then return nil, nil, nil diff --git a/kong/llm/drivers/bedrock.lua b/kong/llm/drivers/bedrock.lua new file mode 100644 index 00000000000..372a57fa827 --- /dev/null +++ b/kong/llm/drivers/bedrock.lua @@ -0,0 +1,442 @@ +local _M = {} + +-- imports +local cjson = require("cjson.safe") +local fmt = string.format +local ai_shared = require("kong.llm.drivers.shared") +local socket_url = require("socket.url") +local string_gsub = string.gsub +local table_insert = table.insert +local string_lower = string.lower +local signer = require("resty.aws.request.sign") +-- + +-- globals +local DRIVER_NAME = "bedrock" +-- + +local _OPENAI_ROLE_MAPPING = { + ["system"] = "assistant", + ["user"] = "user", + ["assistant"] = "assistant", +} + +_M.bedrock_unsupported_system_role_patterns = { + "amazon.titan.-.*", + "cohere.command.-text.-.*", + "cohere.command.-light.-text.-.*", + "mistral.mistral.-7b.-instruct.-.*", + "mistral.mixtral.-8x7b.-instruct.-.*", +} + +local function to_bedrock_generation_config(request_table) + return { + ["maxTokens"] = request_table.max_tokens, + ["stopSequences"] = request_table.stop, + ["temperature"] = request_table.temperature, + ["topP"] = request_table.top_p, + } +end + +local function to_additional_request_fields(request_table) + return { + request_table.bedrock.additionalModelRequestFields + } +end + +local function to_tool_config(request_table) + return { + request_table.bedrock.toolConfig + } +end + +local function handle_stream_event(event_t, model_info, route_type) + local new_event, metadata + + if (not event_t) or (not event_t.data) then + return "", nil, nil + end + + -- decode and determine the event type + local event = cjson.decode(event_t.data) + local event_type = event and event.headers and event.headers[":event-type"] + + if not event_type then + return "", nil, nil + end + + local body = event.body and cjson.decode(event.body) + + if not body then + return "", nil, nil + end + + if event_type == "messageStart" then + new_event = { + choices = { + [1] = { + delta = { + content = "", + role = body.role, + }, + index = 0, + logprobs = cjson.null, + }, + }, + model = model_info.name, + object = "chat.completion.chunk", + system_fingerprint = cjson.null, + } + + elseif event_type == "contentBlockDelta" then + new_event = { + choices = { + [1] = { + delta = { + content = (body.delta + and body.delta.text) + or "", + }, + index = 0, + finish_reason = cjson.null, + logprobs = cjson.null, + }, + }, + model = model_info.name, + object = "chat.completion.chunk", + } + + elseif event_type == "messageStop" then + new_event = { + choices = { + [1] = { + delta = {}, + index = 0, + finish_reason = body.stopReason, + logprobs = cjson.null, + }, + }, + model = model_info.name, + object = "chat.completion.chunk", + } + + elseif event_type == "metadata" then + metadata = { + prompt_tokens = body.usage and body.usage.inputTokens or 0, + completion_tokens = body.usage and body.usage.outputTokens or 0, + } + + new_event = ai_shared._CONST.SSE_TERMINATOR + + -- "contentBlockStop" is absent because it is not used for anything here + end + + if new_event then + if new_event ~= ai_shared._CONST.SSE_TERMINATOR then + new_event = cjson.encode(new_event) + end + + return new_event, nil, metadata + else + return nil, nil, metadata -- caller code will handle "unrecognised" event types + end +end + +local function to_bedrock_chat_openai(request_table, model_info, route_type) + if not request_table then -- try-catch type mechanism + local err = "empty request table received for transformation" + ngx.log(ngx.ERR, "[bedrock] ", err) + return nil, nil, err + end + + local new_r = {} + + -- anthropic models support variable versions, just like self-hosted + new_r.anthropic_version = model_info.options and model_info.options.anthropic_version + or "bedrock-2023-05-31" + + if request_table.messages and #request_table.messages > 0 then + local system_prompts = {} + + for i, v in ipairs(request_table.messages) do + -- for 'system', we just concat them all into one Bedrock instruction + if v.role and v.role == "system" then + system_prompts[#system_prompts+1] = { text = v.content } + + else + -- for any other role, just construct the chat history as 'parts.text' type + new_r.messages = new_r.messages or {} + table_insert(new_r.messages, { + role = _OPENAI_ROLE_MAPPING[v.role or "user"], -- default to 'user' + content = { + { + text = v.content or "" + }, + }, + }) + end + end + + -- only works for some models + if #system_prompts > 0 then + for _, p in ipairs(_M.bedrock_unsupported_system_role_patterns) do + if model_info.name:find(p) then + return nil, nil, "system prompts are unsupported for model '" .. model_info.name + end + end + + new_r.system = system_prompts + end + end + + new_r.inferenceConfig = to_bedrock_generation_config(request_table) + + new_r.toolConfig = request_table.bedrock + and request_table.bedrock.toolConfig + and to_tool_config(request_table) + + new_r.additionalModelRequestFields = request_table.bedrock + and request_table.bedrock.additionalModelRequestFields + and to_additional_request_fields(request_table) + + return new_r, "application/json", nil +end + +local function from_bedrock_chat_openai(response, model_info, route_type) + local response, err = cjson.decode(response) + + if err then + local err_client = "failed to decode response from Bedrock" + ngx.log(ngx.ERR, fmt("[bedrock] %s: %s", err_client, err)) + return nil, err_client + end + + -- messages/choices table is only 1 size, so don't need to static allocate + local client_response = {} + client_response.choices = {} + + if response.output + and response.output.message + and response.output.message.content + and #response.output.message.content > 0 + and response.output.message.content[1].text then + + client_response.choices[1] = { + index = 0, + message = { + role = "assistant", + content = response.output.message.content[1].text, + }, + finish_reason = string_lower(response.stopReason), + } + client_response.object = "chat.completion" + client_response.model = model_info.name + + else -- probably a server fault or other unexpected response + local err = "no generation candidates received from Bedrock, or max_tokens too short" + ngx.log(ngx.ERR, "[bedrock] ", err) + return nil, err + end + + -- process analytics + if response.usage then + client_response.usage = { + prompt_tokens = response.usage.inputTokens, + completion_tokens = response.usage.outputTokens, + total_tokens = response.usage.totalTokens, + } + end + + return cjson.encode(client_response) +end + +local transformers_to = { + ["llm/v1/chat"] = to_bedrock_chat_openai, +} + +local transformers_from = { + ["llm/v1/chat"] = from_bedrock_chat_openai, + ["stream/llm/v1/chat"] = handle_stream_event, +} + +function _M.from_format(response_string, model_info, route_type) + ngx.log(ngx.DEBUG, "converting from ", model_info.provider, "://", route_type, " type to kong") + + -- MUST return a string, to set as the response body + if not transformers_from[route_type] then + return nil, fmt("no transformer available from format %s://%s", model_info.provider, route_type) + end + + local ok, response_string, err, metadata = pcall(transformers_from[route_type], response_string, model_info, route_type) + if not ok or err then + return nil, fmt("transformation failed from type %s://%s: %s", + model_info.provider, + route_type, + err or "unexpected_error" + ) + end + + return response_string, nil, metadata +end + +function _M.to_format(request_table, model_info, route_type) + ngx.log(ngx.DEBUG, "converting from kong type to ", model_info.provider, "/", route_type) + + if route_type == "preserve" then + -- do nothing + return request_table, nil, nil + end + + if not transformers_to[route_type] then + return nil, nil, fmt("no transformer for %s://%s", model_info.provider, route_type) + end + + request_table = ai_shared.merge_config_defaults(request_table, model_info.options, model_info.route_type) + + local ok, response_object, content_type, err = pcall( + transformers_to[route_type], + request_table, + model_info + ) + if err or (not ok) then + return nil, nil, fmt("error transforming to %s://%s: %s", model_info.provider, route_type, err) + end + + return response_object, content_type, nil +end + +function _M.subrequest(body, conf, http_opts, return_res_table) + -- use shared/standard subrequest routine + local body_string, err + + if type(body) == "table" then + body_string, err = cjson.encode(body) + if err then + return nil, nil, "failed to parse body to json: " .. err + end + elseif type(body) == "string" then + body_string = body + else + return nil, nil, "body must be table or string" + end + + -- may be overridden + local url = (conf.model.options and conf.model.options.upstream_url) + or fmt( + "%s%s", + ai_shared.upstream_url_format[DRIVER_NAME], + ai_shared.operation_map[DRIVER_NAME][conf.route_type].path + ) + + local method = ai_shared.operation_map[DRIVER_NAME][conf.route_type].method + + local headers = { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", + } + + if conf.auth and conf.auth.header_name then + headers[conf.auth.header_name] = conf.auth.header_value + end + + local res, err, httpc = ai_shared.http_request(url, body_string, method, headers, http_opts, return_res_table) + if err then + return nil, nil, "request to ai service failed: " .. err + end + + if return_res_table then + return res, res.status, nil, httpc + else + -- At this point, the entire request / response is complete and the connection + -- will be closed or back on the connection pool. + local status = res.status + local body = res.body + + if status > 299 then + return body, res.status, "status code " .. status + end + + return body, res.status, nil + end +end + +function _M.header_filter_hooks(body) + -- nothing to parse in header_filter phase +end + +function _M.post_request(conf) + if ai_shared.clear_response_headers[DRIVER_NAME] then + for i, v in ipairs(ai_shared.clear_response_headers[DRIVER_NAME]) do + kong.response.clear_header(v) + end + end +end + +function _M.pre_request(conf, body) + -- force gzip for bedrock because brotli and others break streaming + kong.service.request.set_header("Accept-Encoding", "gzip, identity") + + return true, nil +end + +-- returns err or nil +function _M.configure_request(conf, aws_sdk) + local operation = kong.ctx.shared.ai_proxy_streaming_mode and "converse-stream" + or "converse" + + local f_url = conf.model.options and conf.model.options.upstream_url + + if not f_url then -- upstream_url override is not set + local uri = fmt(ai_shared.upstream_url_format[DRIVER_NAME], aws_sdk.config.region) + local path = fmt( + ai_shared.operation_map[DRIVER_NAME][conf.route_type].path, + conf.model.name, + operation) + + f_url = fmt("%s%s", uri, path) + end + + local parsed_url = socket_url.parse(f_url) + + if conf.model.options and conf.model.options.upstream_path then + -- upstream path override is set (or templated from request params) + parsed_url.path = conf.model.options.upstream_path + end + + -- if the path is read from a URL capture, ensure that it is valid + parsed_url.path = string_gsub(parsed_url.path, "^/*", "/") + + kong.service.request.set_path(parsed_url.path) + kong.service.request.set_scheme(parsed_url.scheme) + kong.service.set_target(parsed_url.host, (tonumber(parsed_url.port) or 443)) + + -- do the IAM auth and signature headers + aws_sdk.config.signatureVersion = "v4" + aws_sdk.config.endpointPrefix = "bedrock" + + local r = { + headers = {}, + method = ai_shared.operation_map[DRIVER_NAME][conf.route_type].method, + path = parsed_url.path, + host = parsed_url.host, + port = tonumber(parsed_url.port) or 443, + body = kong.request.get_raw_body() + } + + local signature, err = signer(aws_sdk.config, r) + if not signature then + return nil, "failed to sign AWS request: " .. (err or "NONE") + end + + kong.service.request.set_header("Authorization", signature.headers["Authorization"]) + if signature.headers["X-Amz-Security-Token"] then + kong.service.request.set_header("X-Amz-Security-Token", signature.headers["X-Amz-Security-Token"]) + end + if signature.headers["X-Amz-Date"] then + kong.service.request.set_header("X-Amz-Date", signature.headers["X-Amz-Date"]) + end + + return true +end + +return _M diff --git a/kong/llm/drivers/cohere.lua b/kong/llm/drivers/cohere.lua index b96cbbbc2d4..1aafc9405b0 100644 --- a/kong/llm/drivers/cohere.lua +++ b/kong/llm/drivers/cohere.lua @@ -97,7 +97,7 @@ local function handle_stream_event(event_t, model_info, route_type) elseif event.event_type == "stream-end" then -- return a metadata object, with the OpenAI termination event - new_event = "[DONE]" + new_event = ai_shared._CONST.SSE_TERMINATOR metadata = { completion_tokens = event.response @@ -123,7 +123,7 @@ local function handle_stream_event(event_t, model_info, route_type) end if new_event then - if new_event ~= "[DONE]" then + if new_event ~= ai_shared._CONST.SSE_TERMINATOR then new_event = cjson.encode(new_event) end diff --git a/kong/llm/drivers/gemini.lua b/kong/llm/drivers/gemini.lua new file mode 100644 index 00000000000..57ca7127ef2 --- /dev/null +++ b/kong/llm/drivers/gemini.lua @@ -0,0 +1,424 @@ +local _M = {} + +-- imports +local cjson = require("cjson.safe") +local fmt = string.format +local ai_shared = require("kong.llm.drivers.shared") +local socket_url = require("socket.url") +local string_gsub = string.gsub +local buffer = require("string.buffer") +local table_insert = table.insert +local string_lower = string.lower +-- + +-- globals +local DRIVER_NAME = "gemini" +-- + +local _OPENAI_ROLE_MAPPING = { + ["system"] = "system", + ["user"] = "user", + ["assistant"] = "model", +} + +local function to_gemini_generation_config(request_table) + return { + ["maxOutputTokens"] = request_table.max_tokens, + ["stopSequences"] = request_table.stop, + ["temperature"] = request_table.temperature, + ["topK"] = request_table.top_k, + ["topP"] = request_table.top_p, + } +end + +local function is_response_content(content) + return content + and content.candidates + and #content.candidates > 0 + and content.candidates[1].content + and content.candidates[1].content.parts + and #content.candidates[1].content.parts > 0 + and content.candidates[1].content.parts[1].text +end + +local function handle_stream_event(event_t, model_info, route_type) + -- discard empty frames, it should either be a random new line, or comment + if (not event_t.data) or (#event_t.data < 1) then + return + end + + if event_t.data == ai_shared._CONST.SSE_TERMINATOR then + return ai_shared._CONST.SSE_TERMINATOR, nil, nil + end + + local event, err = cjson.decode(event_t.data) + if err then + ngx.log(ngx.WARN, "failed to decode stream event frame from gemini: " .. err) + return nil, "failed to decode stream event frame from gemini", nil + end + + if is_response_content(event) then + local metadata = {} + metadata.finished_reason = event.candidates + and #event.candidates > 0 + and event.candidates[1].finishReason + or "STOP" + metadata.completion_tokens = event.usageMetadata and event.usageMetadata.candidatesTokenCount or 0 + metadata.prompt_tokens = event.usageMetadata and event.usageMetadata.promptTokenCount or 0 + + local new_event = { + choices = { + [1] = { + delta = { + content = event.candidates[1].content.parts[1].text or "", + role = "assistant", + }, + index = 0, + }, + }, + } + + return cjson.encode(new_event), nil, metadata + end +end + +local function to_gemini_chat_openai(request_table, model_info, route_type) + if request_table then -- try-catch type mechanism + local new_r = {} + + if request_table.messages and #request_table.messages > 0 then + local system_prompt + + for i, v in ipairs(request_table.messages) do + + -- for 'system', we just concat them all into one Gemini instruction + if v.role and v.role == "system" then + system_prompt = system_prompt or buffer.new() + system_prompt:put(v.content or "") + else + -- for any other role, just construct the chat history as 'parts.text' type + new_r.contents = new_r.contents or {} + table_insert(new_r.contents, { + role = _OPENAI_ROLE_MAPPING[v.role or "user"], -- default to 'user' + parts = { + { + text = v.content or "" + }, + }, + }) + end + end + + -- This was only added in Gemini 1.5 + if system_prompt and model_info.name:sub(1, 10) == "gemini-1.0" then + return nil, nil, "system prompts aren't supported on gemini-1.0 models" + + elseif system_prompt then + new_r.systemInstruction = { + parts = { + { + text = system_prompt:get(), + }, + }, + } + end + end + + new_r.generationConfig = to_gemini_generation_config(request_table) + + return new_r, "application/json", nil + end + + local new_r = {} + + if request_table.messages and #request_table.messages > 0 then + local system_prompt + + for i, v in ipairs(request_table.messages) do + + -- for 'system', we just concat them all into one Gemini instruction + if v.role and v.role == "system" then + system_prompt = system_prompt or buffer.new() + system_prompt:put(v.content or "") + else + -- for any other role, just construct the chat history as 'parts.text' type + new_r.contents = new_r.contents or {} + table_insert(new_r.contents, { + role = _OPENAI_ROLE_MAPPING[v.role or "user"], -- default to 'user' + parts = { + { + text = v.content or "" + }, + }, + }) + end + end + end + + new_r.generationConfig = to_gemini_generation_config(request_table) + + return new_r, "application/json", nil +end + +local function from_gemini_chat_openai(response, model_info, route_type) + local response, err = cjson.decode(response) + + if err then + local err_client = "failed to decode response from Gemini" + ngx.log(ngx.ERR, fmt("%s: %s", err_client, err)) + return nil, err_client + end + + -- messages/choices table is only 1 size, so don't need to static allocate + local messages = {} + messages.choices = {} + + if response.candidates + and #response.candidates > 0 + and is_response_content(response) then + + messages.choices[1] = { + index = 0, + message = { + role = "assistant", + content = response.candidates[1].content.parts[1].text, + }, + finish_reason = string_lower(response.candidates[1].finishReason), + } + messages.object = "chat.completion" + messages.model = model_info.name + + -- process analytics + if response.usageMetadata then + messages.usage = { + prompt_tokens = response.usageMetadata.promptTokenCount, + completion_tokens = response.usageMetadata.candidatesTokenCount, + total_tokens = response.usageMetadata.totalTokenCount, + } + end + + else -- probably a server fault or other unexpected response + local err = "no generation candidates received from Gemini, or max_tokens too short" + ngx.log(ngx.ERR, err) + return nil, err + end + + return cjson.encode(messages) +end + +local transformers_to = { + ["llm/v1/chat"] = to_gemini_chat_openai, +} + +local transformers_from = { + ["llm/v1/chat"] = from_gemini_chat_openai, + ["stream/llm/v1/chat"] = handle_stream_event, +} + +function _M.from_format(response_string, model_info, route_type) + ngx.log(ngx.DEBUG, "converting from ", model_info.provider, "://", route_type, " type to kong") + + -- MUST return a string, to set as the response body + if not transformers_from[route_type] then + return nil, fmt("no transformer available from format %s://%s", model_info.provider, route_type) + end + + local ok, response_string, err, metadata = pcall(transformers_from[route_type], response_string, model_info, route_type) + if not ok or err then + return nil, fmt("transformation failed from type %s://%s: %s", + model_info.provider, + route_type, + err or "unexpected_error" + ) + end + + return response_string, nil, metadata +end + +function _M.to_format(request_table, model_info, route_type) + ngx.log(ngx.DEBUG, "converting from kong type to ", model_info.provider, "/", route_type) + + if route_type == "preserve" then + -- do nothing + return request_table, nil, nil + end + + if not transformers_to[route_type] then + return nil, nil, fmt("no transformer for %s://%s", model_info.provider, route_type) + end + + request_table = ai_shared.merge_config_defaults(request_table, model_info.options, model_info.route_type) + + local ok, response_object, content_type, err = pcall( + transformers_to[route_type], + request_table, + model_info + ) + if err or (not ok) then + return nil, nil, fmt("error transforming to %s://%s: %s", model_info.provider, route_type, err) + end + + return response_object, content_type, nil +end + +function _M.subrequest(body, conf, http_opts, return_res_table) + -- use shared/standard subrequest routine + local body_string, err + + if type(body) == "table" then + body_string, err = cjson.encode(body) + if err then + return nil, nil, "failed to parse body to json: " .. err + end + elseif type(body) == "string" then + body_string = body + else + return nil, nil, "body must be table or string" + end + + -- may be overridden + local url = (conf.model.options and conf.model.options.upstream_url) + or fmt( + "%s%s", + ai_shared.upstream_url_format[DRIVER_NAME], + ai_shared.operation_map[DRIVER_NAME][conf.route_type].path + ) + + local method = ai_shared.operation_map[DRIVER_NAME][conf.route_type].method + + local headers = { + ["Accept"] = "application/json", + ["Content-Type"] = "application/json", + } + + if conf.auth and conf.auth.header_name then + headers[conf.auth.header_name] = conf.auth.header_value + end + + local res, err, httpc = ai_shared.http_request(url, body_string, method, headers, http_opts, return_res_table) + if err then + return nil, nil, "request to ai service failed: " .. err + end + + if return_res_table then + return res, res.status, nil, httpc + else + -- At this point, the entire request / response is complete and the connection + -- will be closed or back on the connection pool. + local status = res.status + local body = res.body + + if status > 299 then + return body, res.status, "status code " .. status + end + + return body, res.status, nil + end +end + +function _M.header_filter_hooks(body) + -- nothing to parse in header_filter phase +end + +function _M.post_request(conf) + if ai_shared.clear_response_headers[DRIVER_NAME] then + for i, v in ipairs(ai_shared.clear_response_headers[DRIVER_NAME]) do + kong.response.clear_header(v) + end + end +end + +function _M.pre_request(conf, body) + -- disable gzip for gemini because it breaks streaming + kong.service.request.set_header("Accept-Encoding", "identity") + + return true, nil +end + +-- returns err or nil +function _M.configure_request(conf, identity_interface) + local parsed_url + local operation = kong.ctx.shared.ai_proxy_streaming_mode and "streamGenerateContent" + or "generateContent" + local f_url = conf.model.options and conf.model.options.upstream_url + + if not f_url then -- upstream_url override is not set + -- check if this is "public" or "vertex" gemini deployment + if conf.model.options + and conf.model.options.gemini + and conf.model.options.gemini.api_endpoint + and conf.model.options.gemini.project_id + and conf.model.options.gemini.location_id + then + -- vertex mode + f_url = fmt(ai_shared.upstream_url_format["gemini_vertex"], + conf.model.options.gemini.api_endpoint) .. + fmt(ai_shared.operation_map["gemini_vertex"][conf.route_type].path, + conf.model.options.gemini.project_id, + conf.model.options.gemini.location_id, + conf.model.name, + operation) + else + -- public mode + f_url = ai_shared.upstream_url_format["gemini"] .. + fmt(ai_shared.operation_map["gemini"][conf.route_type].path, + conf.model.name, + operation) + end + end + + parsed_url = socket_url.parse(f_url) + + if conf.model.options and conf.model.options.upstream_path then + -- upstream path override is set (or templated from request params) + parsed_url.path = conf.model.options.upstream_path + end + + -- if the path is read from a URL capture, ensure that it is valid + parsed_url.path = string_gsub(parsed_url.path, "^/*", "/") + + kong.service.request.set_path(parsed_url.path) + kong.service.request.set_scheme(parsed_url.scheme) + kong.service.set_target(parsed_url.host, (tonumber(parsed_url.port) or 443)) + + local auth_header_name = conf.auth and conf.auth.header_name + local auth_header_value = conf.auth and conf.auth.header_value + local auth_param_name = conf.auth and conf.auth.param_name + local auth_param_value = conf.auth and conf.auth.param_value + local auth_param_location = conf.auth and conf.auth.param_location + + -- DBO restrictions makes sure that only one of these auth blocks runs in one plugin config + if auth_header_name and auth_header_value then + kong.service.request.set_header(auth_header_name, auth_header_value) + end + + if auth_param_name and auth_param_value and auth_param_location == "query" then + local query_table = kong.request.get_query() + query_table[auth_param_name] = auth_param_value + kong.service.request.set_query(query_table) + end + -- if auth_param_location is "form", it will have already been set in a global pre-request hook + + -- if we're passed a GCP SDK, for cloud identity / SSO, use it appropriately + if identity_interface then + if identity_interface:needsRefresh() then + -- HACK: A bug in lua-resty-gcp tries to re-load the environment + -- variable every time, which fails in nginx + -- Create a whole new interface instead. + -- Memory leaks are mega unlikely because this should only + -- happen about once an hour, and the old one will be + -- cleaned up anyway. + local service_account_json = identity_interface.service_account_json + local identity_interface_new = identity_interface:new(service_account_json) + identity_interface.token = identity_interface_new.token + + kong.log.notice("gcp identity token for ", kong.plugin.get_id(), " has been refreshed") + end + + kong.service.request.set_header("Authorization", "Bearer " .. identity_interface.token) + end + + return true +end + +return _M diff --git a/kong/llm/drivers/shared.lua b/kong/llm/drivers/shared.lua index 9d62998c34c..8c0c88e6573 100644 --- a/kong/llm/drivers/shared.lua +++ b/kong/llm/drivers/shared.lua @@ -1,11 +1,12 @@ local _M = {} -- imports -local cjson = require("cjson.safe") -local http = require("resty.http") -local fmt = string.format -local os = os -local parse_url = require("socket.url").parse +local cjson = require("cjson.safe") +local http = require("resty.http") +local fmt = string.format +local os = os +local parse_url = require("socket.url").parse +local aws_stream = require("kong.tools.aws_stream") -- -- static @@ -16,7 +17,11 @@ local split = require("kong.tools.string").split local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy local function str_ltrim(s) -- remove leading whitespace from string. - return (s:gsub("^%s*", "")) + return type(s) == "string" and s:gsub("^%s*", "") +end + +local function str_rtrim(s) -- remove trailing whitespace from string. + return type(s) == "string" and s:match('^(.*%S)%s*$') end -- @@ -35,11 +40,13 @@ local log_entry_keys = { PROVIDER_NAME = "provider_name", REQUEST_MODEL = "request_model", RESPONSE_MODEL = "response_model", + LLM_LATENCY = "llm_latency", -- usage keys PROMPT_TOKENS = "prompt_tokens", COMPLETION_TOKENS = "completion_tokens", TOTAL_TOKENS = "total_tokens", + TIME_PER_TOKEN = "time_per_token", COST = "cost", -- cache keys @@ -51,17 +58,26 @@ local log_entry_keys = { local openai_override = os.getenv("OPENAI_TEST_PORT") +_M._CONST = { + ["SSE_TERMINATOR"] = "[DONE]", +} + _M.streaming_has_token_counts = { ["cohere"] = true, ["llama2"] = true, ["anthropic"] = true, + ["gemini"] = true, + ["bedrock"] = true, } _M.upstream_url_format = { - openai = fmt("%s://api.openai.com:%s", (openai_override and "http") or "https", (openai_override) or "443"), - anthropic = "https://api.anthropic.com:443", - cohere = "https://api.cohere.com:443", - azure = "https://%s.openai.azure.com:443/openai/deployments/%s", + openai = fmt("%s://api.openai.com:%s", (openai_override and "http") or "https", (openai_override) or "443"), + anthropic = "https://api.anthropic.com:443", + cohere = "https://api.cohere.com:443", + azure = "https://%s.openai.azure.com:443/openai/deployments/%s", + gemini = "https://generativelanguage.googleapis.com", + gemini_vertex = "https://%s", + bedrock = "https://bedrock-runtime.%s.amazonaws.com", } _M.operation_map = { @@ -105,6 +121,24 @@ _M.operation_map = { method = "POST", }, }, + gemini = { + ["llm/v1/chat"] = { + path = "/v1beta/models/%s:%s", + method = "POST", + }, + }, + gemini_vertex = { + ["llm/v1/chat"] = { + path = "/v1/projects/%s/locations/%s/publishers/google/models/%s:%s", + method = "POST", + }, + }, + bedrock = { + ["llm/v1/chat"] = { + path = "/model/%s/%s", + method = "POST", + }, + }, } _M.clear_response_headers = { @@ -120,6 +154,12 @@ _M.clear_response_headers = { mistral = { "Set-Cookie", }, + gemini = { + "Set-Cookie", + }, + bedrock = { + "Set-Cookie", + }, } --- @@ -199,21 +239,67 @@ end -- as if it were an SSE message. -- -- @param {string} frame input string to format into SSE events --- @param {string} delimiter delimeter (can be complex string) to split by +-- @param {boolean} raw_json sets application/json byte-parser mode -- @return {table} n number of split SSE messages, or empty table -function _M.frame_to_events(frame) +function _M.frame_to_events(frame, provider) local events = {} + if (not frame) or (#frame < 1) or (type(frame)) ~= "string" then + return + end + + -- some new LLMs return the JSON object-by-object, + -- because that totally makes sense to parse?! + if provider == "gemini" then + local done = false + + -- if this is the first frame, it will begin with array opener '[' + frame = (string.sub(str_ltrim(frame), 1, 1) == "[" and string.sub(str_ltrim(frame), 2)) or frame + + -- it may start with ',' which is the start of the new frame + frame = (string.sub(str_ltrim(frame), 1, 1) == "," and string.sub(str_ltrim(frame), 2)) or frame + + -- it may end with the array terminator ']' indicating the finished stream + if string.sub(str_rtrim(frame), -1) == "]" then + frame = string.sub(str_rtrim(frame), 1, -2) + done = true + end + + -- for multiple events that arrive in the same frame, split by top-level comma + for _, v in ipairs(split(frame, "\n,")) do + events[#events+1] = { data = v } + end + + if done then + -- add the done signal here + -- but we have to retrieve the metadata from a previous filter run + events[#events+1] = { data = _M._CONST.SSE_TERMINATOR } + end + + elseif provider == "bedrock" then + local parser = aws_stream:new(frame) + while true do + local msg = parser:next_message() + + if not msg then + break + end + + events[#events+1] = { data = cjson.encode(msg) } + end + + -- check if it's raw json and just return the split up data frame -- Cohere / Other flat-JSON format parser -- just return the split up data frame - if (not kong or not kong.ctx.plugin.truncated_frame) and string.sub(str_ltrim(frame), 1, 1) == "{" then + elseif (not kong or not kong.ctx.plugin.truncated_frame) and string.sub(str_ltrim(frame), 1, 1) == "{" then for event in frame:gmatch("[^\r\n]+") do events[#events + 1] = { data = event, } end + + -- standard SSE parser else - -- standard SSE parser local event_lines = split(frame, "\n") local struct = { event = nil, id = nil, data = nil } @@ -226,7 +312,10 @@ function _M.frame_to_events(frame) -- test for truncated chunk on the last line (no trailing \r\n\r\n) if #dat > 0 and #event_lines == i then ngx.log(ngx.DEBUG, "[ai-proxy] truncated sse frame head") - kong.ctx.plugin.truncated_frame = dat + if kong then + kong.ctx.plugin.truncated_frame = dat + end + break -- stop parsing immediately, server has done something wrong end @@ -357,7 +446,7 @@ function _M.from_ollama(response_string, model_info, route_type) end end - if output and output ~= "[DONE]" then + if output and output ~= _M._CONST.SSE_TERMINATOR then output, err = cjson.encode(output) end @@ -404,24 +493,26 @@ function _M.resolve_plugin_conf(kong_request, conf) -- handle all other options for k, v in pairs(conf.model.options or {}) do - local prop_m = string_match(v or "", '%$%((.-)%)') - if prop_m then - local splitted = split(prop_m, '.') - if #splitted ~= 2 then - return nil, "cannot parse expression for field '" .. v .. "'" - end - - -- find the request parameter, with the configured name - prop_m, err = _M.conf_from_request(kong_request, splitted[1], splitted[2]) - if err then - return nil, err - end - if not prop_m then - return nil, splitted[1] .. " key " .. splitted[2] .. " was not provided" - end + if type(v) == "string" then + local prop_m = string_match(v or "", '%$%((.-)%)') + if prop_m then + local splitted = split(prop_m, '.') + if #splitted ~= 2 then + return nil, "cannot parse expression for field '" .. v .. "'" + end + + -- find the request parameter, with the configured name + prop_m, err = _M.conf_from_request(kong_request, splitted[1], splitted[2]) + if err then + return nil, err + end + if not prop_m then + return nil, splitted[1] .. " key " .. splitted[2] .. " was not provided" + end - -- replace the value - conf_m.model.options[k] = prop_m + -- replace the value + conf_m.model.options[k] = prop_m + end end end @@ -438,13 +529,14 @@ function _M.pre_request(conf, request_table) request_table[auth_param_name] = auth_param_value end + -- retrieve the plugin name + local plugin_name = conf.__key__:match('plugins:(.-):') + if not plugin_name or plugin_name == "" then + return nil, "no plugin name is being passed by the plugin" + end + -- if enabled AND request type is compatible, capture the input for analytics if conf.logging and conf.logging.log_payloads then - local plugin_name = conf.__key__:match('plugins:(.-):') - if not plugin_name or plugin_name == "" then - return nil, "no plugin name is being passed by the plugin" - end - kong.log.set_serialize_value(fmt("ai.%s.%s.%s", plugin_name, log_entry_keys.PAYLOAD_CONTAINER, log_entry_keys.REQUEST_BODY), kong.request.get_raw_body()) end @@ -458,12 +550,19 @@ function _M.pre_request(conf, request_table) kong.ctx.shared.ai_prompt_tokens = (kong.ctx.shared.ai_prompt_tokens or 0) + prompt_tokens end + local start_time_key = "ai_request_start_time_" .. plugin_name + kong.ctx.plugin[start_time_key] = ngx.now() + return true, nil end function _M.post_request(conf, response_object) local body_string, err + if not response_object then + return + end + if type(response_object) == "string" then -- set raw string body first, then decode body_string = response_object @@ -507,6 +606,20 @@ function _M.post_request(conf, response_object) request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.REQUEST_MODEL] = kong.ctx.plugin.llm_model_requested or conf.model.name request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.RESPONSE_MODEL] = response_object.model or conf.model.name + -- Set the llm latency meta, and time per token usage + local start_time_key = "ai_request_start_time_" .. plugin_name + if kong.ctx.plugin[start_time_key] then + local llm_latency = math.floor((ngx.now() - kong.ctx.plugin[start_time_key]) * 1000) + request_analytics_plugin[log_entry_keys.META_CONTAINER][log_entry_keys.LLM_LATENCY] = llm_latency + kong.ctx.shared.ai_request_latency = llm_latency + + if response_object.usage and response_object.usage.completion_tokens then + local time_per_token = math.floor(llm_latency / response_object.usage.completion_tokens) + request_analytics_plugin[log_entry_keys.USAGE_CONTAINER][log_entry_keys.TIME_PER_TOKEN] = time_per_token + kong.ctx.shared.ai_request_time_per_token = time_per_token + end + end + -- set extra per-provider meta if kong.ctx.plugin.ai_extra_meta and type(kong.ctx.plugin.ai_extra_meta) == "table" then for k, v in pairs(kong.ctx.plugin.ai_extra_meta) do @@ -527,7 +640,7 @@ function _M.post_request(conf, response_object) end if response_object.usage.prompt_tokens and response_object.usage.completion_tokens - and conf.model.options.input_cost and conf.model.options.output_cost then + and conf.model.options and conf.model.options.input_cost and conf.model.options.output_cost then request_analytics_plugin[log_entry_keys.USAGE_CONTAINER][log_entry_keys.COST] = (response_object.usage.prompt_tokens * conf.model.options.input_cost + response_object.usage.completion_tokens * conf.model.options.output_cost) / 1000000 -- 1 million diff --git a/kong/llm/init.lua b/kong/llm/init.lua index aaf3af08a79..b4b7bba5ae7 100644 --- a/kong/llm/init.lua +++ b/kong/llm/init.lua @@ -10,8 +10,6 @@ local _M = { config_schema = require "kong.llm.schemas", } - - do -- formats_compatible is a map of formats that are compatible with each other. local formats_compatible = { @@ -93,20 +91,51 @@ do function LLM:ai_introspect_body(request, system_prompt, http_opts, response_regex_match) local err, _ - -- set up the request - local ai_request = { - messages = { - [1] = { - role = "system", - content = system_prompt, + -- set up the LLM request for transformation instructions + local ai_request + + -- mistral, cohere, titan (via Bedrock) don't support system commands + if self.driver == "bedrock" then + for _, p in ipairs(self.driver.bedrock_unsupported_system_role_patterns) do + if request.model:find(p) then + ai_request = { + messages = { + [1] = { + role = "user", + content = system_prompt, + }, + [2] = { + role = "assistant", + content = "What is the message?", + }, + [3] = { + role = "user", + content = request, + } + }, + stream = false, + } + break + end + end + end + + -- not Bedrock, or didn't match banned pattern - continue as normal + if not ai_request then + ai_request = { + messages = { + [1] = { + role = "system", + content = system_prompt, + }, + [2] = { + role = "user", + content = request, + } }, - [2] = { - role = "user", - content = request, - } - }, - stream = false, - } + stream = false, + } + end -- convert it to the specified driver format ai_request, _, err = self.driver.to_format(ai_request, self.conf.model, "llm/v1/chat") @@ -206,8 +235,9 @@ do } setmetatable(self, LLM) - local provider = (self.conf.model or {}).provider or "NONE_SET" - local driver_module = "kong.llm.drivers." .. provider + self.provider = (self.conf.model or {}).provider or "NONE_SET" + local driver_module = "kong.llm.drivers." .. self.provider + local ok ok, self.driver = pcall(require, driver_module) if not ok then diff --git a/kong/llm/proxy/handler.lua b/kong/llm/proxy/handler.lua new file mode 100644 index 00000000000..de028bd7ee4 --- /dev/null +++ b/kong/llm/proxy/handler.lua @@ -0,0 +1,570 @@ +-- This software is copyright Kong Inc. and its licensors. +-- Use of the software is subject to the agreement between your organization +-- and Kong Inc. If there is no such agreement, use is governed by and +-- subject to the terms of the Kong Master Software License Agreement found +-- at https://konghq.com/enterprisesoftwarelicense/. +-- [ END OF LICENSE 0867164ffc95e54f04670b5169c09574bdbd9bba ] + +local ai_shared = require("kong.llm.drivers.shared") +local llm = require("kong.llm") +local cjson = require("cjson.safe") +local kong_utils = require("kong.tools.gzip") +local buffer = require "string.buffer" +local strip = require("kong.tools.utils").strip + +-- cloud auth/sdk providers +local GCP_SERVICE_ACCOUNT do + GCP_SERVICE_ACCOUNT = os.getenv("GCP_SERVICE_ACCOUNT") +end + +local GCP = require("resty.gcp.request.credentials.accesstoken") +local aws_config = require "resty.aws.config" -- reads environment variables whilst available +local AWS = require("resty.aws") +local AWS_REGION do + AWS_REGION = os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION") +end +-- + + +local EMPTY = {} + +local _M = {} + +local function bail(code, msg) + if code == 400 and msg then + kong.log.info(msg) + end + + if ngx.get_phase() ~= "balancer" then + return kong.response.exit(code, msg and { error = { message = msg } } or nil) + end +end + + +-- static messages +local ERROR__NOT_SET = 'data: {"error": true, "message": "empty or unsupported transformer response"}' + + +local _KEYBASTION = setmetatable({}, { + __mode = "k", + __index = function(this_cache, plugin_config) + if plugin_config.model.provider == "gemini" and + plugin_config.auth and + plugin_config.auth.gcp_use_service_account then + + ngx.log(ngx.NOTICE, "loading gcp sdk for plugin ", kong.plugin.get_id()) + + local service_account_json = (plugin_config.auth and plugin_config.auth.gcp_service_account_json) or GCP_SERVICE_ACCOUNT + + local ok, gcp_auth = pcall(GCP.new, nil, service_account_json) + if ok and gcp_auth then + -- store our item for the next time we need it + gcp_auth.service_account_json = service_account_json + this_cache[plugin_config] = { interface = gcp_auth, error = nil } + return this_cache[plugin_config] + end + + return { interface = nil, error = "cloud-authentication with GCP failed" } + + elseif plugin_config.model.provider == "bedrock" then + ngx.log(ngx.NOTICE, "loading aws sdk for plugin ", kong.plugin.get_id()) + local aws + + local region = plugin_config.model.options + and plugin_config.model.options.bedrock + and plugin_config.model.options.bedrock.aws_region + or AWS_REGION + + if not region then + return { interface = nil, error = "AWS region not specified anywhere" } + end + + local access_key_set = (plugin_config.auth and plugin_config.auth.aws_access_key_id) + or aws_config.global.AWS_ACCESS_KEY_ID + local secret_key_set = plugin_config.auth and plugin_config.auth.aws_secret_access_key + or aws_config.global.AWS_SECRET_ACCESS_KEY + + aws = AWS({ + -- if any of these are nil, they either use the SDK default or + -- are deliberately null so that a different auth chain is used + region = region, + }) + + if access_key_set and secret_key_set then + -- Override credential config according to plugin config, if set + local creds = aws:Credentials { + accessKeyId = access_key_set, + secretAccessKey = secret_key_set, + } + + aws.config.credentials = creds + end + + this_cache[plugin_config] = { interface = aws, error = nil } + + return this_cache[plugin_config] + end + end, +}) + + +-- get the token text from an event frame +local function get_token_text(event_t) + -- get: event_t.choices[1] + local first_choice = ((event_t or EMPTY).choices or EMPTY)[1] or EMPTY + -- return: + -- - event_t.choices[1].delta.content + -- - event_t.choices[1].text + -- - "" + local token_text = (first_choice.delta or EMPTY).content or first_choice.text or "" + return (type(token_text) == "string" and token_text) or "" +end + + +local function handle_streaming_frame(conf) + -- make a re-usable framebuffer + local framebuffer = buffer.new() + local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" + + local ai_driver = require("kong.llm.drivers." .. conf.model.provider) + + local kong_ctx_plugin = kong.ctx.plugin + -- create a buffer to store each response token/frame, on first pass + if (conf.logging or EMPTY).log_payloads and + (not kong_ctx_plugin.ai_stream_log_buffer) then + kong_ctx_plugin.ai_stream_log_buffer = buffer.new() + end + + -- now handle each chunk/frame + local chunk = ngx.arg[1] + local finished = ngx.arg[2] + + if type(chunk) == "string" and chunk ~= "" then + -- transform each one into flat format, skipping transformer errors + -- because we have already 200 OK'd the client by now + + if (not finished) and (is_gzip) then + chunk = kong_utils.inflate_gzip(ngx.arg[1]) + end + + local events = ai_shared.frame_to_events(chunk, conf.model.provider) + + if not events then + -- usually a not-supported-transformer or empty frames. + -- header_filter has already run, so all we can do is log it, + -- and then send the client a readable error in a single chunk + local response = ERROR__NOT_SET + + if is_gzip then + response = kong_utils.deflate_gzip(response) + end + + ngx.arg[1] = response + ngx.arg[2] = true + + return + end + + for _, event in ipairs(events) do + local formatted, _, metadata = ai_driver.from_format(event, conf.model, "stream/" .. conf.route_type) + + local event_t = nil + local token_t = nil + local err + + if formatted then -- only stream relevant frames back to the user + if conf.logging and conf.logging.log_payloads and (formatted ~= ai_shared._CONST.SSE_TERMINATOR) then + -- append the "choice" to the buffer, for logging later. this actually works! + if not event_t then + event_t, err = cjson.decode(formatted) + end + + if not err then + if not token_t then + token_t = get_token_text(event_t) + end + + kong_ctx_plugin.ai_stream_log_buffer:put(token_t) + end + end + + -- handle event telemetry + if conf.logging and conf.logging.log_statistics then + if not ai_shared.streaming_has_token_counts[conf.model.provider] then + if formatted ~= ai_shared._CONST.SSE_TERMINATOR then + if not event_t then + event_t, err = cjson.decode(formatted) + end + + if not err then + if not token_t then + token_t = get_token_text(event_t) + end + + -- incredibly loose estimate based on https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them + -- but this is all we can do until OpenAI fixes this... + -- + -- essentially, every 4 characters is a token, with minimum of 1*4 per event + kong_ctx_plugin.ai_stream_completion_tokens = + (kong_ctx_plugin.ai_stream_completion_tokens or 0) + math.ceil(#strip(token_t) / 4) + end + end + end + end + + framebuffer:put("data: ") + framebuffer:put(formatted or "") + framebuffer:put((formatted ~= ai_shared._CONST.SSE_TERMINATOR) and "\n\n" or "") + end + + if conf.logging and conf.logging.log_statistics and metadata then + -- gemini metadata specifically, works differently + if conf.model.provider == "gemini" then + kong_ctx_plugin.ai_stream_completion_tokens = metadata.completion_tokens or 0 + kong_ctx_plugin.ai_stream_prompt_tokens = metadata.prompt_tokens or 0 + else + kong_ctx_plugin.ai_stream_completion_tokens = + (kong_ctx_plugin.ai_stream_completion_tokens or 0) + + (metadata.completion_tokens or 0) + or kong_ctx_plugin.ai_stream_completion_tokens + kong_ctx_plugin.ai_stream_prompt_tokens = + (kong_ctx_plugin.ai_stream_prompt_tokens or 0) + + (metadata.prompt_tokens or 0) + or kong_ctx_plugin.ai_stream_prompt_tokens + end + end + end + end + + local response_frame = framebuffer:get() + if (not finished) and (is_gzip) then + response_frame = kong_utils.deflate_gzip(response_frame) + end + + ngx.arg[1] = response_frame + + if finished then + local fake_response_t = { + response = kong_ctx_plugin.ai_stream_log_buffer and kong_ctx_plugin.ai_stream_log_buffer:get(), + usage = { + prompt_tokens = kong_ctx_plugin.ai_stream_prompt_tokens or 0, + completion_tokens = kong_ctx_plugin.ai_stream_completion_tokens or 0, + total_tokens = (kong_ctx_plugin.ai_stream_prompt_tokens or 0) + + (kong_ctx_plugin.ai_stream_completion_tokens or 0), + } + } + + ngx.arg[1] = nil + ai_shared.post_request(conf, fake_response_t) + kong_ctx_plugin.ai_stream_log_buffer = nil + end +end + +function _M:header_filter(conf) + -- free up the buffered body used in the access phase + kong.ctx.shared.ai_request_body = nil + + local kong_ctx_plugin = kong.ctx.plugin + local kong_ctx_shared = kong.ctx.shared + + if kong_ctx_shared.skip_response_transformer then + return + end + + -- clear shared restricted headers + for _, v in ipairs(ai_shared.clear_response_headers.shared) do + kong.response.clear_header(v) + end + + -- only act on 200 in first release - pass the unmodifed response all the way through if any failure + if kong.response.get_status() ~= 200 then + return + end + + -- we use openai's streaming mode (SSE) + if kong_ctx_shared.ai_proxy_streaming_mode then + -- we are going to send plaintext event-stream frames for ALL models + kong.response.set_header("Content-Type", "text/event-stream") + return + end + + local response_body = kong.service.response.get_raw_body() + if not response_body then + return + end + + local ai_driver = require("kong.llm.drivers." .. conf.model.provider) + local route_type = conf.route_type + + -- if this is a 'streaming' request, we can't know the final + -- result of the response body, so we just proceed to body_filter + -- to translate each SSE event frame + if not kong_ctx_shared.ai_proxy_streaming_mode then + local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" + if is_gzip then + response_body = kong_utils.inflate_gzip(response_body) + end + + if route_type == "preserve" then + kong_ctx_plugin.parsed_response = response_body + else + local new_response_string, err = ai_driver.from_format(response_body, conf.model, route_type) + if err then + kong_ctx_plugin.ai_parser_error = true + + ngx.status = 500 + kong_ctx_plugin.parsed_response = cjson.encode({ error = { message = err } }) + + elseif new_response_string then + -- preserve the same response content type; assume the from_format function + -- has returned the body in the appropriate response output format + kong_ctx_plugin.parsed_response = new_response_string + end + end + end + + ai_driver.post_request(conf) +end + + +function _M:body_filter(conf) + local kong_ctx_plugin = kong.ctx.plugin + local kong_ctx_shared = kong.ctx.shared + + -- if body_filter is called twice, then return + if kong_ctx_plugin.body_called and not kong_ctx_shared.ai_proxy_streaming_mode then + return + end + + local route_type = conf.route_type + + if kong_ctx_shared.skip_response_transformer and (route_type ~= "preserve") then + local response_body + + if kong_ctx_shared.parsed_response then + response_body = kong_ctx_shared.parsed_response + + elseif kong.response.get_status() == 200 then + response_body = kong.service.response.get_raw_body() + if not response_body then + kong.log.warn("issue when retrieve the response body for analytics in the body filter phase.", + " Please check AI request transformer plugin response.") + else + local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" + if is_gzip then + response_body = kong_utils.inflate_gzip(response_body) + end + end + end + + local ai_driver = require("kong.llm.drivers." .. conf.model.provider) + local new_response_string, err = ai_driver.from_format(response_body, conf.model, route_type) + + if err then + kong.log.warn("issue when transforming the response body for analytics in the body filter phase, ", err) + + elseif new_response_string then + ai_shared.post_request(conf, new_response_string) + end + end + + if not kong_ctx_shared.skip_response_transformer then + if (kong.response.get_status() ~= 200) and (not kong_ctx_plugin.ai_parser_error) then + return + end + + if route_type ~= "preserve" then + if kong_ctx_shared.ai_proxy_streaming_mode then + handle_streaming_frame(conf) + else + -- all errors MUST be checked and returned in header_filter + -- we should receive a replacement response body from the same thread + local original_request = kong_ctx_plugin.parsed_response + local deflated_request = original_request + + if deflated_request then + local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" + if is_gzip then + deflated_request = kong_utils.deflate_gzip(deflated_request) + end + + kong.response.set_raw_body(deflated_request) + end + + -- call with replacement body, or original body if nothing changed + local _, err = ai_shared.post_request(conf, original_request) + if err then + kong.log.warn("analytics phase failed for request, ", err) + end + end + end + end + + kong_ctx_plugin.body_called = true +end + + +function _M:access(conf) + local kong_ctx_plugin = kong.ctx.plugin + local kong_ctx_shared = kong.ctx.shared + + -- store the route_type in ctx for use in response parsing + local route_type = conf.route_type + + kong_ctx_plugin.operation = route_type + + local request_table + local multipart = false + + -- TODO: the access phase may be called mulitple times also in the balancer phase + -- Refactor this function a bit so that we don't mess them in the same function + local balancer_phase = ngx.get_phase() == "balancer" + + -- we may have received a replacement / decorated request body from another AI plugin + if kong_ctx_shared.replacement_request then + kong.log.debug("replacement request body received from another AI plugin") + request_table = kong_ctx_shared.replacement_request + + else + -- first, calculate the coordinates of the request + local content_type = kong.request.get_header("Content-Type") or "application/json" + + request_table = kong_ctx_shared.ai_request_body + if not request_table then + if balancer_phase then + error("Too late to read body", 2) + end + + request_table = kong.request.get_body(content_type, nil, conf.max_request_body_size) + kong_ctx_shared.ai_request_body = request_table + end + + if not request_table then + if not string.find(content_type, "multipart/form-data", nil, true) then + return bail(400, "content-type header does not match request body, or bad JSON formatting") + end + + multipart = true -- this may be a large file upload, so we have to proxy it directly + end + end + + -- resolve the real plugin config values + local conf_m, err = ai_shared.resolve_plugin_conf(kong.request, conf) + if err then + return bail(400, err) + end + + -- copy from the user request if present + if (not multipart) and (not conf_m.model.name) and (request_table.model) then + if type(request_table.model) == "string" then + conf_m.model.name = request_table.model + end + elseif multipart then + conf_m.model.name = "NOT_SPECIFIED" + end + + -- check that the user isn't trying to override the plugin conf model in the request body + if request_table and request_table.model and type(request_table.model) == "string" and request_table.model ~= "" then + if request_table.model ~= conf_m.model.name then + return bail(400, "cannot use own model - must be: " .. conf_m.model.name) + end + end + + -- model is stashed in the copied plugin conf, for consistency in transformation functions + if not conf_m.model.name then + return bail(400, "model parameter not found in request, nor in gateway configuration") + end + + kong_ctx_plugin.llm_model_requested = conf_m.model.name + + -- check the incoming format is the same as the configured LLM format + if not multipart then + local compatible, err = llm.is_compatible(request_table, route_type) + if not compatible then + kong_ctx_shared.skip_response_transformer = true + return bail(400, err) + end + end + + -- check if the user has asked for a stream, and/or if + -- we are forcing all requests to be of streaming type + if request_table and request_table.stream or + (conf_m.response_streaming and conf_m.response_streaming == "always") then + request_table.stream = true + + -- this condition will only check if user has tried + -- to activate streaming mode within their request + if conf_m.response_streaming and conf_m.response_streaming == "deny" then + return bail(400, "response streaming is not enabled for this LLM") + end + + -- store token cost estimate, on first pass, if the + -- provider doesn't reply with a prompt token count + if (not kong.ctx.plugin.ai_stream_prompt_tokens) and (not ai_shared.streaming_has_token_counts[conf_m.model.provider]) then + local prompt_tokens, err = ai_shared.calculate_cost(request_table or {}, {}, 1.8) + if err then + kong.log.err("unable to estimate request token cost: ", err) + return bail(500) + end + + kong_ctx_plugin.ai_stream_prompt_tokens = prompt_tokens + end + + -- specific actions need to skip later for this to work + kong_ctx_shared.ai_proxy_streaming_mode = true + + else + kong.service.request.enable_buffering() + end + + local ai_driver = require("kong.llm.drivers." .. conf.model.provider) + + -- execute pre-request hooks for this driver + local ok, err = ai_driver.pre_request(conf_m, request_table) + if not ok then + return bail(400, err) + end + + -- transform the body to Kong-format for this provider/model + local parsed_request_body, content_type, err + if route_type ~= "preserve" and (not multipart) then + -- transform the body to Kong-format for this provider/model + parsed_request_body, content_type, err = ai_driver.to_format(request_table, conf_m.model, route_type) + if err then + kong_ctx_shared.skip_response_transformer = true + return bail(400, err) + end + end + + -- execute pre-request hooks for "all" drivers before set new body + local ok, err = ai_shared.pre_request(conf_m, parsed_request_body) + if not ok then + return bail(400, err) + end + + if route_type ~= "preserve" and not balancer_phase then + kong.service.request.set_body(parsed_request_body, content_type) + end + + -- get the provider's cached identity interface - nil may come back, which is fine + local identity_interface = _KEYBASTION[conf] + if identity_interface and identity_interface.error then + kong.ctx.shared.skip_response_transformer = true + kong.log.err("error authenticating with cloud-provider, ", identity_interface.error) + return bail(500, "LLM request failed before proxying") + end + + -- now re-configure the request for this operation type + local ok, err = ai_driver.configure_request(conf_m, + identity_interface and identity_interface.interface) + if not ok then + kong_ctx_shared.skip_response_transformer = true + kong.log.err("failed to configure request for AI service: ", err) + return bail(500) + end + + -- lights out, and away we go + +end + +return _M diff --git a/kong/llm/schemas/init.lua b/kong/llm/schemas/init.lua index 15ce1a2a1ef..c975c49c26f 100644 --- a/kong/llm/schemas/init.lua +++ b/kong/llm/schemas/init.lua @@ -2,6 +2,41 @@ local typedefs = require("kong.db.schema.typedefs") local fmt = string.format +local bedrock_options_schema = { + type = "record", + required = false, + fields = { + { aws_region = { + description = "If using AWS providers (Bedrock) you can override the `AWS_REGION` " .. + "environment variable by setting this option.", + type = "string", + required = false }}, + }, +} + + +local gemini_options_schema = { + type = "record", + required = false, + fields = { + { api_endpoint = { + type = "string", + description = "If running Gemini on Vertex, specify the regional API endpoint (hostname only).", + required = false }}, + { project_id = { + type = "string", + description = "If running Gemini on Vertex, specify the project ID.", + required = false }}, + { location_id = { + type = "string", + description = "If running Gemini on Vertex, specify the location ID.", + required = false }}, + }, + entity_checks = { + { mutually_required = { "api_endpoint", "project_id", "location_id" }, }, + }, +} + local auth_schema = { type = "record", @@ -34,11 +69,38 @@ local auth_schema = { description = "Specify whether the 'param_name' and 'param_value' options go in a query string, or the POST form/JSON body.", required = false, one_of = { "query", "body" } }}, + { gcp_use_service_account = { + type = "boolean", + description = "Use service account auth for GCP-based providers and models.", + required = false, + default = false }}, + { gcp_service_account_json = { + type = "string", + description = "Set this field to the full JSON of the GCP service account to authenticate, if required. " .. + "If null (and gcp_use_service_account is true), Kong will attempt to read from " .. + "environment variable `GCP_SERVICE_ACCOUNT`.", + required = false, + referenceable = true }}, + { aws_access_key_id = { + type = "string", + description = "Set this if you are using an AWS provider (Bedrock) and you are authenticating " .. + "using static IAM User credentials. Setting this will override the AWS_ACCESS_KEY_ID " .. + "environment variable for this plugin instance.", + required = false, + encrypted = true, + referenceable = true }}, + { aws_secret_access_key = { + type = "string", + description = "Set this if you are using an AWS provider (Bedrock) and you are authenticating " .. + "using static IAM User credentials. Setting this will override the AWS_SECRET_ACCESS_KEY " .. + "environment variable for this plugin instance.", + required = false, + encrypted = true, + referenceable = true }}, } } - local model_options_schema = { description = "Key/value settings for the model", type = "record", @@ -110,6 +172,8 @@ local model_options_schema = { .. "used when e.g. using the 'preserve' route_type.", type = "string", required = false }}, + { gemini = gemini_options_schema }, + { bedrock = bedrock_options_schema }, } } @@ -123,7 +187,7 @@ local model_schema = { type = "string", description = "AI provider request format - Kong translates " .. "requests to and from the specified backend compatible formats.", required = true, - one_of = { "openai", "azure", "anthropic", "cohere", "mistral", "llama2" }}}, + one_of = { "openai", "azure", "anthropic", "cohere", "mistral", "llama2", "gemini", "bedrock" }}}, { name = { type = "string", description = "Model name to execute.", diff --git a/kong/observability/logs.lua b/kong/observability/logs.lua new file mode 100644 index 00000000000..0b7de49fb71 --- /dev/null +++ b/kong/observability/logs.lua @@ -0,0 +1,212 @@ +local _M = { + maybe_push = function() end, + get_request_logs = function() return {} end, + get_worker_logs = function() return {} end, +} + +if ngx.config.subsystem ~= "http" then + return _M +end + + +local request_id_get = require "kong.observability.tracing.request_id".get +local time_ns = require "kong.tools.time".time_ns +local table_merge = require "kong.tools.table".table_merge +local deep_copy = require "kong.tools.utils".deep_copy + +local get_log_level = require "resty.kong.log".get_log_level +local constants_log_levels = require "kong.constants".LOG_LEVELS + +local table_new = require "table.new" +local string_buffer = require "string.buffer" + +local ngx = ngx +local kong = kong +local table = table +local tostring = tostring +local native_ngx_log = _G.native_ngx_log or ngx.log + +local ngx_null = ngx.null +local table_pack = table.pack -- luacheck: ignore + +local MAX_WORKER_LOGS = 1000 +local MAX_REQUEST_LOGS = 1000 +local INITIAL_SIZE_WORKER_LOGS = 100 +local NGX_CTX_REQUEST_LOGS_KEY = "o11y_logs_request_scoped" + +local worker_logs = table_new(INITIAL_SIZE_WORKER_LOGS, 0) +local logline_buf = string_buffer.new() + +local notified_buffer_full = false + + +-- WARNING: avoid using `ngx.log` in this function to prevent recursive loops +local function configured_log_level() + local ok, level = pcall(get_log_level) + if not ok then + -- This is unexpected outside of the context of unit tests + local level_str = kong.configuration.log_level + native_ngx_log(ngx.WARN, + "[observability] OpenTelemetry logs failed reading dynamic log level. " .. + "Using log level: " .. level_str .. " from configuration." + ) + level = constants_log_levels[level_str] + end + + return level +end + + +-- needed because table.concat doesn't like booleans +local function concat_tostring(tab) + local tab_len = #tab + if tab_len == 0 then + return "" + end + + for i = 1, tab_len do + local value = tab[i] + + if value == ngx_null then + value = "nil" + else + value = tostring(value) + end + + logline_buf:put(value) + end + + return logline_buf:get() +end + + +local function generate_log_entry(request_scoped, inj_attributes, log_level, log_str, request_id, debug_info) + + local span_id + + if request_scoped then + -- add tracing information if tracing is enabled + local active_span = kong and kong.tracing and kong.tracing.active_span() + if active_span then + span_id = active_span.span_id + end + end + + local attributes = { + ["request.id"] = request_id, + ["introspection.current.line"] = debug_info.currentline, + ["introspection.name"] = debug_info.name, + ["introspection.namewhat"] = debug_info.namewhat, + ["introspection.source"] = debug_info.source, + ["introspection.what"] = debug_info.what, + } + if inj_attributes then + attributes = table_merge(attributes, inj_attributes) + end + + local now_ns = time_ns() + return { + time_unix_nano = now_ns, + observed_time_unix_nano = now_ns, + log_level = log_level, + body = log_str, + attributes = attributes, + span_id = span_id, + } +end + + +local function get_request_log_buffer() + local log_buffer = ngx.ctx[NGX_CTX_REQUEST_LOGS_KEY] + if not log_buffer then + log_buffer = table_new(10, 0) + ngx.ctx[NGX_CTX_REQUEST_LOGS_KEY] = log_buffer + end + return log_buffer +end + + +-- notifies the user that the log buffer is full, once (per worker) +local function notify_buffer_full_once() + if not notified_buffer_full then + notified_buffer_full = true + native_ngx_log(ngx.NOTICE, + "[observability] OpenTelemetry logs buffer is full: dropping new log entries." + ) + end +end + + +local function notify_if_resumed() + -- if we are in a "resumed" state + if notified_buffer_full then + notified_buffer_full = false + native_ngx_log(ngx.NOTICE, + "[observability] OpenTelemetry logs buffer resumed accepting log entries." + ) + end +end + + +function _M.maybe_push(stack_level, attributes, log_level, ...) + -- WARNING: do not yield in this function, as it is called from ngx.log + + -- Early return cases: + + -- log level too low + if log_level and configured_log_level() < log_level then + return + end + + local log_buffer, max_logs + local request_id = request_id_get() + local request_scoped = request_id ~= nil + + -- get the appropriate log buffer depending on the current context + if request_scoped then + log_buffer = get_request_log_buffer() + max_logs = MAX_REQUEST_LOGS + + else + log_buffer = worker_logs + max_logs = MAX_WORKER_LOGS + end + + -- return if log buffer is full + if #log_buffer >= max_logs then + notify_buffer_full_once() + return + end + notify_if_resumed() + + local args = table_pack(...) + local log_str = concat_tostring(args) + + -- generate & push log entry + local debug_info = debug.getinfo(stack_level, "nSl") + local log_entry = generate_log_entry( + request_scoped, + attributes, + log_level, + log_str, + request_id, + debug_info + ) + table.insert(log_buffer, log_entry) +end + + +function _M.get_worker_logs() + local wl = worker_logs + worker_logs = table_new(INITIAL_SIZE_WORKER_LOGS, 0) + return wl +end + + +function _M.get_request_logs() + local request_logs = get_request_log_buffer() + return deep_copy(request_logs) +end + + +return _M diff --git a/kong/tracing/instrumentation.lua b/kong/observability/tracing/instrumentation.lua similarity index 96% rename from kong/tracing/instrumentation.lua rename to kong/observability/tracing/instrumentation.lua index 8c8bc013c34..d4f93035b63 100644 --- a/kong/tracing/instrumentation.lua +++ b/kong/observability/tracing/instrumentation.lua @@ -6,7 +6,7 @@ local tablex = require "pl.tablex" local base = require "resty.core.base" local cjson = require "cjson" local ngx_re = require "ngx.re" -local tracing_context = require "kong.tracing.tracing_context" +local tracing_context = require "kong.observability.tracing.tracing_context" local ngx = ngx local var = ngx.var @@ -27,7 +27,7 @@ local setmetatable = setmetatable local cjson_encode = cjson.encode local _log_prefix = "[tracing] " local split = ngx_re.split -local request_id_get = require "kong.tracing.request_id".get +local request_id_get = require "kong.observability.tracing.request_id".get local _M = {} local tracer = pdk_tracer @@ -44,6 +44,7 @@ function _M.db_query(connector) local span = tracer.start_span("kong.database.query") span:set_attribute("db.system", kong.db and kong.db.strategy) span:set_attribute("db.statement", sql) + tracer.set_active_span(span) -- raw query local ret = pack(f(self, sql, ...)) -- ends span @@ -58,7 +59,9 @@ end -- Record Router span function _M.router() - return tracer.start_span("kong.router") + local span = tracer.start_span("kong.router") + tracer.set_active_span(span) + return span end @@ -127,6 +130,7 @@ function _M.balancer(ctx) else -- last try: load the last span (already created/propagated) span = last_try_balancer_span + tracer.set_active_span(span) tracer:link_span(span, span_name, span_options) if try.state then @@ -156,7 +160,9 @@ local function plugin_callback(phase) name_memo[plugin_name] = name end - return tracer.start_span(name) + local span = tracer.start_span(name) + tracer.set_active_span(span) + return span end end @@ -283,6 +289,7 @@ do span = tracer.start_span("kong.dns", { span_kind = 3, -- client }) + tracer.set_active_span(span) end local ip_addr, res_port, try_list = raw_func(host, port, ...) diff --git a/kong/tracing/propagation/extractors/_base.lua b/kong/observability/tracing/propagation/extractors/_base.lua similarity index 96% rename from kong/tracing/propagation/extractors/_base.lua rename to kong/observability/tracing/propagation/extractors/_base.lua index 6aa6ff496bf..b749cb0facb 100644 --- a/kong/tracing/propagation/extractors/_base.lua +++ b/kong/observability/tracing/propagation/extractors/_base.lua @@ -1,4 +1,4 @@ -local propagation_utils = require "kong.tracing.propagation.utils" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local ipairs = ipairs local type = type @@ -165,6 +165,7 @@ end -- should_sample = {boolean | nil}, -- baggage = {table | nil}, -- flags = {string | nil}, +-- w3c_flags = {string | nil}, -- single_header = {boolean | nil}, -- } -- @@ -186,6 +187,7 @@ end -- 6. baggage: A table with the baggage items extracted from the incoming -- tracing headers. -- 7. flags: Flags extracted from the incoming tracing headers (B3) +-- 7. w3c_flags: Flags extracted from the incoming tracing headers (W3C) -- 8. single_header: For extractors that support multiple formats, whether the -- context was extracted from the single or the multi-header format. function _EXTRACTOR:get_context(headers) diff --git a/kong/tracing/propagation/extractors/aws.lua b/kong/observability/tracing/propagation/extractors/aws.lua similarity index 94% rename from kong/tracing/propagation/extractors/aws.lua rename to kong/observability/tracing/propagation/extractors/aws.lua index fc026317996..3568af8e36f 100644 --- a/kong/tracing/propagation/extractors/aws.lua +++ b/kong/observability/tracing/propagation/extractors/aws.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local split = require "kong.tools.string".split local strip = require "kong.tools.string".strip diff --git a/kong/tracing/propagation/extractors/b3.lua b/kong/observability/tracing/propagation/extractors/b3.lua similarity index 97% rename from kong/tracing/propagation/extractors/b3.lua rename to kong/observability/tracing/propagation/extractors/b3.lua index efeb0154a5b..a764839f325 100644 --- a/kong/tracing/propagation/extractors/b3.lua +++ b/kong/observability/tracing/propagation/extractors/b3.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local from_hex = propagation_utils.from_hex local match = string.match diff --git a/kong/tracing/propagation/extractors/datadog.lua b/kong/observability/tracing/propagation/extractors/datadog.lua similarity index 94% rename from kong/tracing/propagation/extractors/datadog.lua rename to kong/observability/tracing/propagation/extractors/datadog.lua index fec30e61e8d..73b54cf0191 100644 --- a/kong/tracing/propagation/extractors/datadog.lua +++ b/kong/observability/tracing/propagation/extractors/datadog.lua @@ -1,4 +1,4 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" local bn = require "resty.openssl.bn" local from_dec = bn.from_dec diff --git a/kong/tracing/propagation/extractors/gcp.lua b/kong/observability/tracing/propagation/extractors/gcp.lua similarity index 86% rename from kong/tracing/propagation/extractors/gcp.lua rename to kong/observability/tracing/propagation/extractors/gcp.lua index 98c381b8c82..a6eb0030d1d 100644 --- a/kong/tracing/propagation/extractors/gcp.lua +++ b/kong/observability/tracing/propagation/extractors/gcp.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local bn = require "resty.openssl.bn" local type = type diff --git a/kong/tracing/propagation/extractors/jaeger.lua b/kong/observability/tracing/propagation/extractors/jaeger.lua similarity index 92% rename from kong/tracing/propagation/extractors/jaeger.lua rename to kong/observability/tracing/propagation/extractors/jaeger.lua index 8de8df02443..f226fcc61db 100644 --- a/kong/tracing/propagation/extractors/jaeger.lua +++ b/kong/observability/tracing/propagation/extractors/jaeger.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local from_hex = propagation_utils.from_hex local parse_baggage_headers = propagation_utils.parse_baggage_headers diff --git a/kong/tracing/propagation/extractors/ot.lua b/kong/observability/tracing/propagation/extractors/ot.lua similarity index 90% rename from kong/tracing/propagation/extractors/ot.lua rename to kong/observability/tracing/propagation/extractors/ot.lua index e5249693d9c..da87af59aed 100644 --- a/kong/tracing/propagation/extractors/ot.lua +++ b/kong/observability/tracing/propagation/extractors/ot.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local from_hex = propagation_utils.from_hex local parse_baggage_headers = propagation_utils.parse_baggage_headers diff --git a/kong/tracing/propagation/extractors/w3c.lua b/kong/observability/tracing/propagation/extractors/w3c.lua similarity index 86% rename from kong/tracing/propagation/extractors/w3c.lua rename to kong/observability/tracing/propagation/extractors/w3c.lua index 490d1dfd00c..75687a2ba5f 100644 --- a/kong/tracing/propagation/extractors/w3c.lua +++ b/kong/observability/tracing/propagation/extractors/w3c.lua @@ -1,5 +1,5 @@ -local _EXTRACTOR = require "kong.tracing.propagation.extractors._base" -local propagation_utils = require "kong.tracing.propagation.utils" +local _EXTRACTOR = require "kong.observability.tracing.propagation.extractors._base" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local type = type local tonumber = tonumber @@ -53,8 +53,9 @@ function W3C_EXTRACTOR:get_context(headers) return end + local flags_number = tonumber(flags, 16) -- W3C sampled flag: https://www.w3.org/TR/trace-context/#sampled-flag - local should_sample = tonumber(flags, 16) % 2 == 1 + local should_sample = flags_number % 2 == 1 trace_id = from_hex(trace_id) parent_id = from_hex(parent_id) @@ -68,7 +69,7 @@ function W3C_EXTRACTOR:get_context(headers) parent_id = nil, should_sample = should_sample, baggage = nil, - flags = nil, + w3c_flags = flags_number, } end diff --git a/kong/tracing/propagation/init.lua b/kong/observability/tracing/propagation/init.lua similarity index 95% rename from kong/tracing/propagation/init.lua rename to kong/observability/tracing/propagation/init.lua index 5e724113a28..e9568573f5a 100644 --- a/kong/tracing/propagation/init.lua +++ b/kong/observability/tracing/propagation/init.lua @@ -1,7 +1,7 @@ -local tracing_context = require "kong.tracing.tracing_context" +local tracing_context = require "kong.observability.tracing.tracing_context" local table_new = require "table.new" -local formats = require "kong.tracing.propagation.utils".FORMATS +local formats = require "kong.observability.tracing.propagation.utils".FORMATS local clear_header = kong.service.request.clear_header local ngx_req_get_headers = ngx.req.get_headers @@ -12,8 +12,8 @@ local pairs = pairs local ipairs = ipairs local setmetatable = setmetatable -local EXTRACTORS_PATH = "kong.tracing.propagation.extractors." -local INJECTORS_PATH = "kong.tracing.propagation.injectors." +local EXTRACTORS_PATH = "kong.observability.tracing.propagation.extractors." +local INJECTORS_PATH = "kong.observability.tracing.propagation.injectors." -- This function retrieves the propagation parameters from a plugin diff --git a/kong/tracing/propagation/injectors/_base.lua b/kong/observability/tracing/propagation/injectors/_base.lua similarity index 98% rename from kong/tracing/propagation/injectors/_base.lua rename to kong/observability/tracing/propagation/injectors/_base.lua index f20e9b89c2b..fc40506b155 100644 --- a/kong/tracing/propagation/injectors/_base.lua +++ b/kong/observability/tracing/propagation/injectors/_base.lua @@ -1,4 +1,4 @@ -local propagation_utils = require "kong.tracing.propagation.utils" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local to_id_size = propagation_utils.to_id_size local set_header = kong.service.request.set_header diff --git a/kong/tracing/propagation/injectors/aws.lua b/kong/observability/tracing/propagation/injectors/aws.lua similarity index 91% rename from kong/tracing/propagation/injectors/aws.lua rename to kong/observability/tracing/propagation/injectors/aws.lua index 92bb9978fc6..a96d6f14d4b 100644 --- a/kong/tracing/propagation/injectors/aws.lua +++ b/kong/observability/tracing/propagation/injectors/aws.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local sub = string.sub diff --git a/kong/tracing/propagation/injectors/b3-single.lua b/kong/observability/tracing/propagation/injectors/b3-single.lua similarity index 93% rename from kong/tracing/propagation/injectors/b3-single.lua rename to kong/observability/tracing/propagation/injectors/b3-single.lua index 7731f34e34f..6ebf7263b84 100644 --- a/kong/tracing/propagation/injectors/b3-single.lua +++ b/kong/observability/tracing/propagation/injectors/b3-single.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local B3_SINGLE_INJECTOR = _INJECTOR:new({ diff --git a/kong/tracing/propagation/injectors/b3.lua b/kong/observability/tracing/propagation/injectors/b3.lua similarity index 92% rename from kong/tracing/propagation/injectors/b3.lua rename to kong/observability/tracing/propagation/injectors/b3.lua index d5816e87fb0..10dcac1daa8 100644 --- a/kong/tracing/propagation/injectors/b3.lua +++ b/kong/observability/tracing/propagation/injectors/b3.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local B3_INJECTOR = _INJECTOR:new({ diff --git a/kong/tracing/propagation/injectors/datadog.lua b/kong/observability/tracing/propagation/injectors/datadog.lua similarity index 93% rename from kong/tracing/propagation/injectors/datadog.lua rename to kong/observability/tracing/propagation/injectors/datadog.lua index a7270c9b995..0efb96fb28c 100644 --- a/kong/tracing/propagation/injectors/datadog.lua +++ b/kong/observability/tracing/propagation/injectors/datadog.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local bn = require "resty.openssl.bn" local from_binary = bn.from_binary diff --git a/kong/tracing/propagation/injectors/gcp.lua b/kong/observability/tracing/propagation/injectors/gcp.lua similarity index 88% rename from kong/tracing/propagation/injectors/gcp.lua rename to kong/observability/tracing/propagation/injectors/gcp.lua index 1ff747218d6..0b3e56e1525 100644 --- a/kong/tracing/propagation/injectors/gcp.lua +++ b/kong/observability/tracing/propagation/injectors/gcp.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local bn = require "resty.openssl.bn" local to_hex = require "resty.string".to_hex diff --git a/kong/tracing/propagation/injectors/jaeger.lua b/kong/observability/tracing/propagation/injectors/jaeger.lua similarity index 92% rename from kong/tracing/propagation/injectors/jaeger.lua rename to kong/observability/tracing/propagation/injectors/jaeger.lua index 2bf103b930b..72e48996ccb 100644 --- a/kong/tracing/propagation/injectors/jaeger.lua +++ b/kong/observability/tracing/propagation/injectors/jaeger.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local pairs = pairs diff --git a/kong/tracing/propagation/injectors/ot.lua b/kong/observability/tracing/propagation/injectors/ot.lua similarity index 91% rename from kong/tracing/propagation/injectors/ot.lua rename to kong/observability/tracing/propagation/injectors/ot.lua index f0bdc529e8c..2e0b53ae790 100644 --- a/kong/tracing/propagation/injectors/ot.lua +++ b/kong/observability/tracing/propagation/injectors/ot.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local pairs = pairs diff --git a/kong/tracing/propagation/injectors/w3c.lua b/kong/observability/tracing/propagation/injectors/w3c.lua similarity index 89% rename from kong/tracing/propagation/injectors/w3c.lua rename to kong/observability/tracing/propagation/injectors/w3c.lua index 139428143bb..b5a1ca511cf 100644 --- a/kong/tracing/propagation/injectors/w3c.lua +++ b/kong/observability/tracing/propagation/injectors/w3c.lua @@ -1,4 +1,4 @@ -local _INJECTOR = require "kong.tracing.propagation.injectors._base" +local _INJECTOR = require "kong.observability.tracing.propagation.injectors._base" local to_hex = require "resty.string".to_hex local string_format = string.format diff --git a/kong/tracing/propagation/schema.lua b/kong/observability/tracing/propagation/schema.lua similarity index 96% rename from kong/tracing/propagation/schema.lua rename to kong/observability/tracing/propagation/schema.lua index 3911b061bd9..6ae6fa1c60d 100644 --- a/kong/tracing/propagation/schema.lua +++ b/kong/observability/tracing/propagation/schema.lua @@ -1,5 +1,5 @@ local Schema = require "kong.db.schema" -local formats = require "kong.tracing.propagation.utils".FORMATS +local formats = require "kong.observability.tracing.propagation.utils".FORMATS local validate_header_name = require("kong.tools.http").validate_header_name diff --git a/kong/tracing/propagation/utils.lua b/kong/observability/tracing/propagation/utils.lua similarity index 100% rename from kong/tracing/propagation/utils.lua rename to kong/observability/tracing/propagation/utils.lua diff --git a/kong/tracing/request_id.lua b/kong/observability/tracing/request_id.lua similarity index 100% rename from kong/tracing/request_id.lua rename to kong/observability/tracing/request_id.lua diff --git a/kong/tracing/tracing_context.lua b/kong/observability/tracing/tracing_context.lua similarity index 89% rename from kong/tracing/tracing_context.lua rename to kong/observability/tracing/tracing_context.lua index ebf42ec4bce..fee464d0d36 100644 --- a/kong/tracing/tracing_context.lua +++ b/kong/observability/tracing/tracing_context.lua @@ -14,7 +14,8 @@ local function init_tracing_context(ctx) -- Unlinked spans are spans that were created (to generate their ID) -- but not added to `KONG_SPANS` (because their execution details were not -- yet available). - unlinked_spans = table_new(0, 1) + unlinked_spans = table_new(0, 1), + flags = nil, } return ctx.TRACING_CONTEXT @@ -89,6 +90,18 @@ local function set_raw_trace_id(trace_id, ctx) end +local function get_flags(ctx) + local tracing_context = get_tracing_context(ctx) + return tracing_context.flags +end + + +local function set_flags(flags, ctx) + local tracing_context = get_tracing_context(ctx) + tracing_context.flags = flags +end + + local function get_unlinked_span(name, ctx) local tracing_context = get_tracing_context(ctx) return tracing_context.unlinked_spans[name] @@ -108,4 +121,6 @@ return { set_raw_trace_id = set_raw_trace_id, get_unlinked_span = get_unlinked_span, set_unlinked_span = set_unlinked_span, + get_flags = get_flags, + set_flags = set_flags, } diff --git a/kong/pdk/init.lua b/kong/pdk/init.lua index 858795a368e..13974dd3bb1 100644 --- a/kong/pdk/init.lua +++ b/kong/pdk/init.lua @@ -208,6 +208,7 @@ local MAJOR_MODULES = { "vault", "tracing", "plugin", + "telemetry", } if ngx.config.subsystem == 'http' then diff --git a/kong/pdk/log.lua b/kong/pdk/log.lua index adcca23dfc9..8127a21872e 100644 --- a/kong/pdk/log.lua +++ b/kong/pdk/log.lua @@ -17,12 +17,14 @@ local inspect = require("inspect") local phase_checker = require("kong.pdk.private.phases") local constants = require("kong.constants") local clear_tab = require("table.clear") +local ngx_null = ngx.null -local request_id_get = require("kong.tracing.request_id").get +local request_id_get = require("kong.observability.tracing.request_id").get local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy local get_tls1_version_str = require("ngx.ssl").get_tls1_version_str local get_workspace_name = require("kong.workspaces").get_workspace_name +local dynamic_hook = require("kong.dynamic_hook") local sub = string.sub @@ -309,6 +311,13 @@ local function gen_log_func(lvl_const, imm_buf, to_string, stack_level, sep) return end + -- OpenTelemetry Logs + -- stack level otel logs = stack_level + 3: + -- 1: maybe_push + -- 2: dynamic_hook.pcall + -- 3: dynamic_hook.run_hook + dynamic_hook.run_hook("observability_logs", "push", stack_level + 3, nil, lvl_const, ...) + local n = select("#", ...) if imm_buf.debug_flags then @@ -632,7 +641,12 @@ do local function is_valid_value(v, visited) local t = type(v) - if v == nil or t == "number" or t == "string" or t == "boolean" then + + -- cdata is not supported by cjson.encode + if type(v) == 'cdata' then + return false + + elseif v == nil or v == ngx_null or t == "number" or t == "string" or t == "boolean" then return true end diff --git a/kong/pdk/private/phases.lua b/kong/pdk/private/phases.lua index e5787a0f2ef..d3a2bca5717 100644 --- a/kong/pdk/private/phases.lua +++ b/kong/pdk/private/phases.lua @@ -122,6 +122,7 @@ end local public_phases = setmetatable({ request = new_phase(PHASES.rewrite, PHASES.access, + PHASES.balancer, PHASES.response, PHASES.header_filter, PHASES.body_filter, diff --git a/kong/pdk/request.lua b/kong/pdk/request.lua index f13585b11e9..fbd55a74194 100644 --- a/kong/pdk/request.lua +++ b/kong/pdk/request.lua @@ -10,6 +10,7 @@ local cjson = require "kong.tools.cjson" local multipart = require "multipart" local phase_checker = require "kong.pdk.private.phases" local normalize = require("kong.tools.uri").normalize +local yield = require("kong.tools.yield").yield local ngx = ngx @@ -693,10 +694,16 @@ local function new(self) -- -- If the size of the body is greater than the Nginx buffer size (set by -- `client_body_buffer_size`), this function fails and returns an error - -- message explaining this limitation. + -- message explaining this limitation, unless `max_allowed_file_size` + -- is set and equal to 0 or larger than the body size buffered to disk. + -- Use of `max_allowed_file_size` requires Kong to read data from filesystem + -- and has performance implications. -- -- @function kong.request.get_raw_body -- @phases rewrite, access, response, admin_api + -- @max_allowed_file_size[opt] number the max allowed file size to be read from, + -- 0 means unlimited, but the size of this body will still be limited + -- by Nginx's client_max_body_size. -- @treturn string|nil The plain request body or nil if it does not fit into -- the NGINX temporary buffer. -- @treturn nil|string An error message. @@ -704,19 +711,54 @@ local function new(self) -- -- Given a body with payload "Hello, Earth!": -- -- kong.request.get_raw_body():gsub("Earth", "Mars") -- "Hello, Mars!" - function _REQUEST.get_raw_body() + function _REQUEST.get_raw_body(max_allowed_file_size) check_phase(before_content) read_body() local body = get_body_data() if not body then - if get_body_file() then + local body_file = get_body_file() + if not body_file then + return "" + end + + if not max_allowed_file_size or max_allowed_file_size < 0 then return nil, "request body did not fit into client body buffer, consider raising 'client_body_buffer_size'" + end - else - return "" + local file, err = io.open(body_file, "r") + if not file then + return nil, "failed to open cached request body '" .. body_file .. "': " .. err + end + + local size = file:seek("end") or 0 + if max_allowed_file_size > 0 and size > max_allowed_file_size then + return nil, ("request body file too big: %d > %d"):format(size, max_allowed_file_size) end + + -- go to beginning + file:seek("set") + local chunk = {} + local chunk_idx = 1 + + while true do + local data, err = file:read(1048576) -- read in chunks of 1mb + if not data then + if err then + return nil, "failed to read cached request body '" .. body_file .. "': " .. err + end + break + end + chunk[chunk_idx] = data + chunk_idx = chunk_idx + 1 + + yield() -- yield to prevent starvation while doing blocking IO-reads + end + + file:close() + + return table.concat(chunk, "") end return body @@ -767,6 +809,7 @@ local function new(self) -- @phases rewrite, access, response, admin_api -- @tparam[opt] string mimetype The MIME type. -- @tparam[opt] number max_args Sets a limit on the maximum number of parsed + -- @tparam[opt] number max_allowed_file_size the max allowed file size to be read from -- arguments. -- @treturn table|nil A table representation of the body. -- @treturn string|nil An error message. @@ -775,7 +818,7 @@ local function new(self) -- local body, err, mimetype = kong.request.get_body() -- body.name -- "John Doe" -- body.age -- "42" - function _REQUEST.get_body(mimetype, max_args) + function _REQUEST.get_body(mimetype, max_args, max_allowed_file_size) check_phase(before_content) local content_type = mimetype or _REQUEST.get_header(CONTENT_TYPE) @@ -825,7 +868,7 @@ local function new(self) return pargs, nil, CONTENT_TYPE_POST elseif find(content_type_lower, CONTENT_TYPE_JSON, 1, true) == 1 then - local body, err = _REQUEST.get_raw_body() + local body, err = _REQUEST.get_raw_body(max_allowed_file_size) if not body then return nil, err, CONTENT_TYPE_JSON end @@ -838,7 +881,7 @@ local function new(self) return json, nil, CONTENT_TYPE_JSON elseif find(content_type_lower, CONTENT_TYPE_FORM_DATA, 1, true) == 1 then - local body, err = _REQUEST.get_raw_body() + local body, err = _REQUEST.get_raw_body(max_allowed_file_size) if not body then return nil, err, CONTENT_TYPE_FORM_DATA end diff --git a/kong/pdk/response.lua b/kong/pdk/response.lua index 44035bf54af..844d5c7d139 100644 --- a/kong/pdk/response.lua +++ b/kong/pdk/response.lua @@ -16,7 +16,7 @@ local buffer = require "string.buffer" local cjson = require "cjson.safe" local checks = require "kong.pdk.private.checks" local phase_checker = require "kong.pdk.private.phases" -local request_id = require "kong.tracing.request_id" +local request_id = require "kong.observability.tracing.request_id" local constants = require "kong.constants" local tools_http = require "kong.tools.http" @@ -56,6 +56,7 @@ local header_body_log = phase_checker.new(PHASES.response, local rewrite_access_header = phase_checker.new(PHASES.rewrite, PHASES.access, PHASES.response, + PHASES.balancer, PHASES.header_filter, PHASES.error, PHASES.admin_api) diff --git a/kong/pdk/service.lua b/kong/pdk/service.lua index 5b84e4293ab..182c771753d 100644 --- a/kong/pdk/service.lua +++ b/kong/pdk/service.lua @@ -74,7 +74,8 @@ local function new() -- Using this method is equivalent to ask Kong to not run the load-balancing -- phase for this request, and consider it manually overridden. -- Load-balancing components such as retries and health-checks will also be - -- ignored for this request. + -- ignored for this request. Use `kong.service.set_retries` to overwrite + -- retries count. -- -- The `host` argument expects the hostname or IP address of the upstream -- server, and the `port` expects a port number. @@ -87,7 +88,7 @@ local function new() -- kong.service.set_target("service.local", 443) -- kong.service.set_target("192.168.130.1", 80) function service.set_target(host, port) - check_phase(PHASES.access) + check_phase(access_and_rewrite_and_balancer_preread) if type(host) ~= "string" then error("host must be a string", 2) @@ -107,6 +108,96 @@ local function new() end + -- Sets the retry callback function when the target set by `service.set_target` + -- failed to connect. The callback function will be called with no argument and + -- must return `host`, `port` and `err` if any. + -- + -- + -- @function kong.service.set_target_retry_callback + -- @phases access + -- @tparam function retry_callback + -- @usage + -- kong.service.set_target_retry_callback(function() return "service.local", 443 end) + function service.set_target_retry_callback(retry_callback) + check_phase(PHASES.access) + + if type(retry_callback) ~= "function" then + error("retry_callback must be a function", 2) + end + + ngx.ctx.balancer_data.retry_callback = retry_callback + end + + + --- + -- Sets the retries count for the current request. This will override the + -- default retries count set in the Upstream entity. + -- + -- The `retries` argument expects an integer between 0 and 32767. + -- + -- @function kong.service.set_retries + -- @phases access + -- @tparam number retries + -- @usage + -- kong.service.set_retries(233) + function service.set_retries(retries) + check_phase(PHASES.access) + + if type(retries) ~= "number" or floor(retries) ~= retries then + error("retries must be an integer", 2) + end + if retries < 0 or retries > 32767 then + error("port must be an integer between 0 and 32767: given " .. retries, 2) + end + + local ctx = ngx.ctx + ctx.balancer_data.retries = retries + end + + --- + -- Sets the timeouts for the current request. This will override the + -- default timeouts set in the Upstream entity. + -- + -- The `connect_timeout`, `write_timeout`, and `read_timeout` arguments expect + -- an integer between 1 and 2147483646. + -- + -- @function kong.service.set_timeouts + -- @phases access + -- @tparam number connect_timeout + -- @tparam number write_timeout + -- @tparam number read_timeout + -- @usage + -- kong.service.set_timeouts(233, 233, 233) + function service.set_timeouts(connect_timeout, write_timeout, read_timeout) + check_phase(PHASES.access) + + if type(connect_timeout) ~= "number" or floor(connect_timeout) ~= connect_timeout then + error("connect_timeout must be an integer", 2) + end + if connect_timeout < 1 or connect_timeout > 2147483646 then + error("connect_timeout must be an integer between 1 and 2147483646: given " .. connect_timeout, 2) + end + + if type(write_timeout) ~= "number" or floor(write_timeout) ~= write_timeout then + error("write_timeout must be an integer", 2) + end + if write_timeout < 1 or write_timeout > 2147483646 then + error("write_timeout must be an integer between 1 and 2147483646: given " .. write_timeout, 2) + end + + if type(read_timeout) ~= "number" or floor(read_timeout) ~= read_timeout then + error("read_timeout must be an integer", 2) + end + if read_timeout < 1 or read_timeout > 2147483646 then + error("read_timeout must be an integer between 1 and 2147483646: given " .. read_timeout, 2) + end + + local ctx = ngx.ctx + ctx.balancer_data.connect_timeout = connect_timeout + ctx.balancer_data.write_timeout = write_timeout + ctx.balancer_data.read_timeout = read_timeout + end + local tls = require("resty.kong.tls") local set_upstream_cert_and_key = tls.set_upstream_cert_and_key diff --git a/kong/pdk/service/request.lua b/kong/pdk/service/request.lua index 495dbf0febc..28fab489e6a 100644 --- a/kong/pdk/service/request.lua +++ b/kong/pdk/service/request.lua @@ -83,12 +83,12 @@ local function new(self) -- Enables buffered proxying, which allows plugins to access Service body and -- response headers at the same time. -- @function kong.service.request.enable_buffering - -- @phases `rewrite`, `access` + -- @phases `rewrite`, `access`, `balancer` -- @return Nothing. -- @usage -- kong.service.request.enable_buffering() request.enable_buffering = function() - check_phase(access_and_rewrite) + check_phase(access_rewrite_balancer) if ngx.req.http_version() >= 2 then error("buffered proxying cannot currently be enabled with http/" .. @@ -102,13 +102,13 @@ local function new(self) --- -- Sets the protocol to use when proxying the request to the Service. -- @function kong.service.request.set_scheme - -- @phases `access` + -- @phases `access`, `rewrite`, `balancer` -- @tparam string scheme The scheme to be used. Supported values are `"http"` or `"https"`. -- @return Nothing; throws an error on invalid inputs. -- @usage -- kong.service.request.set_scheme("https") request.set_scheme = function(scheme) - check_phase(PHASES.access) + check_phase(access_rewrite_balancer) if type(scheme) ~= "string" then error("scheme must be a string", 2) @@ -131,14 +131,14 @@ local function new(self) -- -- Input should **not** include the query string. -- @function kong.service.request.set_path - -- @phases `access` + -- @phases `access`, `rewrite`, `balancer` -- @tparam string path The path string. Special characters and UTF-8 -- characters are allowed, for example: `"/v2/movies"` or `"/foo/😀"`. -- @return Nothing; throws an error on invalid inputs. -- @usage -- kong.service.request.set_path("/v2/movies") request.set_path = function(path) - check_phase(PHASES.access) + check_phase(access_rewrite_balancer) if type(path) ~= "string" then error("path must be a string", 2) @@ -440,13 +440,13 @@ local function new(self) -- For a higher-level function to set the body based on the request content type, -- see `kong.service.request.set_body()`. -- @function kong.service.request.set_raw_body - -- @phases `rewrite`, `access` + -- @phases `rewrite`, `access`, `balancer` -- @tparam string body The raw body. -- @return Nothing; throws an error on invalid inputs. -- @usage -- kong.service.request.set_raw_body("Hello, world!") request.set_raw_body = function(body) - check_phase(access_and_rewrite) + check_phase(access_rewrite_balancer) if type(body) ~= "string" then error("body must be a string", 2) @@ -459,7 +459,9 @@ local function new(self) -- Ensure client request body has been read. -- This function is a nop if body has already been read, -- and necessary to write the request to the service if it has not. - ngx.req.read_body() + if ngx.get_phase() ~= "balancer" then + ngx.req.read_body() + end ngx.req.set_body_data(body) end @@ -594,7 +596,7 @@ local function new(self) -- a string with `kong.service.request.set_raw_body()`. -- -- @function kong.service.request.set_body - -- @phases `rewrite`, `access` + -- @phases `rewrite`, `access`, `balancer` -- @tparam table args A table with data to be converted to the appropriate format -- and stored in the body. -- @tparam[opt] string mimetype can be one of: diff --git a/kong/pdk/telemetry.lua b/kong/pdk/telemetry.lua new file mode 100644 index 00000000000..e47d8ef5ea6 --- /dev/null +++ b/kong/pdk/telemetry.lua @@ -0,0 +1,91 @@ +--- +-- The telemetry module provides capabilities for telemetry operations. +-- +-- @module kong.telemetry.log + + +local dynamic_hook = require("kong.dynamic_hook") + +local dyn_hook_run_hook = dynamic_hook.run_hook +local dyn_hook_is_group_enabled = dynamic_hook.is_group_enabled + +local function new() + local telemetry = {} + + + --- + -- Records a structured log entry, to be reported via the OpenTelemetry plugin. + -- + -- This function has a dependency on the OpenTelemetry plugin, which must be + -- configured to report OpenTelemetry logs. + -- + -- @function kong.telemetry.log + -- @phases `rewrite`, `access`, `balancer`, `timer`, `header_filter`, + -- `response`, `body_filter`, `log` + -- @tparam string plugin_name the name of the plugin + -- @tparam table plugin_config the plugin configuration + -- @tparam string message_type the type of the log message, useful to categorize + -- the log entry + -- @tparam string message the log message + -- @tparam table attributes structured information to be included in the + -- `attributes` field of the log entry + -- @usage + -- local attributes = { + -- http_method = kong.request.get_method() + -- ["node.id"] = kong.node.get_id(), + -- hostname = kong.node.get_hostname(), + -- } + -- + -- local ok, err = kong.telemetry.log("my_plugin", conf, "result", "successful operation", attributes) + telemetry.log = function(plugin_name, plugin_config, message_type, message, attributes) + if type(plugin_name) ~= "string" then + return nil, "plugin_name must be a string" + end + + if type(plugin_config) ~= "table" then + return nil, "plugin_config must be a table" + end + + if type(message_type) ~= "string" then + return nil, "message_type must be a string" + end + + if message and type(message) ~= "string" then + return nil, "message must be a string" + end + + if attributes and type(attributes) ~= "table" then + return nil, "attributes must be a table" + end + + local hook_group = "observability_logs" + if not dyn_hook_is_group_enabled(hook_group) then + return nil, "Telemetry logging is disabled: log entry will not be recorded. " .. + "Ensure the OpenTelemetry plugin is correctly configured to " .. + "report logs in order to use this feature." + end + + attributes = attributes or {} + attributes["message.type"] = message_type + attributes["plugin.name"] = plugin_name + attributes["plugin.id"] = plugin_config.__plugin_id + attributes["plugin.instance.name"] = plugin_config.plugin_instance_name + + -- stack level = 5: + -- 1: maybe_push + -- 2: dynamic_hook.pcall + -- 3: dynamic_hook.run_hook + -- 4: kong.telemetry.log + -- 5: caller + dyn_hook_run_hook(hook_group, "push", 5, attributes, nil, message) + return true + end + + + return telemetry +end + + +return { + new = new, +} diff --git a/kong/pdk/tracing.lua b/kong/pdk/tracing.lua index a1ab6533e6e..5a94e980578 100644 --- a/kong/pdk/tracing.lua +++ b/kong/pdk/tracing.lua @@ -10,7 +10,7 @@ local ffi = require "ffi" local tablepool = require "tablepool" local new_tab = require "table.new" local phase_checker = require "kong.pdk.private.phases" -local tracing_context = require "kong.tracing.tracing_context" +local tracing_context = require "kong.observability.tracing.tracing_context" local ngx = ngx local type = type diff --git a/kong/plugins/acl/handler.lua b/kong/plugins/acl/handler.lua index 803fa472ad1..f188a17697e 100644 --- a/kong/plugins/acl/handler.lua +++ b/kong/plugins/acl/handler.lua @@ -80,7 +80,7 @@ function ACLHandler:access(conf) else local credential = kong.client.get_credential() local authenticated_groups - if not credential then + if (not credential) or conf.always_use_authenticated_groups then -- authenticated groups overrides anonymous groups authenticated_groups = groups.get_authenticated_groups() end diff --git a/kong/plugins/acl/schema.lua b/kong/plugins/acl/schema.lua index df0afc638ed..14a70e67ba5 100644 --- a/kong/plugins/acl/schema.lua +++ b/kong/plugins/acl/schema.lua @@ -16,6 +16,7 @@ return { type = "array", elements = { type = "string" }, }, }, { hide_groups_header = { type = "boolean", required = true, default = false, description = "If enabled (`true`), prevents the `X-Consumer-Groups` header from being sent in the request to the upstream service." }, }, + { always_use_authenticated_groups = { type = "boolean", required = true, default = false, description = "If enabled (`true`), the authenticated groups will always be used even when an authenticated consumer already exists. If the authenticated groups don't exist, it will fallback to use the groups associated with the consumer. By default the authenticated groups will only be used when there is no consumer or the consumer is anonymous." } }, }, } } diff --git a/kong/plugins/ai-prompt-decorator/handler.lua b/kong/plugins/ai-prompt-decorator/handler.lua index 7103ce5903b..23a18ea7399 100644 --- a/kong/plugins/ai-prompt-decorator/handler.lua +++ b/kong/plugins/ai-prompt-decorator/handler.lua @@ -55,7 +55,7 @@ function plugin:access(conf) kong.ctx.shared.ai_prompt_decorated = true -- future use -- if plugin ordering was altered, receive the "decorated" request - local request = kong.request.get_body("application/json") + local request = kong.request.get_body("application/json", nil, conf.max_request_body_size) if type(request) ~= "table" then return bad_request("this LLM route only supports application/json requests") end diff --git a/kong/plugins/ai-prompt-decorator/schema.lua b/kong/plugins/ai-prompt-decorator/schema.lua index ad0c5a85d72..2d8abfab59f 100644 --- a/kong/plugins/ai-prompt-decorator/schema.lua +++ b/kong/plugins/ai-prompt-decorator/schema.lua @@ -39,7 +39,9 @@ return { { config = { type = "record", fields = { - { prompts = prompts_record } + { prompts = prompts_record }, + { max_request_body_size = { type = "integer", default = 8 * 1024, gt = 0, + description = "max allowed body size allowed to be introspected" } }, } } } diff --git a/kong/plugins/ai-prompt-guard/handler.lua b/kong/plugins/ai-prompt-guard/handler.lua index 321fefad202..b2aab78dbc7 100644 --- a/kong/plugins/ai-prompt-guard/handler.lua +++ b/kong/plugins/ai-prompt-guard/handler.lua @@ -29,48 +29,46 @@ local execute do -- @treturn[2] nil -- @treturn[2] string The error message function execute(request, conf) - local user_prompt + local collected_prompts + local messages = request.messages - -- concat all 'user' prompts into one string, if conversation history must be checked - if type(request.messages) == "table" and not conf.allow_all_conversation_history then + -- concat all prompts into one string, if conversation history must be checked + if type(messages) == "table" then local buf = buffer.new() + -- Note allow_all_conversation_history means ignores history + local just_pick_latest = conf.allow_all_conversation_history - for _, v in ipairs(request.messages) do + -- iterate in reverse so we get the latest user prompt first + -- instead of the oldest one in history + for i=#messages, 1, -1 do + local v = messages[i] if type(v.role) ~= "string" then return nil, bad_format_error end - if v.role == "user" then + if v.role == "user" or conf.match_all_roles then if type(v.content) ~= "string" then return nil, bad_format_error end buf:put(v.content) - end - end - - user_prompt = buf:get() - elseif type(request.messages) == "table" then - -- just take the trailing 'user' prompt - for _, v in ipairs(request.messages) do - if type(v.role) ~= "string" then - return nil, bad_format_error - end - if v.role == "user" then - if type(v.content) ~= "string" then - return nil, bad_format_error + if just_pick_latest then + break end - user_prompt = v.content + + buf:put(" ") -- put a seperator to avoid adhension of words end end + collected_prompts = buf:get() + elseif type(request.prompt) == "string" then - user_prompt = request.prompt + collected_prompts = request.prompt else return nil, bad_format_error end - if not user_prompt then + if not collected_prompts then return nil, "no 'prompt' or 'messages' received" end @@ -78,7 +76,7 @@ local execute do -- check the prompt for explcit ban patterns for _, v in ipairs(conf.deny_patterns or EMPTY) do -- check each denylist; if prompt matches it, deny immediately - local m, _, err = ngx_re_find(user_prompt, v, "jo") + local m, _, err = ngx_re_find(collected_prompts, v, "jo") if err then -- regex failed, that's an error by the administrator kong.log.err("bad regex pattern '", v ,"', failed to execute: ", err) @@ -98,7 +96,7 @@ local execute do -- if any allow_patterns specified, make sure the prompt matches one of them for _, v in ipairs(conf.allow_patterns or EMPTY) do -- check each denylist; if prompt matches it, deny immediately - local m, _, err = ngx_re_find(user_prompt, v, "jo") + local m, _, err = ngx_re_find(collected_prompts, v, "jo") if err then -- regex failed, that's an error by the administrator @@ -121,7 +119,7 @@ function plugin:access(conf) kong.ctx.shared.ai_prompt_guarded = true -- future use -- if plugin ordering was altered, receive the "decorated" request - local request = kong.request.get_body("application/json") + local request = kong.request.get_body("application/json", nil, conf.max_request_body_size) if type(request) ~= "table" then return bad_request("this LLM route only supports application/json requests") end diff --git a/kong/plugins/ai-prompt-guard/schema.lua b/kong/plugins/ai-prompt-guard/schema.lua index 9c0172752bd..2629f07154d 100644 --- a/kong/plugins/ai-prompt-guard/schema.lua +++ b/kong/plugins/ai-prompt-guard/schema.lua @@ -32,6 +32,16 @@ return { type = "boolean", required = true, default = false } }, + { max_request_body_size = { + type = "integer", + default = 8 * 1024, + gt = 0, + description = "max allowed body size allowed to be introspected" } }, + { match_all_roles = { + description = "If true, will match all roles in addition to 'user' role in conversation history.", + type = "boolean", + required = true, + default = false } }, } } } @@ -39,6 +49,10 @@ return { entity_checks = { { at_least_one_of = { "config.allow_patterns", "config.deny_patterns" }, - } + }, + { conditional = { + if_field = "config.match_all_roles", if_match = { eq = true }, + then_field = "config.allow_all_conversation_history", then_match = { eq = false }, + } }, } } diff --git a/kong/plugins/ai-prompt-template/handler.lua b/kong/plugins/ai-prompt-template/handler.lua index 63224223a43..2be9137c9fe 100644 --- a/kong/plugins/ai-prompt-template/handler.lua +++ b/kong/plugins/ai-prompt-template/handler.lua @@ -64,10 +64,10 @@ function AIPromptTemplateHandler:access(conf) kong.ctx.shared.ai_prompt_templated = true if conf.log_original_request then - kong.log.set_serialize_value(LOG_ENTRY_KEYS.REQUEST_BODY, kong.request.get_raw_body()) + kong.log.set_serialize_value(LOG_ENTRY_KEYS.REQUEST_BODY, kong.request.get_raw_body(conf.max_request_body_size)) end - local request = kong.request.get_body("application/json") + local request = kong.request.get_body("application/json", nil, conf.max_request_body_size) if type(request) ~= "table" then return bad_request("this LLM route only supports application/json requests") end diff --git a/kong/plugins/ai-prompt-template/schema.lua b/kong/plugins/ai-prompt-template/schema.lua index 0c3615557c2..38e9d418ecd 100644 --- a/kong/plugins/ai-prompt-template/schema.lua +++ b/kong/plugins/ai-prompt-template/schema.lua @@ -44,6 +44,12 @@ return { required = true, default = false, }}, + { max_request_body_size = { + type = "integer", + default = 8 * 1024, + gt = 0, + description = "max allowed body size allowed to be introspected", + }}, } }} }, diff --git a/kong/plugins/ai-proxy/handler.lua b/kong/plugins/ai-proxy/handler.lua index 35e13fbe8d9..f2fc8df8985 100644 --- a/kong/plugins/ai-proxy/handler.lua +++ b/kong/plugins/ai-proxy/handler.lua @@ -1,447 +1,13 @@ -local ai_shared = require("kong.llm.drivers.shared") -local llm = require("kong.llm") -local cjson = require("cjson.safe") -local kong_utils = require("kong.tools.gzip") -local kong_meta = require("kong.meta") -local buffer = require "string.buffer" -local strip = require("kong.tools.utils").strip - - -local EMPTY = {} - - -local _M = { - PRIORITY = 770, - VERSION = kong_meta.version -} - - - ---- Return a 400 response with a JSON body. This function is used to --- return errors to the client while also logging the error. -local function bad_request(msg) - kong.log.info(msg) - return kong.response.exit(400, { error = { message = msg } }) -end - - - --- get the token text from an event frame -local function get_token_text(event_t) - -- get: event_t.choices[1] - local first_choice = ((event_t or EMPTY).choices or EMPTY)[1] or EMPTY - -- return: - -- - event_t.choices[1].delta.content - -- - event_t.choices[1].text - -- - "" - local token_text = (first_choice.delta or EMPTY).content or first_choice.text or "" - return (type(token_text) == "string" and token_text) or "" -end - - - -local function handle_streaming_frame(conf) - -- make a re-usable framebuffer - local framebuffer = buffer.new() - local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" - - local ai_driver = require("kong.llm.drivers." .. conf.model.provider) - - local kong_ctx_plugin = kong.ctx.plugin - -- create a buffer to store each response token/frame, on first pass - if (conf.logging or EMPTY).log_payloads and - (not kong_ctx_plugin.ai_stream_log_buffer) then - kong_ctx_plugin.ai_stream_log_buffer = buffer.new() - end - - -- now handle each chunk/frame - local chunk = ngx.arg[1] - local finished = ngx.arg[2] - - if type(chunk) == "string" and chunk ~= "" then - -- transform each one into flat format, skipping transformer errors - -- because we have already 200 OK'd the client by now - - if (not finished) and (is_gzip) then - chunk = kong_utils.inflate_gzip(chunk) - end - - local events = ai_shared.frame_to_events(chunk) - - for _, event in ipairs(events) do - local formatted, _, metadata = ai_driver.from_format(event, conf.model, "stream/" .. conf.route_type) - - local event_t = nil - local token_t = nil - local err - - if formatted then -- only stream relevant frames back to the user - if conf.logging and conf.logging.log_payloads and (formatted ~= "[DONE]") then - -- append the "choice" to the buffer, for logging later. this actually works! - if not event_t then - event_t, err = cjson.decode(formatted) - end - - if not err then - if not token_t then - token_t = get_token_text(event_t) - end - - kong_ctx_plugin.ai_stream_log_buffer:put(token_t) - end - end - - -- handle event telemetry - if conf.logging and conf.logging.log_statistics then - if not ai_shared.streaming_has_token_counts[conf.model.provider] then - if formatted ~= "[DONE]" then - if not event_t then - event_t, err = cjson.decode(formatted) - end - - if not err then - if not token_t then - token_t = get_token_text(event_t) - end - - -- incredibly loose estimate based on https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them - -- but this is all we can do until OpenAI fixes this... - -- - -- essentially, every 4 characters is a token, with minimum of 1*4 per event - kong_ctx_plugin.ai_stream_completion_tokens = - (kong_ctx_plugin.ai_stream_completion_tokens or 0) + math.ceil(#strip(token_t) / 4) - end - end - end - end - - framebuffer:put("data: ") - framebuffer:put(formatted or "") - framebuffer:put((formatted ~= "[DONE]") and "\n\n" or "") - end - - if conf.logging and conf.logging.log_statistics and metadata then - kong_ctx_plugin.ai_stream_completion_tokens = - (kong_ctx_plugin.ai_stream_completion_tokens or 0) + - (metadata.completion_tokens or 0) - or kong_ctx_plugin.ai_stream_completion_tokens - kong_ctx_plugin.ai_stream_prompt_tokens = - (kong_ctx_plugin.ai_stream_prompt_tokens or 0) + - (metadata.prompt_tokens or 0) - or kong_ctx_plugin.ai_stream_prompt_tokens - end - end - end - - local response_frame = framebuffer:get() - if (not finished) and (is_gzip) then - response_frame = kong_utils.deflate_gzip(response_frame) - end - - ngx.arg[1] = response_frame - - if finished then - local fake_response_t = { - response = kong_ctx_plugin.ai_stream_log_buffer and kong_ctx_plugin.ai_stream_log_buffer:get(), - usage = { - prompt_tokens = kong_ctx_plugin.ai_stream_prompt_tokens or 0, - completion_tokens = kong_ctx_plugin.ai_stream_completion_tokens or 0, - total_tokens = (kong_ctx_plugin.ai_stream_prompt_tokens or 0) - + (kong_ctx_plugin.ai_stream_completion_tokens or 0), - } - } - - ngx.arg[1] = nil - ai_shared.post_request(conf, fake_response_t) - kong_ctx_plugin.ai_stream_log_buffer = nil - end -end - -function _M:header_filter(conf) - local kong_ctx_plugin = kong.ctx.plugin - local kong_ctx_shared = kong.ctx.shared - - if kong_ctx_shared.skip_response_transformer then - return - end - - -- clear shared restricted headers - for _, v in ipairs(ai_shared.clear_response_headers.shared) do - kong.response.clear_header(v) - end - - -- only act on 200 in first release - pass the unmodifed response all the way through if any failure - if kong.response.get_status() ~= 200 then - return - end - - -- we use openai's streaming mode (SSE) - if kong_ctx_shared.ai_proxy_streaming_mode then - -- we are going to send plaintext event-stream frames for ALL models - kong.response.set_header("Content-Type", "text/event-stream") - return - end - - local response_body = kong.service.response.get_raw_body() - if not response_body then - return - end - - local ai_driver = require("kong.llm.drivers." .. conf.model.provider) - local route_type = conf.route_type - - -- if this is a 'streaming' request, we can't know the final - -- result of the response body, so we just proceed to body_filter - -- to translate each SSE event frame - if not kong_ctx_shared.ai_proxy_streaming_mode then - local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" - if is_gzip then - response_body = kong_utils.inflate_gzip(response_body) - end - - if route_type == "preserve" then - kong_ctx_plugin.parsed_response = response_body - else - local new_response_string, err = ai_driver.from_format(response_body, conf.model, route_type) - if err then - kong_ctx_plugin.ai_parser_error = true - ngx.status = 500 - kong_ctx_plugin.parsed_response = cjson.encode({ error = { message = err } }) - - elseif new_response_string then - -- preserve the same response content type; assume the from_format function - -- has returned the body in the appropriate response output format - kong_ctx_plugin.parsed_response = new_response_string - end - end - end - - ai_driver.post_request(conf) -end - - -function _M:body_filter(conf) - local kong_ctx_plugin = kong.ctx.plugin - local kong_ctx_shared = kong.ctx.shared - - -- if body_filter is called twice, then return - if kong_ctx_plugin.body_called and not kong_ctx_shared.ai_proxy_streaming_mode then - return - end - - local route_type = conf.route_type - - if kong_ctx_shared.skip_response_transformer and (route_type ~= "preserve") then - local response_body - if kong_ctx_shared.parsed_response then - response_body = kong_ctx_shared.parsed_response - elseif kong.response.get_status() == 200 then - response_body = kong.service.response.get_raw_body() - if not response_body then - kong.log.warn("issue when retrieve the response body for analytics in the body filter phase.", - " Please check AI request transformer plugin response.") - else - local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" - if is_gzip then - response_body = kong_utils.inflate_gzip(response_body) - end - end - end - - local ai_driver = require("kong.llm.drivers." .. conf.model.provider) - local new_response_string, err = ai_driver.from_format(response_body, conf.model, route_type) - - if err then - kong.log.warn("issue when transforming the response body for analytics in the body filter phase, ", err) - elseif new_response_string then - ai_shared.post_request(conf, new_response_string) - end - end - - if not kong_ctx_shared.skip_response_transformer then - if (kong.response.get_status() ~= 200) and (not kong_ctx_plugin.ai_parser_error) then - return - end - - if route_type ~= "preserve" then - if kong_ctx_shared.ai_proxy_streaming_mode then - handle_streaming_frame(conf) - else - -- all errors MUST be checked and returned in header_filter - -- we should receive a replacement response body from the same thread - local original_request = kong_ctx_plugin.parsed_response - local deflated_request = original_request - - if deflated_request then - local is_gzip = kong.response.get_header("Content-Encoding") == "gzip" - if is_gzip then - deflated_request = kong_utils.deflate_gzip(deflated_request) - end - - kong.response.set_raw_body(deflated_request) - end - - -- call with replacement body, or original body if nothing changed - local _, err = ai_shared.post_request(conf, original_request) - if err then - kong.log.warn("analytics phase failed for request, ", err) - end - end - end - end - - kong_ctx_plugin.body_called = true -end - - -function _M:access(conf) - local kong_ctx_plugin = kong.ctx.plugin - local kong_ctx_shared = kong.ctx.shared - - -- store the route_type in ctx for use in response parsing - local route_type = conf.route_type - - kong_ctx_plugin.operation = route_type - - local request_table - local multipart = false - - -- we may have received a replacement / decorated request body from another AI plugin - if kong_ctx_shared.replacement_request then - kong.log.debug("replacement request body received from another AI plugin") - request_table = kong_ctx_shared.replacement_request - - else - -- first, calculate the coordinates of the request - local content_type = kong.request.get_header("Content-Type") or "application/json" - - request_table = kong.request.get_body(content_type) - - if not request_table then - if not string.find(content_type, "multipart/form-data", nil, true) then - return bad_request("content-type header does not match request body") - end - - multipart = true -- this may be a large file upload, so we have to proxy it directly - end - end - - -- resolve the real plugin config values - local conf_m, err = ai_shared.resolve_plugin_conf(kong.request, conf) - if err then - return bad_request(err) - end - - -- copy from the user request if present - if (not multipart) and (not conf_m.model.name) and (request_table.model) then - if type(request_table.model) == "string" then - conf_m.model.name = request_table.model - end - elseif multipart then - conf_m.model.name = "NOT_SPECIFIED" - end - - -- check that the user isn't trying to override the plugin conf model in the request body - if request_table and request_table.model and type(request_table.model) == "string" and request_table.model ~= "" then - if request_table.model ~= conf_m.model.name then - return bad_request("cannot use own model - must be: " .. conf_m.model.name) - end - end - - -- model is stashed in the copied plugin conf, for consistency in transformation functions - if not conf_m.model.name then - return bad_request("model parameter not found in request, nor in gateway configuration") - end - - kong_ctx_plugin.llm_model_requested = conf_m.model.name - - -- check the incoming format is the same as the configured LLM format - if not multipart then - local compatible, err = llm.is_compatible(request_table, route_type) - if not compatible then - kong_ctx_shared.skip_response_transformer = true - return bad_request(err) - end - end - - -- check the incoming format is the same as the configured LLM format - local compatible, err = llm.is_compatible(request_table, route_type) - if not compatible then - kong_ctx_shared.skip_response_transformer = true - return bad_request(err) - end - - -- check if the user has asked for a stream, and/or if - -- we are forcing all requests to be of streaming type - if request_table and request_table.stream or - (conf_m.response_streaming and conf_m.response_streaming == "always") then - request_table.stream = true - - -- this condition will only check if user has tried - -- to activate streaming mode within their request - if conf_m.response_streaming and conf_m.response_streaming == "deny" then - return bad_request("response streaming is not enabled for this LLM") - end - - -- store token cost estimate, on first pass - if not kong_ctx_plugin.ai_stream_prompt_tokens then - local prompt_tokens, err = ai_shared.calculate_cost(request_table or {}, {}, 1.8) - if err then - kong.log.err("unable to estimate request token cost: ", err) - return kong.response.exit(500) - end - - kong_ctx_plugin.ai_stream_prompt_tokens = prompt_tokens - end - - -- specific actions need to skip later for this to work - kong_ctx_shared.ai_proxy_streaming_mode = true - - else - kong.service.request.enable_buffering() - end - - local ai_driver = require("kong.llm.drivers." .. conf.model.provider) - - -- execute pre-request hooks for this driver - local ok, err = ai_driver.pre_request(conf_m, request_table) - if not ok then - return bad_request(err) - end - - -- transform the body to Kong-format for this provider/model - local parsed_request_body, content_type, err - if route_type ~= "preserve" and (not multipart) then - -- transform the body to Kong-format for this provider/model - parsed_request_body, content_type, err = ai_driver.to_format(request_table, conf_m.model, route_type) - if err then - kong_ctx_shared.skip_response_transformer = true - return bad_request(err) - end - end - - -- execute pre-request hooks for "all" drivers before set new body - local ok, err = ai_shared.pre_request(conf_m, parsed_request_body) - if not ok then - return bad_request(err) - end +local kong_meta = require("kong.meta") +local deep_copy = require "kong.tools.table".deep_copy - if route_type ~= "preserve" then - kong.service.request.set_body(parsed_request_body, content_type) - end - -- now re-configure the request for this operation type - local ok, err = ai_driver.configure_request(conf_m) - if not ok then - kong_ctx_shared.skip_response_transformer = true - kong.log.err("failed to configure request for AI service: ", err) - return kong.response.exit(500) - end +local _M = deep_copy(require("kong.llm.proxy.handler")) - -- lights out, and away we go -end +_M.PRIORITY = 770 +_M.VERSION = kong_meta.version return _M diff --git a/kong/plugins/ai-proxy/schema.lua b/kong/plugins/ai-proxy/schema.lua index 2aa77c56611..0754a0348cd 100644 --- a/kong/plugins/ai-proxy/schema.lua +++ b/kong/plugins/ai-proxy/schema.lua @@ -13,6 +13,13 @@ local ai_proxy_only_config = { default = "allow", one_of = { "allow", "deny", "always" }}, }, + { + max_request_body_size = { + type = "integer", + default = 8 * 1024, + gt = 0, + description = "max allowed body size allowed to be introspected",} + }, } for i, v in pairs(ai_proxy_only_config) do diff --git a/kong/plugins/ai-request-transformer/handler.lua b/kong/plugins/ai-request-transformer/handler.lua index 0eb5cd89d8f..222ed079aa8 100644 --- a/kong/plugins/ai-request-transformer/handler.lua +++ b/kong/plugins/ai-request-transformer/handler.lua @@ -55,7 +55,7 @@ function _M:access(conf) -- if asked, introspect the request before proxying kong.log.debug("introspecting request with LLM") local new_request_body, err = ai_driver:ai_introspect_body( - kong.request.get_raw_body(), + kong.request.get_raw_body(conf.max_request_body_size), conf.prompt, http_opts, conf.transformation_extract_pattern diff --git a/kong/plugins/ai-request-transformer/schema.lua b/kong/plugins/ai-request-transformer/schema.lua index c7ce498ba68..9ebd3b4b8d6 100644 --- a/kong/plugins/ai-request-transformer/schema.lua +++ b/kong/plugins/ai-request-transformer/schema.lua @@ -37,6 +37,14 @@ return { default = true, }}, + { + max_request_body_size = { + type = "integer", + default = 8 * 1024, + gt = 0, + description = "max allowed body size allowed to be introspected",} + }, + -- from forward-proxy { http_proxy_host = typedefs.host }, { http_proxy_port = typedefs.port }, @@ -46,7 +54,7 @@ return { { llm = llm.config_schema }, }, }}, - + }, entity_checks = { { diff --git a/kong/plugins/ai-response-transformer/handler.lua b/kong/plugins/ai-response-transformer/handler.lua index 94a82a5ff2d..c1e154dbd06 100644 --- a/kong/plugins/ai-response-transformer/handler.lua +++ b/kong/plugins/ai-response-transformer/handler.lua @@ -113,7 +113,9 @@ function _M:access(conf) kong.log.debug("intercepting plugin flow with one-shot request") local httpc = http.new() - local res, err = subrequest(httpc, kong.request.get_raw_body(), http_opts) + local res, err = subrequest(httpc, + kong.request.get_raw_body(conf.max_request_body_size), + http_opts) if err then return internal_server_error(err) end diff --git a/kong/plugins/ai-response-transformer/schema.lua b/kong/plugins/ai-response-transformer/schema.lua index 565d467fe2d..2f52f6f27e2 100644 --- a/kong/plugins/ai-response-transformer/schema.lua +++ b/kong/plugins/ai-response-transformer/schema.lua @@ -45,6 +45,13 @@ return { default = true, }}, + { max_request_body_size = { + type = "integer", + default = 8 * 1024, + gt = 0, + description = "max allowed body size allowed to be introspected",} + }, + -- from forward-proxy { http_proxy_host = typedefs.host }, { http_proxy_port = typedefs.port }, diff --git a/kong/plugins/aws-lambda/handler.lua b/kong/plugins/aws-lambda/handler.lua index 430f1f4f271..5e590fb90bc 100644 --- a/kong/plugins/aws-lambda/handler.lua +++ b/kong/plugins/aws-lambda/handler.lua @@ -1,9 +1,8 @@ -- Copyright (C) Kong Inc. local ngx_var = ngx.var -local ngx_now = ngx.now -local ngx_update_time = ngx.update_time local md5_bin = ngx.md5_bin +local re_match = ngx.re.match local fmt = string.format local buffer = require "string.buffer" local lrucache = require "resty.lrucache" @@ -13,9 +12,10 @@ local meta = require "kong.meta" local constants = require "kong.constants" local aws_config = require "resty.aws.config" -- reads environment variables, thus specified here local VIA_HEADER = constants.HEADERS.VIA -local VIA_HEADER_VALUE = meta._NAME .. "/" .. meta._VERSION +local server_tokens = meta._SERVER_TOKENS local request_util = require "kong.plugins.aws-lambda.request-util" +local get_now = require("kong.tools.time").get_updated_now_ms local build_request_payload = request_util.build_request_payload local extract_proxy_response = request_util.extract_proxy_response local remove_array_mt_for_empty_table = request_util.remove_array_mt_for_empty_table @@ -29,12 +29,6 @@ local AWS local LAMBDA_SERVICE_CACHE -local function get_now() - ngx_update_time() - return ngx_now() * 1000 -- time is kept in seconds with millisecond resolution. -end - - local function initialize() LAMBDA_SERVICE_CACHE = lrucache.new(1000) AWS_GLOBAL_CONFIG = aws_config.global @@ -48,6 +42,7 @@ local build_cache_key do -- vault refresh can take effect when key/secret is rotated local SERVICE_RELATED_FIELD = { "timeout", "keepalive", "aws_key", "aws_secret", "aws_assume_role_arn", "aws_role_session_name", + "aws_sts_endpoint_url", "aws_region", "host", "port", "disable_https", "proxy_url", "aws_imds_protocol_version" } @@ -132,6 +127,7 @@ function AWSLambdaHandler:access(conf) credentials = credentials, region = region, stsRegionalEndpoints = AWS_GLOBAL_CONFIG.sts_regional_endpoints, + endpoint = conf.aws_sts_endpoint_url, ssl_verify = false, http_proxy = conf.proxy_url, https_proxy = conf.proxy_url, @@ -238,7 +234,9 @@ function AWSLambdaHandler:access(conf) headers = kong.table.merge(headers) -- create a copy of headers if kong.configuration.enabled_headers[VIA_HEADER] then - headers[VIA_HEADER] = VIA_HEADER_VALUE + local outbound_via = (ngx_var.http2 and "2 " or "1.1 ") .. server_tokens + headers[VIA_HEADER] = headers[VIA_HEADER] and headers[VIA_HEADER] .. ", " .. outbound_via + or outbound_via end -- TODO: remove this in the next major release @@ -248,7 +246,13 @@ function AWSLambdaHandler:access(conf) -- instead of JSON arrays for empty arrays. if conf.empty_arrays_mode == "legacy" then local ct = headers["Content-Type"] - if ct and ct:lower():match("application/.*json") then + -- If Content-Type is specified by multiValueHeader then + -- it will be an array, so we need to get the first element + if type(ct) == "table" and #ct > 0 then + ct = ct[1] + end + + if ct and type(ct) == "string" and re_match(ct:lower(), "application/.*json", "jo") then content = remove_array_mt_for_empty_table(content) end end diff --git a/kong/plugins/aws-lambda/request-util.lua b/kong/plugins/aws-lambda/request-util.lua index 5f936ce8185..478f8c619e8 100644 --- a/kong/plugins/aws-lambda/request-util.lua +++ b/kong/plugins/aws-lambda/request-util.lua @@ -4,7 +4,7 @@ local ngx_decode_base64 = ngx.decode_base64 local cjson = require "cjson.safe" local date = require("date") -local get_request_id = require("kong.tracing.request_id").get +local get_request_id = require("kong.observability.tracing.request_id").get local EMPTY = {} diff --git a/kong/plugins/aws-lambda/schema.lua b/kong/plugins/aws-lambda/schema.lua index 767262d6604..744ca4debbf 100644 --- a/kong/plugins/aws-lambda/schema.lua +++ b/kong/plugins/aws-lambda/schema.lua @@ -38,6 +38,7 @@ return { { aws_role_session_name = { description = "The identifier of the assumed role session.", type = "string", default = "kong" } }, + { aws_sts_endpoint_url = typedefs.url }, { aws_region = typedefs.host }, { function_name = { type = "string", diff --git a/kong/plugins/azure-functions/handler.lua b/kong/plugins/azure-functions/handler.lua index 1fdcb664330..4aa8e7911d9 100644 --- a/kong/plugins/azure-functions/handler.lua +++ b/kong/plugins/azure-functions/handler.lua @@ -1,5 +1,4 @@ local constants = require "kong.constants" -local meta = require "kong.meta" local http = require "resty.http" local kong_meta = require "kong.meta" @@ -9,7 +8,9 @@ local fmt = string.format local byte = string.byte local match = string.match local var = ngx.var -local server_header = meta._SERVER_TOKENS + +local server_tokens = kong_meta._SERVER_TOKENS +local VIA_HEADER = constants.HEADERS.VIA local SLASH = byte("/") @@ -77,9 +78,11 @@ function azure:access(conf) response_headers["Transfer-Encoding"] = nil end - if kong.configuration.enabled_headers[constants.HEADERS.VIA] then - response_headers[constants.HEADERS.VIA] = server_header - end + if kong.configuration.enabled_headers[VIA_HEADER] then + local outbound_via = (var.http2 and "2 " or "1.1 ") .. server_tokens + response_headers[VIA_HEADER] = response_headers[VIA_HEADER] and response_headers[VIA_HEADER] .. ", " .. outbound_via + or outbound_via + end return kong.response.exit(res.status, res.body, response_headers) end diff --git a/kong/plugins/cors/handler.lua b/kong/plugins/cors/handler.lua index 5050f4bd022..3d62be38818 100644 --- a/kong/plugins/cors/handler.lua +++ b/kong/plugins/cors/handler.lua @@ -96,6 +96,11 @@ local function configure_origin(conf, header_filter) cached_domains = {} for _, entry in ipairs(conf.origins) do + if entry == "*" then + set_header("Access-Control-Allow-Origin", "*") + return true + end + local domain local maybe_regex, _, err = re_find(entry, "[^A-Za-z0-9.:/-]", "jo") if err then diff --git a/kong/plugins/opentelemetry/handler.lua b/kong/plugins/opentelemetry/handler.lua index 7fe30340a17..ba3c635425f 100644 --- a/kong/plugins/opentelemetry/handler.lua +++ b/kong/plugins/opentelemetry/handler.lua @@ -1,27 +1,10 @@ -local Queue = require "kong.tools.queue" -local http = require "resty.http" -local clone = require "table.clone" -local otlp = require "kong.plugins.opentelemetry.otlp" -local propagation = require "kong.tracing.propagation" -local tracing_context = require "kong.tracing.tracing_context" -local kong_meta = require "kong.meta" - - -local ngx = ngx -local kong = kong -local tostring = tostring -local ngx_log = ngx.log -local ngx_ERR = ngx.ERR -local ngx_DEBUG = ngx.DEBUG -local ngx_now = ngx.now -local ngx_update_time = ngx.update_time -local null = ngx.null -local encode_traces = otlp.encode_traces -local encode_span = otlp.transform_span -local to_hex = require "resty.string".to_hex +local otel_traces = require "kong.plugins.opentelemetry.traces" +local otel_logs = require "kong.plugins.opentelemetry.logs" +local dynamic_hook = require "kong.dynamic_hook" +local o11y_logs = require "kong.observability.logs" +local kong_meta = require "kong.meta" -local _log_prefix = "[otel] " local OpenTelemetryHandler = { @@ -29,184 +12,45 @@ local OpenTelemetryHandler = { PRIORITY = 14, } -local CONTENT_TYPE_HEADER_NAME = "Content-Type" -local DEFAULT_CONTENT_TYPE_HEADER = "application/x-protobuf" -local DEFAULT_HEADERS = { - [CONTENT_TYPE_HEADER_NAME] = DEFAULT_CONTENT_TYPE_HEADER -} - -local function get_headers(conf_headers) - if not conf_headers or conf_headers == null then - return DEFAULT_HEADERS - end - - if conf_headers[CONTENT_TYPE_HEADER_NAME] then - return conf_headers - end - - local headers = clone(conf_headers) - headers[CONTENT_TYPE_HEADER_NAME] = DEFAULT_CONTENT_TYPE_HEADER - return headers -end - - -local function http_export_request(conf, pb_data, headers) - local httpc = http.new() - httpc:set_timeouts(conf.connect_timeout, conf.send_timeout, conf.read_timeout) - local res, err = httpc:request_uri(conf.endpoint, { - method = "POST", - body = pb_data, - headers = headers, - }) - if not res then - return false, "failed to send request: " .. err - - elseif res and res.status ~= 200 then - return false, "response error: " .. tostring(res.status) .. ", body: " .. tostring(res.body) - end - - return true -end - - -local function http_export(conf, spans) - local start = ngx_now() - local headers = get_headers(conf.headers) - local payload = encode_traces(spans, conf.resource_attributes) - - local ok, err = http_export_request(conf, payload, headers) - - ngx_update_time() - local duration = ngx_now() - start - ngx_log(ngx_DEBUG, _log_prefix, "exporter sent ", #spans, - " traces to ", conf.endpoint, " in ", duration, " seconds") - - if not ok then - ngx_log(ngx_ERR, _log_prefix, err) - end - - return ok, err -end - - -local function get_inject_ctx(extracted_ctx, conf) - local root_span = ngx.ctx.KONG_SPANS and ngx.ctx.KONG_SPANS[1] - - -- get the global tracer when available, or instantiate a new one - local tracer = kong.tracing.name == "noop" and kong.tracing.new("otel") - or kong.tracing - - -- make propagation work with tracing disabled - if not root_span then - root_span = tracer.start_span("root") - root_span:set_attribute("kong.propagation_only", true) - - -- since tracing is disabled, turn off sampling entirely for this trace - kong.ctx.plugin.should_sample = false - end - - local injected_parent_span = tracing_context.get_unlinked_span("balancer") or root_span - local trace_id = extracted_ctx.trace_id - local span_id = extracted_ctx.span_id - local parent_id = extracted_ctx.parent_id - local parent_sampled = extracted_ctx.should_sample - - -- Overwrite trace ids - -- with the value extracted from incoming tracing headers - if trace_id then - -- to propagate the correct trace ID we have to set it here - -- before passing this span to propagation - injected_parent_span.trace_id = trace_id - -- update the Tracing Context with the trace ID extracted from headers - tracing_context.set_raw_trace_id(trace_id) - end - - -- overwrite root span's parent_id - if span_id then - root_span.parent_id = span_id - - elseif parent_id then - root_span.parent_id = parent_id - end - - -- Configure the sampled flags - local sampled - if kong.ctx.plugin.should_sample == false then - sampled = false - else - -- Sampling decision for the current trace. - local err - -- get_sampling_decision() depends on the value of the trace id: call it - -- after the trace_id is updated - sampled, err = tracer:get_sampling_decision(parent_sampled, conf.sampling_rate) - if err then - ngx_log(ngx_ERR, _log_prefix, "sampler failure: ", err) +function OpenTelemetryHandler:configure(configs) + if configs then + for _, config in ipairs(configs) do + if config.logs_endpoint then + dynamic_hook.hook("observability_logs", "push", o11y_logs.maybe_push) + dynamic_hook.enable_by_default("observability_logs") + end end end - tracer:set_should_sample(sampled) - -- Set the sampled flag for the outgoing header's span - injected_parent_span.should_sample = sampled - - extracted_ctx.trace_id = injected_parent_span.trace_id - extracted_ctx.span_id = injected_parent_span.span_id - extracted_ctx.should_sample = injected_parent_span.should_sample - extracted_ctx.parent_id = injected_parent_span.parent_id - - -- return the injected ctx (data to be injected with outgoing tracing headers) - return extracted_ctx end function OpenTelemetryHandler:access(conf) - propagation.propagate( - propagation.get_plugin_params(conf), - get_inject_ctx, - conf - ) + -- Traces + if conf.traces_endpoint then + otel_traces.access(conf) + end end function OpenTelemetryHandler:header_filter(conf) - if conf.http_response_header_for_traceid then - local trace_id = tracing_context.get_raw_trace_id() - if not trace_id then - local root_span = ngx.ctx.KONG_SPANS and ngx.ctx.KONG_SPANS[1] - trace_id = root_span and root_span.trace_id - end - if trace_id then - trace_id = to_hex(trace_id) - kong.response.add_header(conf.http_response_header_for_traceid, trace_id) - end + -- Traces + if conf.traces_endpoint then + otel_traces.header_filter(conf) end end function OpenTelemetryHandler:log(conf) - ngx_log(ngx_DEBUG, _log_prefix, "total spans in current request: ", ngx.ctx.KONG_SPANS and #ngx.ctx.KONG_SPANS) - - kong.tracing.process_span(function (span) - if span.should_sample == false or kong.ctx.plugin.should_sample == false then - -- ignore - return - end - - -- overwrite - local trace_id = tracing_context.get_raw_trace_id() - if trace_id then - span.trace_id = trace_id - end + -- Traces + if conf.traces_endpoint then + otel_traces.log(conf) + end - local ok, err = Queue.enqueue( - Queue.get_plugin_params("opentelemetry", conf), - http_export, - conf, - encode_span(span) - ) - if not ok then - kong.log.err("Failed to enqueue span to log server: ", err) - end - end) + -- Logs + if conf.logs_endpoint then + otel_logs.log(conf) + end end diff --git a/kong/plugins/opentelemetry/logs.lua b/kong/plugins/opentelemetry/logs.lua new file mode 100644 index 00000000000..7e64c12e204 --- /dev/null +++ b/kong/plugins/opentelemetry/logs.lua @@ -0,0 +1,85 @@ +local Queue = require "kong.tools.queue" +local o11y_logs = require "kong.observability.logs" +local otlp = require "kong.plugins.opentelemetry.otlp" +local tracing_context = require "kong.observability.tracing.tracing_context" +local otel_utils = require "kong.plugins.opentelemetry.utils" +local clone = require "table.clone" + +local table_concat = require "kong.tools.table".concat + +local ngx = ngx +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_DEBUG = ngx.DEBUG + +local http_export_request = otel_utils.http_export_request +local get_headers = otel_utils.get_headers +local _log_prefix = otel_utils._log_prefix +local encode_logs = otlp.encode_logs +local prepare_logs = otlp.prepare_logs + + +local function http_export_logs(conf, logs_batch) + local headers = get_headers(conf.headers) + local payload = encode_logs(logs_batch, conf.resource_attributes) + + local ok, err = http_export_request({ + connect_timeout = conf.connect_timeout, + send_timeout = conf.send_timeout, + read_timeout = conf.read_timeout, + endpoint = conf.logs_endpoint, + }, payload, headers) + + if ok then + ngx_log(ngx_DEBUG, _log_prefix, "exporter sent ", #logs_batch, + " logs to ", conf.logs_endpoint) + + else + ngx_log(ngx_ERR, _log_prefix, err) + end + + return ok, err +end + + +local function log(conf) + local worker_logs = o11y_logs.get_worker_logs() + local request_logs = o11y_logs.get_request_logs() + + local worker_logs_len = #worker_logs + local request_logs_len = #request_logs + ngx_log(ngx_DEBUG, _log_prefix, "total request_logs in current request: ", + request_logs_len, " total worker_logs in current request: ", worker_logs_len) + + if request_logs_len + worker_logs_len == 0 then + return + end + + local raw_trace_id = tracing_context.get_raw_trace_id() + local flags = tracing_context.get_flags() + local worker_logs_ready = prepare_logs(worker_logs) + local request_logs_ready = prepare_logs(request_logs, raw_trace_id, flags) + + local queue_conf = clone(Queue.get_plugin_params("opentelemetry", conf)) + queue_conf.name = queue_conf.name .. ":logs" + + for _, log in ipairs(table_concat(worker_logs_ready, request_logs_ready)) do + -- Check if the entry can be enqueued before calling `Queue.enqueue` + -- This is done because newer logs are not more important than old ones. + -- Enqueueing without checking would result in older logs being dropped + -- which affects performance because it's done synchronously. + if Queue.can_enqueue(queue_conf, log) then + Queue.enqueue( + queue_conf, + http_export_logs, + conf, + log + ) + end + end +end + + +return { + log = log, +} diff --git a/kong/plugins/opentelemetry/migrations/001_331_to_332.lua b/kong/plugins/opentelemetry/migrations/001_331_to_332.lua index 3916fba7203..b188c105183 100644 --- a/kong/plugins/opentelemetry/migrations/001_331_to_332.lua +++ b/kong/plugins/opentelemetry/migrations/001_331_to_332.lua @@ -4,6 +4,10 @@ local operations = require "kong.db.migrations.operations.331_to_332" local function ws_migration_teardown(ops) return function(connector) return ops:fixup_plugin_config(connector, "opentelemetry", function(config) + if not config.queue then + return false + end + if config.queue.max_batch_size == 1 then config.queue.max_batch_size = 200 return true diff --git a/kong/plugins/opentelemetry/otlp.lua b/kong/plugins/opentelemetry/otlp.lua index 649b427c26d..ded49eb3ed2 100644 --- a/kong/plugins/opentelemetry/otlp.lua +++ b/kong/plugins/opentelemetry/otlp.lua @@ -3,7 +3,7 @@ local pb = require "pb" local new_tab = require "table.new" local nkeys = require "table.nkeys" local tablepool = require "tablepool" -local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy +local deep_copy = require("kong.tools.table").deep_copy local kong = kong local insert = table.insert @@ -123,7 +123,7 @@ local function transform_span(span) return pb_span end -local encode_traces +local encode_traces, encode_logs, prepare_logs do local attributes_cache = setmetatable({}, { __mode = "k" }) local function default_resource_attributes() @@ -151,7 +151,7 @@ do return resource_attributes end - local pb_memo = { + local pb_memo_trace = { resource_spans = { { resource = { attributes = {} @@ -169,7 +169,7 @@ do encode_traces = function(spans, resource_attributes) local tab = tablepool_fetch(POOL_OTLP, 0, 2) if not tab.resource_spans then - tab.resource_spans = cycle_aware_deep_copy(pb_memo.resource_spans) + tab.resource_spans = deep_copy(pb_memo_trace.resource_spans) end local resource = tab.resource_spans[1].resource @@ -185,10 +185,76 @@ do return pb_data end + + local pb_memo_log = { + resource_logs = { + { resource = { + attributes = {} + }, + scope_logs = { + { scope = { + name = "kong-internal", + version = "0.1.0", + }, + log_records = {}, }, + }, }, + }, + } + + encode_logs = function(log_batch, resource_attributes) + local tab = tablepool_fetch(POOL_OTLP, 0, 3) + if not tab.resource_logs then + tab.resource_logs = deep_copy(pb_memo_log.resource_logs) + end + + local resource = tab.resource_logs[1].resource + resource.attributes = render_resource_attributes(resource_attributes) + + local scoped = tab.resource_logs[1].scope_logs[1] + + scoped.log_records = log_batch + + local pb_data = pb.encode("opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest", tab) + + -- remove reference + scoped.logs = nil + tablepool_release(POOL_OTLP, tab, true) -- no clear + + return pb_data + end + + -- see: kong/include/opentelemetry/proto/logs/v1/logs.proto + local map_severity = { + [ngx.DEBUG] = { 5, "DEBUG" }, + [ngx.INFO] = { 9, "INFO" }, + [ngx.NOTICE] = { 11, "NOTICE" }, + [ngx.WARN] = { 13, "WARN" }, + [ngx.ERR] = { 17, "ERR" }, + [ngx.CRIT] = { 19, "CRIT" }, + [ngx.ALERT] = { 21, "ALERT" }, + [ngx.EMERG] = { 23, "EMERG" }, + } + + prepare_logs = function(logs, trace_id, flags) + for _, log in ipairs(logs) do + local severity = map_severity[log.log_level] + log.severity_number = severity and severity[1] + log.severity_text = severity and severity[2] + log.log_level = nil + log.trace_id = trace_id + log.flags = flags + log.attributes = transform_attributes(log.attributes) + log.body = { string_value = log.body } + end + + return logs + end end return { to_ot_trace_id = to_ot_trace_id, transform_span = transform_span, encode_traces = encode_traces, + encode_logs = encode_logs, + prepare_logs = prepare_logs, } diff --git a/kong/plugins/opentelemetry/proto.lua b/kong/plugins/opentelemetry/proto.lua index 484f0374716..bf63e9ebb03 100644 --- a/kong/plugins/opentelemetry/proto.lua +++ b/kong/plugins/opentelemetry/proto.lua @@ -1,12 +1,14 @@ local grpc = require "kong.tools.grpc" local proto_fpath = "opentelemetry/proto/collector/trace/v1/trace_service.proto" +local proto_logs_fpath = "opentelemetry/proto/collector/logs/v1/logs_service.proto" local function load_proto() local grpc_util = grpc.new() local protoc_instance = grpc_util.protoc_instance protoc_instance:loadfile(proto_fpath) + protoc_instance:loadfile(proto_logs_fpath) end load_proto() diff --git a/kong/plugins/opentelemetry/schema.lua b/kong/plugins/opentelemetry/schema.lua index bdbd27056f2..41a127859c6 100644 --- a/kong/plugins/opentelemetry/schema.lua +++ b/kong/plugins/opentelemetry/schema.lua @@ -35,7 +35,8 @@ return { { config = { type = "record", fields = { - { endpoint = typedefs.url { required = true, referenceable = true } }, -- OTLP/HTTP + { traces_endpoint = typedefs.url { referenceable = true } }, -- OTLP/HTTP + { logs_endpoint = typedefs.url { referenceable = true } }, { headers = { description = "The custom headers to be added in the HTTP request sent to the OTLP server. This setting is useful for adding the authentication headers (token) for the APM backend.", type = "map", keys = typedefs.header_name, values = { @@ -91,6 +92,26 @@ return { }, } }, }, + entity_checks = { + { at_least_one_of = { + "traces_endpoint", + "logs_endpoint", + } }, + }, + shorthand_fields = { + -- TODO: deprecated fields, to be removed in Kong 4.0 + { + endpoint = typedefs.url { + referenceable = true, + deprecation = { + message = "OpenTelemetry: config.endpoint is deprecated, please use config.traces_endpoint instead", + removal_in_version = "4.0", }, + func = function(value) + return { traces_endpoint = value } + end, + }, + }, + } }, }, }, } diff --git a/kong/plugins/opentelemetry/traces.lua b/kong/plugins/opentelemetry/traces.lua new file mode 100644 index 00000000000..2f6ffe3b406 --- /dev/null +++ b/kong/plugins/opentelemetry/traces.lua @@ -0,0 +1,181 @@ +local Queue = require "kong.tools.queue" +local propagation = require "kong.observability.tracing.propagation" +local tracing_context = require "kong.observability.tracing.tracing_context" +local otlp = require "kong.plugins.opentelemetry.otlp" +local otel_utils = require "kong.plugins.opentelemetry.utils" +local clone = require "table.clone" + +local to_hex = require "resty.string".to_hex +local bor = require "bit".bor + +local ngx = ngx +local kong = kong +local ngx_log = ngx.log +local ngx_ERR = ngx.ERR +local ngx_DEBUG = ngx.DEBUG + +local http_export_request = otel_utils.http_export_request +local get_headers = otel_utils.get_headers +local _log_prefix = otel_utils._log_prefix +local encode_traces = otlp.encode_traces +local encode_span = otlp.transform_span + + +local function get_inject_ctx(extracted_ctx, conf) + local root_span = ngx.ctx.KONG_SPANS and ngx.ctx.KONG_SPANS[1] + + -- get the global tracer when available, or instantiate a new one + local tracer = kong.tracing.name == "noop" and kong.tracing.new("otel") + or kong.tracing + + -- make propagation work with tracing disabled + if not root_span then + root_span = tracer.start_span("root") + root_span:set_attribute("kong.propagation_only", true) + + -- since tracing is disabled, turn off sampling entirely for this trace + kong.ctx.plugin.should_sample = false + end + + local injected_parent_span = tracing_context.get_unlinked_span("balancer") or root_span + local trace_id = extracted_ctx.trace_id + local span_id = extracted_ctx.span_id + local parent_id = extracted_ctx.parent_id + local parent_sampled = extracted_ctx.should_sample + local flags = extracted_ctx.w3c_flags or extracted_ctx.flags + + -- Overwrite trace ids + -- with the value extracted from incoming tracing headers + if trace_id then + -- to propagate the correct trace ID we have to set it here + -- before passing this span to propagation + injected_parent_span.trace_id = trace_id + -- update the Tracing Context with the trace ID extracted from headers + tracing_context.set_raw_trace_id(trace_id) + end + + -- overwrite root span's parent_id + if span_id then + root_span.parent_id = span_id + + elseif parent_id then + root_span.parent_id = parent_id + end + + -- Configure the sampled flags + local sampled + if kong.ctx.plugin.should_sample == false then + sampled = false + + else + -- Sampling decision for the current trace. + local err + -- get_sampling_decision() depends on the value of the trace id: call it + -- after the trace_id is updated + sampled, err = tracer:get_sampling_decision(parent_sampled, conf.sampling_rate) + if err then + ngx_log(ngx_ERR, _log_prefix, "sampler failure: ", err) + end + end + tracer:set_should_sample(sampled) + -- Set the sampled flag for the outgoing header's span + injected_parent_span.should_sample = sampled + + extracted_ctx.trace_id = injected_parent_span.trace_id + extracted_ctx.span_id = injected_parent_span.span_id + extracted_ctx.should_sample = injected_parent_span.should_sample + extracted_ctx.parent_id = injected_parent_span.parent_id + + flags = flags or 0x00 + local sampled_flag = sampled and 1 or 0 + local out_flags = bor(flags, sampled_flag) + tracing_context.set_flags(out_flags) + + -- return the injected ctx (data to be injected with outgoing tracing headers) + return extracted_ctx +end + + +local function access(conf) + propagation.propagate( + propagation.get_plugin_params(conf), + get_inject_ctx, + conf + ) +end + + +local function header_filter(conf) + if conf.http_response_header_for_traceid then + local trace_id = tracing_context.get_raw_trace_id() + if not trace_id then + local root_span = ngx.ctx.KONG_SPANS and ngx.ctx.KONG_SPANS[1] + trace_id = root_span and root_span.trace_id + end + if trace_id then + trace_id = to_hex(trace_id) + kong.response.add_header(conf.http_response_header_for_traceid, trace_id) + end + end +end + + +local function http_export_traces(conf, spans) + local headers = get_headers(conf.headers) + local payload = encode_traces(spans, conf.resource_attributes) + + local ok, err = http_export_request({ + connect_timeout = conf.connect_timeout, + send_timeout = conf.send_timeout, + read_timeout = conf.read_timeout, + endpoint = conf.traces_endpoint, + }, payload, headers) + + if ok then + ngx_log(ngx_DEBUG, _log_prefix, "exporter sent ", #spans, + " spans to ", conf.traces_endpoint) + + else + ngx_log(ngx_ERR, _log_prefix, err) + end + + return ok, err +end + + +local function log(conf) + ngx_log(ngx_DEBUG, _log_prefix, "total spans in current request: ", ngx.ctx.KONG_SPANS and #ngx.ctx.KONG_SPANS) + + kong.tracing.process_span(function (span) + if span.should_sample == false or kong.ctx.plugin.should_sample == false then + -- ignore + return + end + + -- overwrite + local trace_id = tracing_context.get_raw_trace_id() + if trace_id then + span.trace_id = trace_id + end + + local queue_conf = clone(Queue.get_plugin_params("opentelemetry", conf)) + queue_conf.name = queue_conf.name .. ":traces" + + local ok, err = Queue.enqueue( + queue_conf, + http_export_traces, + conf, + encode_span(span) + ) + if not ok then + kong.log.err("Failed to enqueue span to log server: ", err) + end + end) +end + + +return { + access = access, + header_filter = header_filter, + log = log, +} diff --git a/kong/plugins/opentelemetry/utils.lua b/kong/plugins/opentelemetry/utils.lua new file mode 100644 index 00000000000..5802ceeadc9 --- /dev/null +++ b/kong/plugins/opentelemetry/utils.lua @@ -0,0 +1,55 @@ +local http = require "resty.http" +local clone = require "table.clone" + +local tostring = tostring +local null = ngx.null + + +local CONTENT_TYPE_HEADER_NAME = "Content-Type" +local DEFAULT_CONTENT_TYPE_HEADER = "application/x-protobuf" +local DEFAULT_HEADERS = { + [CONTENT_TYPE_HEADER_NAME] = DEFAULT_CONTENT_TYPE_HEADER +} + +local _log_prefix = "[otel] " + +local function http_export_request(conf, pb_data, headers) + local httpc = http.new() + httpc:set_timeouts(conf.connect_timeout, conf.send_timeout, conf.read_timeout) + local res, err = httpc:request_uri(conf.endpoint, { + method = "POST", + body = pb_data, + headers = headers, + }) + + if not res then + return false, "failed to send request: " .. err + + elseif res and res.status ~= 200 then + return false, "response error: " .. tostring(res.status) .. ", body: " .. tostring(res.body) + end + + return true +end + + +local function get_headers(conf_headers) + if not conf_headers or conf_headers == null then + return DEFAULT_HEADERS + end + + if conf_headers[CONTENT_TYPE_HEADER_NAME] then + return conf_headers + end + + local headers = clone(conf_headers) + headers[CONTENT_TYPE_HEADER_NAME] = DEFAULT_CONTENT_TYPE_HEADER + return headers +end + + +return { + http_export_request = http_export_request, + get_headers = get_headers, + _log_prefix = _log_prefix, +} diff --git a/kong/plugins/prometheus/exporter.lua b/kong/plugins/prometheus/exporter.lua index 2a94ebac272..bdc5eeafcbc 100644 --- a/kong/plugins/prometheus/exporter.lua +++ b/kong/plugins/prometheus/exporter.lua @@ -17,8 +17,9 @@ local stream_available, stream_api = pcall(require, "kong.tools.stream_api") local role = kong.configuration.role -local KONG_LATENCY_BUCKETS = { 1, 2, 5, 7, 10, 15, 20, 30, 50, 75, 100, 200, 500, 750, 1000} -local UPSTREAM_LATENCY_BUCKETS = {25, 50, 80, 100, 250, 400, 700, 1000, 2000, 5000, 10000, 30000, 60000 } +local KONG_LATENCY_BUCKETS = { 1, 2, 5, 7, 10, 15, 20, 30, 50, 75, 100, 200, 500, 750, 1000 } +local UPSTREAM_LATENCY_BUCKETS = { 25, 50, 80, 100, 250, 400, 700, 1000, 2000, 5000, 10000, 30000, 60000 } +local AI_LLM_PROVIDER_LATENCY_BUCKETS = { 250, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 10000, 30000, 60000 } local IS_PROMETHEUS_ENABLED @@ -157,6 +158,11 @@ local function init() "AI requests cost per ai_provider/cache in Kong", {"ai_provider", "ai_model", "cache_status", "vector_db", "embeddings_provider", "embeddings_model", "token_type", "workspace"}) + metrics.ai_llm_provider_latency = prometheus:histogram("ai_llm_provider_latency_ms", + "LLM response Latency for each AI plugins per ai_provider in Kong", + {"ai_provider", "ai_model", "cache_status", "vector_db", "embeddings_provider", "embeddings_model", "workspace"}, + AI_LLM_PROVIDER_LATENCY_BUCKETS) + -- Hybrid mode status if role == "control_plane" then metrics.data_plane_last_seen = prometheus:gauge("data_plane_last_seen", @@ -349,6 +355,10 @@ local function log(message, serialized) metrics.ai_llm_cost:inc(ai_plugin.usage.cost, labels_table_ai_llm_status) end + if ai_plugin.meta.llm_latency and ai_plugin.meta.llm_latency > 0 then + metrics.ai_llm_provider_latency:observe(ai_plugin.meta.llm_latency, labels_table_ai_llm_status) + end + labels_table_ai_llm_tokens[1] = ai_plugin.meta.provider_name labels_table_ai_llm_tokens[2] = ai_plugin.meta.request_model labels_table_ai_llm_tokens[3] = cache_status diff --git a/kong/plugins/proxy-cache/clustering/compat/response_headers_translation.lua b/kong/plugins/proxy-cache/clustering/compat/response_headers_translation.lua deleted file mode 100644 index 56c602f23bb..00000000000 --- a/kong/plugins/proxy-cache/clustering/compat/response_headers_translation.lua +++ /dev/null @@ -1,13 +0,0 @@ -local function adapter(config_to_update) - if config_to_update.response_headers["Age"] ~= nil then - config_to_update.response_headers.age = config_to_update.response_headers["Age"] - config_to_update.response_headers["Age"] = nil - return true - end - - return false -end - -return { - adapter = adapter -} diff --git a/kong/plugins/proxy-cache/handler.lua b/kong/plugins/proxy-cache/handler.lua index 69e11ae081b..4b2c0442195 100644 --- a/kong/plugins/proxy-cache/handler.lua +++ b/kong/plugins/proxy-cache/handler.lua @@ -401,7 +401,7 @@ function ProxyCacheHandler:access(conf) reset_res_header(res) - set_res_header(res, "Age", floor(time() - res.timestamp), conf) + set_res_header(res, "age", floor(time() - res.timestamp), conf) set_res_header(res, "X-Cache-Status", "Hit", conf) set_res_header(res, "X-Cache-Key", cache_key, conf) diff --git a/kong/plugins/proxy-cache/schema.lua b/kong/plugins/proxy-cache/schema.lua index 34efa9648b5..768e6f06975 100644 --- a/kong/plugins/proxy-cache/schema.lua +++ b/kong/plugins/proxy-cache/schema.lua @@ -78,7 +78,7 @@ return { description = "Caching related diagnostic headers that should be included in cached responses", type = "record", fields = { - { ["Age"] = {type = "boolean", default = true} }, + { age = {type = "boolean", default = true} }, { ["X-Cache-Status"] = {type = "boolean", default = true} }, { ["X-Cache-Key"] = {type = "boolean", default = true} }, }, diff --git a/kong/plugins/zipkin/handler.lua b/kong/plugins/zipkin/handler.lua index a422a0d9c3b..d20742f9e54 100644 --- a/kong/plugins/zipkin/handler.lua +++ b/kong/plugins/zipkin/handler.lua @@ -1,6 +1,6 @@ local new_zipkin_reporter = require "kong.plugins.zipkin.reporter".new local new_span = require "kong.plugins.zipkin.span".new -local propagation = require "kong.tracing.propagation" +local propagation = require "kong.observability.tracing.propagation" local request_tags = require "kong.plugins.zipkin.request_tags" local kong_meta = require "kong.meta" local ngx_re = require "ngx.re" diff --git a/kong/reports.lua b/kong/reports.lua index 333a4fceb00..2ce5777b29f 100644 --- a/kong/reports.lua +++ b/kong/reports.lua @@ -303,6 +303,12 @@ function get_current_suffix(ctx) return nil end + -- 400 case is for invalid requests, eg: if a client sends a HTTP + -- request to a HTTPS port, it does not initialized any Nginx variables + if proxy_mode == "" and kong.response.get_status() == 400 then + return nil + end + log(WARN, "could not determine log suffix (scheme=", tostring(scheme), ", proxy_mode=", tostring(proxy_mode), ")") end diff --git a/kong/resty/dns/client.lua b/kong/resty/dns/client.lua index 03625790ee5..0c7359c54ea 100644 --- a/kong/resty/dns/client.lua +++ b/kong/resty/dns/client.lua @@ -1,3 +1,10 @@ +-- Use the new dns client library instead. If you want to switch to the original +-- one, you can set `legacy_dns_client = on` in kong.conf. +if ngx.shared.kong_dns_cache and not _G.busted_legacy_dns_client then + package.loaded["kong.dns.client"] = nil + return require("kong.dns.client") +end + -------------------------------------------------------------------------- -- DNS client. -- diff --git a/kong/runloop/balancer/init.lua b/kong/runloop/balancer/init.lua index 550c1055d84..51ad4872b5a 100644 --- a/kong/runloop/balancer/init.lua +++ b/kong/runloop/balancer/init.lua @@ -359,6 +359,16 @@ local function execute(balancer_data, ctx) balancer_data.balancer_handle = handle else + -- Note: balancer_data.retry_callback is only set by PDK once in access phase + -- if kong.service.set_target_retry_callback is called + if balancer_data.try_count ~= 0 and balancer_data.retry_callback then + local pok, perr, err = pcall(balancer_data.retry_callback) + if not pok or not perr then + log(ERR, "retry handler failed: ", err or perr) + return nil, "failure to get a peer from retry handler", 503 + end + end + -- have to do a regular DNS lookup local try_list local hstate = run_hook("balancer:to_ip:pre", balancer_data.host) diff --git a/kong/runloop/balancer/latency.lua b/kong/runloop/balancer/latency.lua index 323aff833d9..d47ac23d31b 100644 --- a/kong/runloop/balancer/latency.lua +++ b/kong/runloop/balancer/latency.lua @@ -32,12 +32,12 @@ local ewma = {} ewma.__index = ewma local function decay_ewma(ewma, last_touched_at, rtt, now) - local td = now - last_touched_at - td = (td > 0) and td or 0 - local weight = math_exp(-td / DECAY_TIME) - - ewma = ewma * weight + rtt * (1.0 - weight) - return ewma + local td = now - last_touched_at + td = (td > 0) and td or 0 + local weight = math_exp(-td / DECAY_TIME) + + ewma = ewma * weight + rtt * (1.0 - weight) + return ewma end @@ -47,30 +47,30 @@ end local function calculate_slow_start_ewma(self) local total_ewma = 0 local address_count = 0 - + for _, target in ipairs(self.balancer.targets) do - for _, address in ipairs(target.addresses) do - if address.available then - local ewma = self.ewma[address] or 0 - address_count = address_count + 1 - total_ewma = total_ewma + ewma - end + for _, address in ipairs(target.addresses) do + if address.available then + local ewma = self.ewma[address] or 0 + address_count = address_count + 1 + total_ewma = total_ewma + ewma end - end - - if address_count == 0 then - ngx_log(ngx_DEBUG, "no ewma value exists for the endpoints") - return nil end + end + + if address_count == 0 then + ngx_log(ngx_DEBUG, "no ewma value exists for the endpoints") + return nil + end - self.address_count = address_count - return total_ewma / address_count + self.address_count = address_count + return total_ewma / address_count end function ewma:afterHostUpdate() table_clear(new_addresses) - + for _, target in ipairs(self.balancer.targets) do for _, address in ipairs(target.addresses) do if address.available then @@ -117,7 +117,7 @@ local function get_or_update_ewma(self, address, rtt, update) end -function ewma:afterBalance(ctx, handle) +function ewma:afterBalance(_, handle) local ngx_var = ngx.var local response_time = tonumber(ngx_var.upstream_response_time) or 0 local connect_time = tonumber(ngx_var.upstream_connect_time) or 0 @@ -133,21 +133,21 @@ function ewma:afterBalance(ctx, handle) end -local function pick_and_score(self, address, k) +local function pick_and_score(self, addresses, k) local lowest_score_index = 1 - local lowest_score = get_or_update_ewma(self, address[lowest_score_index], 0, false) / address[lowest_score_index].weight + local lowest_score = get_or_update_ewma(self, addresses[lowest_score_index], 0, false) / addresses[lowest_score_index].weight for i = 2, k do - local new_score = get_or_update_ewma(self, address[i], 0, false) / address[i].weight + local new_score = get_or_update_ewma(self, addresses[i], 0, false) / addresses[i].weight if new_score < lowest_score then lowest_score_index = i lowest_score = new_score end end - return address[lowest_score_index], lowest_score + return addresses[lowest_score_index], lowest_score end -function ewma:getPeer(cache_only, handle, value_to_hash) +function ewma:getPeer(cache_only, handle) if handle then -- existing handle, so it's a retry handle.retryCount = handle.retryCount + 1 @@ -186,27 +186,27 @@ function ewma:getPeer(cache_only, handle, value_to_hash) -- retry end if address_count > 1 then local k = (address_count < PICK_SET_SIZE) and address_count or PICK_SET_SIZE - local filtered_address = {} - + local filtered_addresses = {} + for addr, ewma in pairs(self.ewma) do if not handle.failedAddresses[addr] then - table_insert(filtered_address, addr) + table_insert(filtered_addresses, addr) end end - - local filtered_address_num = table_nkeys(filtered_address) - if filtered_address_num == 0 then + + local filtered_addresses_num = table_nkeys(filtered_addresses) + if filtered_addresses_num == 0 then ngx_log(ngx_WARN, "all endpoints have been retried") return nil, balancers.errors.ERR_NO_PEERS_AVAILABLE end local score - if filtered_address_num > 1 then - k = filtered_address_num > k and filtered_address_num or k - address, score = pick_and_score(self, filtered_address, k) + if filtered_addresses_num > 1 then + k = filtered_addresses_num > k and filtered_addresses_num or k + address, score = pick_and_score(self, filtered_addresses, k) else - address = filtered_address[1] - score = get_or_update_ewma(self, filtered_address[1], 0, false) + address = filtered_addresses[1] + score = get_or_update_ewma(self, filtered_addresses[1], 0, false) end ngx_log(ngx_DEBUG, "get ewma score: ", score) end diff --git a/kong/runloop/events.lua b/kong/runloop/events.lua index ddc3e9750a4..7c4a7aaecf7 100644 --- a/kong/runloop/events.lua +++ b/kong/runloop/events.lua @@ -258,24 +258,33 @@ local function crud_plugins_handler(data) end -local function crud_snis_handler(data) - log(DEBUG, "[events] SNI updated, invalidating cached certificates") - - local sni = data.old_entity or data.entity - local sni_name = sni.name +local function invalidate_snis(sni_name) local sni_wild_pref, sni_wild_suf = certificate.produce_wild_snis(sni_name) core_cache:invalidate("snis:" .. sni_name) - if sni_wild_pref then + if sni_wild_pref and sni_wild_pref ~= sni_name then core_cache:invalidate("snis:" .. sni_wild_pref) end - if sni_wild_suf then + if sni_wild_suf and sni_wild_suf ~= sni_name then core_cache:invalidate("snis:" .. sni_wild_suf) end end +local function crud_snis_handler(data) + log(DEBUG, "[events] SNI updated, invalidating cached certificates") + + local new_name = data.entity.name + local old_name = data.old_entity and data.old_entity.name + + invalidate_snis(new_name) + if old_name and old_name ~= new_name then + invalidate_snis(old_name) + end +end + + local function crud_consumers_handler(data) workspaces.set_workspace(data.workspace) @@ -498,12 +507,18 @@ local stream_reconfigure_listener do local buffer = require "string.buffer" - -- `kong.configuration.prefix` is already normalized to an absolute path, - -- but `ngx.config.prefix()` is not - local PREFIX = kong and kong.configuration and - kong.configuration.prefix or - require("pl.path").abspath(ngx.config.prefix()) - local STREAM_CONFIG_SOCK = "unix:" .. PREFIX .. "/stream_config.sock" + -- this module may be loaded before `kong.configuration` is initialized + local socket_path = kong and kong.configuration + and kong.configuration.socket_path + + if not socket_path then + -- `kong.configuration.socket_path` is already normalized to an absolute + -- path, but `ngx.config.prefix()` is not + socket_path = require("pl.path").abspath(ngx.config.prefix() .. "/" + .. constants.SOCKET_DIRECTORY) + end + + local STREAM_CONFIG_SOCK = "unix:" .. socket_path .. "/stream_config.sock" local IS_HTTP_SUBSYSTEM = ngx.config.subsystem == "http" local function broadcast_reconfigure_event(data) diff --git a/kong/runloop/handler.lua b/kong/runloop/handler.lua index 1f3210d2a88..cd05327021a 100644 --- a/kong/runloop/handler.lua +++ b/kong/runloop/handler.lua @@ -11,12 +11,12 @@ local constants = require "kong.constants" local concurrency = require "kong.concurrency" local lrucache = require "resty.lrucache" local ktls = require "resty.kong.tls" -local request_id = require "kong.tracing.request_id" +local request_id = require "kong.observability.tracing.request_id" local PluginsIterator = require "kong.runloop.plugins_iterator" local log_level = require "kong.runloop.log_level" -local instrumentation = require "kong.tracing.instrumentation" +local instrumentation = require "kong.observability.tracing.instrumentation" local req_dyn_hook = require "kong.dynamic_hook" @@ -35,6 +35,7 @@ local gsub = string.gsub local find = string.find local lower = string.lower local fmt = string.format + local ngx = ngx local var = ngx.var local log = ngx.log @@ -71,6 +72,9 @@ local ROUTER_CACHE = lrucache.new(ROUTER_CACHE_SIZE) local ROUTER_CACHE_NEG = lrucache.new(ROUTER_CACHE_SIZE) +local DEFAULT_PROXY_HTTP_VERSION = "1.1" + + local NOOP = function() end @@ -892,11 +896,9 @@ return { init_worker = { before = function() - -- TODO: PR #9337 may affect the following line - local prefix = kong.configuration.prefix or ngx.config.prefix() - - STREAM_TLS_TERMINATE_SOCK = fmt("unix:%s/stream_tls_terminate.sock", prefix) - STREAM_TLS_PASSTHROUGH_SOCK = fmt("unix:%s/stream_tls_passthrough.sock", prefix) + local socket_path = kong.configuration.socket_path + STREAM_TLS_TERMINATE_SOCK = fmt("unix:%s/stream_tls_terminate.sock", socket_path) + STREAM_TLS_PASSTHROUGH_SOCK = fmt("unix:%s/stream_tls_passthrough.sock", socket_path) log_level.init_worker() @@ -1288,6 +1290,14 @@ return { var.upstream_x_forwarded_path = forwarded_path var.upstream_x_forwarded_prefix = forwarded_prefix + do + local req_via = get_header(constants.HEADERS.VIA, ctx) + local kong_inbound_via = protocol_version and protocol_version .. " " .. SERVER_HEADER + or SERVER_HEADER + var.upstream_via = req_via and req_via .. ", " .. kong_inbound_via + or kong_inbound_via + end + -- At this point, the router and `balancer_setup_stage1` have been -- executed; detect requests that need to be redirected from `proxy_pass` -- to `grpc_pass`. After redirection, this function will return early @@ -1477,7 +1487,31 @@ return { end if enabled_headers[headers.VIA] then - header[headers.VIA] = SERVER_HEADER + -- Kong does not support injected directives like 'nginx_location_proxy_http_version', + -- so we skip checking them. + + local proxy_http_version + + local upstream_scheme = var.upstream_scheme + if upstream_scheme == "grpc" or upstream_scheme == "grpcs" then + proxy_http_version = "2" + end + if not proxy_http_version then + proxy_http_version = ctx.proxy_http_version or + kong.configuration.proxy_http_version or + DEFAULT_PROXY_HTTP_VERSION + end + + local kong_outbound_via = proxy_http_version .. " " .. SERVER_HEADER + local resp_via = var["upstream_http_" .. headers.VIA] + header[headers.VIA] = resp_via and resp_via .. ", " .. kong_outbound_via + or kong_outbound_via + end + + -- If upstream does not provide the 'Server' header, an 'openresty' header + -- would be inserted by default. We override it with the Kong server header. + if not header[headers.SERVER] and enabled_headers[headers.SERVER] then + header[headers.SERVER] = SERVER_HEADER end else diff --git a/kong/runloop/plugin_servers/pb_rpc.lua b/kong/runloop/plugin_servers/pb_rpc.lua index 8dbb85857ab..d05b40ecb2f 100644 --- a/kong/runloop/plugin_servers/pb_rpc.lua +++ b/kong/runloop/plugin_servers/pb_rpc.lua @@ -409,9 +409,11 @@ function Rpc:handle_event(plugin_name, conf, phase) self.reset_instance(plugin_name, conf) kong.log.warn(err) return self:handle_event(plugin_name, conf, phase) - end - kong.log.err(err) + else + kong.log.err("pluginserver error: ", err or "unknown error") + kong.response.error(500) + end end end diff --git a/kong/runloop/plugins_iterator.lua b/kong/runloop/plugins_iterator.lua index 6fe55800403..78a6421011a 100644 --- a/kong/runloop/plugins_iterator.lua +++ b/kong/runloop/plugins_iterator.lua @@ -1,6 +1,7 @@ local workspaces = require "kong.workspaces" local constants = require "kong.constants" local tablepool = require "tablepool" +local req_dyn_hook = require "kong.dynamic_hook" local kong = kong @@ -17,6 +18,7 @@ local fetch_table = tablepool.fetch local release_table = tablepool.release local uuid = require("kong.tools.uuid").uuid local get_updated_monotonic_ms = require("kong.tools.time").get_updated_monotonic_ms +local req_dyn_hook_disable_by_default = req_dyn_hook.disable_by_default local TTL_ZERO = { ttl = 0 } @@ -428,6 +430,10 @@ end local function configure(configurable, ctx) + -- Disable hooks that are selectively enabled by plugins + -- in their :configure handler + req_dyn_hook_disable_by_default("observability_logs") + ctx = ctx or ngx.ctx local kong_global = require "kong.global" for _, plugin in ipairs(CONFIGURABLE_PLUGINS) do diff --git a/kong/templates/kong_defaults.lua b/kong/templates/kong_defaults.lua index ce532fd4b7c..b03980d9fb9 100644 --- a/kong/templates/kong_defaults.lua +++ b/kong/templates/kong_defaults.lua @@ -42,6 +42,7 @@ cluster_max_payload = 16777216 cluster_use_proxy = off cluster_dp_labels = NONE cluster_rpc = off +cluster_cjson = off lmdb_environment_path = dbless.lmdb lmdb_map_size = 2048m @@ -168,6 +169,7 @@ dns_cache_size = 10000 dns_not_found_ttl = 30 dns_error_ttl = 1 dns_no_sync = off +legacy_dns_client = off dedicated_config_processing = on worker_consistency = eventual diff --git a/kong/templates/nginx.lua b/kong/templates/nginx.lua index 108d268f56b..4833d9218c1 100644 --- a/kong/templates/nginx.lua +++ b/kong/templates/nginx.lua @@ -83,7 +83,7 @@ stream { > if cluster_ssl_tunnel then server { - listen unix:${{PREFIX}}/cluster_proxy_ssl_terminator.sock; + listen unix:${{SOCKET_PATH}}/cluster_proxy_ssl_terminator.sock; proxy_pass ${{cluster_ssl_tunnel}}; proxy_ssl on; diff --git a/kong/templates/nginx_kong.lua b/kong/templates/nginx_kong.lua index db83ba95782..c024a723a68 100644 --- a/kong/templates/nginx_kong.lua +++ b/kong/templates/nginx_kong.lua @@ -23,6 +23,10 @@ lua_shared_dict kong_db_cache ${{MEM_CACHE_SIZE}}; lua_shared_dict kong_db_cache_miss 12m; lua_shared_dict kong_secrets 5m; +> if not legacy_dns_client then +lua_shared_dict kong_dns_cache 5m; +> end + underscores_in_headers on; > if ssl_cipher_suite == 'old' then lua_ssl_conf_command CipherString DEFAULT:@SECLEVEL=0; @@ -39,6 +43,8 @@ ssl_ciphers ${{SSL_CIPHERS}}; $(el.name) $(el.value); > end +uninitialized_variable_warn off; + init_by_lua_block { > if test and coverage then require 'luacov' @@ -155,6 +161,7 @@ server { set $ctx_ref ''; set $upstream_te ''; + set $upstream_via ''; set $upstream_host ''; set $upstream_upgrade ''; set $upstream_connection ''; @@ -178,6 +185,7 @@ server { > end proxy_set_header TE $upstream_te; + proxy_set_header Via $upstream_via; proxy_set_header Host $upstream_host; proxy_set_header Upgrade $upstream_upgrade; proxy_set_header Connection $upstream_connection; @@ -212,6 +220,7 @@ server { proxy_request_buffering off; proxy_set_header TE $upstream_te; + proxy_set_header Via $upstream_via; proxy_set_header Host $upstream_host; proxy_set_header Upgrade $upstream_upgrade; proxy_set_header Connection $upstream_connection; @@ -246,6 +255,7 @@ server { proxy_request_buffering off; proxy_set_header TE $upstream_te; + proxy_set_header Via $upstream_via; proxy_set_header Host $upstream_host; proxy_set_header Upgrade $upstream_upgrade; proxy_set_header Connection $upstream_connection; @@ -280,6 +290,7 @@ server { proxy_request_buffering on; proxy_set_header TE $upstream_te; + proxy_set_header Via $upstream_via; proxy_set_header Host $upstream_host; proxy_set_header Upgrade $upstream_upgrade; proxy_set_header Connection $upstream_connection; @@ -310,6 +321,7 @@ server { set $kong_proxy_mode 'grpc'; grpc_set_header TE $upstream_te; + grpc_set_header Via $upstream_via; grpc_set_header X-Forwarded-For $upstream_x_forwarded_for; grpc_set_header X-Forwarded-Proto $upstream_x_forwarded_proto; grpc_set_header X-Forwarded-Host $upstream_x_forwarded_host; @@ -353,6 +365,7 @@ server { proxy_http_version 1.1; proxy_set_header TE $upstream_te; + proxy_set_header Via $upstream_via; proxy_set_header Host $upstream_host; proxy_set_header Upgrade $upstream_upgrade; proxy_set_header Connection $upstream_connection; @@ -379,9 +392,8 @@ server { location = /kong_error_handler { internal; - default_type ''; - uninitialized_variable_warn off; + default_type ''; rewrite_by_lua_block {;} access_by_lua_block {;} @@ -580,7 +592,7 @@ server { server { charset UTF-8; server_name kong_worker_events; - listen unix:${{PREFIX}}/worker_events.sock; + listen unix:${{SOCKET_PATH}}/worker_events.sock; access_log off; location / { content_by_lua_block { diff --git a/kong/templates/nginx_kong_stream.lua b/kong/templates/nginx_kong_stream.lua index 68a165110a8..4ff956caaf8 100644 --- a/kong/templates/nginx_kong_stream.lua +++ b/kong/templates/nginx_kong_stream.lua @@ -94,7 +94,7 @@ server { > end > if stream_proxy_ssl_enabled then - listen unix:${{PREFIX}}/stream_tls_terminate.sock ssl proxy_protocol; + listen unix:${{SOCKET_PATH}}/stream_tls_terminate.sock ssl proxy_protocol; > end access_log ${{PROXY_STREAM_ACCESS_LOG}}; @@ -175,7 +175,7 @@ server { } server { - listen unix:${{PREFIX}}/stream_tls_passthrough.sock proxy_protocol; + listen unix:${{SOCKET_PATH}}/stream_tls_passthrough.sock proxy_protocol; access_log ${{PROXY_STREAM_ACCESS_LOG}}; error_log ${{PROXY_STREAM_ERROR_LOG}} ${{LOG_LEVEL}}; @@ -205,7 +205,7 @@ server { > if database == "off" then server { - listen unix:${{PREFIX}}/stream_config.sock; + listen unix:${{SOCKET_PATH}}/stream_config.sock; error_log ${{ADMIN_ERROR_LOG}} ${{LOG_LEVEL}}; @@ -216,7 +216,7 @@ server { > end -- database == "off" server { # ignore (and close }, to ignore content) - listen unix:${{PREFIX}}/stream_rpc.sock; + listen unix:${{SOCKET_PATH}}/stream_rpc.sock; error_log ${{ADMIN_ERROR_LOG}} ${{LOG_LEVEL}}; content_by_lua_block { Kong.stream_api() @@ -225,7 +225,7 @@ server { # ignore (and close }, to ignore content) > end -- #stream_listeners > 0 server { - listen unix:${{PREFIX}}/stream_worker_events.sock; + listen unix:${{SOCKET_PATH}}/stream_worker_events.sock; error_log ${{ADMIN_ERROR_LOG}} ${{LOG_LEVEL}}; access_log off; content_by_lua_block { diff --git a/kong/timing/init.lua b/kong/timing/init.lua index 6b88a4dd67b..39b253fb1c4 100644 --- a/kong/timing/init.lua +++ b/kong/timing/init.lua @@ -11,7 +11,7 @@ local assert = assert local ipairs = ipairs local string_format = string.format -local request_id_get = require("kong.tracing.request_id").get +local request_id_get = require("kong.observability.tracing.request_id").get local FILTER_ALL_PHASES = { ssl_cert = nil, -- NYI @@ -222,7 +222,7 @@ function _M.init_worker(is_enabled) enabled = is_enabled and ngx.config.subsystem == "http" if enabled then - req_dyn_hook.always_enable("timing:auth") + req_dyn_hook.enable_by_default("timing:auth") end end diff --git a/kong/tools/aws_stream.lua b/kong/tools/aws_stream.lua new file mode 100644 index 00000000000..ebefc2c2656 --- /dev/null +++ b/kong/tools/aws_stream.lua @@ -0,0 +1,181 @@ +--- Stream class. +-- Decodes AWS response-stream types, currently application/vnd.amazon.eventstream +-- @classmod Stream + +local buf = require("string.buffer") +local to_hex = require("resty.string").to_hex + +local Stream = {} +Stream.__index = Stream + + +local _HEADER_EXTRACTORS = { + -- bool true + [0] = function(stream) + return true, 0 + end, + + -- bool false + [1] = function(stream) + return false, 0 + end, + + -- string type + [7] = function(stream) + local header_value_len = stream:next_int(16) + return stream:next_utf_8(header_value_len), header_value_len + 2 -- add the 2 bits read for the length + end, + + -- TODO ADD THE REST OF THE DATA TYPES + -- EVEN THOUGH THEY'RE NOT REALLY USED +} + +--- Constructor. +-- @function aws:Stream +-- @param chunk string complete AWS response stream chunk for decoding +-- @param is_hex boolean specify if the chunk bytes are already decoded to hex +-- @usage +-- local stream_parser = stream:new("00000120af0310f.......", true) +-- local next, err = stream_parser:next_message() +function Stream:new(chunk, is_hex) + local self = {} -- override 'self' to be the new object/class + setmetatable(self, Stream) + + if #chunk < ((is_hex and 32) or 16) then + return nil, "cannot parse a chunk less than 16 bytes long" + end + + self.read_count = 0 + self.chunk = buf.new() + self.chunk:put((is_hex and chunk) or to_hex(chunk)) + + return self +end + + +--- return the next `count` ascii bytes from the front of the chunk +--- and then trims the chunk of those bytes +-- @param count number whole utf-8 bytes to return +-- @return string resulting utf-8 string +function Stream:next_utf_8(count) + local utf_bytes = self:next_bytes(count) + + local ascii_string = "" + for i = 1, #utf_bytes, 2 do + local hex_byte = utf_bytes:sub(i, i + 1) + local ascii_byte = string.char(tonumber(hex_byte, 16)) + ascii_string = ascii_string .. ascii_byte + end + return ascii_string +end + +--- returns the next `count` bytes from the front of the chunk +--- and then trims the chunk of those bytes +-- @param count number whole integer of bytes to return +-- @return string hex-encoded next `count` bytes +function Stream:next_bytes(count) + if not self.chunk then + return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)" + end + + local bytes = self.chunk:get(count * 2) + self.read_count = (count) + self.read_count + + return bytes +end + +--- returns the next unsigned int from the front of the chunk +--- and then trims the chunk of those bytes +-- @param size integer bit length (8, 16, 32, etc) +-- @return number whole integer of size specified +-- @return string the original bytes, for reference/checksums +function Stream:next_int(size) + if not self.chunk then + return nil, nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)" + end + + if size < 8 then + return nil, nil, "cannot work on integers smaller than 8 bits long" + end + + local int, err = self:next_bytes(size / 8) + if err then + return nil, nil, err + end + + return tonumber(int, 16), int +end + +--- returns the next message in the chunk, as a table. +--- can be used as an iterator. +-- @return table formatted next message from the given constructor chunk +function Stream:next_message() + if not self.chunk then + return nil, "function cannot be called on its own - initialise a chunk reader with :new(chunk)" + end + + if #self.chunk < 1 then + return false + end + + -- get the message length and pull that many bytes + -- + -- this is a chicken and egg problem, because we need to + -- read the message to get the length, to then re-read the + -- whole message at correct offset + local msg_len, _, err = self:next_int(32) + if err then + return err + end + + -- get the headers length + local headers_len, _, err = self:next_int(32) + if err then + return err + end + + -- get the preamble checksum + -- skip it because we're not using UDP + self:next_int(32) + + -- pull the headers from the buf + local headers = {} + local headers_bytes_read = 0 + + while headers_bytes_read < headers_len do + -- the next 8-bit int is the "header key length" + local header_key_len = self:next_int(8) + local header_key = self:next_utf_8(header_key_len) + headers_bytes_read = 1 + header_key_len + headers_bytes_read + + -- next 8-bits is the header type, which is an enum + local header_type = self:next_int(8) + headers_bytes_read = 1 + headers_bytes_read + + -- depending on the header type, depends on how long the header should max out at + local header_value, header_value_len = _HEADER_EXTRACTORS[header_type](self) + headers_bytes_read = header_value_len + headers_bytes_read + + headers[header_key] = header_value + end + + -- finally, extract the body as a string by + -- subtracting what's read so far from the + -- total length obtained right at the start + local body = self:next_utf_8(msg_len - self.read_count - 4) + + -- last 4 bytes is a body checksum + -- skip it because we're not using UDP + self:next_int(32) + + + -- rewind the tape + self.read_count = 0 + + return { + headers = headers, + body = body, + } +end + +return Stream \ No newline at end of file diff --git a/kong/tools/gzip.lua b/kong/tools/gzip.lua index 16c8906683c..54c3d9f81fd 100644 --- a/kong/tools/gzip.lua +++ b/kong/tools/gzip.lua @@ -1,5 +1,6 @@ local buffer = require "string.buffer" local zlib = require "ffi-zlib" +local yield = require("kong.tools.yield").yield local inflate_gzip = zlib.inflateGzip @@ -15,7 +16,16 @@ local GZIP_CHUNK_SIZE = 65535 local function read_input_buffer(input_buffer) + local count = 0 + local yield_size = GZIP_CHUNK_SIZE * 2 + return function(size) + count = count + size + if count > yield_size then + count = 0 + yield() + end + local data = input_buffer:get(size) return data ~= "" and data or nil end diff --git a/kong/tools/request_aware_table.lua b/kong/tools/request_aware_table.lua index c2c88e0ea0a..beaa08f4b6e 100644 --- a/kong/tools/request_aware_table.lua +++ b/kong/tools/request_aware_table.lua @@ -3,7 +3,7 @@ local table_new = require("table.new") local table_clear = require("table.clear") -local get_request_id = require("kong.tracing.request_id").get +local get_request_id = require("kong.observability.tracing.request_id").get -- set in new() diff --git a/kong/tools/stream_api.lua b/kong/tools/stream_api.lua index 1710487552b..ac8d7c09b77 100644 --- a/kong/tools/stream_api.lua +++ b/kong/tools/stream_api.lua @@ -3,6 +3,7 @@ -- may changed or be removed in the future Kong releases once a better mechanism -- for inter subsystem communication in OpenResty became available. +local constants = require "kong.constants" local lpack = require "lua_pack" local kong = kong @@ -37,7 +38,9 @@ local MAX_DATA_LEN = 2^22 - 1 local HEADER_LEN = #st_pack(PACK_F, MAX_KEY_LEN, MAX_DATA_LEN) -local SOCKET_PATH = "unix:" .. ngx.config.prefix() .. "/stream_rpc.sock" +-- this module may be loaded before `kong.configuration` is initialized +local SOCKET_PATH = "unix:" .. ngx.config.prefix() .. "/" + .. constants.SOCKET_DIRECTORY .. "/stream_rpc.sock" local stream_api = {} diff --git a/kong/tools/string.lua b/kong/tools/string.lua index ef2d844e62d..4c12e571583 100644 --- a/kong/tools/string.lua +++ b/kong/tools/string.lua @@ -151,6 +151,35 @@ function _M.bytes_to_str(bytes, unit, scale) end +local SCALES = { + k = 1024, + K = 1024, + m = 1024 * 1024, + M = 1024 * 1024, + g = 1024 * 1024 * 1024, + G = 1024 * 1024 * 1024, +} + +function _M.parse_ngx_size(str) + assert(type(str) == "string", "Parameter #1 must be a string") + + local len = #str + local unit = sub(str, len) + local scale = SCALES[unit] + + if scale then + len = len - 1 + + else + scale = 1 + end + + local size = tonumber(sub(str, 1, len)) or 0 + + return size * scale +end + + local try_decode_base64 do local decode_base64 = ngx.decode_base64 diff --git a/scripts/explain_manifest/config.py b/scripts/explain_manifest/config.py index 131f61dad27..ceeab15b376 100644 --- a/scripts/explain_manifest/config.py +++ b/scripts/explain_manifest/config.py @@ -38,25 +38,14 @@ def transform(f: FileInfo): # - https://repology.org/project/gcc/versions # TODO: libstdc++ verions targets = { - "alpine-amd64": ExpectSuite( - name="Alpine Linux (amd64)", - manifest="fixtures/alpine-amd64.txt", - use_rpath=True, - tests={ - common_suites: {}, - libc_libcpp_suites: { - # alpine 3.16: gcc 11.2.1 - "libcxx_max_version": "3.4.29", - "cxxabi_max_version": "1.3.13", - }, - } - ), "amazonlinux-2-amd64": ExpectSuite( name="Amazon Linux 2 (amd64)", manifest="fixtures/amazonlinux-2-amd64.txt", use_rpath=True, tests={ - common_suites: {}, + common_suites: { + "skip_libsimdjson_ffi": True, + }, libc_libcpp_suites: { "libc_max_version": "2.26", # gcc 7.3.1 @@ -80,20 +69,6 @@ def transform(f: FileInfo): }, }, ), - "el7-amd64": ExpectSuite( - name="Redhat 7 (amd64)", - manifest="fixtures/el7-amd64.txt", - use_rpath=True, - tests={ - common_suites: {}, - libc_libcpp_suites: { - "libc_max_version": "2.17", - # gcc 4.8.5 - "libcxx_max_version": "3.4.19", - "cxxabi_max_version": "1.3.7", - }, - } - ), "el8-amd64": ExpectSuite( name="Redhat 8 (amd64)", manifest="fixtures/el8-amd64.txt", @@ -150,19 +125,6 @@ def transform(f: FileInfo): }, } ), - "debian-10-amd64": ExpectSuite( - name="Debian 10 (amd64)", - manifest="fixtures/debian-10-amd64.txt", - tests={ - common_suites: {}, - libc_libcpp_suites: { - "libc_max_version": "2.28", - # gcc 8.3.0 - "libcxx_max_version": "3.4.25", - "cxxabi_max_version": "1.3.11", - }, - } - ), "debian-11-amd64": ExpectSuite( name="Debian 11 (amd64)", manifest="fixtures/debian-11-amd64.txt", diff --git a/scripts/explain_manifest/docker_image_filelist.txt b/scripts/explain_manifest/docker_image_filelist.txt index 4ecad80ed00..6f4024ba008 100644 --- a/scripts/explain_manifest/docker_image_filelist.txt +++ b/scripts/explain_manifest/docker_image_filelist.txt @@ -3,6 +3,7 @@ /usr/local/kong/** /usr/local/bin/kong /usr/local/bin/luarocks +/usr/local/bin/luarocks-admin /usr/local/etc/luarocks/** /usr/local/lib/lua/** /usr/local/lib/luarocks/** diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt index b5e26365095..f12655a7fe8 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2-amd64.txt @@ -13,78 +13,78 @@ - Path : /usr/local/kong/lib/engines-3/afalg.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/engines-3/capi.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/engines-3/loader_attic.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/engines-3/padlock.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/libcrypto.so.3 Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/libexpat.so.1.9.2 Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libc.so.6 - Path : /usr/local/kong/lib/libsnappy.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libgcc_s.so.1 - libc.so.6 - Path : /usr/local/kong/lib/libssl.so.3 Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/kong/lib/ossl-modules/legacy.so Needed : - - libstdc++.so.6 - libm.so.6 + - libstdc++.so.6 - libcrypto.so.3 - libdl.so.2 - libc.so.6 - Runpath : /usr/local/kong/lib + Rpath : /usr/local/kong/lib - Path : /usr/local/lib/lua/5.1/lfs.so Needed : @@ -199,7 +199,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -208,9 +208,6 @@ - libgcc_s.so.1 - librt.so.1 - libpthread.so.0 - - libm.so.6 - libdl.so.2 - libc.so.6 - ld-linux-x86-64.so.2 - - libstdc++.so.6 - diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt index b940c8e8889..eae65953bb5 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2023-amd64.txt @@ -188,7 +188,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -199,3 +199,10 @@ - ld-linux-x86-64.so.2 - libstdc++.so.6 - libm.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt b/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt index 193dd354ecd..25d461da71a 100644 --- a/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt +++ b/scripts/explain_manifest/fixtures/amazonlinux-2023-arm64.txt @@ -195,7 +195,7 @@ - lua-resty-events - lua-resty-lmdb - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -203,3 +203,11 @@ Needed : - libgcc_s.so.1 - libc.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libm.so.6 + - libstdc++.so.6 + - libgcc_s.so.1 + - libc.so.6 + - ld-linux-aarch64.so.1 diff --git a/scripts/explain_manifest/fixtures/debian-10-amd64.txt b/scripts/explain_manifest/fixtures/debian-10-amd64.txt deleted file mode 100644 index 15367259a79..00000000000 --- a/scripts/explain_manifest/fixtures/debian-10-amd64.txt +++ /dev/null @@ -1,215 +0,0 @@ -- Path : /etc/kong/kong.logrotate - -- Path : /lib/systemd/system/kong.service - -- Path : /usr/local/kong/gui - Type : directory - -- Path : /usr/local/kong/include/google - Type : directory - -- Path : /usr/local/kong/include/kong - Type : directory - -- Path : /usr/local/kong/lib/engines-3/afalg.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/capi.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/loader_attic.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/padlock.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/libcrypto.so.3 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/libexpat.so.1.9.2 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libc.so.6 - -- Path : /usr/local/kong/lib/libsnappy.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libgcc_s.so.1 - - libc.so.6 - -- Path : /usr/local/kong/lib/libssl.so.3 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/ossl-modules/legacy.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lfs.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lpeg.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lsyslog.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lua_pack.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lua_system_constants.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lxp.so - Needed : - - libexpat.so.1 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/mime/core.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/pb.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/core.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/serial.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/unix.so - Needed : - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/ssl.so - Needed : - - libssl.so.3 - - libcrypto.so.3 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/yaml.so - Needed : - - libyaml-0.so.2 - - libc.so.6 - -- Path : /usr/local/openresty/lualib/cjson.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/librestysignal.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/rds/parser.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/redis/parser.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/nginx/modules/ngx_wasmx_module.so - Needed : - - libdl.so.2 - - libm.so.6 - - libpthread.so.0 - - libgcc_s.so.1 - - libc.so.6 - - ld-linux-x86-64.so.2 - Runpath : /usr/local/openresty/luajit/lib:/usr/local/kong/lib:/usr/local/openresty/lualib - -- Path : /usr/local/openresty/nginx/sbin/nginx - Needed : - - libdl.so.2 - - libpthread.so.0 - - libcrypt.so.1 - - libluajit-5.1.so.2 - - libm.so.6 - - libssl.so.3 - - libcrypto.so.3 - - libz.so.1 - - libc.so.6 - Runpath : /usr/local/openresty/luajit/lib:/usr/local/kong/lib:/usr/local/openresty/lualib - Modules : - - lua-kong-nginx-module - - lua-kong-nginx-module/stream - - lua-resty-events - - lua-resty-lmdb - - ngx_brotli - - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 - DWARF : True - DWARF - ngx_http_request_t related DWARF DIEs: True - -- Path : /usr/local/openresty/site/lualib/libatc_router.so - Needed : - - libgcc_s.so.1 - - librt.so.1 - - libpthread.so.0 - - libm.so.6 - - libdl.so.2 - - libc.so.6 - - ld-linux-x86-64.so.2 - - libstdc++.so.6 diff --git a/scripts/explain_manifest/fixtures/debian-11-amd64.txt b/scripts/explain_manifest/fixtures/debian-11-amd64.txt index 846b95ad5a6..5bf0749717f 100644 --- a/scripts/explain_manifest/fixtures/debian-11-amd64.txt +++ b/scripts/explain_manifest/fixtures/debian-11-amd64.txt @@ -189,7 +189,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -202,3 +202,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/debian-12-amd64.txt b/scripts/explain_manifest/fixtures/debian-12-amd64.txt index 1ab0eabe5b1..5377277e4c9 100644 --- a/scripts/explain_manifest/fixtures/debian-12-amd64.txt +++ b/scripts/explain_manifest/fixtures/debian-12-amd64.txt @@ -178,7 +178,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -189,3 +189,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/el7-amd64.txt b/scripts/explain_manifest/fixtures/el7-amd64.txt deleted file mode 100644 index 2d8ff671edd..00000000000 --- a/scripts/explain_manifest/fixtures/el7-amd64.txt +++ /dev/null @@ -1,214 +0,0 @@ -- Path : /etc/kong/kong.logrotate - -- Path : /lib/systemd/system/kong.service - -- Path : /usr/local/kong/gui - Type : directory - -- Path : /usr/local/kong/include/google - Type : directory - -- Path : /usr/local/kong/include/kong - Type : directory - -- Path : /usr/local/kong/lib/engines-3/afalg.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/capi.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/loader_attic.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/engines-3/padlock.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/libcrypto.so.3 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/libexpat.so.1.9.2 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libc.so.6 - -- Path : /usr/local/kong/lib/libsnappy.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libgcc_s.so.1 - - libc.so.6 - -- Path : /usr/local/kong/lib/libssl.so.3 - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/kong/lib/ossl-modules/legacy.so - Needed : - - libstdc++.so.6 - - libm.so.6 - - libcrypto.so.3 - - libdl.so.2 - - libc.so.6 - Runpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lfs.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lpeg.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lsyslog.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lua_pack.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lua_system_constants.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/lxp.so - Needed : - - libexpat.so.1 - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/mime/core.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/pb.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/core.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/serial.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/socket/unix.so - Needed : - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/ssl.so - Needed : - - libssl.so.3 - - libcrypto.so.3 - - libc.so.6 - Rpath : /usr/local/kong/lib - -- Path : /usr/local/lib/lua/5.1/yaml.so - Needed : - - libyaml-0.so.2 - - libc.so.6 - -- Path : /usr/local/openresty/lualib/cjson.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/librestysignal.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/rds/parser.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/lualib/redis/parser.so - Needed : - - libc.so.6 - -- Path : /usr/local/openresty/nginx/modules/ngx_wasmx_module.so - Needed : - - libdl.so.2 - - libm.so.6 - - libpthread.so.0 - - libgcc_s.so.1 - - libc.so.6 - - ld-linux-x86-64.so.2 - Rpath : /usr/local/openresty/luajit/lib:/usr/local/kong/lib:/usr/local/openresty/lualib - -- Path : /usr/local/openresty/nginx/sbin/nginx - Needed : - - libdl.so.2 - - libpthread.so.0 - - libcrypt.so.1 - - libluajit-5.1.so.2 - - libm.so.6 - - libssl.so.3 - - libcrypto.so.3 - - libz.so.1 - - libc.so.6 - Rpath : /usr/local/openresty/luajit/lib:/usr/local/kong/lib:/usr/local/openresty/lualib - Modules : - - lua-kong-nginx-module - - lua-kong-nginx-module/stream - - lua-resty-events - - lua-resty-lmdb - - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 - DWARF : True - DWARF - ngx_http_request_t related DWARF DIEs: True - -- Path : /usr/local/openresty/site/lualib/libatc_router.so - Needed : - - libgcc_s.so.1 - - librt.so.1 - - libpthread.so.0 - - libm.so.6 - - libdl.so.2 - - libc.so.6 - - ld-linux-x86-64.so.2 - - libstdc++.so.6 diff --git a/scripts/explain_manifest/fixtures/el8-amd64.txt b/scripts/explain_manifest/fixtures/el8-amd64.txt index ca3351b3a11..483d1aae710 100644 --- a/scripts/explain_manifest/fixtures/el8-amd64.txt +++ b/scripts/explain_manifest/fixtures/el8-amd64.txt @@ -199,7 +199,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -212,3 +212,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/el9-amd64.txt b/scripts/explain_manifest/fixtures/el9-amd64.txt index 7457649228b..e8578693951 100644 --- a/scripts/explain_manifest/fixtures/el9-amd64.txt +++ b/scripts/explain_manifest/fixtures/el9-amd64.txt @@ -188,7 +188,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -199,3 +199,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/el9-arm64.txt b/scripts/explain_manifest/fixtures/el9-arm64.txt index 193dd354ecd..25d461da71a 100644 --- a/scripts/explain_manifest/fixtures/el9-arm64.txt +++ b/scripts/explain_manifest/fixtures/el9-arm64.txt @@ -195,7 +195,7 @@ - lua-resty-events - lua-resty-lmdb - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -203,3 +203,11 @@ Needed : - libgcc_s.so.1 - libc.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libm.so.6 + - libstdc++.so.6 + - libgcc_s.so.1 + - libc.so.6 + - ld-linux-aarch64.so.1 diff --git a/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt b/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt index 41172c07781..41a5961de8c 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-20.04-amd64.txt @@ -193,7 +193,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -206,3 +206,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt b/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt index bee32048e1f..372d8b99d49 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-22.04-amd64.txt @@ -182,7 +182,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -193,3 +193,10 @@ - libc.so.6 - ld-linux-x86-64.so.2 - libstdc++.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libm.so.6 + - libgcc_s.so.1 + - libc.so.6 diff --git a/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt b/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt index 916b90bf1d3..89d10983a04 100644 --- a/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt +++ b/scripts/explain_manifest/fixtures/ubuntu-22.04-arm64.txt @@ -183,7 +183,7 @@ - lua-resty-lmdb - ngx_brotli - ngx_wasmx_module - OpenSSL : OpenSSL 3.2.1 30 Jan 2024 + OpenSSL : OpenSSL 3.2.2 4 Jun 2024 DWARF : True DWARF - ngx_http_request_t related DWARF DIEs: True @@ -191,3 +191,10 @@ Needed : - libgcc_s.so.1 - libc.so.6 + +- Path : /usr/local/openresty/site/lualib/libsimdjson_ffi.so + Needed : + - libstdc++.so.6 + - libgcc_s.so.1 + - libc.so.6 + - ld-linux-aarch64.so.1 diff --git a/scripts/explain_manifest/suites.py b/scripts/explain_manifest/suites.py index 85238d56517..e17854c044f 100644 --- a/scripts/explain_manifest/suites.py +++ b/scripts/explain_manifest/suites.py @@ -20,7 +20,7 @@ def read_requirements(path=None): lines = [re.findall("(.+)=([^# ]+)", d) for d in f.readlines()] return {l[0][0]: l[0][1].strip() for l in lines if l} -def common_suites(expect, libxcrypt_no_obsolete_api: bool = False): +def common_suites(expect, libxcrypt_no_obsolete_api: bool = False, skip_libsimdjson_ffi: bool = False): # file existence expect("/usr/local/kong/include/google/protobuf/**.proto", "includes Google protobuf headers").exists() @@ -82,6 +82,11 @@ def common_suites(expect, libxcrypt_no_obsolete_api: bool = False): .functions \ .contain("router_execute") + if not skip_libsimdjson_ffi: + expect("/usr/local/openresty/site/lualib/libsimdjson_ffi.so", "simdjson should have ffi module compiled") \ + .functions \ + .contain("simdjson_ffi_state_new") + if libxcrypt_no_obsolete_api: expect("/usr/local/openresty/nginx/sbin/nginx", "nginx linked with libxcrypt.so.2") \ .needed_libraries.contain("libcrypt.so.2") @@ -143,6 +148,7 @@ def docker_suites(expect): .gid.equals(0) for path in ("/usr/local/bin/luarocks", + "/usr/local/bin/luarocks-admin", "/usr/local/etc/luarocks/**", "/usr/local/lib/lua/**", "/usr/local/lib/luarocks/**", diff --git a/scripts/release-kong.sh b/scripts/release-kong.sh index aea7858a18c..bf9cf8877c4 100755 --- a/scripts/release-kong.sh +++ b/scripts/release-kong.sh @@ -87,10 +87,6 @@ function push_package () { # TODO: CE gateway-src - if [ "$ARTIFACT_TYPE" == "alpine" ]; then - dist_version= - fi - if [ "$ARTIFACT_VERSION" == "18.04" ]; then dist_version="--dist-version bionic" fi diff --git a/spec/01-unit/01-db/01-schema/06-routes_spec.lua b/spec/01-unit/01-db/01-schema/06-routes_spec.lua index 9b97a031641..50a1bd73329 100644 --- a/spec/01-unit/01-db/01-schema/06-routes_spec.lua +++ b/spec/01-unit/01-db/01-schema/06-routes_spec.lua @@ -621,17 +621,6 @@ describe("routes schema (flavor = " .. flavor .. ")", function() assert.falsy(ok) assert.equal("length must be at least 1", err.headers[1]) end) - - it("value must be a plain pattern or a valid regex pattern", function() - local route = { - headers = { location = { "~[" } }, - protocols = { "http" }, - } - - local ok, err = Routes:validate(route) - assert.falsy(ok) - assert.match("invalid regex", err.headers[1]) - end) end) describe("methods attribute", function() diff --git a/spec/01-unit/01-db/03-arguments_spec.lua b/spec/01-unit/01-db/03-arguments_spec.lua index 332500ad70a..00c443bae23 100644 --- a/spec/01-unit/01-db/03-arguments_spec.lua +++ b/spec/01-unit/01-db/03-arguments_spec.lua @@ -1,366 +1,703 @@ -local arguments = require "kong.api.arguments" local Schema = require "kong.db.schema" local helpers = require "spec.helpers" +local deep_sort = helpers.deep_sort -local infer_value = arguments.infer_value -local infer = arguments.infer -local decode_arg = arguments.decode_arg -local decode = arguments.decode -local combine = arguments.combine -local deep_sort = helpers.deep_sort +describe("arguments tests", function() + local arguments, arguments_decoder, infer_value, infer, decode, combine, old_get_method + local decode_arg, get_param_name_and_keys, nest_path, decode_map_array_arg -describe("arguments.infer_value", function() - it("infers numbers", function() - assert.equal(2, infer_value("2", { type = "number" })) - assert.equal(2, infer_value("2", { type = "integer" })) - assert.equal(2.5, infer_value("2.5", { type = "number" })) - assert.equal(2.5, infer_value("2.5", { type = "integer" })) -- notice that integers are not rounded - end) + lazy_setup(function() + old_get_method = _G.ngx.req.get_method + _G.ngx.req.get_method = function() return "POST" end - it("infers booleans", function() - assert.equal(false, infer_value("false", { type = "boolean" })) - assert.equal(true, infer_value("true", { type = "boolean" })) - end) + package.loaded["kong.api.arguments"] = nil + package.loaded["kong.api.arguments_decoder"] = nil - it("infers arrays and sets", function() - assert.same({ "a" }, infer_value("a", { type = "array", elements = { type = "string" } })) - assert.same({ 2 }, infer_value("2", { type = "array", elements = { type = "number" } })) - assert.same({ "a" }, infer_value({"a"}, { type = "array", elements = { type = "string" } })) - assert.same({ 2 }, infer_value({"2"}, { type = "array", elements = { type = "number" } })) + arguments = require "kong.api.arguments" + infer_value = arguments.infer_value + decode = arguments.decode + infer = arguments._infer + combine = arguments._combine - assert.same({ "a" }, infer_value("a", { type = "set", elements = { type = "string" } })) - assert.same({ 2 }, infer_value("2", { type = "set", elements = { type = "number" } })) - assert.same({ "a" }, infer_value({"a"}, { type = "set", elements = { type = "string" } })) - assert.same({ 2 }, infer_value({"2"}, { type = "set", elements = { type = "number" } })) + arguments_decoder = require "kong.api.arguments_decoder" + decode_arg = arguments_decoder._decode_arg + get_param_name_and_keys = arguments_decoder._get_param_name_and_keys + nest_path = arguments_decoder._nest_path + decode_map_array_arg = arguments_decoder._decode_map_array_arg end) - it("infers nulls from empty strings", function() - assert.equal(ngx.null, infer_value("", { type = "string" })) - assert.equal(ngx.null, infer_value("", { type = "array" })) - assert.equal(ngx.null, infer_value("", { type = "set" })) - assert.equal(ngx.null, infer_value("", { type = "number" })) - assert.equal(ngx.null, infer_value("", { type = "integer" })) - assert.equal(ngx.null, infer_value("", { type = "boolean" })) - assert.equal(ngx.null, infer_value("", { type = "foreign" })) - assert.equal(ngx.null, infer_value("", { type = "map" })) - assert.equal(ngx.null, infer_value("", { type = "record" })) + lazy_teardown(function() + _G.ngx.req.get_method = old_get_method end) - it("doesn't infer nulls from empty strings on unknown types", function() - assert.equal("", infer_value("")) + describe("arguments_decoder.infer_value", function() + it("infers numbers", function() + assert.equal(2, infer_value("2", { type = "number" })) + assert.equal(2, infer_value("2", { type = "integer" })) + assert.equal(2.5, infer_value("2.5", { type = "number" })) + assert.equal(2.5, infer_value("2.5", { type = "integer" })) -- notice that integers are not rounded + end) + + it("infers booleans", function() + assert.equal(false, infer_value("false", { type = "boolean" })) + assert.equal(true, infer_value("true", { type = "boolean" })) + end) + + it("infers arrays and sets", function() + assert.same({ "a" }, infer_value("a", { type = "array", elements = { type = "string" } })) + assert.same({ 2 }, infer_value("2", { type = "array", elements = { type = "number" } })) + assert.same({ "a" }, infer_value({"a"}, { type = "array", elements = { type = "string" } })) + assert.same({ 2 }, infer_value({"2"}, { type = "array", elements = { type = "number" } })) + + assert.same({ "a" }, infer_value("a", { type = "set", elements = { type = "string" } })) + assert.same({ 2 }, infer_value("2", { type = "set", elements = { type = "number" } })) + assert.same({ "a" }, infer_value({"a"}, { type = "set", elements = { type = "string" } })) + assert.same({ 2 }, infer_value({"2"}, { type = "set", elements = { type = "number" } })) + end) + + it("infers nulls from empty strings", function() + assert.equal(ngx.null, infer_value("", { type = "string" })) + assert.equal(ngx.null, infer_value("", { type = "array" })) + assert.equal(ngx.null, infer_value("", { type = "set" })) + assert.equal(ngx.null, infer_value("", { type = "number" })) + assert.equal(ngx.null, infer_value("", { type = "integer" })) + assert.equal(ngx.null, infer_value("", { type = "boolean" })) + assert.equal(ngx.null, infer_value("", { type = "foreign" })) + assert.equal(ngx.null, infer_value("", { type = "map" })) + assert.equal(ngx.null, infer_value("", { type = "record" })) + end) + + it("doesn't infer nulls from empty strings on unknown types", function() + assert.equal("", infer_value("")) + end) + + it("infers maps", function() + assert.same({ x = "1" }, infer_value({ x = "1" }, { type = "map", keys = { type = "string" }, values = { type = "string" } })) + assert.same({ x = 1 }, infer_value({ x = "1" }, { type = "map", keys = { type = "string" }, values = { type = "number" } })) + end) + + it("infers records", function() + assert.same({ age = "1" }, infer_value({ age = "1" }, + { type = "record", fields = {{ age = { type = "string" } } }})) + assert.same({ age = 1 }, infer_value({ age = "1" }, + { type = "record", fields = {{ age = { type = "number" } } }})) + end) + + it("returns the provided value when inferring is not possible", function() + assert.equal("not number", infer_value("not number", { type = "number" })) + assert.equal("not integer", infer_value("not integer", { type = "integer" })) + assert.equal("not boolean", infer_value("not boolean", { type = "boolean" })) + end) end) - it("infers maps", function() - assert.same({ x = "1" }, infer_value({ x = "1" }, { type = "map", keys = { type = "string" }, values = { type = "string" } })) - assert.same({ x = 1 }, infer_value({ x = "1" }, { type = "map", keys = { type = "string" }, values = { type = "number" } })) - end) - it("infers records", function() - assert.same({ age = "1" }, infer_value({ age = "1" }, - { type = "record", fields = {{ age = { type = "string" } } }})) - assert.same({ age = 1 }, infer_value({ age = "1" }, - { type = "record", fields = {{ age = { type = "number" } } }})) - end) + describe("arguments_decoder.infer", function() + it("returns nil for nil args", function() + assert.is_nil(infer()) + end) - it("returns the provided value when inferring is not possible", function() - assert.equal("not number", infer_value("not number", { type = "number" })) - assert.equal("not integer", infer_value("not integer", { type = "integer" })) - assert.equal("not boolean", infer_value("not boolean", { type = "boolean" })) - end) -end) + it("does no inferring without schema", function() + assert.same("args", infer("args")) + end) + it("infers every field using the schema", function() + local schema = Schema.new({ + fields = { + { name = { type = "string" } }, + { age = { type = "number" } }, + { has_license = { type = "boolean" } }, + { aliases = { type = "set", elements = { type = { "string" } } } }, + { comments = { type = "string" } }, + } + }) + + local args = { name = "peter", + age = "45", + has_license = "true", + aliases = "peta", + comments = "" } + assert.same({ + name = "peter", + age = 45, + has_license = true, + aliases = { "peta" }, + comments = ngx.null + }, infer(args, schema)) + end) + + it("infers shorthand_fields but does not run the func", function() + local schema = Schema.new({ + fields = { + { name = { type = "string" } }, + { another_array = { type = "array", elements = { type = { "string" } } } }, + }, + shorthand_fields = { + { an_array = { + type = "array", + elements = { type = { "string" } }, + func = function(value) + return { another_array = value:upper() } + end, + } + }, + } + }) -describe("arguments.infer", function() - it("returns nil for nil args", function() - assert.is_nil(infer()) - end) + local args = { name = "peter", + an_array = "something" } + assert.same({ + name = "peter", + an_array = { "something" }, + }, infer(args, schema)) + end) - it("does no inferring without schema", function() - assert.same("args", infer("args")) end) - it("infers every field using the schema", function() - local schema = Schema.new({ - fields = { - { name = { type = "string" } }, - { age = { type = "number" } }, - { has_license = { type = "boolean" } }, - { aliases = { type = "set", elements = { type = { "string" } } } }, - { comments = { type = "string" } }, + describe("arguments_decoder.combine", function() + it("merges arguments together, creating arrays when finding repeated names, recursively", function() + local monster = { + { a = { [99] = "wayne", }, }, + { a = { "first", }, }, + { a = { b = { c = { "true", }, }, }, }, + { a = { "a", "b", "c" }, }, + { a = { b = { c = { d = "" }, }, }, }, + { c = "test", }, + { a = { "1", "2", "3", }, }, } - }) - - local args = { name = "peter", - age = "45", - has_license = "true", - aliases = "peta", - comments = "" } - assert.same({ - name = "peter", - age = 45, - has_license = true, - aliases = { "peta" }, - comments = ngx.null - }, infer(args, schema)) - end) - it("infers shorthand_fields but does not run the func", function() - local schema = Schema.new({ - fields = { - { name = { type = "string" } }, - { another_array = { type = "array", elements = { type = { "string" } } } }, - }, - shorthand_fields = { - { an_array = { - type = "array", - elements = { type = { "string" } }, - func = function(value) - return { another_array = value:upper() } - end, - } + local combined_monster = { + a = { + { "first", "a", "1" }, { "b", "2" }, { "c", "3" }, + [99] = "wayne", + b = { c = { "true", d = "", }, } }, + c = "test", } - }) - - local args = { name = "peter", - an_array = "something" } - assert.same({ - name = "peter", - an_array = { "something" }, - }, infer(args, schema)) - end) -end) - -describe("arguments.combine", function() - it("merges arguments together, creating arrays when finding repeated names, recursively", function() - local monster = { - { a = { [99] = "wayne", }, }, - { a = { "first", }, }, - { a = { b = { c = { "true", }, }, }, }, - { a = { "a", "b", "c" }, }, - { a = { b = { c = { d = "" }, }, }, }, - { c = "test", }, - { a = { "1", "2", "3", }, }, - } - - local combined_monster = { - a = { - { "first", "a", "1" }, { "b", "2" }, { "c", "3" }, - [99] = "wayne", - b = { c = { "true", d = "", }, } - }, - c = "test", - } - - assert.same(combined_monster, combine(monster)) + assert.same(combined_monster, combine(monster)) + end) end) -end) -describe("arguments.decode_arg", function() - it("does not infer numbers, booleans or nulls from strings", function() - assert.same({ x = "" }, decode_arg("x", "")) - assert.same({ x = "true" }, decode_arg("x", "true")) - assert.same({ x = "false" }, decode_arg("x", "false")) - assert.same({ x = "10" }, decode_arg("x", "10")) + describe("arguments_decoder.decode_arg", function() + it("does not infer numbers, booleans or nulls from strings", function() + assert.same({ x = "" }, decode_arg("x", "")) + assert.same({ x = "true" }, decode_arg("x", "true")) + assert.same({ x = "false" }, decode_arg("x", "false")) + assert.same({ x = "10" }, decode_arg("x", "10")) + end) + + it("decodes arrays", function() + assert.same({ x = { "a" } }, decode_arg("x[]", "a")) + assert.same({ x = { "a" } }, decode_arg("x[1]", "a")) + assert.same({ x = { nil, "a" } }, decode_arg("x[2]", "a")) + end) + + it("decodes nested arrays", function() + assert.same({ x = { { "a" } } }, decode_arg("x[1][1]", "a")) + assert.same({ x = { nil, { "a" } } }, decode_arg("x[2][1]", "a")) + end) end) - it("decodes arrays", function() - assert.same({ x = { "a" } }, decode_arg("x[]", "a")) - assert.same({ x = { "a" } }, decode_arg("x[1]", "a")) - assert.same({ x = { nil, "a" } }, decode_arg("x[2]", "a")) + describe("arguments_decoder.get_param_name_and_keys", function() + it("extracts array keys", function() + local name, _, keys, is_map = get_param_name_and_keys("foo[]") + assert.equals(name, "foo") + assert.same({ "" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[1]") + assert.same(name, "foo") + assert.same({ "1" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[1][2]") + assert.same(name, "foo") + assert.same({ "1", "2" }, keys) + assert.is_false(is_map) + end) + + it("extracts map keys", function() + local name, _, keys, is_map = get_param_name_and_keys("foo[m]") + assert.same(name, "foo") + assert.same({ "m" }, keys) + assert.is_true(is_map) + + name, _, keys, is_map = get_param_name_and_keys("[name][m][n]") + assert.same(name, "[name]") + assert.same({ "m", "n" }, keys) + assert.is_true(is_map) + end) + + it("extracts mixed map/array keys", function() + local name, _, keys, is_map = get_param_name_and_keys("foo[].a") + assert.same(name, "foo") + assert.same({ "", "a" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[1].a") + assert.same(name, "foo") + assert.same({ "1", "a" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[1][2].a") + assert.same(name, "foo") + assert.same({ "1", "2", "a" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo.z[1].a[2].b") + assert.same(name, "foo") + assert.same({ "z", "1", "a", "2", "b" }, keys) + assert.is_false(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[m].a") + assert.same(name, "foo") + assert.same({ "m", "a" }, keys) + assert.is_true(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[m][n].a") + assert.same(name, "foo") + assert.same({ "m", "n", "a" }, keys) + assert.is_true(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[m].a[n].b") + assert.same(name, "foo") + assert.same({ "m", "a", "n", "b" }, keys) + assert.is_true(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[1][m].a") + assert.same(name, "foo") + assert.same({ "1", "m", "a" }, keys) + assert.is_true(is_map) + + name, _, keys, is_map = get_param_name_and_keys("foo[m][1].a[n].b") + assert.same(name, "foo") + assert.same({ "m", "1", "a", "n", "b" }, keys) + assert.is_true(is_map) + end) end) - it("decodes nested arrays", function() - assert.same({ x = { { "a" } } }, decode_arg("x[1][1]", "a")) - assert.same({ x = { nil, { "a" } } }, decode_arg("x[2][1]", "a")) + describe("arguments_decoder.nest_path", function() + it("nests simple value", function() + local container = {} + nest_path(container, { "foo" }, "a") + assert.same({ foo = "a" }, container) + end) + + it("nests arrays", function() + local container = {} + nest_path(container, { "foo", "1" }, "a") + assert.same({ foo = { [1] = "a" } }, container) + + container = {} + nest_path(container, { "foo", "1", "2" }, 12) + assert.same({ foo = { [1] = { [2] = 12 } } }, container) + + container = {} + nest_path(container, { "foo", "1", "2", "3" }, false) + assert.same({ foo = { [1] = { [2] = { [3] = false } } } }, container) + end) + + it("nests maps", function() + local container = {} + nest_path(container, { "foo", "bar" }, "a") + assert.same({ foo = { bar = "a" } }, container) + + container = {} + nest_path(container, { "foo", "bar", "baz" }, true) + assert.same({ foo = { bar = { baz = true } } }, container) + + container = {} + nest_path(container, { "foo", "bar", "baz", "qux" }, 42) + assert.same({ foo = { bar = { baz = { qux = 42 } } } }, container) + end) + + it("nests mixed map/array", function() + local container = {} + nest_path(container, { "foo", "1", "bar" }, "a") + assert.same({ foo = { [1] = { bar = "a" } } }, container) + + container = {} + nest_path(container, { 1, "1", "bar", "2" }, 42) + assert.same({ [1] = { [1] = { bar = { [2] = 42 } } } }, container) + end) end) -end) -describe("arguments.decode", function() + describe("arguments_decoder.decode_map_array_arg", function() + it("decodes arrays", function() + local container = {} - it("decodes complex nested parameters", function() - assert.same(deep_sort{ - c = "test", - a = { - { - "first", - "a", - "1", - }, - { - "b", - "2", - }, - { - "c", - "3", + decode_map_array_arg("x[]", "a", container) + assert.same({ x = { [1] = "a" } }, container) + + container = {} + decode_map_array_arg("x[1]", "a", container) + assert.same({ x = { [1] = "a" } }, container) + + container = {} + decode_map_array_arg("x[1][2][3]", 42, container) + assert.same({ x = { [1] = { [2] = { [3] = 42 } } } }, container) + end) + + it("decodes maps", function() + local container = {} + + decode_map_array_arg("x[a]", "a", container) + assert.same({ x = { a = "a" } }, container) + + container = {} + decode_map_array_arg("x[a][b][c]", 42, container) + assert.same({ x = { a = { b = { c = 42 } } } }, container) + end) + + it("decodes mixed map/array", function() + local container = {} + + decode_map_array_arg("x[][a]", "a", container) + assert.same({ x = { [1] = { a = "a" } } }, container) + + container = {} + decode_map_array_arg("x[1][a]", "a", container) + assert.same({ x = { [1] = { a = "a" } } }, container) + + container = {} + decode_map_array_arg("x[1][2][a]", "a", container) + assert.same({ x = { [1] = { [2] = { a = "a" } } } }, container) + + container = {} + decode_map_array_arg("x[a][1][b]", "a", container) + assert.same({ x = { a = { [1] = { b = "a" } } } }, container) + + container = {} + decode_map_array_arg("x[a][1][b]", "a", container) + assert.same({ x = { a = { [1] = { b = "a" } } } }, container) + + container = {} + decode_map_array_arg("x[1][a][2][b]", "a", container) + assert.same({ x = { [1] = { a = { [2] = { b = "a" } } } } }, container) + + container = {} + decode_map_array_arg("x.r[1].s[a][2].t[b].u", "a", container) + assert.same({ x = { r = { [1] = { s = { a = { [2] = { t = { b = { u = "a" } } } } } } } } }, container) + end) + end) + + describe("arguments_decoder.decode", function() + + it("decodes complex nested parameters", function() + assert.same(deep_sort{ + c = "test", + a = { + { + "first", + "a", + "1", + }, + { + "b", + "2", + }, + { + "c", + "3", + }, + [99] = "wayne", + b = { + [1] = "x", + [2] = "y", + [3] = "z", + [98] = "wayne", + c = { + "true", + d = "", + ["test.key"] = { + "d", + "e", + "f", + }, + ["escaped.k.2"] = { + "d", + "e", + "f", + } + } + }, + foo = "bar", + escaped_k_1 = "bar", }, - [99] = "wayne", - b = { - c = { - "true", - d = "" + }, + deep_sort(decode{ + ["a.b.c.d"] = "", + ["a"] = { "1", "2", "3" }, + ["c"] = "test", + ["a.b.c"] = { "true" }, + ["a[]"] = { "a", "b", "c" }, + ["a[99]"] = "wayne", + ["a[1]"] = "first", + ["a[foo]"] = "bar", + ["a.b%5B%5D"] = { "x", "y", "z" }, + ["a.b%5B98%5D"] = "wayne", + ["a.b.c[test.key]"] = { "d", "e", "f" }, + ["a%5Bescaped_k_1%5D"] = "bar", + ["a.b.c%5Bescaped.k.2%5D"] = { "d", "e", "f" }, + })) + + assert.same(deep_sort{ + a = { + b = { + c = { + ["escaped.k.3"] = { + "d", + "e", + "f", + } + } + }, + escaped_k_1 = "bar", + ESCAPED_K_2 = "baz", + ["escaped%5B_k_4"] = "vvv", + ["escaped.k_5"] = { + nested = "ww", } + }, + }, + deep_sort(decode{ + ["a%5Bescaped_k_1%5D"] = "bar", + ["a%5BESCAPED_K_2%5D"] = "baz", + ["a.b.c%5Bescaped.k.3%5D"] = { "d", "e", "f" }, + ["a%5Bescaped%5B_k_4%5D"] = "vvv", + ["a%5Bescaped.k_5%5D.nested"] = "ww", + })) + end) + + it("decodes complex nested parameters combinations", function() + assert.same({ + a = { + { + "a", + cat = "tommy" + }, + { + "b1", + "b2", + dog = "jake" + }, + { + "c", + cat = { "tommy", "the", "cat" }, + }, + { + "d1", + "d2", + dog = { "jake", "the", "dog" } + }, + { + "e1", + "e2", + dog = { "finn", "the", "human" } + }, + one = { + "a", + cat = "tommy" + }, + two = { + "b1", + "b2", + dog = "jake" + }, + three = { + "c", + cat = { "tommy", "the", "cat" }, + }, + four = { + "d1", + "d2", + dog = { "jake", "the", "dog" } + }, + five = { + "e1", + "e2", + dog = { "finn", "the", "human" } + }, } }, - }, - deep_sort(decode{ - ["a.b.c.d"] = "", - ["a"] = { "1", "2", "3" }, - ["c"] = "test", - ["a.b.c"] = { "true" }, - ["a[]"] = { "a", "b", "c" }, - ["a[99]"] = "wayne", - ["a[1]"] = "first", - })) - end) + decode{ + ["a[1]"] = "a", + ["a[1].cat"] = "tommy", + ["a[2]"] = { "b1", "b2" }, + ["a[2].dog"] = "jake", + ["a[3]"] = "c", + ["a[3].cat"] = { "tommy", "the", "cat" }, + ["a[4]"] = { "d1", "d2" }, + ["a[4].dog"] = { "jake", "the", "dog" }, + ["a%5B5%5D"] = { "e1", "e2" }, + ["a%5B5%5D.dog"] = { "finn", "the", "human" }, + ["a[one]"] = "a", + ["a[one].cat"] = "tommy", + ["a[two]"] = { "b1", "b2" }, + ["a[two].dog"] = "jake", + ["a[three]"] = "c", + ["a[three].cat"] = { "tommy", "the", "cat" }, + ["a[four]"] = { "d1", "d2" }, + ["a[four].dog"] = { "jake", "the", "dog" }, + ["a%5Bfive%5D"] = { "e1", "e2" }, + ["a%5Bfive%5D.dog"] = { "finn", "the", "human" }, + }) + end) + + it("decodes multidimensional arrays and maps", function() + assert.same({ + key = { + { "value" } + } + }, + decode{ + ["key[][]"] = "value", + }) - it("decodes complex nested parameters combinations", function() - assert.same({ - a = { - { - "a", - cat = "tommy" - }, - { - "b1", - "b2", - dog = "jake" - }, - { - "c", - cat = { "tommy", "the", "cat" }, - }, - { - "d1", - "d2", - dog = { "jake", "the", "dog" } + assert.same({ + key = { + [5] = { [4] = "value" } } - } - }, - decode{ - ["a[1]"] = "a", - ["a[1].cat"] = "tommy", - ["a[2]"] = { "b1", "b2" }, - ["a[2].dog"] = "jake", - ["a[3]"] = "c", - ["a[3].cat"] = { "tommy", "the", "cat" }, - ["a[4]"] = { "d1", "d2" }, - ["a[4].dog"] = { "jake", "the", "dog" }, - }) - end) + }, + decode{ + ["key[5][4]"] = "value", + }) - it("decodes multidimensional arrays", function() - assert.same({ - key = { - { "value" } - } - }, - decode{ - ["key[][]"] = "value", - }) - - assert.same({ - key = { - [5] = { [4] = "value" } - } - }, - decode{ - ["key[5][4]"] = "value", - }) - - assert.same({ - key = { - [5] = { [4] = { key = "value" } } - } - }, - decode{ - ["key[5][4].key"] = "value", - }) - - assert.same({ - ["[5]"] = {{ [4] = { key = "value" } }} - }, - decode{ - ["[5][1][4].key"] = "value" - }) - end) + assert.same({ + key = { + foo = { bar = "value" } + } + }, + decode{ + ["key[foo][bar]"] = "value", + }) - pending("decodes different array representations", function() - -- undefined: the result depends on whether `["a"]` or `["a[2]"]` is applied first - -- but there's no way to guarantee order without adding a "presort keys" step. - -- but it's unlikely that a real-world client uses both forms in the same request, - -- instead of making `decode()` slower, split test in two - local decoded = decode{ - ["a"] = { "1", "2" }, - ["a[]"] = "3", - ["a[1]"] = "4", - ["a[2]"] = { "5", "6" }, - } - - assert.same( - deep_sort{ a = { - { "4", "1", "3" }, - { "5", "6", "2" }, + assert.same({ + key = { + [5] = { [4] = { key = "value" } } } }, - deep_sort(decoded) - ) - end) + decode{ + ["key[5][4].key"] = "value", + }) - it("decodes different array representations", function() - -- same as previous test, but split to reduce ordering dependency - assert.same( - { a = { - "2", - { "1", "3", "4" }, + assert.same({ + key = { + [5] = { [4] = { key = "value" } } } }, - deep_sort(decode{ - ["a"] = { "1", "2" }, - ["a[]"] = "3", - ["a[1]"] = "4", - })) + decode{ + ["key%5B5%5D%5B4%5D.key"] = "value", + }) - assert.same( - { a = { - { "3", "4" }, - { "5", "6" }, + assert.same({ + key = { + foo = { bar = { key = "value" } } } }, - deep_sort(decode{ + decode{ + ["key[foo][bar].key"] = "value", + }) + + assert.same({ + ["[5]"] = {{ [4] = { key = "value" } }} + }, + decode{ + ["[5][1][4].key"] = "value" + }) + + assert.same({ + ["[5]"] = { foo = { bar = { key = "value" } }} + }, + decode{ + ["[5][foo][bar].key"] = "value" + }) + + assert.same({ + key = { + foo = { bar = { key = "value" } } + } + }, + decode{ + ["key%5Bfoo%5D%5Bbar%5D.key"] = "value", + }) + end) + + pending("decodes different array representations", function() + -- undefined: the result depends on whether `["a"]` or `["a[2]"]` is applied first + -- but there's no way to guarantee order without adding a "presort keys" step. + -- but it's unlikely that a real-world client uses both forms in the same request, + -- instead of making `decode()` slower, split test in two + local decoded = decode{ + ["a"] = { "1", "2" }, ["a[]"] = "3", ["a[1]"] = "4", ["a[2]"] = { "5", "6" }, - })) - end) - - it("infers values when provided with a schema", function() - local schema = Schema.new({ - fields = { - { name = { type = "string" } }, - { age = { type = "number" } }, - { has_license = { type = "boolean" } }, - { aliases = { type = "set", elements = { type = { "string" } } } }, - { comments = { type = "string" } }, } - }) - - local args = { name = "peter", - age = "45", - has_license = "true", - ["aliases[]"] = "peta", - comments = "" } - assert.same({ - name = "peter", - age = 45, - has_license = true, - aliases = { "peta" }, - comments = ngx.null - }, decode(args, schema)) + + assert.same( + deep_sort{ a = { + { "4", "1", "3" }, + { "5", "6", "2" }, + } + }, + deep_sort(decoded) + ) + end) + + it("decodes different array representations", function() + -- same as previous test, but split to reduce ordering dependency + assert.same( + { a = { + "2", + { "1", "3", "4" }, + } + }, + deep_sort(decode{ + ["a"] = { "1", "2" }, + ["a[]"] = "3", + ["a[1]"] = "4", + })) + + assert.same( + { a = { + { "3", "4" }, + { "5", "6" }, + } + }, + deep_sort(decode{ + ["a[]"] = "3", + ["a[1]"] = "4", + ["a[2]"] = { "5", "6" }, + })) + end) + + it("infers values when provided with a schema", function() + local schema = Schema.new({ + fields = { + { name = { type = "string" } }, + { age = { type = "number" } }, + { has_license = { type = "boolean" } }, + { aliases = { type = "set", elements = { type = { "string" } } } }, + { comments = { type = "string" } }, + } + }) + + local args = { name = "peter", + age = "45", + has_license = "true", + ["aliases[]"] = "peta", + comments = "" } + assert.same({ + name = "peter", + age = 45, + has_license = true, + aliases = { "peta" }, + comments = ngx.null + }, decode(args, schema)) + end) end) end) diff --git a/spec/01-unit/01-db/11-declarative_lmdb_spec.lua b/spec/01-unit/01-db/11-declarative_lmdb_spec.lua index e1b2a79fa21..42756078b8a 100644 --- a/spec/01-unit/01-db/11-declarative_lmdb_spec.lua +++ b/spec/01-unit/01-db/11-declarative_lmdb_spec.lua @@ -187,7 +187,7 @@ describe("#off preserve nulls", function() kong.configuration = kong_config kong.worker_events = kong.worker_events or kong.cache and kong.cache.worker_events or - assert(kong_global.init_worker_events()) + assert(kong_global.init_worker_events(kong.configuration)) kong.cluster_events = kong.cluster_events or kong.cache and kong.cache.cluster_events or assert(kong_global.init_cluster_events(kong.configuration, kong.db)) diff --git a/spec/01-unit/03-conf_loader_spec.lua b/spec/01-unit/03-conf_loader_spec.lua index 47c96492e44..604792c6047 100644 --- a/spec/01-unit/03-conf_loader_spec.lua +++ b/spec/01-unit/03-conf_loader_spec.lua @@ -2395,6 +2395,7 @@ describe("Configuration loader", function() local FIELDS = { -- CONF_BASIC prefix = true, + socket_path = true, vaults = true, database = true, lmdb_environment_path = true, diff --git a/spec/01-unit/04-prefix_handler_spec.lua b/spec/01-unit/04-prefix_handler_spec.lua index 40dca3dd474..c1e36f8060f 100644 --- a/spec/01-unit/04-prefix_handler_spec.lua +++ b/spec/01-unit/04-prefix_handler_spec.lua @@ -968,7 +968,7 @@ describe("NGINX conf compiler", function() end) it("injects default configurations if wasm=on", function() assert.matches( - ".+proxy_wasm_lua_resolver off;.+", + ".+proxy_wasm_lua_resolver on;.+", kong_ngx_cfg({ wasm = true, }, debug) ) end) @@ -986,14 +986,6 @@ describe("NGINX conf compiler", function() }, debug) ) end) - it("permits overriding proxy_wasm_lua_resolver to on", function() - assert.matches( - ".+proxy_wasm_lua_resolver on;.+", - kong_ngx_cfg({ wasm = true, - nginx_http_proxy_wasm_lua_resolver = "on", - }, debug) - ) - end) it("injects runtime-specific directives (wasmtime)", function() assert.matches( "wasm {.+wasmtime {.+flag flag1 on;.+flag flag2 1m;.+}.+", diff --git a/spec/01-unit/05-utils_spec.lua b/spec/01-unit/05-utils_spec.lua index 32bea716132..e3100490ebe 100644 --- a/spec/01-unit/05-utils_spec.lua +++ b/spec/01-unit/05-utils_spec.lua @@ -197,6 +197,19 @@ describe("Utils", function() assert.False(validate_utf8(string.char(255))) -- impossible byte assert.False(validate_utf8(string.char(237, 160, 128))) -- Single UTF-16 surrogate end) + + it("checks valid nginx size values", function() + local parse_ngx_size = require("kong.tools.string").parse_ngx_size + + assert.equal(1024 * 1024 * 1024, parse_ngx_size("1G")) + assert.equal(1024 * 1024 * 1024, parse_ngx_size("1g")) + assert.equal(1024 * 1024, parse_ngx_size("1M")) + assert.equal(1024 * 1024, parse_ngx_size("1m")) + assert.equal(1024, parse_ngx_size("1k")) + assert.equal(1024, parse_ngx_size("1K")) + assert.equal(10, parse_ngx_size("10")) + end) + describe("random_string()", function() local utils = require "kong.tools.rand" it("should return a random string", function() diff --git a/spec/01-unit/09-balancer/01-generic_spec.lua b/spec/01-unit/09-balancer/01-generic_spec.lua index ec4c58f1c60..b56fb1ad8f5 100644 --- a/spec/01-unit/09-balancer/01-generic_spec.lua +++ b/spec/01-unit/09-balancer/01-generic_spec.lua @@ -214,6 +214,7 @@ for _, algorithm in ipairs{ "consistent-hashing", "least-connections", "round-ro -- so that CI and docker can have reliable results -- but remove `search` and `domain` search = {}, + cache_purge = true, }) snapshot = assert:snapshot() assert:set_parameter("TableFormatLevel", 10) @@ -1198,7 +1199,7 @@ for _, algorithm in ipairs{ "consistent-hashing", "least-connections", "round-ro }, }, b:getStatus()) - dnsExpire(record) + dnsExpire(client, record) dnsSRV({ { name = "srvrecord.test", target = "1.1.1.1", port = 9000, weight = 20 }, { name = "srvrecord.test", target = "2.2.2.2", port = 9001, weight = 20 }, @@ -1382,7 +1383,7 @@ for _, algorithm in ipairs{ "consistent-hashing", "least-connections", "round-ro }, b:getStatus()) -- update weight, through dns renewal - dnsExpire(record) + dnsExpire(client, record) dnsSRV({ { name = "srvrecord.test", target = "1.1.1.1", port = 9000, weight = 20 }, { name = "srvrecord.test", target = "2.2.2.2", port = 9001, weight = 20 }, @@ -1695,6 +1696,7 @@ for _, algorithm in ipairs{ "consistent-hashing", "least-connections", "round-ro -- update DNS with a new backend IP -- balancer should now recover since a new healthy backend is available record.expire = 0 + dnsExpire(client, record) dnsA({ { name = "getkong.test", address = "5.6.7.8", ttl = 60 }, }) diff --git a/spec/01-unit/09-balancer/02-least_connections_spec.lua b/spec/01-unit/09-balancer/02-least_connections_spec.lua index 3db545dec09..caae6c8bbe0 100644 --- a/spec/01-unit/09-balancer/02-least_connections_spec.lua +++ b/spec/01-unit/09-balancer/02-least_connections_spec.lua @@ -219,6 +219,7 @@ describe("[least-connections]", function() resolvConf = { "nameserver 198.51.100.0" }, + cache_purge = true, }) snapshot = assert:snapshot() end) diff --git a/spec/01-unit/09-balancer/03-consistent_hashing_spec.lua b/spec/01-unit/09-balancer/03-consistent_hashing_spec.lua index 17f46f46fa5..aaecbdd4301 100644 --- a/spec/01-unit/09-balancer/03-consistent_hashing_spec.lua +++ b/spec/01-unit/09-balancer/03-consistent_hashing_spec.lua @@ -21,6 +21,7 @@ local sleep = helpers.sleep local dnsSRV = function(...) return helpers.dnsSRV(client, ...) end local dnsA = function(...) return helpers.dnsA(client, ...) end local dnsAAAA = function(...) return helpers.dnsAAAA(client, ...) end +local dnsExpire = helpers.dnsExpire @@ -265,6 +266,7 @@ describe("[consistent_hashing]", function() -- so that CI and docker can have reliable results -- but remove `search` and `domain` search = {}, + cache_purge = true, }) snapshot = assert:snapshot() end) @@ -844,6 +846,7 @@ describe("[consistent_hashing]", function() -- expire the existing record record.expire = 0 record.expired = true + dnsExpire(client, record) -- do a lookup to trigger the async lookup client.resolve("really.really.really.does.not.exist.host.test", {qtype = client.TYPE_A}) sleep(1) -- provide time for async lookup to complete diff --git a/spec/01-unit/09-balancer/04-round_robin_spec.lua b/spec/01-unit/09-balancer/04-round_robin_spec.lua index 35f63f2c452..341ec4fe459 100644 --- a/spec/01-unit/09-balancer/04-round_robin_spec.lua +++ b/spec/01-unit/09-balancer/04-round_robin_spec.lua @@ -19,6 +19,7 @@ local sleep = helpers.sleep local dnsSRV = function(...) return helpers.dnsSRV(client, ...) end local dnsA = function(...) return helpers.dnsA(client, ...) end local dnsAAAA = function(...) return helpers.dnsAAAA(client, ...) end +local dnsExpire = helpers.dnsExpire local unset_register = {} @@ -304,6 +305,7 @@ describe("[round robin balancer]", function() -- so that CI and docker can have reliable results -- but remove `search` and `domain` search = {}, + cache_purge = true, }) snapshot = assert:snapshot() end) @@ -412,6 +414,7 @@ describe("[round robin balancer]", function() resolvConf = { "nameserver 127.0.0.1:22000" -- make sure dns query fails }, + cache_purge = true, }) -- create balancer local b = check_balancer(new_balancer { @@ -617,7 +620,7 @@ describe("[round robin balancer]", function() end) it("does not hit the resolver when 'cache_only' is set", function() local record = dnsA({ - { name = "mashape.test", address = "1.2.3.4" }, + { name = "mashape.test", address = "1.2.3.4", ttl = 0.1 }, }) local b = check_balancer(new_balancer { hosts = { { name = "mashape.test", port = 80, weight = 5 } }, @@ -625,6 +628,7 @@ describe("[round robin balancer]", function() wheelSize = 10, }) record.expire = gettime() - 1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsA({ -- create a new record { name = "mashape.test", address = "5.6.7.8" }, }) @@ -1018,7 +1022,7 @@ describe("[round robin balancer]", function() end) it("weight change for unresolved record, updates properly", function() local record = dnsA({ - { name = "really.really.really.does.not.exist.hostname.test", address = "1.2.3.4" }, + { name = "really.really.really.does.not.exist.hostname.test", address = "1.2.3.4", ttl = 0.1 }, }) dnsAAAA({ { name = "getkong.test", address = "::1" }, @@ -1039,6 +1043,8 @@ describe("[round robin balancer]", function() -- expire the existing record record.expire = 0 record.expired = true + dnsExpire(client, record) + sleep(0.2) -- wait for record expiration -- do a lookup to trigger the async lookup client.resolve("really.really.really.does.not.exist.hostname.test", {qtype = client.TYPE_A}) sleep(0.5) -- provide time for async lookup to complete @@ -1102,8 +1108,8 @@ describe("[round robin balancer]", function() end) it("renewed DNS A record; no changes", function() local record = dnsA({ - { name = "mashape.test", address = "1.2.3.4" }, - { name = "mashape.test", address = "1.2.3.5" }, + { name = "mashape.test", address = "1.2.3.4", ttl = 0.1 }, + { name = "mashape.test", address = "1.2.3.5", ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1118,6 +1124,7 @@ describe("[round robin balancer]", function() }) local state = copyWheel(b) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsA({ -- create a new record (identical) { name = "mashape.test", address = "1.2.3.4" }, { name = "mashape.test", address = "1.2.3.5" }, @@ -1133,8 +1140,8 @@ describe("[round robin balancer]", function() it("renewed DNS AAAA record; no changes", function() local record = dnsAAAA({ - { name = "mashape.test", address = "::1" }, - { name = "mashape.test", address = "::2" }, + { name = "mashape.test", address = "::1" , ttl = 0.1 }, + { name = "mashape.test", address = "::2" , ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1149,6 +1156,7 @@ describe("[round robin balancer]", function() }) local state = copyWheel(b) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsAAAA({ -- create a new record (identical) { name = "mashape.test", address = "::1" }, { name = "mashape.test", address = "::2" }, @@ -1163,9 +1171,9 @@ describe("[round robin balancer]", function() end) it("renewed DNS SRV record; no changes", function() local record = dnsSRV({ - { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5 }, - { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5 }, - { name = "gelato.test", target = "1.2.3.6", port = 8003, weight = 5 }, + { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5, ttl = 0.1 }, + { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5, ttl = 0.1 }, + { name = "gelato.test", target = "1.2.3.6", port = 8003, weight = 5, ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1180,6 +1188,7 @@ describe("[round robin balancer]", function() }) local state = copyWheel(b) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsSRV({ -- create a new record (identical) { name = "gelato.test", target = "1.2.3.6", port = 8001, weight = 5 }, { name = "gelato.test", target = "1.2.3.6", port = 8002, weight = 5 }, @@ -1195,8 +1204,8 @@ describe("[round robin balancer]", function() end) it("renewed DNS A record; address changes", function() local record = dnsA({ - { name = "mashape.test", address = "1.2.3.4" }, - { name = "mashape.test", address = "1.2.3.5" }, + { name = "mashape.test", address = "1.2.3.4", ttl = 0.1 }, + { name = "mashape.test", address = "1.2.3.5", ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1212,6 +1221,7 @@ describe("[round robin balancer]", function() }) local state = copyWheel(b) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsA({ -- insert an updated record { name = "mashape.test", address = "1.2.3.4" }, { name = "mashape.test", address = "1.2.3.6" }, -- target updated @@ -1229,7 +1239,7 @@ describe("[round robin balancer]", function() -- 2016/11/07 16:48:33 [error] 81932#0: *2 recv() failed (61: Connection refused), context: ngx.timer local record = dnsA({ - { name = "mashape.test", address = "1.2.3.4" }, + { name = "mashape.test", address = "1.2.3.4", ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1251,8 +1261,10 @@ describe("[round robin balancer]", function() resolvConf = { "nameserver 127.0.0.1:22000" -- make sure dns query fails }, + cache_purge = true, }) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration -- run entire wheel to make sure the expired one is requested, so it can fail for _ = 1, b.wheelSize do b:getPeer() end -- the only indice is now getkong.test @@ -1282,6 +1294,7 @@ describe("[round robin balancer]", function() local test_name = "really.really.really.does.not.exist.hostname.test" local ttl = 0.1 local staleTtl = 0 -- stale ttl = 0, force lookup upon expiring + client.getobj().stale_ttl = 0 local record = dnsA({ { name = test_name, address = "1.2.3.4", ttl = ttl }, }, staleTtl) @@ -1304,11 +1317,12 @@ describe("[round robin balancer]", function() assert.is_nil(ip) assert.equal(port, "Balancer is unhealthy") end + client.getobj().stale_ttl = 4 end) it("renewed DNS A record; unhealthy entries remain unhealthy after renewal", function() local record = dnsA({ - { name = "mashape.test", address = "1.2.3.4" }, - { name = "mashape.test", address = "1.2.3.5" }, + { name = "mashape.test", address = "1.2.3.4", ttl = 0.1 }, + { name = "mashape.test", address = "1.2.3.5", ttl = 0.1 }, }) dnsA({ { name = "getkong.test", address = "9.9.9.9" }, @@ -1342,6 +1356,7 @@ describe("[round robin balancer]", function() local state = copyWheel(b) record.expire = gettime() -1 -- expire current dns cache record + sleep(0.2) -- wait for record expiration dnsA({ -- create a new record (identical) { name = "mashape.test", address = "1.2.3.4" }, { name = "mashape.test", address = "1.2.3.5" }, diff --git a/spec/01-unit/09-balancer/06-latency_spec.lua b/spec/01-unit/09-balancer/06-latency_spec.lua index 89def3b4529..be9a23279e7 100644 --- a/spec/01-unit/09-balancer/06-latency_spec.lua +++ b/spec/01-unit/09-balancer/06-latency_spec.lua @@ -218,6 +218,7 @@ describe("[latency]", function() resolvConf = { "nameserver 198.51.100.0" }, + cache_purge = true, }) snapshot = assert:snapshot() end) diff --git a/spec/01-unit/10-log_serializer_spec.lua b/spec/01-unit/10-log_serializer_spec.lua index 682e7fbd7e4..e982c4efcfc 100644 --- a/spec/01-unit/10-log_serializer_spec.lua +++ b/spec/01-unit/10-log_serializer_spec.lua @@ -54,7 +54,7 @@ describe("kong.log.serialize", function() get_phase = function() return "access" end, } - package.loaded["kong.tracing.request_id"] = nil + package.loaded["kong.observability.tracing.request_id"] = nil package.loaded["kong.pdk.log"] = nil kong.log = require "kong.pdk.log".new(kong) @@ -231,6 +231,20 @@ describe("kong.log.serialize", function() assert.not_equal(tostring(ngx.ctx.service), tostring(res.service)) end) + + it("handle 'json.null' and 'cdata null'", function() + kong.log.set_serialize_value("response.body", ngx.null) + local pok, value = pcall(kong.log.serialize, {}) + assert.is_true(pok) + assert.is_true(type(value) == "table") + + local ffi = require "ffi" + local n = ffi.new("void*") + kong.log.set_serialize_value("response.body", n) + local pok, value = pcall(kong.log.serialize, {}) + assert.is_false(pok) + assert.is_true(type(value) == "string") + end) end) end) diff --git a/spec/01-unit/14-dns_spec.lua b/spec/01-unit/14-dns_spec.lua index fda591d4df6..677977593cf 100644 --- a/spec/01-unit/14-dns_spec.lua +++ b/spec/01-unit/14-dns_spec.lua @@ -29,6 +29,7 @@ local function setup_it_block() nameservers = { "198.51.100.0" }, enable_ipv6 = true, order = { "LAST", "SRV", "A", "CNAME" }, + cache_purge = true, } end diff --git a/spec/01-unit/19-hybrid/03-compat_spec.lua b/spec/01-unit/19-hybrid/03-compat_spec.lua index 60f7bfb4d48..b2e16b07113 100644 --- a/spec/01-unit/19-hybrid/03-compat_spec.lua +++ b/spec/01-unit/19-hybrid/03-compat_spec.lua @@ -507,7 +507,7 @@ describe("kong.clustering.compat", function() id = "00000000-0000-0000-0000-000000000005", name = "opentelemetry", config = { - endpoint = "http://example.com", + traces_endpoint = "http://example.com", queue = { max_batch_size = 9, max_coalescing_delay = 9, diff --git a/spec/01-unit/21-dns-client/02-client_spec.lua b/spec/01-unit/21-dns-client/02-client_spec.lua index acd597ec2ec..e5a88c8e8d9 100644 --- a/spec/01-unit/21-dns-client/02-client_spec.lua +++ b/spec/01-unit/21-dns-client/02-client_spec.lua @@ -39,6 +39,7 @@ describe("[DNS client]", function() local client, resolver before_each(function() + _G.busted_legacy_dns_client = true client = require("kong.resty.dns.client") resolver = require("resty.dns.resolver") @@ -71,6 +72,7 @@ describe("[DNS client]", function() end) after_each(function() + _G.busted_legacy_dns_client = nil package.loaded["kong.resty.dns.client"] = nil package.loaded["resty.dns.resolver"] = nil client = nil diff --git a/spec/01-unit/21-dns-client/03-client_cache_spec.lua b/spec/01-unit/21-dns-client/03-client_cache_spec.lua index eb57d1ec2a2..448bd8b8a92 100644 --- a/spec/01-unit/21-dns-client/03-client_cache_spec.lua +++ b/spec/01-unit/21-dns-client/03-client_cache_spec.lua @@ -22,6 +22,7 @@ describe("[DNS client cache]", function() local client, resolver before_each(function() + _G.busted_legacy_dns_client = true client = require("kong.resty.dns.client") resolver = require("resty.dns.resolver") @@ -55,6 +56,7 @@ describe("[DNS client cache]", function() end) after_each(function() + _G.busted_legacy_dns_client = nil package.loaded["kong.resty.dns.client"] = nil package.loaded["resty.dns.resolver"] = nil client = nil diff --git a/spec/01-unit/26-tracing/01-tracer_pdk_spec.lua b/spec/01-unit/26-observability/01-tracer_pdk_spec.lua similarity index 89% rename from spec/01-unit/26-tracing/01-tracer_pdk_spec.lua rename to spec/01-unit/26-observability/01-tracer_pdk_spec.lua index 494037dd1d3..62bae98ee68 100644 --- a/spec/01-unit/26-tracing/01-tracer_pdk_spec.lua +++ b/spec/01-unit/26-observability/01-tracer_pdk_spec.lua @@ -12,41 +12,6 @@ local function assert_sample_rate(actual, expected) assert(diff < SAMPLING_PRECISION, "sampling rate is not correct: " .. actual .. " expected: " .. expected) end ---- hook ngx.log to a spy for unit test ---- usage: local log_spy = hook_log_spy() -- hook ngx.log to a spy ---- -- do stuff ---- assert.spy(log_spy).was_called_with(ngx.ERR, "some error") ---- -- unhook ---- unhook_log_spy() ---- note that all messages arguments are concatenated together. ---- this hook slows down the test execution by a lot so only use if necessary. --- @function hook_log_spy --- @return log_spy the spy -local function hook_log_spy() - local log_spy = spy(function() end) - local level, msg - -- the only reliable way to hook into ngx.log - -- is to use debug.sethook as ngx.log is always - -- localized and even reload the module does not work - debug.sethook(function() - if debug.getinfo(2, 'f').func == ngx.log then - level, msg = select(2, debug.getlocal(2, 1)), - table.concat { - select(2, debug.getlocal(2, 2)), - select(2, debug.getlocal(2, 3)), - select(2, debug.getlocal(2, 4)), - select(2, debug.getlocal(2, 5)), - select(2, debug.getlocal(2, 6)), - } - print(msg) - log_spy(level, msg) - end - end, "c", 1) - return log_spy -end - -local unhook_log_spy = debug.sethook - describe("Tracer PDK", function() local ok, err, old_ngx_get_phase, _ local log_spy @@ -55,14 +20,13 @@ describe("Tracer PDK", function() local kong_global = require "kong.global" _G.kong = kong_global.new() kong_global.init_pdk(kong) - log_spy = hook_log_spy() + log_spy = spy.on(ngx, "log") old_ngx_get_phase = ngx.get_phase -- trick the pdk into thinking we are not in the timer context _G.ngx.get_phase = function() return "access" end -- luacheck: ignore end) lazy_teardown(function() - unhook_log_spy() _G.ngx.get_phase = old_ngx_get_phase -- luacheck: ignore end) diff --git a/spec/01-unit/26-tracing/02-propagation_strategies_spec.lua b/spec/01-unit/26-observability/02-propagation_strategies_spec.lua similarity index 98% rename from spec/01-unit/26-tracing/02-propagation_strategies_spec.lua rename to spec/01-unit/26-observability/02-propagation_strategies_spec.lua index d0bc3718994..ca780242101 100644 --- a/spec/01-unit/26-tracing/02-propagation_strategies_spec.lua +++ b/spec/01-unit/26-observability/02-propagation_strategies_spec.lua @@ -1,4 +1,4 @@ -local propagation_utils = require "kong.tracing.propagation.utils" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local bn = require "resty.openssl.bn" local from_hex = propagation_utils.from_hex @@ -8,8 +8,8 @@ local shallow_copy = require("kong.tools.table").shallow_copy local fmt = string.format local sub = string.sub -local EXTRACTORS_PATH = "kong.tracing.propagation.extractors." -local INJECTORS_PATH = "kong.tracing.propagation.injectors." +local EXTRACTORS_PATH = "kong.observability.tracing.propagation.extractors." +local INJECTORS_PATH = "kong.observability.tracing.propagation.injectors." local trace_id_16 = "0af7651916cd43dd8448eb211c80319c" local trace_id_8 = "8448eb211c80319c" @@ -100,6 +100,7 @@ local test_data = { { ["traceparent"] = fmt("00-%s-%s-01", trace_id_16, span_id_8_1), }, ctx = { + w3c_flags = 0x01, trace_id = trace_id_16, span_id = span_id_8_1, should_sample = true, @@ -114,6 +115,7 @@ local test_data = { { ["traceparent"] = fmt("00-%s-%s-09", trace_id_16, span_id_8_1), }, ctx = { + w3c_flags = 0x09, trace_id = trace_id_16, span_id = span_id_8_1, should_sample = true, @@ -128,11 +130,27 @@ local test_data = { { ["traceparent"] = fmt("00-%s-%s-08", trace_id_16, span_id_8_1), }, ctx = { + w3c_flags = 0x08, trace_id = trace_id_16, span_id = span_id_8_1, should_sample = false, trace_id_original_size = 16, } + }, { + description = "extraction with hex flags", + extract = true, + inject = false, + trace_id = trace_id_16, + headers = { + ["traceparent"] = fmt("00-%s-%s-ef", trace_id_16, span_id_8_1), + }, + ctx = { + w3c_flags = 0xef, + trace_id = trace_id_16, + span_id = span_id_8_1, + should_sample = true, + trace_id_original_size = 16, + } }, { description = "sampled = false", extract = true, @@ -142,6 +160,7 @@ local test_data = { { ["traceparent"] = fmt("00-%s-%s-00", trace_id_16, span_id_8_1), }, ctx = { + w3c_flags = 0x00, trace_id = trace_id_16, span_id = span_id_8_1, should_sample = false, diff --git a/spec/01-unit/26-tracing/03-propagation_module_spec.lua b/spec/01-unit/26-observability/03-propagation_module_spec.lua similarity index 98% rename from spec/01-unit/26-tracing/03-propagation_module_spec.lua rename to spec/01-unit/26-observability/03-propagation_module_spec.lua index c08ebae7cff..d78fd674922 100644 --- a/spec/01-unit/26-tracing/03-propagation_module_spec.lua +++ b/spec/01-unit/26-observability/03-propagation_module_spec.lua @@ -1,4 +1,4 @@ -local propagation_utils = require "kong.tracing.propagation.utils" +local propagation_utils = require "kong.observability.tracing.propagation.utils" local tablex = require "pl.tablex" local shallow_copy = require "kong.tools.table".shallow_copy local to_hex = require "resty.string".to_hex @@ -423,7 +423,7 @@ describe("Tracing Headers Propagation Module", function() } } } - local propagation = require "kong.tracing.propagation" + local propagation = require "kong.observability.tracing.propagation" lazy_setup(function() err = spy.on(kong.log, "err") diff --git a/spec/01-unit/26-tracing/04-request-id_spec.lua b/spec/01-unit/26-observability/04-request-id_spec.lua similarity index 90% rename from spec/01-unit/26-tracing/04-request-id_spec.lua rename to spec/01-unit/26-observability/04-request-id_spec.lua index e4b85be593d..940131f3b12 100644 --- a/spec/01-unit/26-tracing/04-request-id_spec.lua +++ b/spec/01-unit/26-observability/04-request-id_spec.lua @@ -48,7 +48,7 @@ describe("Request ID unit tests", function() it("returns the expected Request ID and caches it in ctx", function() - local request_id = reload_module("kong.tracing.request_id") + local request_id = reload_module("kong.observability.tracing.request_id") local id, err = request_id.get() assert.is_nil(err) @@ -61,7 +61,7 @@ describe("Request ID unit tests", function() it("fails if accessed from phase that cannot read ngx.var", function() _G.ngx.get_phase = function() return "init" end - local request_id = reload_module("kong.tracing.request_id") + local request_id = reload_module("kong.observability.tracing.request_id") local id, err = request_id.get() assert.is_nil(id) diff --git a/spec/01-unit/26-observability/05-logs_spec.lua b/spec/01-unit/26-observability/05-logs_spec.lua new file mode 100644 index 00000000000..7683d71771f --- /dev/null +++ b/spec/01-unit/26-observability/05-logs_spec.lua @@ -0,0 +1,93 @@ +require "kong.tools.utils" + + +describe("Observability/Logs unit tests", function() + describe("maybe_push()", function() + local o11y_logs, maybe_push, get_request_logs, get_worker_logs + local old_ngx, old_kong + + lazy_setup(function() + old_ngx = _G.ngx + old_kong = _G.kong + + _G.ngx = { + config = { subsystem = "http" }, + ctx = {}, + DEBUG = ngx.DEBUG, + INFO = ngx.INFO, + WARN = ngx.WARN, + } + + _G.kong = { + configuration = { + log_level = "info", + }, + } + + o11y_logs = require "kong.observability.logs" + maybe_push = o11y_logs.maybe_push + get_request_logs = o11y_logs.get_request_logs + get_worker_logs = o11y_logs.get_worker_logs + end) + + before_each(function() + _G.ngx.ctx = {} + end) + + lazy_teardown(function() + _G.ngx = old_ngx + _G.kong = old_kong + end) + + it("has no effect when log level is lower than the configured value", function() + maybe_push(1, nil, ngx.DEBUG, "Don't mind me, I'm just a debug log") + local worker_logs = get_worker_logs() + assert.same({}, worker_logs) + local request_logs = get_request_logs() + assert.same({}, request_logs) + end) + + it("considers log message as optional", function() + local log_level = ngx.INFO + + maybe_push(1, nil, log_level) + local worker_logs = get_worker_logs() + assert.equals(1, #worker_logs) + + local logged_entry = worker_logs[1] + assert.same(log_level, logged_entry.log_level) + assert.equals("", logged_entry.body) + assert.is_table(logged_entry.attributes) + assert.is_number(logged_entry.observed_time_unix_nano) + assert.is_number(logged_entry.time_unix_nano) + assert.is_number(logged_entry.attributes["introspection.current.line"]) + assert.is_string(logged_entry.attributes["introspection.name"]) + assert.is_string(logged_entry.attributes["introspection.namewhat"]) + assert.is_string(logged_entry.attributes["introspection.source"]) + assert.is_string(logged_entry.attributes["introspection.what"]) + end) + + it("generates worker-scoped log entries", function() + local log_level = ngx.WARN + local body = "Careful! I'm a warning!" + + maybe_push(1, { foo = "bar", tst = "baz" }, log_level, body, true, 123, ngx.null, nil, function()end, { foo = "bar" }) + local worker_logs = get_worker_logs() + assert.equals(1, #worker_logs) + + local logged_entry = worker_logs[1] + assert.same(log_level, logged_entry.log_level) + assert.matches(body .. "true123nilnilfunction:%s0x%x+table:%s0x%x+", logged_entry.body) + assert.is_table(logged_entry.attributes) + assert.is_number(logged_entry.attributes["introspection.current.line"]) + assert.is_string(logged_entry.attributes["introspection.name"]) + assert.is_string(logged_entry.attributes["introspection.namewhat"]) + assert.is_string(logged_entry.attributes["introspection.source"]) + assert.is_string(logged_entry.attributes["introspection.what"]) + assert.equals("bar", logged_entry.attributes.foo) + assert.equals("baz", logged_entry.attributes.tst) + assert.is_number(logged_entry.observed_time_unix_nano) + assert.is_number(logged_entry.time_unix_nano) + end) + end) +end) diff --git a/spec/01-unit/26-observability/06-telemetry-pdk_spec.lua b/spec/01-unit/26-observability/06-telemetry-pdk_spec.lua new file mode 100644 index 00000000000..14139810770 --- /dev/null +++ b/spec/01-unit/26-observability/06-telemetry-pdk_spec.lua @@ -0,0 +1,38 @@ +require "kong.tools.utils" + + +describe("Telemetry PDK unit tests", function() + describe("log()", function() + local old_kong = _G.kong + + lazy_setup(function() + local kong_global = require "kong.global" + _G.kong = kong_global.new() + kong_global.init_pdk(kong) + end) + + lazy_teardown(function() + _G.kong = old_kong + end) + + it("fails as expected with invalid input", function() + local ok, err = kong.telemetry.log() + assert.is_nil(ok) + assert.equals("plugin_name must be a string", err) + + ok, err = kong.telemetry.log("plugin_name") + assert.is_nil(ok) + assert.equals("plugin_config must be a table", err) + + ok, err = kong.telemetry.log("plugin_name", {}) + assert.is_nil(ok) + assert.equals("message_type must be a string", err) + end) + + it ("considers attributes and message as optional", function() + local ok, err = kong.telemetry.log("plugin_name", {}, "message_type") + assert.is_nil(ok) + assert.matches("Telemetry logging is disabled", err) + end) + end) +end) diff --git a/spec/01-unit/30-new-dns-client/01-utils_spec.lua b/spec/01-unit/30-new-dns-client/01-utils_spec.lua new file mode 100644 index 00000000000..ae24750d2ab --- /dev/null +++ b/spec/01-unit/30-new-dns-client/01-utils_spec.lua @@ -0,0 +1,510 @@ +local utils = require "kong.dns.utils" +local tempfilename = require("pl.path").tmpname +local writefile = require("pl.utils").writefile +local splitlines = require("pl.stringx").splitlines + +describe("[utils]", function () + + describe("is_fqdn(name, ndots)", function () + it("test @name: end with `.`", function () + assert.is_true(utils.is_fqdn("www.", 2)) + assert.is_true(utils.is_fqdn("www.example.", 3)) + assert.is_true(utils.is_fqdn("www.example.test.", 4)) + end) + + it("test @ndots", function () + assert.is_true(utils.is_fqdn("www", 0)) + + assert.is_false(utils.is_fqdn("www", 1)) + assert.is_true(utils.is_fqdn("www.example", 1)) + assert.is_true(utils.is_fqdn("www.example.test", 1)) + + assert.is_false(utils.is_fqdn("www", 2)) + assert.is_false(utils.is_fqdn("www.example", 2)) + assert.is_true(utils.is_fqdn("www.example.test", 2)) + assert.is_true(utils.is_fqdn("www1.www2.example.test", 2)) + end) + end) + + describe("is_srv(name)", function () + local test_domains = { + ["_imaps._tcp.example.test"] = true, + ["_http._tcp.example.test"] = true, + ["_imaps._udp.example.test"] = true, + ["_http._udp.example.test"] = true, + ["_ldap._udp.example.test"] = true, + ["_ldap._udp.example"] = true, + ["_ldap._udp."] = false, + ["_ldap._udp"] = false, + ["_ldap._udp._example.test"] = true, + ["_ldap._udp._example"] = true, + ["_ldap._udp._"] = true, + ["_imaps.tcp.example.test"] = false, + ["imaps._tcp.example.test"] = false, + ["imaps.tcp.example.test"] = false, + ["_._tcp.example.test"] = false, + ["_imaps._.example.test"] = false, + ["_._.example.test"] = false, + ["_..example.test"] = false, + ["._.example.test"] = false, + ["www.example.test"] = false, + ["localhost"] = false, + } + + for k,v in pairs(test_domains) do + assert.equal(utils.is_srv(k), v, "checking " .. k .. ", " .. tostring(v)) + end + end) + + describe("search_names()", function () + it("empty resolv, not apply the search list", function () + local resolv = {} + local names = utils.search_names("www.example.test", resolv) + assert.same(names, { "www.example.test" }) + end) + + it("FQDN name: end with `.`, not apply the search list", function () + local names = utils.search_names("www.example.test.", { ndots = 1 }) + assert.same(names, { "www.example.test." }) + -- name with 3 dots, and ndots=4 > 3 + local names = utils.search_names("www.example.test.", { ndots = 4 }) + assert.same(names, { "www.example.test." }) + end) + + it("dots number in the name >= ndots, not apply the search list", function () + local resolv = { + ndots = 1, + search = { "example.net" }, + } + local names = utils.search_names("www.example.test", resolv) + assert.same(names, { "www.example.test" }) + + local names = utils.search_names("example.test", resolv) + assert.same(names, { "example.test" }) + end) + + it("dots number in the name < ndots, apply the search list", function () + local resolv = { + ndots = 2, + search = { "example.net" }, + } + local names = utils.search_names("www", resolv) + assert.same(names, { "www.example.net", "www" }) + + local names = utils.search_names("www1.www2", resolv) + assert.same(names, { "www1.www2.example.net", "www1.www2" }) + + local names = utils.search_names("www1.www2.www3", resolv) + assert.same(names, { "www1.www2.www3" }) -- not apply + + local resolv = { + ndots = 2, + search = { "example.net", "example.test" }, + } + local names = utils.search_names("www", resolv) + assert.same(names, { "www.example.net", "www.example.test", "www" }) + + local names = utils.search_names("www1.www2", resolv) + assert.same(names, { "www1.www2.example.net", "www1.www2.example.test", "www1.www2" }) + + local names = utils.search_names("www1.www2.www3", resolv) + assert.same(names, { "www1.www2.www3" }) -- not apply + end) + end) + + describe("parsing hostname", function () + it("hostname_type()", function () + assert.equal(utils.hostname_type("10.0.0.1"), "ipv4") + assert.equal(utils.hostname_type("127.0.0.1"), "ipv4") + + assert.equal(utils.hostname_type("::1"), "ipv6") + assert.equal(utils.hostname_type("[::1]"), "ipv6") + assert.equal(utils.hostname_type("2001:db8::1"), "ipv6") + assert.equal(utils.hostname_type("[2001:db8::1]"), "ipv6") + + assert.equal(utils.hostname_type("localhost"), "domain") + assert.equal(utils.hostname_type("example.test"), "domain") + assert.equal(utils.hostname_type("example.org"), "domain") + assert.equal(utils.hostname_type("example.com"), "domain") + assert.equal(utils.hostname_type("10.0.0.1.example.test"), "domain") + end) + + it("parse_hostname()", function () + local function check(name, expected_name, expected_port, expected_name_type) + local name_ip, port, name_type = utils.parse_hostname(name) + + assert.equal(name_ip, expected_name, "checking the returned name/ip of " .. name) + assert.equal(port, expected_port, "checking the returned port of " .. name) + assert.equal(name_type, expected_name_type, "checking the returned type of " .. name) + end + + check("127.0.0.1", "127.0.0.1", nil, "ipv4") + check("127.0.0.1:", "127.0.0.1", nil, "ipv4") + check("127.0.0.1:0", "127.0.0.1", 0, "ipv4") + check("127.0.0.1:80", "127.0.0.1", 80, "ipv4") + + check("::1", "[::1]", nil, "ipv6") + check("[::1]:", "[::1]", nil, "ipv6") + check("[::1]:0", "[::1]", 0, "ipv6") + check("[::1]:80", "[::1]", 80, "ipv6") + + check("www.example.test", "www.example.test", nil, "domain") + check("www.example.test:", "www.example.test", nil, "domain") + check("www.example.test:0", "www.example.test", 0, "domain") + check("www.example.test:80", "www.example.test", 80, "domain") + + check("localhost", "localhost", nil, "domain") + check("localhost:", "localhost", nil, "domain") + check("localhost:0", "localhost", 0, "domain") + check("localhost:80", "localhost", 80, "domain") + end) + end) + + describe("ipv6_bracket()", function () + it("IPv6 address", function () + assert.equal(utils.ipv6_bracket("::1"), "[::1]") + assert.equal(utils.ipv6_bracket("[::1]"), "[::1]") + assert.equal(utils.ipv6_bracket("2001:db8::1"), "[2001:db8::1]") + assert.equal(utils.ipv6_bracket("[2001:db8::1]"), "[2001:db8::1]") + end) + + it("IPv4 address", function () + assert.equal(utils.ipv6_bracket("127.0.0.1"), "127.0.0.1") + end) + + it("host name", function () + assert.equal(utils.ipv6_bracket("example.test"), "example.test") + end) + end) + + describe("answer selection", function () + local function get_and_count(answers, n, get_ans) + local count = {} + for _ = 1, n do + local answer = get_ans(answers) + count[answer.target] = (count[answer.target] or 0) + 1 + end + return count + end + + it("round-robin", function () + local answers = { + { target = "1" }, -- 25% + { target = "2" }, -- 25% + { target = "3" }, -- 25% + { target = "4" }, -- 25% + } + local count = get_and_count(answers, 100, utils.get_next_round_robin_answer) + assert.same(count, { ["1"] = 25, ["2"] = 25, ["3"] = 25, ["4"] = 25 }) + end) + + it("slight weight round-robin", function () + -- simple one + local answers = { + { target = "w5-p10-a", weight = 5, priority = 10, }, -- hit 100% + } + local count = get_and_count(answers, 20, utils.get_next_weighted_round_robin_answer) + assert.same(count, { ["w5-p10-a"] = 20 }) + + -- only get the lowest priority + local answers = { + { target = "w5-p10-a", weight = 5, priority = 10, }, -- hit 50% + { target = "w5-p20", weight = 5, priority = 20, }, -- hit 0% + { target = "w5-p10-b", weight = 5, priority = 10, }, -- hit 50% + { target = "w0-p10", weight = 0, priority = 10, }, -- hit 0% + } + local count = get_and_count(answers, 20, utils.get_next_weighted_round_robin_answer) + assert.same(count, { ["w5-p10-a"] = 10, ["w5-p10-b"] = 10 }) + + -- weight: 6, 3, 1 + local answers = { + { target = "w6", weight = 6, priority = 10, }, -- hit 60% + { target = "w3", weight = 3, priority = 10, }, -- hit 30% + { target = "w1", weight = 1, priority = 10, }, -- hit 10% + } + local count = get_and_count(answers, 100 * 1000, utils.get_next_weighted_round_robin_answer) + assert.same(count, { ["w6"] = 60000, ["w3"] = 30000, ["w1"] = 10000 }) + + -- random start + _G.math.native_randomseed(9975098) -- math.randomseed() ignores @seed + local answers1 = { + { target = "1", weight = 1, priority = 10, }, + { target = "2", weight = 1, priority = 10, }, + { target = "3", weight = 1, priority = 10, }, + { target = "4", weight = 1, priority = 10, }, + } + local answers2 = { + { target = "1", weight = 1, priority = 10, }, + { target = "2", weight = 1, priority = 10, }, + { target = "3", weight = 1, priority = 10, }, + { target = "4", weight = 1, priority = 10, }, + } + + local a1 = utils.get_next_weighted_round_robin_answer(answers1) + local a2 = utils.get_next_weighted_round_robin_answer(answers2) + assert.not_equal(a1.target, a2.target) + + -- weight 0 as 0.1 + local answers = { + { target = "w0", weight = 0, priority = 10, }, + { target = "w1", weight = 1, priority = 10, }, + { target = "w2", weight = 0, priority = 10, }, + { target = "w3", weight = 0, priority = 10, }, + } + local count = get_and_count(answers, 100, utils.get_next_weighted_round_robin_answer) + assert.same(count, { ["w0"] = 7, ["w1"] = 77, ["w2"] = 8, ["w3"] = 8 }) + + -- weight 0 and lowest priority + local answers = { + { target = "w0-a", weight = 0, priority = 0, }, + { target = "w1", weight = 1, priority = 10, }, -- hit 0% + { target = "w0-b", weight = 0, priority = 0, }, + { target = "w0-c", weight = 0, priority = 0, }, + } + local count = get_and_count(answers, 100, utils.get_next_weighted_round_robin_answer) + assert.same(count["w1"], nil) + + -- all weights are 0 + local answers = { + { target = "1", weight = 0, priority = 10, }, + { target = "2", weight = 0, priority = 10, }, + { target = "3", weight = 0, priority = 10, }, + { target = "4", weight = 0, priority = 10, }, + } + local count = get_and_count(answers, 100, utils.get_next_weighted_round_robin_answer) + assert.same(count, { ["1"] = 25, ["2"] = 25, ["3"] = 25, ["4"] = 25 }) + end) + end) + + describe("parsing 'resolv.conf':", function() + + -- override os.getenv to insert env variables + local old_getenv = os.getenv + local envvars -- whatever is in this table, gets served first + before_each(function() + envvars = {} + os.getenv = function(name) -- luacheck: ignore + return envvars[name] or old_getenv(name) + end + end) + + after_each(function() + os.getenv = old_getenv -- luacheck: ignore + envvars = nil + end) + + it("tests parsing when the 'resolv.conf' file does not exist", function() + local result, err = utils.parse_resolv_conf("non/existing/file") + assert.is.Nil(result) + assert.is.string(err) + end) + + it("tests parsing when the 'resolv.conf' file is empty", function() + local filename = tempfilename() + writefile(filename, "") + local resolv, err = utils.parse_resolv_conf(filename) + os.remove(filename) + assert.is.same({ ndots = 1, options = {} }, resolv) + assert.is.Nil(err) + end) + + it("tests parsing 'resolv.conf' with multiple comment types", function() + local file = splitlines( +[[# this is just a comment line +# at the top of the file + +domain myservice.test + +nameserver 198.51.100.0 +nameserver 2001:db8::1 ; and a comment here +nameserver 198.51.100.0:1234 ; this one has a port number (limited systems support this) +nameserver 1.2.3.4 ; this one is 4th, so should be ignored + +# search is commented out, test below for a mutually exclusive one +#search domaina.test domainb.test + +sortlist list1 list2 #list3 is not part of it + +options ndots:2 +options timeout:3 +options attempts:4 + +options debug +options rotate ; let's see about a comment here +options no-check-names +options inet6 +; here's annother comment +options ip6-bytestring +options ip6-dotint +options no-ip6-dotint +options edns0 +options single-request +options single-request-reopen +options no-tld-query +options use-vc +]]) + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + assert.is.equal("myservice.test", resolv.domain) + assert.is.same({ "198.51.100.0", "2001:db8::1", "198.51.100.0:1234" }, resolv.nameserver) + assert.is.same({ "list1", "list2" }, resolv.sortlist) + assert.is.same({ ndots = 2, timeout = 3, attempts = 4, debug = true, rotate = true, + ["no-check-names"] = true, inet6 = true, ["ip6-bytestring"] = true, + ["ip6-dotint"] = nil, -- overridden by the next one, mutually exclusive + ["no-ip6-dotint"] = true, edns0 = true, ["single-request"] = true, + ["single-request-reopen"] = true, ["no-tld-query"] = true, ["use-vc"] = true}, + resolv.options) + end) + + it("tests parsing 'resolv.conf' with mutual exclusive domain vs search", function() + local file = splitlines( +[[domain myservice.test + +# search is overriding domain above +search domaina.test domainb.test + +]]) + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + assert.is.Nil(resolv.domain) + assert.is.same({ "domaina.test", "domainb.test" }, resolv.search) + end) + + it("tests parsing 'resolv.conf' with 'timeout = 0'", function() + local file = splitlines("options timeout:0") + local resolv = utils.parse_resolv_conf(file) + assert.equal(2000, resolv.options.timeout) + end) + + it("tests parsing 'resolv.conf' with max search entries MAXSEARCH", function() + local file = splitlines( +[[ + +search domain1.test domain2.test domain3.test domain4.test domain5.test domain6.test domain7.test + +]]) + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + assert.is.Nil(resolv.domain) + assert.is.same({ + "domain1.test", + "domain2.test", + "domain3.test", + "domain4.test", + "domain5.test", + "domain6.test", + }, resolv.search) + end) + + it("tests parsing 'resolv.conf' with environment variables", function() + local file = splitlines( +[[# this is just a comment line +domain myservice.test + +nameserver 198.51.100.0 +nameserver 198.51.100.1 ; and a comment here + +options ndots:1 +]]) + envvars.LOCALDOMAIN = "domaina.test domainb.test" + envvars.RES_OPTIONS = "ndots:2 debug" + + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + + + assert.is.Nil(resolv.domain) -- must be nil, mutually exclusive + assert.is.same({ "domaina.test", "domainb.test" }, resolv.search) + + assert.is.same({ ndots = 2, debug = true }, resolv.options) + end) + + it("tests parsing 'resolv.conf' with non-existing environment variables", function() + local file = splitlines( +[[# this is just a comment line +domain myservice.test + +nameserver 198.51.100.0 +nameserver 198.51.100.1 ; and a comment here + +options ndots:2 +]]) + envvars.LOCALDOMAIN = "" + envvars.RES_OPTIONS = "" + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + assert.is.equals("myservice.test", resolv.domain) -- must be nil, mutually exclusive + assert.is.same({ ndots = 2 }, resolv.options) + end) + + it("skip ipv6 nameservers with scopes", function() + local file = splitlines( +[[# this is just a comment line +nameserver [fe80::1%enp0s20f0u1u1] +]]) + local resolv, err = utils.parse_resolv_conf(file) + assert.is.Nil(err) + assert.is.same({}, resolv.nameservers) + end) + + end) + + describe("parsing 'hosts':", function() + + it("tests parsing when the 'hosts' file does not exist", function() + local result = utils.parse_hosts("non/existing/file") + assert.same({ localhost = { ipv4 = "127.0.0.1", ipv6 = "[::1]" } }, result) + end) + + it("tests parsing when the 'hosts' file is empty", function() + local filename = tempfilename() + writefile(filename, "") + local result = utils.parse_hosts(filename) + os.remove(filename) + assert.same({ localhost = { ipv4 = "127.0.0.1", ipv6 = "[::1]" } }, result) + end) + + it("tests parsing 'hosts'", function() + local hostsfile = splitlines( +[[# The localhost entry should be in every HOSTS file and is used +# to point back to yourself. + +127.0.0.1 # only ip address, this one will be ignored + +127.0.0.1 localhost +::1 localhost + +# My test server for the website + +192.168.1.2 test.computer.test + 192.168.1.3 ftp.COMPUTER.test alias1 alias2 +192.168.1.4 smtp.computer.test alias3 #alias4 +192.168.1.5 smtp.computer.test alias3 #doubles, first one should win + +#Blocking known malicious sites +127.0.0.1 admin.abcsearch.test +127.0.0.2 www3.abcsearch.test #[Browseraid] +127.0.0.3 www.abcsearch.test wwwsearch #[Restricted Zone site] + +[::1] alsolocalhost #support IPv6 in brackets +]]) + local reverse = utils.parse_hosts(hostsfile) + assert.is.equal("127.0.0.1", reverse.localhost.ipv4) + assert.is.equal("[::1]", reverse.localhost.ipv6) + + assert.is.equal("192.168.1.2", reverse["test.computer.test"].ipv4) + + assert.is.equal("192.168.1.3", reverse["ftp.computer.test"].ipv4) + assert.is.equal("192.168.1.3", reverse["alias1"].ipv4) + assert.is.equal("192.168.1.3", reverse["alias2"].ipv4) + + assert.is.equal("192.168.1.4", reverse["smtp.computer.test"].ipv4) + assert.is.equal("192.168.1.4", reverse["alias3"].ipv4) + + assert.is.equal("192.168.1.4", reverse["smtp.computer.test"].ipv4) -- .1.4; first one wins! + assert.is.equal("192.168.1.4", reverse["alias3"].ipv4) -- .1.4; first one wins! + + assert.is.equal("[::1]", reverse["alsolocalhost"].ipv6) + end) + end) +end) diff --git a/spec/01-unit/30-new-dns-client/02-old_client_spec.lua b/spec/01-unit/30-new-dns-client/02-old_client_spec.lua new file mode 100644 index 00000000000..60ad134e791 --- /dev/null +++ b/spec/01-unit/30-new-dns-client/02-old_client_spec.lua @@ -0,0 +1,1553 @@ +-- This test case file originates from the old version of the DNS client and has +-- been modified to adapt to the new version of the DNS client. + +local _writefile = require("pl.utils").writefile +local tmpname = require("pl.path").tmpname +local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy + +-- hosted in Route53 in the AWS sandbox +local TEST_DOMAIN = "kong-gateway-testing.link" +local TEST_NS = "192.51.100.0" + +local TEST_NSS = { TEST_NS } + +local NOT_FOUND_ERROR = 'dns server error: 3 name error' + +local function assert_same_answers(a1, a2) + a1 = cycle_aware_deep_copy(a1) + a1.ttl = nil + a1.expire = nil + + a2 = cycle_aware_deep_copy(a2) + a2.ttl = nil + a2.expire = nil + + assert.same(a1, a2) +end + +describe("[DNS client]", function() + + local resolver, client, query_func, old_udp, receive_func + + local resolv_path, hosts_path + + local function writefile(path, text) + _writefile(path, type(text) == "table" and table.concat(text, "\n") or text) + end + + local function client_new(opts) + opts = opts or {} + opts.resolv_conf = opts.resolv_conf or resolv_path + opts.hosts = hosts_path + opts.cache_purge = true + return client.new(opts) + end + + lazy_setup(function() + -- create temp resolv.conf and hosts + resolv_path = tmpname() + hosts_path = tmpname() + ngx.log(ngx.DEBUG, "create temp resolv.conf:", resolv_path, + " hosts:", hosts_path) + + -- hook sock:receive to do timeout test + old_udp = ngx.socket.udp + + _G.ngx.socket.udp = function (...) + local sock = old_udp(...) + + local old_receive = sock.receive + + sock.receive = function (...) + if receive_func then + receive_func(...) + end + return old_receive(...) + end + + return sock + end + + end) + + lazy_teardown(function() + if resolv_path then + os.remove(resolv_path) + end + if hosts_path then + os.remove(hosts_path) + end + + _G.ngx.socket.udp = old_udp + end) + + before_each(function() + -- inject r.query + package.loaded["resty.dns.resolver"] = nil + resolver = require("resty.dns.resolver") + + local original_query_func = resolver.query + query_func = function(self, original_query_func, name, options) + return original_query_func(self, name, options) + end + resolver.query = function(self, ...) + return query_func(self, original_query_func, ...) + end + + -- restore its API overlapped by the compatible layer + package.loaded["kong.dns.client"] = nil + client = require("kong.dns.client") + client.resolve = function (self, name, opts, tries) + if opts and opts.return_random then + return self:resolve_address(name, opts.port, opts.cache_only, tries) + else + return self:_resolve(name, opts and opts.qtype, opts and opts.cache_only, tries) + end + end + end) + + after_each(function() + package.loaded["resty.dns.resolver"] = nil + resolver = nil + query_func = nil + + package.loaded["kong.resty.dns.client"] = nil + client = nil + + receive_func = nil + end) + + + describe("initialization", function() + it("check special opts", function() + local opts = { + hosts = "non/existent/hosts", + resolv_conf = "non/exitent/resolv.conf", + retrans = 4, + timeout = 5000, + random_resolver = true, + nameservers = {"1.1.1.1", {"2.2.2.2", 53}}, + } + + local cli = assert(client.new(opts)) + + assert.same(opts.retrans, cli.r_opts.retrans) + assert.same(opts.timeout, cli.r_opts.timeout) + assert.same(not opts.random_resolver, cli.r_opts.no_random) + assert.same(opts.nameservers, cli.r_opts.nameservers) + end) + + it("succeeds if hosts/resolv.conf fails", function() + local cli, err = client.new({ + nameservers = TEST_NSS, + hosts = "non/existent/file", + resolv_conf = "non/exitent/file", + }) + assert.is.Nil(err) + assert.same(cli.r_opts.nameservers, TEST_NSS) + end) + + describe("inject localhost", function() + + it("if absent", function() + writefile(resolv_path, "") + writefile(hosts_path, "") -- empty hosts + + local cli = assert(client_new()) + local answers = cli:resolve("localhost", { qtype = resolver.TYPE_AAAA}) + assert.equal("[::1]", answers[1].address) + + answers = cli:resolve("localhost", { qtype = resolver.TYPE_A}) + assert.equal("127.0.0.1", answers[1].address) + + answers = cli:resolve("localhost") + assert.equal("127.0.0.1", answers[1].address) + end) + + it("not if ipv4 exists", function() + writefile(hosts_path, "1.2.3.4 localhost") + local cli = assert(client_new()) + + -- IPv6 is not defined + cli:resolve("localhost", { qtype = resolver.TYPE_AAAA}) + local answers = cli.cache:get("localhost:28") + assert.is_nil(answers) + + -- IPv4 is not overwritten + cli:resolve("localhost", { qtype = resolver.TYPE_A}) + answers = cli.cache:get("localhost:1") + assert.equal("1.2.3.4", answers[1].address) + end) + + it("not if ipv6 exists", function() + writefile(hosts_path, "::1:2:3:4 localhost") + local cli = assert(client_new()) + + -- IPv6 is not overwritten + cli:resolve("localhost", { qtype = resolver.TYPE_AAAA}) + local answers = cli.cache:get("localhost:28") + assert.equal("[::1:2:3:4]", answers[1].address) + + -- IPv4 is not defined + cli:resolve("localhost", { qtype = resolver.TYPE_A}) + answers = cli.cache:get("localhost:1") + assert.is_nil(answers) + end) + + it("cache evication", function() + writefile(hosts_path, "::1:2:3:4 localhost") + local cli = assert(client_new()) + + cli:resolve("localhost", { qtype = resolver.TYPE_AAAA}) + local answers = cli.cache:get("localhost:28") + assert.equal("[::1:2:3:4]", answers[1].address) + + -- evict it + cli.cache:delete("localhost:28") + answers = cli.cache:get("localhost:28") + assert.equal(nil, answers) + + -- resolve and re-insert it into cache + answers = cli:resolve("localhost") + assert.equal("[::1:2:3:4]", answers[1].address) + + cli:resolve("localhost", { qtype = resolver.TYPE_AAAA}) + answers = cli.cache:get("localhost:28") + assert.equal("[::1:2:3:4]", answers[1].address) + end) + end) + end) + + + describe("iterating searches", function() + local function hook_query_func_get_list() + local list = {} + query_func = function(self, original_query_func, name, options) + table.insert(list, name .. ":" .. options.qtype) + return {} -- empty answers + end + return list + end + + describe("without type", function() + it("works with a 'search' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + local answers, err = cli:resolve("host") + + assert.same(answers, nil) + assert.same(err, "dns client error: 101 empty record received") + assert.same({ + 'host.one.test:1', + 'host.two.test:1', + 'host:1', + 'host.one.test:28', + 'host.two.test:28', + 'host:28', + }, list) + end) + + it("works with SRV name", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + local answers, err = cli:resolve("_imap._tcp.example.test") + + assert.same(answers, nil) + assert.same(err, "dns client error: 101 empty record received") + assert.same({ + '_imap._tcp.example.test:33', + }, list) + end) + + it("works with a 'search .' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search .", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + local answers, err = cli:resolve("host") + + assert.same(answers, nil) + assert.same(err, "dns client error: 101 empty record received") + assert.same({ + 'host:1', + 'host:28', + }, list) + end) + + it("works with a 'domain' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "domain local.domain.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + local answers, err = cli:resolve("host") + + assert.same(answers, nil) + assert.same(err, "dns client error: 101 empty record received") + assert.same({ + 'host.local.domain.test:1', + 'host:1', + 'host.local.domain.test:28', + 'host:28', + }, list) + end) + end) + + describe("FQDN without type", function() + it("works with a 'search' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + cli:resolve("host.") + + assert.same({ + 'host.:1', + 'host.:28', + }, list) + end) + + it("works with a 'search .' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search .", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + cli:resolve("host.") + + assert.same({ + 'host.:1', + 'host.:28', + }, list) + end) + + it("works with a 'domain' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "domain local.domain.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + cli:resolve("host.") + + assert.same({ + 'host.:1', + 'host.:28', + }, list) + end) + end) + + describe("with type", function() + it("works with a 'search' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new({ family = { "AAAA" } })) -- IPv6 type + cli:resolve("host") + + assert.same({ + 'host.one.test:28', + 'host.two.test:28', + 'host:28', + }, list) + end) + + it("works with a 'domain' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "domain local.domain.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new({ family = { "AAAA" } })) -- IPv6 type + cli:resolve("host") + + assert.same({ + 'host.local.domain.test:28', + 'host:28', + }, list) + end) + end) + + describe("FQDN with type", function() + it("works with a 'search' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new({ family = { "AAAA" } })) -- IPv6 type + cli:resolve("host.") + assert.same({ + 'host.:28', + }, list) + end) + + it("works with a 'domain' option", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "domain local.domain.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new({ family = { "AAAA" } })) -- IPv6 type + cli:resolve("host.") + + assert.same({ + 'host.:28', + }, list) + end) + end) + + it("honours 'ndots'", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + + local list = hook_query_func_get_list() + local cli = assert(client_new()) + cli:resolve("local.host") + + assert.same({ + 'local.host:1', + 'local.host:28', + }, list) + end) + + it("hosts file always resolves first, overriding `ndots`", function() + writefile(resolv_path, { + "nameserver 198.51.100.0", + "search one.test two.test", + "options ndots:1", + }) + writefile(hosts_path, { + "127.0.0.1 host", + "::1 host", + }) + + local list = hook_query_func_get_list() + -- perferred IP type: IPv4 (A takes priority in family) + local cli = assert(client_new({ family = { "SRV", "A", "AAAA" } })) + local answers = cli:resolve("host") + assert.same(answers[1].address, "127.0.0.1") + assert.same({}, list) -- hit on cache, so no query to the nameserver + + -- perferred IP type: IPv6 (AAAA takes priority in family) + --[[ + local cli = assert(client_new({ family = { "SRV", "AAAA", "A" } })) + local answers = cli:resolve("host") + assert.same(answers[1].address, "[::1]") + assert.same({}, list) + ]] + end) + end) + + -- This test will report an alert-level error message, ignore it. + it("low-level callback error", function() + receive_func = function(...) + error("CALLBACK") + end + + local cli = assert(client_new()) + + local orig_log = ngx.log + _G.ngx.log = function (...) end -- mute ALERT log + local answers, err = cli:resolve("srv.timeout.test") + _G.ngx.log = orig_log + assert.is_nil(answers) + assert.match("callback threw an error:.*CALLBACK", err) + end) + + describe("timeout", function () + it("dont try other types with the low-level error", function() + -- KAG-2300 https://github.test/Kong/kong/issues/10182 + -- When timed out, don't keep trying with other answers types. + writefile(resolv_path, { + "nameserver 198.51.100.0", + "options timeout:1", + "options attempts:3", + }) + + local query_count = 0 + query_func = function(self, original_query_func, name, options) + assert(options.qtype == resolver.TYPE_A) + query_count = query_count + 1 + return original_query_func(self, name, options) + end + + local receive_count = 0 + receive_func = function(...) + receive_count = receive_count + 1 + return nil, "timeout" + end + + local cli = assert(client_new()) + assert.same(cli.r_opts.retrans, 3) + assert.same(cli.r_opts.timeout, 1) + + local answers, err = cli:resolve("timeout.test") + assert.is_nil(answers) + assert.match("DNS server error: failed to receive reply from UDP server .*: timeout, took %d+ ms", err) + assert.same(receive_count, 3) + assert.same(query_count, 1) + end) + + -- KAG-2300 - https://github.test/Kong/kong/issues/10182 + -- If we encounter a timeout while talking to the DNS server, + -- expect the total timeout to be close to timeout * attemps parameters + for _, attempts in ipairs({1, 2}) do + for _, timeout in ipairs({1, 2}) do + it("options: timeout: " .. timeout .. " seconds, attempts: " .. attempts .. " times", function() + query_func = function(self, original_query_func, name, options) + ngx.sleep(math.min(timeout, 5)) + return nil, "timeout" .. timeout .. attempts + end + writefile(resolv_path, { + "nameserver 198.51.100.0", + "options timeout:" .. timeout, + "options attempts:" .. attempts, + }) + local cli = assert(client_new()) + assert.same(cli.r_opts.retrans, attempts) + assert.same(cli.r_opts.timeout, timeout) + + local start_time = ngx.now() + local answers = cli:resolve("timeout.test") + assert.is.Nil(answers) + assert.is("DNS server error: timeout" .. timeout .. attempts) + local duration = ngx.now() - start_time + assert.truthy(duration < (timeout * attempts + 1)) + end) + end + end + end) + + it("fetching answers without nameservers errors", function() + writefile(resolv_path, "") + local host = TEST_DOMAIN + local typ = resolver.TYPE_A + + local cli = assert(client_new()) + local answers, err = cli:resolve(host, { qtype = typ }) + assert.is_nil(answers) + assert.same(err, "failed to instantiate the resolver: no nameservers specified") + end) + + it("fetching CNAME answers", function() + local host = "smtp."..TEST_DOMAIN + local typ = resolver.TYPE_CNAME + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local answers = cli:resolve(host, { qtype = typ }) + + assert.are.equal(host, answers[1].name) + assert.are.equal(typ, answers[1].type) + assert.are.equal(#answers, 1) + end) + + it("fetching CNAME answers FQDN", function() + local host = "smtp."..TEST_DOMAIN + local typ = resolver.TYPE_CNAME + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local answers = cli:resolve(host .. ".", { qtype = typ }) + + assert.are.equal(host, answers[1].name) -- answers name does not contain "." + assert.are.equal(typ, answers[1].type) + assert.are.equal(#answers, 1) + end) + + it("cache hit and ttl", function() + -- TOOD: The special 0-ttl record may cause this test failed + -- [{"name":"kong-gateway-testing.link","class":1,"address":"198.51.100.0", + -- "ttl":0,"type":1,"section":1}] + local host = TEST_DOMAIN + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local answers = cli:resolve(host) + assert.are.equal(host, answers[1].name) + + local wait_time = 1 + ngx.sleep(wait_time) + + -- fetch again, now from cache + local answers2 = assert(cli:resolve(host)) + assert.are.equal(answers, answers2) -- same table from L1 cache + + local ttl, _, value = cli.cache:peek(host .. ":-1") + assert.same(answers, value) + local ttl_diff = answers.ttl - ttl + assert(math.abs(ttl_diff - wait_time) < 1, + ("ttl diff:%s s should be near to %s s"):format(ttl_diff, wait_time)) + end) + + it("fetching names case insensitive", function() + query_func = function(self, original_query_func, name, options) + return {{ + name = "some.UPPER.case", + type = resolver.TYPE_A, + ttl = 30, + }} + end + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local answers = cli:resolve("some.upper.CASE") + + assert.equal(1, #answers) + assert.equal("some.upper.case", answers[1].name) + end) + + it("fetching multiple A answers", function() + local host = "atest."..TEST_DOMAIN + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf", family = {"A"}})) + local answers = assert(cli:resolve(host)) + assert.are.equal(#answers, 2) + assert.are.equal(host, answers[1].name) + assert.are.equal(resolver.TYPE_A, answers[1].type) + assert.are.equal(host, answers[2].name) + assert.are.equal(resolver.TYPE_A, answers[2].type) + end) + + it("fetching multiple A answers FQDN", function() + local host = "atest."..TEST_DOMAIN + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf", family = {"A"}})) + local answers = assert(cli:resolve(host .. ".")) + assert.are.equal(#answers, 2) + assert.are.equal(host, answers[1].name) + assert.are.equal(resolver.TYPE_A, answers[1].type) + assert.are.equal(host, answers[2].name) + assert.are.equal(resolver.TYPE_A, answers[2].type) + end) + + it("fetching A answers redirected through 2 CNAME answerss (un-typed)", function() + writefile(resolv_path, "") -- search {} empty + + local host = "smtp."..TEST_DOMAIN + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + assert(cli:resolve(host)) + + -- check first CNAME + local key1 = host .. ":" .. resolver.TYPE_CNAME + local entry1 = cli.cache:get(key1) + assert.same(nil, entry1) + + for k,v in pairs(cli.stats.stats) do + v.query_last_time = nil + end + + assert.same({ + ["smtp.kong-gateway-testing.link:-1"] = { + miss = 1, + runs = 1 + }, + ["smtp.kong-gateway-testing.link:1"] = { + query = 1, + query_succ = 1 + }, + }, cli.stats.stats) + end) + + it("fetching multiple SRV answerss (un-typed)", function() + local host = "_ldap._tcp.srv.test" + local typ = resolver.TYPE_SRV + + query_func = function(self, original_query_func, name, options) + return { + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + }, + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + }, + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + } + } + end + + -- un-typed lookup + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local answers = assert(cli:resolve(host)) + assert.are.equal(host, answers[1].name) + assert.are.equal(typ, answers[1].type) + assert.are.equal(host, answers[2].name) + assert.are.equal(typ, answers[2].type) + assert.are.equal(host, answers[3].name) + assert.are.equal(typ, answers[3].type) + assert.are.equal(#answers, 3) + end) + + it("fetching multiple SRV answerss through CNAME (un-typed)", function() + writefile(resolv_path, "") -- search {} empty + local host = "_ldap._tcp.cname2srv.test" + local typ = resolver.TYPE_SRV + + query_func = function(self, original_query_func, name, options) + return { + { + type = resolver.TYPE_CNAME, cname = host, class = 1, name = host, + ttl = 300, + }, + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + }, + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + }, + { + type = typ, target = "srv.test", port = 8002, weight = 10, + priority = 5, class = 1, name = host, ttl = 300, + } + } + end + + -- un-typed lookup + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local answers = assert(cli:resolve(host)) + + -- first check CNAME + local key = host .. ":" .. resolver.TYPE_CNAME + local entry = cli.cache:get(key) + assert.same(nil, entry) + + for k,v in pairs(cli.stats.stats) do + v.query_last_time = nil + end + + assert.same({ + ["_ldap._tcp.cname2srv.test:33"] = { + miss = 1, + runs = 1, + query = 1, + query_succ = 1, + }, + }, cli.stats.stats) + + -- check final target + assert.are.equal(typ, answers[1].type) + assert.are.equal(typ, answers[2].type) + assert.are.equal(typ, answers[3].type) + assert.are.equal(#answers, 3) + end) + + it("fetching non-type-matching answerss", function() + local host = "srvtest."..TEST_DOMAIN + local typ = resolver.TYPE_A --> the entry is SRV not A + + writefile(resolv_path, "") -- search {} empty + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local answers, err = cli:resolve(host, { qtype = typ }) + assert.is_nil(answers) -- returns nil + assert.equal("dns client error: 101 empty record received", err) + end) + + it("fetching non-existing answerss", function() + local host = "IsNotHere."..TEST_DOMAIN + + writefile(resolv_path, "") -- search {} empty + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local answers, err = cli:resolve(host) + assert.is_nil(answers) + assert.equal("dns server error: 3 name error", err) + end) + + it("fetching IP address", function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + + local host = "1.2.3.4" + local answers = cli:resolve(host) + assert.same(answers[1].address, host) + + local host = "[1:2::3:4]" + local answers = cli:resolve(host) + assert.same(answers[1].address, host) + + local host = "1:2::3:4" + local answers = cli:resolve(host) + assert.same(answers[1].address, "[" .. host .. "]") + + -- ignore ipv6 format error, it only check ':' + local host = "[invalid ipv6 address:::]" + local answers = cli:resolve(host) + assert.same(answers[1].address, host) + end) + + it("fetching IPv6 in an SRV answers adds brackets",function() + local host = "hello.world.test" + local address = "::1" + local entry = {{ + type = resolver.TYPE_SRV, + target = address, + port = 321, + weight = 10, + priority = 10, + class = 1, + name = host, + ttl = 10, + }} + + query_func = function(self, original_query_func, name, options) + if name == host and options.qtype == resolver.TYPE_SRV then + return entry + end + return original_query_func(self, name, options) + end + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local answers = cli:resolve( host, { qtype = resolver.TYPE_SRV }) + assert.equal("["..address.."]", answers[1].target) + end) + + it("resolving from the /etc/hosts file; preferred A or AAAA family", function() + writefile(hosts_path, { + "127.3.2.1 localhost", + "1::2 localhost", + }) + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + family = {"SRV", "A", "AAAA"} + })) + assert(cli) + + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + family = {"SRV", "AAAA", "A"} + })) + assert(cli) + end) + + + it("resolving from the /etc/hosts file", function() + writefile(hosts_path, { + "127.3.2.1 localhost", + "1::2 localhost", + "123.123.123.123 mashape", + "1234::1234 kong.for.president", + }) + + local cli = assert(client_new({ nameservers = TEST_NSS })) + + local answers, err = cli:resolve("localhost", {qtype = resolver.TYPE_A}) + assert.is.Nil(err) + assert.are.equal(answers[1].address, "127.3.2.1") + + answers, err = cli:resolve("localhost", {qtype = resolver.TYPE_AAAA}) + assert.is.Nil(err) + assert.are.equal(answers[1].address, "[1::2]") + + answers, err = cli:resolve("mashape", {qtype = resolver.TYPE_A}) + assert.is.Nil(err) + assert.are.equal(answers[1].address, "123.123.123.123") + + answers, err = cli:resolve("kong.for.president", {qtype = resolver.TYPE_AAAA}) + assert.is.Nil(err) + assert.are.equal(answers[1].address, "[1234::1234]") + end) + + describe("toip() function", function() + it("A/AAAA-answers, round-robin",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local host = "atest."..TEST_DOMAIN + local answers = assert(cli:resolve(host)) + answers.last = nil -- make sure to clean + local ips = {} + for _,answers in ipairs(answers) do ips[answers.address] = true end + local family = {} + for n = 1, #answers do + local ip = cli:resolve(host, { return_random = true }) + ips[ip] = nil + family[n] = ip + end + -- this table should be empty again + assert.is_nil(next(ips)) + -- do again, and check same family + for n = 1, #family do + local ip = cli:resolve(host, { return_random = true }) + assert.same(family[n], ip) + end + end) + + it("SRV-answers, round-robin on lowest prio",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local host = "_service._proto.hello.world.test" + local entry = { + { + type = resolver.TYPE_SRV, + target = "1.2.3.4", + port = 8000, + weight = 5, + priority = 10, + class = 1, + name = host, + ttl = 10, + }, + { + type = resolver.TYPE_SRV, + target = "1.2.3.4", + port = 8001, + weight = 5, + priority = 20, + class = 1, + name = host, + ttl = 10, + }, + { + type = resolver.TYPE_SRV, + target = "1.2.3.4", + port = 8002, + weight = 5, + priority = 10, + class = 1, + name = host, + ttl = 10, + }, + } + -- insert in the cache + cli.cache:set(entry[1].name .. ":" .. resolver.TYPE_SRV, {ttl=0}, entry) + + local results = {} + for _ = 1,20 do + local _, port = cli:resolve_address(host) + results[port] = (results[port] or 0) + 1 + end + + -- 20 passes, each should get 10 + assert.equal(0, results[8001] or 0) --priority 20, no hits + assert.equal(10, results[8000] or 0) --priority 10, 50% of hits + assert.equal(10, results[8002] or 0) --priority 10, 50% of hits + end) + + it("SRV-answers with 1 entry, round-robin",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + local host = "_service._proto.hello.world.test" + local entry = {{ + type = resolver.TYPE_SRV, + target = "1.2.3.4", + port = 321, + weight = 10, + priority = 10, + class = 1, + name = host, + ttl = 10, + }} + -- insert in the cache + cli.cache:set(entry[1].name .. ":" .. resolver.TYPE_SRV, { ttl=0 }, entry) + + -- repeated lookups, as the first will simply serve the first entry + -- and the only second will setup the round-robin scheme, this is + -- specific for the SRV answers type, due to the weights + for _ = 1 , 10 do + local ip, port = cli:resolve_address(host) + assert.same("1.2.3.4", ip) + assert.same(321, port) + end + end) + + it("SRV-answers with 0-weight, round-robin",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local host = "_service._proto.hello.world.test" + local entry = { + { + type = resolver.TYPE_SRV, + target = "1.2.3.4", + port = 321, + weight = 0, --> weight 0 + priority = 10, + class = 1, + name = host, + ttl = 10, + }, + { + type = resolver.TYPE_SRV, + target = "1.2.3.5", + port = 321, + weight = 50, --> weight 50 + priority = 10, + class = 1, + name = host, + ttl = 10, + }, + { + type = resolver.TYPE_SRV, + target = "1.2.3.6", + port = 321, + weight = 50, --> weight 50 + priority = 10, + class = 1, + name = host, + ttl = 10, + }, + } + -- insert in the cache + cli.cache:set(entry[1].name .. ":" .. resolver.TYPE_SRV, { ttl=0 }, entry) + + -- weight 0 will be weight 1, without any reduction in weight + -- of the other ones. + local track = {} + for _ = 1 , 2002 do --> run around twice + local ip, _ = assert(cli:resolve_address(host)) + track[ip] = (track[ip] or 0) + 1 + end + assert.equal(1000, track["1.2.3.5"]) + assert.equal(1000, track["1.2.3.6"]) + assert.equal(2, track["1.2.3.4"]) + end) + + it("port passing",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local entry_a = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "a.answers.test", + ttl = 10, + }} + local entry_srv = {{ + type = resolver.TYPE_SRV, + target = "a.answers.test", + port = 8001, + weight = 5, + priority = 20, + class = 1, + name = "_service._proto.srv.answers.test", + ttl = 10, + }} + -- insert in the cache + cli.cache:set(entry_a[1].name..":-1", { ttl = 0 }, entry_a) + cli.cache:set(entry_srv[1].name..":33", { ttl = 0 }, entry_srv) + local ip, port + local host = "a.answers.test" + ip, port = cli:resolve_address(host) + assert.is_string(ip) + assert.is_nil(port) + + ip, port = cli:resolve_address(host, 1234) + assert.is_string(ip) + assert.equal(1234, port) + + host = "_service._proto.srv.answers.test" + ip, port = cli:resolve_address(host) + assert.is_number(port) + assert.is_string(ip) + + ip, port = cli:resolve_address(host, 0) + assert.is_number(port) + assert.is_string(ip) + assert.is_not.equal(0, port) + end) + + it("port passing if SRV port=0",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local ip, port, host + + query_func = function(self, original_query_func, name, options) + if options.qtype ~= resolver.TYPE_SRV then + return original_query_func(self, name, options) + end + + return {{ + type = resolver.TYPE_SRV, + port = 0, + weight = 10, + priority = 20, + target = "kong-gateway-testing.link", + class = 1, + name = name, + ttl = 300, + }} + end + + host = "_service._proto.srvport0.test" + ip, port = cli:resolve_address(host, 10) + assert.is_number(port) + assert.is_string(ip) + assert.is_equal(10, port) + + ip, port = cli:resolve_address(host) + assert.is_string(ip) + assert.is_nil(port) + end) + + it("SRV whole process: SRV -> A",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local ip, port, host + + query_func = function(self, original_query_func, name, options) + if options.qtype == resolver.TYPE_A then + return {{ + type = resolver.TYPE_A, + address = "1.1.1.1", + name = name, + ttl = 300, + }} + + elseif options.qtype == resolver.TYPE_SRV then + return {{ + type = resolver.TYPE_SRV, + port = 0, + weight = 10, + priority = 20, + target = "kong-gateway-testing.link", + class = 1, + name = name, + ttl = 300, + }} + + else + return {} + end + end + + host = "_service._proto.srv_a.test" + ip, port = cli:resolve_address(host) + assert.equal(ip, "1.1.1.1") + assert.is_nil(port) + end) + + it("SRV whole process: SRV -> A failed -> AAAA",function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf"})) + local ip, port, host + + query_func = function(self, original_query_func, name, options) + if options.qtype == resolver.TYPE_A then + return { errcode = 5, errstr = "refused" } + + elseif options.qtype == resolver.TYPE_SRV then + return {{ + type = resolver.TYPE_SRV, + port = 0, + weight = 10, + priority = 20, + target = "kong-gateway-testing.link", + class = 1, + name = name, + ttl = 300, + }} + + else + return {{ + type = resolver.TYPE_AAAA, + address = "::1:2:3:4", + name = name, + ttl = 300, + }} + end + end + + host = "_service._proto.srv_aaaa.test" + ip, port = cli:resolve_address(host) + assert.equal(ip, "[::1:2:3:4]") + assert.is_nil(port) + end) + + it("resolving in correct answers-type family",function() + local function config(cli) + -- function to insert 2 answerss in the cache + local A_entry = {{ + type = resolver.TYPE_A, + address = "5.6.7.8", + class = 1, + name = "hello.world.test", + ttl = 10, + }} + local AAAA_entry = {{ + type = resolver.TYPE_AAAA, + address = "::1", + class = 1, + name = "hello.world.test", + ttl = 10, + }} + -- insert in the cache + cli.cache:set(A_entry[1].name..":-1", { ttl=0 }, A_entry) + cli.cache:set(AAAA_entry[1].name..":-1", { ttl=0 }, AAAA_entry) + end + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf", family = {"AAAA", "A"} })) + config(cli) + local ip, err = cli:resolve_address("hello.world.test") + assert.same(err, nil) + assert.equals(ip, "::1") + + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf", family = {"A", "AAAA"} })) + config(cli) + ip = cli:resolve_address("hello.world.test") + --assert.equals(ip, "5.6.7.8") + assert.equals(ip, "::1") + end) + + it("handling of empty responses", function() + local cli = assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + -- insert empty records into cache + cli.cache:set("hello.world.test:all", { ttl=0 }, { errcode = 3 }) + + -- Note: the bad case would be that the below lookup would hang due to round-robin on an empty table + local ip, port = cli:resolve_address("hello.world.test", 123, true) + assert.is_nil(ip) + assert.is.string(port) -- error message + end) + end) + + it("verifies valid_ttl", function() + local valid_ttl = 0.1 + local error_ttl = 0.1 + local stale_ttl = 0.1 + local qname = "konghq.test" + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + error_ttl = error_ttl, + stale_ttl = stale_ttl, + valid_ttl = valid_ttl, + })) + -- mock query function to return a default answers + query_func = function(self, original_query_func, name, options) + return {{ + type = resolver.TYPE_A, + address = "5.6.7.8", + class = 1, + name = qname, + ttl = 10, + }} -- will add new field .ttl = valid_ttl + end + + local answers, _, _ = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.equal(valid_ttl, answers.ttl) + + local ttl = cli.cache:peek(qname .. ":1") + assert.is_near(valid_ttl, ttl, 0.1) + end) + + it("verifies ttl and caching of empty responses and name errors", function() + --empty/error responses should be cached for a configurable time + local error_ttl = 0.1 + local stale_ttl = 0.1 + local qname = "really.really.really.does.not.exist."..TEST_DOMAIN + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + error_ttl = error_ttl, + stale_ttl = stale_ttl, + })) + + -- mock query function to count calls + local call_count = 0 + query_func = function(self, original_query_func, name, options) + call_count = call_count + 1 + return original_query_func(self, name, options) + end + + -- make a first request, populating the cache + local answers1, answers2, err1, err2, _ + answers1, err1, _ = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.is_nil(answers1) + assert.are.equal(1, call_count) + assert.are.equal(NOT_FOUND_ERROR, err1) + answers1 = assert(cli.cache:get(qname .. ":" .. resolver.TYPE_A)) + + -- make a second request, result from cache, still called only once + answers2, err2, _ = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.is_nil(answers2) + assert.are.equal(1, call_count) + assert.are.equal(NOT_FOUND_ERROR, err2) + answers2 = assert(cli.cache:get(qname .. ":" .. resolver.TYPE_A)) + assert.equal(answers1, answers2) + assert.falsy(answers2._expire_at) + + -- wait for expiry of ttl and retry, it will not use the cached one + -- because the cached one contains no avaible IP addresses + ngx.sleep(error_ttl+0.5 * stale_ttl) + answers2, err2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.is_nil(answers2) + assert.are.equal(2, call_count) + assert.are.equal(NOT_FOUND_ERROR, err2) + + answers2 = assert(cli.cache:get(qname .. ":" .. resolver.TYPE_A)) + assert.falsy(answers2._expire_at) -- refreshed record + + -- wait for expiry of stale_ttl and retry, should be called twice now + ngx.sleep(0.75 * stale_ttl) + assert.are.equal(2, call_count) + answers2, err2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.is_nil(answers2) + assert.are.equal(NOT_FOUND_ERROR, err2) + assert.are.equal(2, call_count) + + answers2 = assert(cli.cache:get(qname .. ":" .. resolver.TYPE_A)) + assert.not_equal(answers1, answers2) + assert.falsy(answers2._expire_at) -- new answers, not expired + end) + + it("verifies stale_ttl for available records", function() + local stale_ttl = 0.1 + local ttl = 0.1 + local qname = "realname.test" + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + stale_ttl = stale_ttl, + })) + + -- mock query function to count calls, and return errors + local call_count = 0 + query_func = function(self, original_query_func, name, options) + call_count = call_count + 1 + return {{ + type = resolver.TYPE_A, + address = "1.1.1.1", + class = 1, + name = name, + ttl = ttl, + }} + end + + -- initial request to populate the cache + local answers1, answers2 + answers1 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.same(answers1[1].address, "1.1.1.1") + assert.are.equal(call_count, 1) + assert.falsy(answers1._expire_at) + + -- try again, HIT from cache, not stale + answers2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.are.equal(call_count, 1) + assert(answers1 == answers2) + + -- wait for expiry of ttl and retry, HIT and stale + ngx.sleep(ttl + 0.5 * stale_ttl) + answers2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.same(answers2[1].address, "1.1.1.1") + assert.are.equal(call_count, 1) -- todo: flakiness + + answers2 = assert(cli.cache:get(qname .. ":" .. resolver.TYPE_A)) + assert(answers2._expire_at) + answers2._expire_at = nil -- clear to be same with answers1 + assert_same_answers(answers1, answers2) + + -- async stale updating task + ngx.sleep(0.1 * stale_ttl) + assert.are.equal(call_count, 2) + + -- hit the cached one that is updated by the stale stask + answers2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.same(answers2[1].address, "1.1.1.1") + assert.are.equal(call_count, 2) + assert.falsy(answers2._expire_at) + + -- The stale one will be completely eliminated from the cache. + ngx.sleep(ttl + stale_ttl) + + answers2 = cli:resolve(qname, { qtype = resolver.TYPE_A }) + assert.same(answers2[1].address, "1.1.1.1") + assert.are.equal(call_count, 3) + assert.falsy(answers2._expire_at) + end) + + describe("verifies the polling of dns queries, retries, and wait times", function() + local function threads_resolve(nthreads, name, cli) + cli = cli or assert(client_new({ resolv_conf = "/etc/resolv.conf" })) + -- we're going to schedule a whole bunch of queries (lookup & stores answers) + local coros = {} + local answers_list = {} + for _ = 1, nthreads do + local co = ngx.thread.spawn(function () + coroutine.yield(coroutine.running()) + local answers, err = cli:resolve(name, { qtype = resolver.TYPE_A }) + table.insert(answers_list, (answers or err)) + end) + table.insert(coros, co) + end + for _, co in ipairs(coros) do + ngx.thread.wait(co) + end + return answers_list + end + + it("simultaneous lookups are synchronized to 1 lookup", function() + local call_count = 0 + query_func = function(self, original_query_func, name, options) + call_count = call_count + 1 + ngx.sleep(0.5) -- block all other threads + return original_query_func(self, name, options) + end + + local answers_list = threads_resolve(10, TEST_DOMAIN) + + assert(call_count == 1) + for _, answers in ipairs(answers_list) do + assert.same(answers_list[1], answers) + end + end) + + it("timeout while waiting", function() + + local ip = "1.4.2.3" + local timeout = 500 -- ms + local name = TEST_DOMAIN + -- insert a stub thats waits and returns a fixed answers + query_func = function() + -- `+ 2` s ensures that the resty-lock expires + ngx.sleep(timeout / 1000 + 2) + return {{ + type = resolver.TYPE_A, + address = ip, + class = 1, + name = name, + ttl = 10, + }} + end + + local cli = assert(client_new({ + resolv_conf = "/etc/resolv.conf", + timeout = timeout, + retrans = 1, + })) + local answers_list = threads_resolve(10, name, cli) + + -- answers[1~9] are equal, as they all will wait for the first response + for i = 1, 9 do + assert.equal("could not acquire callback lock: timeout", answers_list[i]) + end + -- answers[10] comes from synchronous DNS access of the first request + assert.equal(ip, answers_list[10][1]["address"]) + end) + end) + + + it("disable additional section when querying", function() + + local function build_dns_reply(id, name, ip, ns_ip1, ns_ip2) + local function dns_encode_name(name) + local parts = {} + for part in string.gmatch(name, "[^.]+") do + table.insert(parts, string.char(#part) .. part) + end + table.insert(parts, "\0") + return table.concat(parts) + end + + local function ip_to_bytes(ip) + local bytes = { "\x00\x04" } -- RDLENGTH:4bytes (ipv4) + for octet in string.gmatch(ip, "%d+") do + table.insert(bytes, string.char(tonumber(octet))) + end + return table.concat(bytes) + end + + local package = {} + + -- Header + package[#package+1] = id + package[#package+1] = "\x85\x00" -- QR, AA, RD + package[#package+1] = "\x00\x01\x00\x01\x00\x00\x00\x02" -- QD:1 AN:1 NS:0 AR:2 + + -- Question + package[#package+1] = dns_encode_name(name) + package[#package+1] = "\x00\x01\x00\x01" -- QTYPE A; QCLASS IN + + -- Answer + package[#package+1] = dns_encode_name(name) + package[#package+1] = "\x00\x01\x00\x01\x00\x00\x00\x30" -- QTYPE:A; QCLASS:IN TTL:48 + package[#package+1] = ip_to_bytes(ip) + + -- Additional + local function add_additional(name, ip) + package[#package+1] = dns_encode_name(name) + package[#package+1] = "\x00\x01\x00\x01\x00\x00\x00\x30" -- QTYPE:A; QCLASS:IN TTL:48 + package[#package+1] = ip_to_bytes(ip) + end + + add_additional("ns1." .. name, ns_ip1) + add_additional("ns2." .. name, ns_ip2) + + return table.concat(package) + end + + local force_enable_additional_section = false + + -- dns client will ignore additional section + query_func = function(self, original_query_func, name, options) + if options.qtype ~= client.TYPE_A then + return { errcode = 5, errstr = "refused" } + end + + if force_enable_additional_section then + options.additional_section = true + end + + self.tcp_sock = nil -- disable TCP query + + local id + local sock = assert(self.socks[1]) + -- hook send to get id + local orig_sock_send = sock.send + sock.send = function (self, query) + id = query[1] .. query[2] + return orig_sock_send(self, query) + end + -- hook receive to reply raw data + sock.receive = function (self, size) + return build_dns_reply(id, name, "1.1.1.1", "2.2.2.2", "3.3.3.3") + end + + return original_query_func(self, name, options) + end + + local name = "additional-section.test" + + -- no additional_section by default + local cli = client.new({ nameservers = TEST_NSS }) + local answers = cli:resolve(name) + assert.equal(#answers, 1) + assert.same(answers[1].address, "1.1.1.1") + + -- test the buggy scenario + force_enable_additional_section = true + cli = client.new({ nameservers = TEST_NSS, cache_purge = true }) + answers = cli:resolve(name) + assert.equal(#answers, 3) + assert.same(answers[1].address, "1.1.1.1") + assert.same(answers[2].address, "2.2.2.2") + assert.same(answers[3].address, "3.3.3.3") + end) + +end) diff --git a/spec/01-unit/30-new-dns-client/03-old_client_cache_spec.lua b/spec/01-unit/30-new-dns-client/03-old_client_cache_spec.lua new file mode 100644 index 00000000000..eac3c53e55c --- /dev/null +++ b/spec/01-unit/30-new-dns-client/03-old_client_cache_spec.lua @@ -0,0 +1,465 @@ +-- This test case file originates from the old version of the DNS client and has +-- been modified to adapt to the new version of the DNS client. + +local _writefile = require("pl.utils").writefile +local tmpname = require("pl.path").tmpname +local cycle_aware_deep_copy = require("kong.tools.table").cycle_aware_deep_copy + +-- hosted in Route53 in the AWS sandbox +local TEST_NS = "198.51.100.0" + +local TEST_NSS = { TEST_NS } + +local gettime = ngx.now +local sleep = ngx.sleep + +local function assert_same_answers(a1, a2) + a1 = cycle_aware_deep_copy(a1) + a1.ttl = nil + a1.expire = nil + + a2 = cycle_aware_deep_copy(a2) + a2.ttl = nil + a2.expire = nil + + assert.same(a1, a2) +end + +describe("[DNS client cache]", function() + local resolver, client, query_func, old_udp, receive_func + + local resolv_path, hosts_path + + local function writefile(path, text) + _writefile(path, type(text) == "table" and table.concat(text, "\n") or text) + end + + local function client_new(opts) + opts = opts or {} + opts.resolv_conf = resolv_path + opts.hosts = hosts_path + opts.nameservers = opts.nameservers or TEST_NSS + opts.cache_purge = true + return client.new(opts) + end + + lazy_setup(function() + -- create temp resolv.conf and hosts + resolv_path = tmpname() + hosts_path = tmpname() + ngx.log(ngx.DEBUG, "create temp resolv.conf:", resolv_path, + " hosts:", hosts_path) + + -- hook sock:receive to do timeout test + old_udp = ngx.socket.udp + + _G.ngx.socket.udp = function (...) + local sock = old_udp(...) + + local old_receive = sock.receive + + sock.receive = function (...) + if receive_func then + receive_func(...) + end + return old_receive(...) + end + + return sock + end + + end) + + lazy_teardown(function() + if resolv_path then + os.remove(resolv_path) + end + if hosts_path then + os.remove(hosts_path) + end + + _G.ngx.socket.udp = old_udp + end) + + before_each(function() + -- inject r.query + package.loaded["resty.dns.resolver"] = nil + resolver = require("resty.dns.resolver") + local original_query_func = resolver.query + resolver.query = function(self, ...) + return query_func(self, original_query_func, ...) + end + + -- restore its API overlapped by the compatible layer + package.loaded["kong.dns.client"] = nil + client = require("kong.dns.client") + client.resolve = function (self, name, opts, tries) + if opts and opts.return_random then + return self:resolve_address(name, opts.port, opts.cache_only, tries) + else + return self:_resolve(name, opts and opts.qtype, opts and opts.cache_only, tries) + end + end + end) + + after_each(function() + package.loaded["resty.dns.resolver"] = nil + resolver = nil + query_func = nil + + package.loaded["kong.resty.dns.client"] = nil + client = nil + + receive_func = nil + end) + + describe("shortnames caching", function() + + local cli, mock_records, config + before_each(function() + writefile(resolv_path, "search domain.test") + config = { + nameservers = { "198.51.100.0" }, + ndots = 1, + search = { "domain.test" }, + hosts = {}, + order = { "LAST", "SRV", "A", "AAAA" }, + error_ttl = 0.5, + stale_ttl = 0.5, + enable_ipv6 = false, + } + cli = assert(client_new(config)) + + query_func = function(self, original_query_func, qname, opts) + return mock_records[qname..":"..opts.qtype] or { errcode = 3, errstr = "name error" } + end + end) + + it("are stored in cache without type", function() + mock_records = { + ["myhost1.domain.test:"..resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost1.domain.test", + ttl = 30, + }} + } + + local answers = cli:resolve("myhost1") + assert.equal(answers, cli.cache:get("myhost1:-1")) + end) + + it("are stored in cache with type", function() + mock_records = { + ["myhost2.domain.test:"..resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost2.domain.test", + ttl = 30, + }} + } + + local answers = cli:resolve("myhost2", { qtype = resolver.TYPE_A }) + assert.equal(answers, cli.cache:get("myhost2:" .. resolver.TYPE_A)) + end) + + it("are resolved from cache without type", function() + mock_records = {} + cli.cache:set("myhost3:-1", {ttl=30+4}, {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost3.domain.test", + ttl = 30, + }, + ttl = 30, + expire = gettime() + 30, + }) + + local answers = cli:resolve("myhost3") + assert.same(answers, cli.cache:get("myhost3:-1")) + end) + + it("are resolved from cache with type", function() + mock_records = {} + local cli = client_new() + cli.cache:set("myhost4:" .. resolver.TYPE_A, {ttl=30+4}, {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost4.domain.test", + ttl = 30, + }, + ttl = 30, + expire = gettime() + 30, + }) + + local answers = cli:resolve("myhost4", { qtype = resolver.TYPE_A }) + assert.equal(answers, cli.cache:get("myhost4:" .. resolver.TYPE_A)) + end) + + it("ttl in cache is honored for short name entries", function() + local ttl = 0.2 + -- in the short name case the same record is inserted again in the cache + -- and the lru-ttl has to be calculated, make sure it is correct + mock_records = { + ["myhost6.domain.test:"..resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost6.domain.test", + ttl = ttl, + }} + } + local mock_copy = cycle_aware_deep_copy(mock_records) + + -- resolve and check whether we got the mocked record + local answers = cli:resolve("myhost6") + assert_same_answers(answers, mock_records["myhost6.domain.test:"..resolver.TYPE_A]) + + -- replace our mocked list with the copy made (new table, so no equality) + mock_records = mock_copy + + -- wait for expiring + sleep(ttl + config.stale_ttl / 2) + + -- fresh result, but it should not affect answers2 + mock_records["myhost6.domain.test:"..resolver.TYPE_A][1].tag = "new" + + -- resolve again, now getting same record, but stale, this will trigger + -- background refresh query + local answers2 = cli:resolve("myhost6") + assert.falsy(answers2[1].tag) + assert.is_number(answers2._expire_at) -- stale; marked as expired + answers2._expire_at = nil + assert_same_answers(answers2, answers) + + -- wait for the refresh to complete. Ensure that the sleeping time is less + -- than ttl, avoiding the updated record from becoming stale again. + sleep(ttl / 2) + + -- resolve and check whether we got the new record from the mock copy + local answers3 = cli:resolve("myhost6") + assert.equal(answers3[1].tag, "new") + assert.falsy(answers3._expired_at) + assert.not_equal(answers, answers3) -- must be a different record now + assert_same_answers(answers3, mock_records["myhost6.domain.test:"..resolver.TYPE_A]) + + -- the 'answers3' resolve call above will also trigger a new background query + -- (because the sleep of 0.1 equals the records ttl of 0.1) + -- so let's yield to activate that background thread now. If not done so, + -- the `after_each` will clear `query_func` and an error will appear on the + -- next test after this one that will yield. + sleep(0.1) + end) + + it("errors are not stored", function() + local rec = { + errcode = 4, + errstr = "server failure", + } + mock_records = { + ["myhost7.domain.test:"..resolver.TYPE_A] = rec, + ["myhost7:"..resolver.TYPE_A] = rec, + } + + local answers, err = cli:resolve("myhost7", { qtype = resolver.TYPE_A }) + assert.is_nil(answers) + assert.equal("dns server error: 4 server failure", err) + assert.is_nil(cli.cache:get("short:myhost7:" .. resolver.TYPE_A)) + end) + + it("name errors are not stored", function() + local rec = { + errcode = 3, + errstr = "name error", + } + mock_records = { + ["myhost8.domain.test:"..resolver.TYPE_A] = rec, + ["myhost8:"..resolver.TYPE_A] = rec, + } + + local answers, err = cli:resolve("myhost8", { qtype = resolver.TYPE_A }) + assert.is_nil(answers) + assert.equal("dns server error: 3 name error", err) + assert.is_nil(cli.cache:get("short:myhost8:" .. resolver.TYPE_A)) + end) + + end) + + + describe("fqdn caching", function() + + local cli, mock_records, config + before_each(function() + writefile(resolv_path, "search domain.test") + config = { + nameservers = { "198.51.100.0" }, + ndots = 1, + search = { "domain.test" }, + hosts = {}, + resolvConf = {}, + order = { "LAST", "SRV", "A", "AAAA" }, + error_ttl = 0.5, + stale_ttl = 0.5, + enable_ipv6 = false, + } + cli = assert(client_new(config)) + + query_func = function(self, original_query_func, qname, opts) + return mock_records[qname..":"..opts.qtype] or { errcode = 3, errstr = "name error" } + end + end) + + it("errors do not replace stale records", function() + local rec1 = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost9.domain.test", + ttl = 0.1, + }} + mock_records = { + ["myhost9.domain.test:"..resolver.TYPE_A] = rec1, + } + + local answers, err = cli:resolve("myhost9", { qtype = resolver.TYPE_A }) + assert.is_nil(err) + -- check that the cache is properly populated + assert_same_answers(rec1, answers) + answers = cli.cache:get("myhost9:" .. resolver.TYPE_A) + assert_same_answers(rec1, answers) + + sleep(0.15) -- make sure we surpass the ttl of 0.1 of the record, so it is now stale. + -- new mock records, such that we return server failures installed of records + local rec2 = { + errcode = 4, + errstr = "server failure", + } + mock_records = { + ["myhost9.domain.test:"..resolver.TYPE_A] = rec2, + ["myhost9:"..resolver.TYPE_A] = rec2, + } + -- doing a resolve will trigger the background query now + answers = cli:resolve("myhost9", { qtype = resolver.TYPE_A }) + assert.is_number(answers._expire_at) -- we get the stale record, now marked as expired + -- wait again for the background query to complete + sleep(0.1) + -- background resolve is now complete, check the cache, it should still have the + -- stale record, and it should not have been replaced by the error + -- + answers = cli.cache:get("myhost9:" .. resolver.TYPE_A) + assert.is_number(answers._expire_at) + answers._expire_at = nil + assert_same_answers(rec1, answers) + end) + + it("empty records do not replace stale records", function() + local rec1 = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myhost9.domain.test", + ttl = 0.1, + }} + mock_records = { + ["myhost9.domain.test:"..resolver.TYPE_A] = rec1, + } + + local answers = cli:resolve("myhost9", { qtype = resolver.TYPE_A }) + -- check that the cache is properly populated + assert_same_answers(rec1, answers) + assert_same_answers(rec1, cli.cache:get("myhost9:" .. resolver.TYPE_A)) + + sleep(0.15) -- stale + -- clear mock records, such that we return name errors instead of records + local rec2 = {} + mock_records = { + ["myhost9.domain.test:"..resolver.TYPE_A] = rec2, + ["myhost9:"..resolver.TYPE_A] = rec2, + } + -- doing a resolve will trigger the background query now + answers = cli:resolve("myhost9", { qtype = resolver.TYPE_A }) + assert.is_number(answers._expire_at) -- we get the stale record, now marked as expired + -- wait again for the background query to complete + sleep(0.1) + -- background resolve is now complete, check the cache, it should still have the + -- stale record, and it should not have been replaced by the empty record + answers = cli.cache:get("myhost9:" .. resolver.TYPE_A) + assert.is_number(answers._expire_at) -- we get the stale record, now marked as expired + answers._expire_at = nil + assert_same_answers(rec1, answers) + end) + + it("AS records do replace stale records", function() + -- when the additional section provides recordds, they should be stored + -- in the cache, as in some cases lookups of certain types (eg. CNAME) are + -- blocked, and then we rely on the A record to get them in the AS + -- (additional section), but then they must be stored obviously. + local CNAME1 = { + type = resolver.TYPE_CNAME, + cname = "myotherhost.domain.test", + class = 1, + name = "myhost9.domain.test", + ttl = 0.1, + } + local A2 = { + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "myotherhost.domain.test", + ttl = 60, + } + mock_records = setmetatable({ + ["myhost9.domain.test:"..resolver.TYPE_CNAME] = { cycle_aware_deep_copy(CNAME1) }, -- copy to make it different + ["myhost9.domain.test:"..resolver.TYPE_A] = { CNAME1, A2 }, -- not there, just a reference and target + ["myotherhost.domain.test:"..resolver.TYPE_A] = { A2 }, + }, { + -- do not do lookups, return empty on anything else + __index = function(self, key) + --print("looking for ",key) + return {} + end, + }) + + assert(cli:resolve("myhost9", { qtype = resolver.TYPE_CNAME })) + ngx.sleep(0.2) -- wait for it to become stale + assert(cli:resolve("myhost9"), { return_random = true }) + + local cached = cli.cache:get("myhost9.domain.test:" .. resolver.TYPE_CNAME) + assert.same(nil, cached) + end) + + end) + + describe("hosts entries", function() + -- hosts file names are cached for 10 years, verify that + -- it is not overwritten with valid_ttl settings. + -- Regressions reported in https://github.test/Kong/kong/issues/7444 + local cli, mock_records, config -- luacheck: ignore + writefile(resolv_path, "") + writefile(hosts_path, "127.0.0.1 myname.lan") + before_each(function() + config = { + nameservers = { "198.51.100.0" }, + --hosts = {"127.0.0.1 myname.lan"}, + --resolvConf = {}, + valid_ttl = 0.1, + stale_ttl = 0, + } + + cli = assert(client_new(config)) + end) + + it("entries from hosts file ignores valid_ttl overrides, Kong/kong #7444", function() + local record = cli:resolve("myname.lan") + assert.equal("127.0.0.1", record[1].address) + ngx.sleep(0.2) -- must be > valid_ttl + stale_ttl + + record = cli.cache:get("myname.lan:-1") + assert.equal("127.0.0.1", record[1].address) + end) + end) +end) diff --git a/spec/01-unit/30-new-dns-client/04-client_ipc_spec.lua b/spec/01-unit/30-new-dns-client/04-client_ipc_spec.lua new file mode 100644 index 00000000000..5ed287def1d --- /dev/null +++ b/spec/01-unit/30-new-dns-client/04-client_ipc_spec.lua @@ -0,0 +1,63 @@ +local helpers = require "spec.helpers" +local pl_file = require "pl.file" + + +local function count_log_lines(pattern) + local cfg = helpers.test_conf + local logs = pl_file.read(cfg.prefix .. "/" .. cfg.proxy_error_log) + local _, count = logs:gsub(pattern, "") + return count +end + + +describe("[dns-client] inter-process communication:",function() + local num_workers = 2 + + setup(function() + local bp = helpers.get_db_utils("postgres", { + "routes", + "services", + "plugins", + }, { + "dns-client-test", + }) + + bp.plugins:insert { + name = "dns-client-test", + } + + assert(helpers.start_kong({ + nginx_conf = "spec/fixtures/custom_nginx.template", + plugins = "bundled,dns-client-test", + nginx_main_worker_processes = num_workers, + legacy_dns_client = "off", + })) + end) + + teardown(function() + helpers.stop_kong() + end) + + it("stale updating task broadcast events", function() + helpers.wait_until(function() + return count_log_lines("DNS query completed") == num_workers + end, 5) + + assert.same(count_log_lines("first:query:ipc.test"), 1) + assert.same(count_log_lines("first:answers:1.2.3.4"), num_workers) + + assert.same(count_log_lines("stale:query:ipc.test"), 1) + assert.same(count_log_lines("stale:answers:1.2.3.4."), num_workers) + + -- wait background tasks to finish + helpers.wait_until(function() + return count_log_lines("stale:broadcast:ipc.test:%-1") == 1 + end, 5) + + -- "stale:lru ..." means the progress of the two workers is about the same. + -- "first:lru ..." means one of the workers is far behind the other. + helpers.wait_until(function() + return count_log_lines(":lru delete:ipc.test:%-1") == 1 + end, 5) + end) +end) diff --git a/spec/01-unit/30-new-dns-client/05-client_stat_spec.lua b/spec/01-unit/30-new-dns-client/05-client_stat_spec.lua new file mode 100644 index 00000000000..fa926ddb1b1 --- /dev/null +++ b/spec/01-unit/30-new-dns-client/05-client_stat_spec.lua @@ -0,0 +1,197 @@ +local sleep = ngx.sleep + +describe("[DNS client stats]", function() + local resolver, client, query_func + + local function client_new(opts) + opts = opts or {} + opts.hosts = {} + opts.nameservers = { "198.51.100.0" } -- placeholder, not used + return client.new(opts) + end + + before_each(function() + -- inject r.query + package.loaded["resty.dns.resolver"] = nil + resolver = require("resty.dns.resolver") + resolver.query = function(...) + if not query_func then + return nil + end + return query_func(...) + end + + -- restore its API overlapped by the compatible layer + package.loaded["kong.dns.client"] = nil + client = require("kong.dns.client") + client.resolve = client._resolve + end) + + after_each(function() + package.loaded["resty.dns.resolver"] = nil + resolver = nil + query_func = nil + + package.loaded["kong.resty.dns.client"] = nil + client = nil + end) + + describe("stats", function() + local mock_records + before_each(function() + query_func = function(self, qname, opts) + local records = mock_records[qname..":"..opts.qtype] + if type(records) == "string" then + return nil, records -- as error message + end + return records or { errcode = 3, errstr = "name error" } + end + end) + + it("resolve SRV", function() + mock_records = { + ["_ldaps._tcp.srv.test:" .. resolver.TYPE_SRV] = {{ + type = resolver.TYPE_SRV, + target = "srv.test", + port = 636, + weight = 10, + priority = 10, + class = 1, + name = "_ldaps._tcp.srv.test", + ttl = 10, + }}, + ["srv.test:" .. resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "srv.test", + ttl = 30, + }}, + } + + local cli = assert(client_new()) + cli:resolve("_ldaps._tcp.srv.test") + + local query_last_time + for k, v in pairs(cli.stats.stats) do + if v.query_last_time then + query_last_time = v.query_last_time + v.query_last_time = nil + end + end + assert.match("^%d+$", query_last_time) + + assert.same({ + ["_ldaps._tcp.srv.test:33"] = { + ["query"] = 1, + ["query_succ"] = 1, + ["miss"] = 1, + ["runs"] = 1, + }, + }, cli.stats.stats) + end) + + it("resolve all types", function() + mock_records = { + ["hit.test:" .. resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "hit.test", + ttl = 30, + }}, + ["nameserver_fail.test:" .. resolver.TYPE_A] = "nameserver failed", + ["stale.test:" .. resolver.TYPE_A] = {{ + type = resolver.TYPE_A, + address = "1.2.3.4", + class = 1, + name = "stale.test", + ttl = 0.1, + }}, + ["empty_result_not_stale.test:" .. resolver.TYPE_A] = {{ + type = resolver.TYPE_CNAME, -- will be ignored compared to type A + cname = "stale.test", + class = 1, + name = "empty_result_not_stale.test", + ttl = 0.1, + }}, + } + + local cli = assert(client_new({ + order = { "A" }, + error_ttl = 0.1, + empty_ttl = 0.1, + stale_ttl = 1, + })) + + -- "hit_lru" + cli:resolve("hit.test") + cli:resolve("hit.test") + -- "hit_shm" + cli.cache.lru:delete("hit.test:all") + cli:resolve("hit.test") + + -- "query_err:nameserver failed" + cli:resolve("nameserver_fail.test") + + -- "stale" + cli:resolve("stale.test") + sleep(0.2) + cli:resolve("stale.test") + + cli:resolve("empty_result_not_stale.test") + sleep(0.2) + cli:resolve("empty_result_not_stale.test") + + local query_last_time + for k, v in pairs(cli.stats.stats) do + if v.query_last_time then + query_last_time = v.query_last_time + v.query_last_time = nil + end + end + assert.match("^%d+$", query_last_time) + + assert.same({ + ["hit.test:1"] = { + ["query"] = 1, + ["query_succ"] = 1, + }, + ["hit.test:-1"] = { + ["hit_lru"] = 2, + ["miss"] = 1, + ["runs"] = 3, + }, + ["nameserver_fail.test:-1"] = { + ["fail"] = 1, + ["runs"] = 1, + }, + ["nameserver_fail.test:1"] = { + ["query"] = 1, + ["query_fail_nameserver"] = 1, + }, + ["stale.test:-1"] = { + ["miss"] = 2, + ["runs"] = 2, + ["stale"] = 1, + }, + ["stale.test:1"] = { + ["query"] = 2, + ["query_succ"] = 2, + }, + ["empty_result_not_stale.test:-1"] = { + ["miss"] = 2, + ["runs"] = 2, + }, + ["empty_result_not_stale.test:1"] = { + ["query"] = 2, + ["query_fail:empty record received"] = 2, + }, + ["empty_result_not_stale.test:28"] = { + ["query"] = 2, + ["query_fail:name error"] = 2, + }, + }, cli.stats.stats) + end) + end) +end) diff --git a/spec/01-unit/31-simdjson/01-cjson_compatibility_spec.lua b/spec/01-unit/31-simdjson/01-cjson_compatibility_spec.lua new file mode 100644 index 00000000000..584954f9413 --- /dev/null +++ b/spec/01-unit/31-simdjson/01-cjson_compatibility_spec.lua @@ -0,0 +1,284 @@ +local helpers = require("spec.helpers") +local cjson = require("cjson.safe") +local simdjson = require("resty.simdjson") + + +local deep_sort = helpers.deep_sort + + +describe("[cjson compatibility] examples from cjson repo", function () + + local strs = { +[[ +{ + "glossary": { + "title": "example glossary", + "GlossDiv": { + "title": "S", + "GlossList": { + "GlossEntry": { + "ID": "SGML", + "SortAs": "SGML", + "GlossTerm": "Standard Generalized Mark up Language", + "Acronym": "SGML", + "Abbrev": "ISO 8879:1986", + "GlossDef": { + "para": "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso": ["GML", "XML"] + }, + "GlossSee": "markup" + } + } + } + } +} +]], + +[[ +{"menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + {"value": "New", "onclick": "CreateNewDoc()"}, + {"value": "Open", "onclick": "OpenDoc()"}, + {"value": "Close", "onclick": "CloseDoc()"} + ] + } +}} +]], + +[[ +{"widget": { + "debug": "on", + "window": { + "title": "Sample Konfabulator Widget", + "name": "main_window", + "width": 500, + "height": 500 + }, + "image": { + "src": "Images/Sun.png", + "name": "sun1", + "hOffset": 250, + "vOffset": 250, + "alignment": "center" + }, + "text": { + "data": "Click Here", + "size": 36, + "style": "bold", + "name": "text1", + "hOffset": 250, + "vOffset": 100, + "alignment": "center", + "onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;" + } +}} +]], + +[[ +{"web-app": { + "servlet": [ + { + "servlet-name": "cofaxCDS", + "servlet-class": "org.cofax.cds.CDSServlet", + "init-param": { + "configGlossary:installationAt": "Philadelphia, PA", + "configGlossary:adminEmail": "ksm@pobox.com", + "configGlossary:poweredBy": "Cofax", + "configGlossary:poweredByIcon": "/images/cofax.gif", + "configGlossary:staticPath": "/content/static", + "templateProcessorClass": "org.cofax.WysiwygTemplate", + "templateLoaderClass": "org.cofax.FilesTemplateLoader", + "templatePath": "templates", + "templateOverridePath": "", + "defaultListTemplate": "listTemplate.htm", + "defaultFileTemplate": "articleTemplate.htm", + "useJSP": false, + "jspListTemplate": "listTemplate.jsp", + "jspFileTemplate": "articleTemplate.jsp", + "cachePackageTagsTrack": 200, + "cachePackageTagsStore": 200, + "cachePackageTagsRefresh": 60, + "cacheTemplatesTrack": 100, + "cacheTemplatesStore": 50, + "cacheTemplatesRefresh": 15, + "cachePagesTrack": 200, + "cachePagesStore": 100, + "cachePagesRefresh": 10, + "cachePagesDirtyRead": 10, + "searchEngineListTemplate": "forSearchEnginesList.htm", + "searchEngineFileTemplate": "forSearchEngines.htm", + "searchEngineRobotsDb": "WEB-INF/robots.db", + "useDataStore": true, + "dataStoreClass": "org.cofax.SqlDataStore", + "redirectionClass": "org.cofax.SqlRedirection", + "dataStoreName": "cofax", + "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver", + "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon", + "dataStoreUser": "sa", + "dataStorePassword": "dataStoreTestQuery", + "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';", + "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log", + "dataStoreInitConns": 10, + "dataStoreMaxConns": 100, + "dataStoreConnUsageLimit": 100, + "dataStoreLogLevel": "debug", + "maxUrlLength": 500}}, + { + "servlet-name": "cofaxEmail", + "servlet-class": "org.cofax.cds.EmailServlet", + "init-param": { + "mailHost": "mail1", + "mailHostOverride": "mail2"}}, + { + "servlet-name": "cofaxAdmin", + "servlet-class": "org.cofax.cds.AdminServlet"}, + + { + "servlet-name": "fileServlet", + "servlet-class": "org.cofax.cds.FileServlet"}, + { + "servlet-name": "cofaxTools", + "servlet-class": "org.cofax.cms.CofaxToolsServlet", + "init-param": { + "templatePath": "toolstemplates/", + "log": 1, + "logLocation": "/usr/local/tomcat/logs/CofaxTools.log", + "logMaxSize": "", + "dataLog": 1, + "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log", + "dataLogMaxSize": "", + "removePageCache": "/content/admin/remove?cache=pages&id=", + "removeTemplateCache": "/content/admin/remove?cache=templates&id=", + "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder", + "lookInContext": 1, + "adminGroupID": 4, + "betaServer": true}}], + "servlet-mapping": { + "cofaxCDS": "/", + "cofaxEmail": "/cofaxutil/aemail/*", + "cofaxAdmin": "/admin/*", + "fileServlet": "/static/*", + "cofaxTools": "/tools/*"}, + + "taglib": { + "taglib-uri": "cofax.tld", + "taglib-location": "/WEB-INF/tlds/cofax.tld"}}} +]], + +[[ +{"menu": { + "header": "SVG Viewer", + "items": [ + {"id": "Open"}, + {"id": "OpenNew", "label": "Open New"}, + null, + {"id": "ZoomIn", "label": "Zoom In"}, + {"id": "ZoomOut", "label": "Zoom Out"}, + {"id": "OriginalView", "label": "Original View"}, + null, + {"id": "Quality"}, + {"id": "Pause"}, + {"id": "Mute"}, + null, + {"id": "Find", "label": "Find..."}, + {"id": "FindAgain", "label": "Find Again"}, + {"id": "Copy"}, + {"id": "CopyAgain", "label": "Copy Again"}, + {"id": "CopySVG", "label": "Copy SVG"}, + {"id": "ViewSVG", "label": "View SVG"}, + {"id": "ViewSource", "label": "View Source"}, + {"id": "SaveAs", "label": "Save As"}, + null, + {"id": "Help"}, + {"id": "About", "label": "About Adobe CVG Viewer..."} + ] +}} +]], + +[[ +[ 0.110001, + 0.12345678910111, + 0.412454033640, + 2.6651441426902, + 2.718281828459, + 3.1415926535898, + 2.1406926327793 +] +]], + +[[ +{ + "Image": { + "Width": 800, + "Height": 600, + "Title": "View from 15th Floor", + "Thumbnail": { + "Url": "http://www.example.com/image/481989943", + "Height": 125, + "Width": "100" + }, + "IDs": [116, 943, 234, 38793] + } +} +]], + +[[ +[ + { + "precision": "zip", + "Latitude": 37.7668, + "Longitude": -122.3959, + "Address": "", + "City": "SAN FRANCISCO", + "State": "CA", + "Zip": "94107", + "Country": "US" + }, + { + "precision": "zip", + "Latitude": 37.371991, + "Longitude": -122.026020, + "Address": "", + "City": "SUNNYVALE", + "State": "CA", + "Zip": "94085", + "Country": "US" + } +] +]], + } + + it("runs with unified interface", function () + + local parser = simdjson.new() + assert(parser) + + for _, str in ipairs(strs) do + local obj1 = parser:decode(str) + local obj2 = cjson.decode(parser:encode(obj1)) + assert.same(deep_sort(obj1), deep_sort(obj2)) + end + + parser:destroy() + end) + + it("runs with separated interface", function () + + local dec = require("resty.simdjson.decoder").new() + local enc = require("resty.simdjson.encoder").new() + + assert(dec and enc) + + for _, str in ipairs(strs) do + local obj1 = dec:process(str) + local obj2 = cjson.decode(enc:process(obj1)) + assert.same(deep_sort(obj1), deep_sort(obj2)) + end + + dec:destroy() + end) + +end) diff --git a/spec/01-unit/31-simdjson/02-yield_spec.lua b/spec/01-unit/31-simdjson/02-yield_spec.lua new file mode 100644 index 00000000000..020a066d130 --- /dev/null +++ b/spec/01-unit/31-simdjson/02-yield_spec.lua @@ -0,0 +1,86 @@ +local orig_ngx_sleep = ngx.sleep + + +local spy_ngx_sleep +local simdjson +local test_obj +local test_arr +local test_str + + +describe("[yield]", function() + lazy_setup(function() + test_obj = { str = string.rep("a", 2100), } + + test_arr = {} + for i = 1, 1000 do + test_arr[i] = i + end + + test_str = "[" .. table.concat(test_arr, ",") .. "]" + end) + + + before_each(function() + spy_ngx_sleep = spy.on(ngx, "sleep") + simdjson = require("resty.simdjson") + end) + + + after_each(function() + ngx.sleep = orig_ngx_sleep -- luacheck: ignore + package.loaded["resty.simdjson"] = nil + package.loaded["resty.simdjson.decoder"] = nil + package.loaded["resty.simdjson.encoder"] = nil + end) + + + for _, v in ipairs { true, false, } do + it("enable = " .. tostring(v) .." when encoding", function() + + local parser = simdjson.new(v) + assert(parser) + + local str = parser:encode(test_obj) + + parser:destroy() + + assert(str) + assert(type(str) == "string") + assert.equal(string.format([[{"str":"%s"}]], string.rep("a", 2100)), str) + + if v then + assert.spy(spy_ngx_sleep).was_called(1) -- yield once + assert.spy(spy_ngx_sleep).was_called_with(0) -- yield 0ms + + else + assert.spy(spy_ngx_sleep).was_called(0) -- no yield + end + end) + end + + + for _, v in ipairs { true, false, } do + it("enable = " .. tostring(v) .." when decoding", function() + + local parser = simdjson.new(v) + assert(parser) + + local obj = parser:decode(test_str) + + parser:destroy() + + assert(obj) + assert(type(obj) == "table") + assert.same(test_arr, obj) + + if v then + assert.spy(spy_ngx_sleep).was_called(1) -- yield once + assert.spy(spy_ngx_sleep).was_called_with(0) -- yield 0ms + + else + assert.spy(spy_ngx_sleep).was_called(0) -- no yield + end + end) + end +end) diff --git a/spec/02-integration/02-cmd/02-start_stop_spec.lua b/spec/02-integration/02-cmd/02-start_stop_spec.lua index 15b61ad5255..31c55665d00 100644 --- a/spec/02-integration/02-cmd/02-start_stop_spec.lua +++ b/spec/02-integration/02-cmd/02-start_stop_spec.lua @@ -10,12 +10,14 @@ local read_file = helpers.file.read local PREFIX = helpers.test_conf.prefix +local SOCKET_PATH = helpers.test_conf.socket_path local TEST_CONF = helpers.test_conf local TEST_CONF_PATH = helpers.test_conf_path local function wait_until_healthy(prefix) prefix = prefix or PREFIX + local socket_path = prefix .. "/sockets" local cmd @@ -41,11 +43,11 @@ local function wait_until_healthy(prefix) local conf = assert(helpers.get_running_conf(prefix)) if conf.proxy_listen and conf.proxy_listen ~= "off" then - helpers.wait_for_file("socket", prefix .. "/worker_events.sock") + helpers.wait_for_file("socket", socket_path .. "/worker_events.sock") end if conf.stream_listen and conf.stream_listen ~= "off" then - helpers.wait_for_file("socket", prefix .. "/stream_worker_events.sock") + helpers.wait_for_file("socket", socket_path .. "/stream_worker_events.sock") end if conf.admin_listen and conf.admin_listen ~= "off" then @@ -1034,11 +1036,51 @@ describe("kong start/stop #" .. strategy, function() end) end) + describe("socket_path", function() + it("is created on demand by `kong prepare`", function() + local dir, cleanup = helpers.make_temp_dir() + finally(cleanup) + + local cmd = fmt("prepare -p %q", dir) + assert.truthy(kong_exec(cmd), "expected '" .. cmd .. "' to succeed") + assert.truthy(helpers.path.isdir(dir .. "/sockets"), + "expected '" .. dir .. "/sockets' directory to be created") + end) + + it("can be a user-created symlink", function() + local prefix, cleanup = helpers.make_temp_dir() + finally(cleanup) + + local socket_path + socket_path, cleanup = helpers.make_temp_dir() + finally(cleanup) + + assert.truthy(helpers.execute(fmt("ln -sf %q %q/sockets", socket_path, prefix)), + "failed to symlink socket path") + + local preserve_prefix = true + assert(helpers.start_kong({ + prefix = prefix, + database = "off", + nginx_conf = "spec/fixtures/custom_nginx.template", + }, nil, preserve_prefix)) + + finally(function() + helpers.stop_kong(prefix) + end) + + wait_until_healthy(prefix) + + assert.truthy(helpers.path.exists(socket_path .. "/worker_events.sock"), + "worker events socket was not created in the socket_path dir") + end) + end) + describe("dangling socket cleanup", function() local pidfile = TEST_CONF.nginx_pid -- the worker events socket is just one of many unix sockets we use - local event_sock = PREFIX .. "/worker_events.sock" + local event_sock = SOCKET_PATH .. "/worker_events.sock" local env = { prefix = PREFIX, @@ -1134,7 +1176,7 @@ describe("kong start/stop #" .. strategy, function() local _, stderr = assert_start() assert.matches("[warn] Found dangling unix sockets in the prefix directory", stderr, nil, true) - assert.matches(PREFIX, stderr, nil, true) + assert.matches(SOCKET_PATH, stderr, nil, true) assert.matches("removing unix socket", stderr) assert.matches(event_sock, stderr, nil, true) @@ -1175,6 +1217,7 @@ describe("kong start/stop #" .. strategy, function() it("works with resty.events when KONG_PREFIX is a relative path", function() local prefix = "relpath" + local socket_path = "relpath/sockets" finally(function() -- this test uses a non-default prefix, so it must manage @@ -1201,8 +1244,8 @@ describe("kong start/stop #" .. strategy, function() -- wait until everything is running wait_until_healthy(prefix) - assert.truthy(helpers.path.exists(prefix .. "/worker_events.sock")) - assert.truthy(helpers.path.exists(prefix .. "/stream_worker_events.sock")) + assert.truthy(helpers.path.exists(socket_path .. "/worker_events.sock")) + assert.truthy(helpers.path.exists(socket_path .. "/stream_worker_events.sock")) local log = prefix .. "/logs/error.log" assert.logfile(log).has.no.line("[error]", true, 0) diff --git a/spec/02-integration/03-db/08-declarative_spec.lua b/spec/02-integration/03-db/08-declarative_spec.lua index 4bfc44f1650..9c6b80af2e1 100644 --- a/spec/02-integration/03-db/08-declarative_spec.lua +++ b/spec/02-integration/03-db/08-declarative_spec.lua @@ -132,6 +132,7 @@ for _, strategy in helpers.each_strategy() do deny = ngx.null, allow = { "*" }, hide_groups_header = false, + always_use_authenticated_groups = false, } } @@ -146,6 +147,7 @@ for _, strategy in helpers.each_strategy() do deny = ngx.null, allow = { "*" }, hide_groups_header = false, + always_use_authenticated_groups = false, } } diff --git a/spec/02-integration/03-db/14-dao_spec.lua b/spec/02-integration/03-db/14-dao_spec.lua index a8f20498d1c..0b15bc49b4a 100644 --- a/spec/02-integration/03-db/14-dao_spec.lua +++ b/spec/02-integration/03-db/14-dao_spec.lua @@ -85,7 +85,7 @@ for _, strategy in helpers.all_strategies() do local kong_global = require("kong.global") local kong = _G.kong - kong.worker_events = assert(kong_global.init_worker_events()) + kong.worker_events = assert(kong_global.init_worker_events(kong.configuration)) kong.cluster_events = assert(kong_global.init_cluster_events(kong.configuration, kong.db)) kong.cache = assert(kong_global.init_cache(kong.configuration, kong.cluster_events, kong.worker_events)) kong.core_cache = assert(kong_global.init_core_cache(kong.configuration, kong.cluster_events, kong.worker_events)) diff --git a/spec/02-integration/04-admin_api/02-kong_routes_spec.lua b/spec/02-integration/04-admin_api/02-kong_routes_spec.lua index 4c3c502a119..49c00b01d3b 100644 --- a/spec/02-integration/04-admin_api/02-kong_routes_spec.lua +++ b/spec/02-integration/04-admin_api/02-kong_routes_spec.lua @@ -1,6 +1,8 @@ local helpers = require "spec.helpers" +local ssl_fixtures = require "spec.fixtures.ssl" local cjson = require "cjson" local constants = require "kong.constants" +local Errors = require "kong.db.errors" local UUID_PATTERN = "%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x" @@ -554,6 +556,45 @@ describe("Admin API - Kong routes with strategy #" .. strategy, function() local json = cjson.decode(body) assert.equal("schema validation successful", json.message) end) + + it("returns 200 on certificates schema with snis", function() + + local res = assert(client:post("/schemas/certificates/validate", { + body = { + cert = ssl_fixtures.cert, + key = ssl_fixtures.key, + snis = {"a", "b", "c" }, + }, + headers = { ["Content-Type"] = "application/json" } + })) + local body = assert.res_status(200, res) + local json = cjson.decode(body) + assert.equal("schema validation successful", json.message) + end) + + it("returns 400 on certificates schema with invalid snis", function() + + local res = assert(client:post("/schemas/certificates/validate", { + body = { + cert = ssl_fixtures.cert, + key = ssl_fixtures.key, + snis = {"120.0.9.32:90" }, + }, + headers = { ["Content-Type"] = "application/json" } + })) + local body = assert.res_status(400, res) + local json = cjson.decode(body) + local expected_body = { + fields= { + snis= { "must not be an IP" } + }, + name= "schema violation", + message= "schema violation (snis.1: must not be an IP)", + code= Errors.codes.SCHEMA_VIOLATION, + } + assert.same(expected_body, json) + end) + it("returns 200 on a valid plugin schema", function() local res = assert(client:post("/schemas/plugins/validate", { body = { diff --git a/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua b/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua index c0fde48646c..dbea8ec7983 100644 --- a/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua +++ b/spec/02-integration/04-admin_api/06-certificates_routes_spec.lua @@ -36,12 +36,23 @@ describe("Admin API: #" .. strategy, function() local n2 = get_name() local names = { n1, n2 } + local certificate = { + cert = ssl_fixtures.cert, + key = ssl_fixtures.key, + snis = names, + } + + local validate_res = client:post("/schemas/certificates/validate", { + body = certificate, + headers = { ["Content-Type"] = "application/json" }, + }) + + local validate_body = assert.res_status(200, validate_res) + local json = cjson.decode(validate_body) + assert.equal("schema validation successful", json.message) + local res = client:post("/certificates", { - body = { - cert = ssl_fixtures.cert, - key = ssl_fixtures.key, - snis = names, - }, + body = certificate, headers = { ["Content-Type"] = "application/json" }, }) @@ -370,9 +381,8 @@ describe("Admin API: #" .. strategy, function() assert.equal(cjson.null, json.key_alt) assert.same({ n1 }, json.snis) - json.snis = nil - local in_db = assert(db.certificates:select({ id = id }, { nulls = true })) + local in_db = assert(db.certificates:select_with_name_list({ id = id }, { nulls = true })) assert.same(json, in_db) end) @@ -395,9 +405,8 @@ describe("Admin API: #" .. strategy, function() assert.equal(cjson.null, json.key_alt) assert.same({ n1, n2 }, json.snis) - json.snis = nil - local in_db = assert(db.certificates:select(json, { nulls = true })) + local in_db = assert(db.certificates:select_with_name_list(json, { nulls = true })) assert.same(json, in_db) end) @@ -420,9 +429,8 @@ describe("Admin API: #" .. strategy, function() assert.equal(cjson.null, json.key_alt) assert.same({ n1, n2 }, json.snis) - json.snis = nil - local in_db = assert(db.certificates:select(json, { nulls = true })) + local in_db = assert(db.certificates:select_with_name_list(json, { nulls = true })) assert.same(json, in_db) end) @@ -444,9 +452,7 @@ describe("Admin API: #" .. strategy, function() assert.same({}, json.snis) assert.truthy(certificate.updated_at < json.updated_at) - json.snis = nil - - local in_db = assert(db.certificates:select(certificate, { nulls = true })) + local in_db = assert(db.certificates:select_with_name_list(certificate, { nulls = true })) assert.same(json, in_db) end) @@ -470,9 +476,7 @@ describe("Admin API: #" .. strategy, function() assert.same(ssl_fixtures.key_alt_ecdsa, json.key_alt) assert.same({}, json.snis) - json.snis = nil - - local in_db = assert(db.certificates:select(certificate, { nulls = true })) + local in_db = assert(db.certificates:select_with_name_list(certificate, { nulls = true })) assert.same(json, in_db) end) diff --git a/spec/02-integration/04-admin_api/26-dns_client_spec.lua b/spec/02-integration/04-admin_api/26-dns_client_spec.lua new file mode 100644 index 00000000000..036671732a8 --- /dev/null +++ b/spec/02-integration/04-admin_api/26-dns_client_spec.lua @@ -0,0 +1,102 @@ +local helpers = require "spec.helpers" +local cjson = require "cjson" + + +for _, strategy in helpers.each_strategy() do + describe("Admin API - DNS client route with [#" .. strategy .. "]" , function() + local client + + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "upstreams", + "targets", + }) + + local upstream = bp.upstreams:insert() + bp.targets:insert({ + upstream = upstream, + target = "_service._proto.srv.test", + }) + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + legacy_dns_client = "off", + })) + + client = helpers.admin_client() + end) + + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + end) + + it("/status/dns - status code 200", function () + local res = assert(client:send { + method = "GET", + path = "/status/dns", + headers = { ["Content-Type"] = "application/json" } + }) + + local body = assert.res_status(200 , res) + local json = cjson.decode(body) + + assert(type(json.worker.id) == "number") + assert(type(json.worker.count) == "number") + + assert(type(json.stats) == "table") + assert(type(json.stats["127.0.0.1|A/AAAA"].runs) == "number") + + -- Wait for the upstream target to be updated in the background + helpers.wait_until(function () + local res = assert(client:send { + method = "GET", + path = "/status/dns", + headers = { ["Content-Type"] = "application/json" } + }) + + local body = assert.res_status(200 , res) + local json = cjson.decode(body) + return type(json.stats["_service._proto.srv.test|SRV"]) == "table" + end, 5) + end) + end) + + describe("Admin API - DNS client route with [#" .. strategy .. "]" , function() + local client + + lazy_setup(function() + helpers.get_db_utils(strategy) + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + legacy_dns_client = true, + })) + + client = helpers.admin_client() + end) + + teardown(function() + if client then + client:close() + end + helpers.stop_kong() + end) + + it("/status/dns - status code 501", function () + local res = assert(client:send { + method = "GET", + path = "/status/dns", + headers = { ["Content-Type"] = "application/json" } + }) + + local body = assert.res_status(501, res) + local json = cjson.decode(body) + assert.same("not implemented with the legacy DNS client", json.message) + end) + end) +end diff --git a/spec/02-integration/05-proxy/01-proxy_spec.lua b/spec/02-integration/05-proxy/01-proxy_spec.lua index 53f31a050f4..6192d3ab4e6 100644 --- a/spec/02-integration/05-proxy/01-proxy_spec.lua +++ b/spec/02-integration/05-proxy/01-proxy_spec.lua @@ -102,10 +102,10 @@ describe("#stream proxy interface listeners", function() stream_listen = "127.0.0.1:9011, 127.0.0.1:9012", })) - local stream_events_sock_path = "unix:" .. helpers.test_conf.prefix .. "/stream_worker_events.sock" + local stream_events_sock_path = "unix:" .. helpers.test_conf.socket_path .. "/stream_worker_events.sock" if helpers.test_conf.database == "off" then - local stream_config_sock_path = "unix:" .. helpers.test_conf.prefix .. "/stream_config.sock" + local stream_config_sock_path = "unix:" .. helpers.test_conf.socket_path .. "/stream_config.sock" assert.equals(3, count_server_blocks(helpers.test_conf.nginx_kong_stream_conf)) assert.same({ diff --git a/spec/02-integration/05-proxy/05-dns_spec.lua b/spec/02-integration/05-proxy/05-dns_spec.lua index 9607352a26c..3e2c9475723 100644 --- a/spec/02-integration/05-proxy/05-dns_spec.lua +++ b/spec/02-integration/05-proxy/05-dns_spec.lua @@ -108,7 +108,7 @@ for _, strategy in helpers.each_strategy() do local service = bp.services:insert { name = "tests-retries", - host = "nowthisdoesnotexistatall", + host = "nowthisdoesnotexistatall.test", path = "/exist", port = 80, protocol = "http" diff --git a/spec/02-integration/05-proxy/09-websockets_spec.lua b/spec/02-integration/05-proxy/09-websockets_spec.lua index a70d8a4c585..8b404a6c729 100644 --- a/spec/02-integration/05-proxy/09-websockets_spec.lua +++ b/spec/02-integration/05-proxy/09-websockets_spec.lua @@ -90,7 +90,7 @@ for _, strategy in helpers.each_strategy() do assert.equal(true, string.find(header, "Upgrade: websocket") ~= nil, 1, true) if is_kong then - assert.equal(true, string.find(header, "Via: kong") ~= nil, 1, true) + assert.equal(true, string.find(header, "Via: 1.1 kong") ~= nil, 1, true) end end diff --git a/spec/02-integration/05-proxy/10-balancer/01-healthchecks_spec.lua b/spec/02-integration/05-proxy/10-balancer/01-healthchecks_spec.lua index 0d3872c093c..56769c6f26a 100644 --- a/spec/02-integration/05-proxy/10-balancer/01-healthchecks_spec.lua +++ b/spec/02-integration/05-proxy/10-balancer/01-healthchecks_spec.lua @@ -38,7 +38,7 @@ for _, strategy in helpers.each_strategy() do } fixtures.dns_mock:SRV { - name = "my.srv.test.test", + name = "_srv._pro.my.srv.test.test", target = "a.my.srv.test.test", port = 80, -- port should fail to connect } @@ -57,7 +57,7 @@ for _, strategy in helpers.each_strategy() do } fixtures.dns_mock:SRV { - name = "srv-changes-port.test", + name = "_srv._pro.srv-changes-port.test", target = "a-changes-port.test", port = 90, -- port should fail to connect } @@ -114,7 +114,7 @@ for _, strategy in helpers.each_strategy() do }) -- the following port will not be used, will be overwritten by -- the mocked SRV record. - bu.add_target(bp, upstream_id, "my.srv.test.test", 80) + bu.add_target(bp, upstream_id, "_srv._pro.my.srv.test.test", 80) local api_host = bu.add_api(bp, upstream_name) bu.end_testcase_setup(strategy, bp) @@ -301,7 +301,7 @@ for _, strategy in helpers.each_strategy() do }) -- the following port will not be used, will be overwritten by -- the mocked SRV record. - bu.add_target(bp, upstream_id, "srv-changes-port.test", 80) + bu.add_target(bp, upstream_id, "_srv._pro.srv-changes-port.test", 80) local api_host = bu.add_api(bp, upstream_name, { connect_timeout = 100, }) bu.end_testcase_setup(strategy, bp) @@ -328,7 +328,7 @@ for _, strategy in helpers.each_strategy() do assert.equals("UNHEALTHY", health.data[1].health) assert.equals("UNHEALTHY", health.data[1].data.addresses[1].health) - local status = bu.put_target_address_health(upstream_id, "srv-changes-port.test:80", "a-changes-port.test:90", "healthy") + local status = bu.put_target_address_health(upstream_id, "_srv._pro.srv-changes-port.test:80", "a-changes-port.test:90", "healthy") assert.same(204, status) end, 15) @@ -1780,7 +1780,7 @@ for _, strategy in helpers.each_strategy() do for i = 1, 3 do hosts[i] = { - hostname = bu.gen_multi_host(), + hostname = "_srv._pro." .. bu.gen_multi_host(), port1 = helpers.get_available_port(), port2 = helpers.get_available_port(), } diff --git a/spec/02-integration/05-proxy/14-server_tokens_spec.lua b/spec/02-integration/05-proxy/14-server_tokens_spec.lua index 95447d9b371..ac18a876968 100644 --- a/spec/02-integration/05-proxy/14-server_tokens_spec.lua +++ b/spec/02-integration/05-proxy/14-server_tokens_spec.lua @@ -6,7 +6,7 @@ local uuid = require("kong.tools.uuid").uuid local default_server_header = meta._SERVER_TOKENS - +local default_via_value = "1.1 " .. default_server_header for _, strategy in helpers.each_strategy() do describe("headers [#" .. strategy .. "]", function() @@ -95,7 +95,7 @@ describe("headers [#" .. strategy .. "]", function() assert.res_status(200, res) assert.not_equal(default_server_header, res.headers["server"]) - assert.equal(default_server_header, res.headers["via"]) + assert.equal(default_via_value, res.headers["via"]) end) it("should return Kong 'Server' header but not the Kong 'Via' header when no API matched (no proxy)", function() @@ -146,8 +146,8 @@ describe("headers [#" .. strategy .. "]", function() }) assert.res_status(200, res) - assert.equal(default_server_header, res.headers["via"]) - assert.not_equal(default_server_header, res.headers["server"]) + assert.equal(default_via_value, res.headers["via"]) + assert.not_equal(default_via_value, res.headers["server"]) end) it("should not return Kong 'Via' header or Kong 'Via' header when no API matched (no proxy)", function() @@ -223,7 +223,7 @@ describe("headers [#" .. strategy .. "]", function() assert.res_status(200, res) assert.not_equal(default_server_header, res.headers["server"]) - assert.equal(default_server_header, res.headers["via"]) + assert.equal(default_via_value, res.headers["via"]) end) it("should return Kong 'Server' header but not the Kong 'Via' header when no API matched (no proxy)", function() @@ -746,7 +746,7 @@ describe("headers [#" .. strategy .. "]", function() assert.res_status(200, res) assert.not_equal(default_server_header, res.headers["server"]) - assert.equal(default_server_header, res.headers["via"]) + assert.equal(default_via_value, res.headers["via"]) assert.is_not_nil(res.headers[constants.HEADERS.PROXY_LATENCY]) assert.is_nil(res.headers[constants.HEADERS.RESPONSE_LATENCY]) end) @@ -807,7 +807,7 @@ describe("headers [#" .. strategy .. "]", function() assert.res_status(200, res) assert.not_equal(default_server_header, res.headers["server"]) - assert.equal(default_server_header, res.headers["via"]) + assert.equal(default_via_value, res.headers["via"]) assert.is_not_nil(res.headers[constants.HEADERS.PROXY_LATENCY]) assert.is_nil(res.headers[constants.HEADERS.RESPONSE_LATENCY]) end) @@ -885,7 +885,7 @@ describe("headers [#" .. strategy .. "]", function() assert.res_status(200, res) assert.not_equal(default_server_header, res.headers["server"]) - assert.equal(default_server_header, res.headers["via"]) + assert.equal(default_via_value, res.headers["via"]) assert.is_not_nil(res.headers[constants.HEADERS.PROXY_LATENCY]) assert.is_nil(res.headers[constants.HEADERS.RESPONSE_LATENCY]) end) diff --git a/spec/02-integration/05-proxy/22-reports_spec.lua b/spec/02-integration/05-proxy/22-reports_spec.lua index eab85e46803..7b43172860e 100644 --- a/spec/02-integration/05-proxy/22-reports_spec.lua +++ b/spec/02-integration/05-proxy/22-reports_spec.lua @@ -242,6 +242,20 @@ for _, strategy in helpers.each_strategy() do proxy_ssl_client:close() end) + it("when send http request to https port, no other error in error.log", function() + local https_port = assert(helpers.get_proxy_port(true)) + local proxy_client = assert(helpers.proxy_client(nil, https_port)) + local res = proxy_client:get("/", { + headers = { host = "http-service.test" } + }) + reports_send_ping({port=constants.REPORTS.STATS_TLS_PORT}) + + assert.response(res).has_status(400) + assert.logfile().has.no.line("using uninitialized") + assert.logfile().has.no.line("could not determine log suffix (scheme=http, proxy_mode=)") + proxy_client:close() + end) + it("reports h2c requests", function() local h2c_client = assert(helpers.proxy_client_h2c()) local body, headers = h2c_client({ diff --git a/spec/02-integration/05-proxy/24-buffered_spec.lua b/spec/02-integration/05-proxy/24-buffered_spec.lua index c95cd726678..15e639c0bb5 100644 --- a/spec/02-integration/05-proxy/24-buffered_spec.lua +++ b/spec/02-integration/05-proxy/24-buffered_spec.lua @@ -1,6 +1,6 @@ local helpers = require "spec.helpers" local cjson = require "cjson" - +local http_mock = require "spec.helpers.http_mock" local md5 = ngx.md5 local TCP_PORT = helpers.get_available_port() @@ -255,8 +255,8 @@ for _, strategy in helpers.each_strategy() do -- to produce an nginx output filter error and status code 412 -- the response has to go through kong_error_handler (via error_page) it("remains healthy when if-match header is used with buffering", function() - local thread = helpers.tcp_server(TCP_PORT) - + local mock = http_mock.new(TCP_PORT) + mock:start() local res = assert(proxy_client:send { method = "GET", path = "/0", @@ -265,9 +265,9 @@ for _, strategy in helpers.each_strategy() do } }) - thread:join() assert.response(res).has_status(412) assert.logfile().has.no.line("exited on signal 11") + mock:stop(true) end) end) end) diff --git a/spec/02-integration/05-proxy/30-max-args_spec.lua b/spec/02-integration/05-proxy/30-max-args_spec.lua index b954fa66911..d47c603f9bd 100644 --- a/spec/02-integration/05-proxy/30-max-args_spec.lua +++ b/spec/02-integration/05-proxy/30-max-args_spec.lua @@ -145,6 +145,7 @@ local function validate_proxy(params, body, truncated) request_headers["x-forwarded-prefix"] = nil request_headers["x-forwarded-proto"] = nil request_headers["x-real-ip"] = nil + request_headers["via"] = nil assert.same(params.headers, request_headers) assert.same(params.query, body.uri_args) diff --git a/spec/02-integration/05-proxy/35-via_spec.lua b/spec/02-integration/05-proxy/35-via_spec.lua new file mode 100644 index 00000000000..a6177973d19 --- /dev/null +++ b/spec/02-integration/05-proxy/35-via_spec.lua @@ -0,0 +1,226 @@ +local helpers = require "spec.helpers" +local http_mock = require "spec.helpers.http_mock" +local cjson = require "cjson" +local meta = require "kong.meta" +local re_match = ngx.re.match + + +local str_fmt = string.format + +local SERVER_TOKENS = meta._SERVER_TOKENS + +for _, strategy in helpers.all_strategies() do + describe("append Kong Gateway info to the 'Via' header [#" .. strategy .. "]", function() + local mock, declarative_config, proxy_client, proxy_client_h2, proxy_client_grpc, proxy_client_grpcs + + lazy_setup(function() + local mock_port = helpers.get_available_port() + mock = http_mock.new(mock_port, { + ["/via"] = { + access = [=[ + ngx.req.set_header("X-Req-To", "http_mock") + ]=], + content = [=[ + local cjson = require "cjson" + ngx.say(cjson.encode({ via = tostring(ngx.var.http_via) })) + ]=], + -- bug: https://github.com/Kong/kong/pull/12753 + header_filter = "", header = [=[ + ngx.header["Server"] = 'http-mock' + ngx.header["Via"] = '2 nginx, HTTP/1.1 http_mock' + ngx.header["Content-type"] = 'application/json' + ]=], + }, + }, { + prefix = "servroot_mock", + req = true, + resp = false, + }) + assert(mock:start()) + + local bp = helpers.get_db_utils( + strategy == "off" and "postgres" or strategy, + { + "routes", + "services", + } + ) + + local service1 = assert(bp.services:insert { + name = "via_service", + url = "http://127.0.0.1:" .. mock_port .. "/via", + }) + + assert(bp.routes:insert { + name = "via_route", + hosts = { "test.via" }, + paths = { "/get" }, + service = { id = service1.id }, + }) + + local service2 = assert(bp.services:insert { + name = "grpc_service", + url = helpers.grpcbin_url, + }) + + assert(bp.routes:insert { + name = "grpc_route", + hosts = { "grpc" }, + paths = { "/" }, + service = { id = service2.id }, + }) + + local service3 = assert(bp.services:insert { + name = "grpcs_service", + url = helpers.grpcbin_ssl_url, + }) + + assert(bp.routes:insert { + name = "grpcs_route", + hosts = { "grpcs" }, + paths = { "/" }, + service = { id = service3.id }, + }) + + declarative_config = helpers.make_yaml_file(str_fmt([=[ + _format_version: '3.0' + _transform: true + services: + - name: via_service + url: "http://127.0.0.1:%s/via" + routes: + - name: via_route + hosts: + - test.via + paths: + - /get + - name: grpc_service + url: %s + routes: + - name: grpc_route + protocols: + - grpc + hosts: + - grpc + paths: + - / + - name: grpcs_service + url: %s + routes: + - name: grpcs_route + protocols: + - grpc + hosts: + - grpcs + paths: + - / + ]=], mock_port, helpers.grpcbin_url, helpers.grpcbin_ssl_url)) + + assert(helpers.start_kong({ + database = strategy, + plugins = "bundled", + nginx_conf = "spec/fixtures/custom_nginx.template", + declarative_config = strategy == "off" and declarative_config or nil, + pg_host = strategy == "off" and "unknownhost.konghq.com" or nil, + nginx_worker_processes = 1, + })) + + end) + + lazy_teardown(function() + helpers.stop_kong() + mock:stop() + end) + + it("HTTP/1.1 in both the inbound and outbound directions", function() + proxy_client = helpers.proxy_client() + + local res = assert(proxy_client:send { + method = "GET", + path = "/get", + headers = { + ["Host"] = "test.via", + ["Via"] = "1.1 dev", + } + }) + + local body = assert.res_status(200, res) + local json_body = cjson.decode(body) + assert.are_same({ via = "1.1 dev, 1.1 " .. SERVER_TOKENS }, json_body) + assert.are_same("2 nginx, HTTP/1.1 http_mock, 1.1 " .. SERVER_TOKENS, res.headers["Via"]) + assert.are_same("http-mock", res.headers["Server"]) + + if proxy_client then + proxy_client:close() + end + end) + + it("HTTP/2 in both the inbound and outbound directions", function() + proxy_client_h2 = helpers.proxy_client_h2() + + local body, headers = assert(proxy_client_h2({ + headers = { + [":method"] = "GET", + [":scheme"] = "https", + [":authority"] = "test.via", + [":path"] = "/get", + ["via"] = [['1.1 dev']], + } + })) + + assert.are_equal(200, tonumber(headers:get(":status"))) + local json_body = cjson.decode(body) + assert.are_same({ via = "1.1 dev, 2 " .. SERVER_TOKENS }, json_body) + assert.are_same("2 nginx, HTTP/1.1 http_mock, 1.1 " .. SERVER_TOKENS, headers:get("Via")) + assert.are_same("http-mock", headers:get("Server")) + end) + + it("gRPC without SSL in both the inbound and outbound directions", function() + proxy_client_grpc = helpers.proxy_client_grpc() + + local ok, resp = assert(proxy_client_grpc({ + service = "hello.HelloService.SayHello", + body = { + greeting = "world!" + }, + opts = { + ["-v"] = true, + ["-authority"] = "grpc", + } + })) + + assert.truthy(ok) + local server = re_match(resp, [=[Response headers received\:[\s\S]*\nserver\:\s(.*?)\n]=], "jo") + assert.are_equal(SERVER_TOKENS, server[1]) + local via = re_match(resp, [=[Response headers received\:[\s\S]*\nvia\:\s(.*?)\n]=], "jo") + assert.are_equal("2 " .. SERVER_TOKENS, via[1]) + local body = re_match(resp, [=[Response contents\:([\s\S]+?)\nResponse trailers received]=], "jo") + local json_body = cjson.decode(body[1]) + assert.are_equal("hello world!", json_body.reply) + end) + + it("gRPC with SSL in both the inbound and outbound directions", function() + proxy_client_grpcs = helpers.proxy_client_grpcs() + + local ok, resp = assert(proxy_client_grpcs({ + service = "hello.HelloService.SayHello", + body = { + greeting = "world!" + }, + opts = { + ["-v"] = true, + ["-authority"] = "grpcs", + } + })) + + assert.truthy(ok) + local server = re_match(resp, [=[Response headers received\:[\s\S]*\nserver\:\s(.*?)\n]=], "jo") + assert.are_equal(SERVER_TOKENS, server[1]) + local via = re_match(resp, [=[Response headers received\:[\s\S]*\nvia\:\s(.*?)\n]=], "jo") + assert.are_equal("2 " .. SERVER_TOKENS, via[1]) + local body = re_match(resp, [=[Response contents\:([\s\S]+?)\nResponse trailers received]=], "jo") + local json_body = cjson.decode(body[1]) + assert.are_equal("hello world!", json_body.reply) + end) + end) +end diff --git a/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua b/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua index d9946e39b04..0f2ec5d3931 100644 --- a/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua +++ b/spec/02-integration/06-invalidations/02-core_entities_invalidations_spec.lua @@ -471,6 +471,12 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate delete+re-creation", function() + -- populate cache + get_cert(8443, "ssl-example.com") + get_cert(8443, "new-ssl-example.com") + get_cert(9443, "ssl-example.com") + get_cert(9443, "new-ssl-example.com") + -- TODO: PATCH update are currently not possible -- with the admin API because snis have their name as their -- primary key and the DAO has limited support for such updates. @@ -514,6 +520,10 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate update", function() + -- populate cache + get_cert(8443, "new-ssl-example.com") + get_cert(9443, "new-ssl-example.com") + -- update our certificate *without* updating the -- attached sni @@ -548,6 +558,12 @@ for _, strategy in helpers.each_strategy() do end) it("on sni update via id", function() + -- populate cache + get_cert(8443, "new-ssl-example.com") + get_cert(8443, "updated-sn-via-id.com") + get_cert(9443, "new-ssl-example.com") + get_cert(9443, "updated-sn-via-id.com") + local admin_res = admin_client_1:get("/snis") local body = assert.res_status(200, admin_res) local sni = assert(cjson.decode(body).data[1]) @@ -579,6 +595,12 @@ for _, strategy in helpers.each_strategy() do end) it("on sni update via name", function() + -- populate cache + get_cert(8443, "updated-sn-via-id.com") + get_cert(8443, "updated-sn.com") + get_cert(9443, "updated-sn-via-id.com") + get_cert(9443, "updated-sn.com") + local admin_res = admin_client_1:patch("/snis/updated-sn-via-id.com", { body = { name = "updated-sn.com" }, headers = { ["Content-Type"] = "application/json" }, @@ -606,6 +628,10 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate delete", function() + -- populate cache + get_cert(8443, "updated-sn.com") + get_cert(9443, "updated-sn.com") + -- delete our certificate local admin_res = admin_client_1:delete("/certificates/updated-sn.com") @@ -630,6 +656,14 @@ for _, strategy in helpers.each_strategy() do describe("wildcard snis", function() it("on create", function() + -- populate cache + get_cert(8443, "test.wildcard.com") + get_cert(8443, "test2.wildcard.com") + get_cert(8443, "wildcard.com") + get_cert(9443, "test.wildcard.com") + get_cert(9443, "test2.wildcard.com") + get_cert(9443, "wildcard.com") + local admin_res = admin_client_1:post("/certificates", { body = { cert = ssl_fixtures.cert_alt, @@ -680,6 +714,12 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate update", function() + -- populate cache + get_cert(8443, "test.wildcard.com") + get_cert(8443, "test2.wildcard.com") + get_cert(9443, "test.wildcard.com") + get_cert(9443, "test2.wildcard.com") + -- update our certificate *without* updating the -- attached sni @@ -723,6 +763,14 @@ for _, strategy in helpers.each_strategy() do end) it("on sni update via id", function() + -- populate cache + get_cert(8443, "test.wildcard.com") + get_cert(8443, "test2.wildcard.com") + get_cert(8443, "test.wildcard_updated.com") + get_cert(9443, "test.wildcard.com") + get_cert(9443, "test2.wildcard.com") + get_cert(9443, "test.wildcard_updated.com") + local admin_res = admin_client_1:get("/snis/%2A.wildcard.com") local body = assert.res_status(200, admin_res) local sni = assert(cjson.decode(body)) @@ -762,6 +810,14 @@ for _, strategy in helpers.each_strategy() do end) it("on sni update via name", function() + -- populate cache + get_cert(8443, "test.wildcard.org") + get_cert(8443, "test2.wildcard.org") + get_cert(8443, "test.wildcard_updated.com") + get_cert(9443, "test.wildcard.org") + get_cert(9443, "test2.wildcard.org") + get_cert(9443, "test.wildcard_updated.com") + local admin_res = admin_client_1:patch("/snis/%2A.wildcard_updated.com", { body = { name = "*.wildcard.org" }, headers = { ["Content-Type"] = "application/json" }, @@ -797,6 +853,12 @@ for _, strategy in helpers.each_strategy() do end) it("on certificate delete", function() + -- populate cache + get_cert(8443, "test.wildcard.org") + get_cert(8443, "test2.wildcard.org") + get_cert(9443, "test.wildcard.org") + get_cert(9443, "test2.wildcard.org") + -- delete our certificate local admin_res = admin_client_1:delete("/certificates/%2A.wildcard.org") diff --git a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua index 18f79daf5fc..9eecc8ec7a4 100644 --- a/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua +++ b/spec/02-integration/09-hybrid_mode/09-config-compat_spec.lua @@ -253,6 +253,10 @@ describe("CP/DP config compat transformations #" .. strategy, function() expected_otel_prior_35.config.header_type = "preserve" expected_otel_prior_35.config.sampling_rate = nil expected_otel_prior_35.config.propagation = nil + expected_otel_prior_35.config.traces_endpoint = nil + expected_otel_prior_35.config.logs_endpoint = nil + expected_otel_prior_35.config.endpoint = "http://1.1.1.1:12345/v1/trace" + do_assert(uuid(), "3.4.0", expected_otel_prior_35) -- cleanup @@ -274,6 +278,9 @@ describe("CP/DP config compat transformations #" .. strategy, function() expected_otel_prior_34.config.header_type = "preserve" expected_otel_prior_34.config.sampling_rate = nil expected_otel_prior_34.config.propagation = nil + expected_otel_prior_34.config.traces_endpoint = nil + expected_otel_prior_34.config.logs_endpoint = nil + expected_otel_prior_34.config.endpoint = "http://1.1.1.1:12345/v1/trace" do_assert(uuid(), "3.3.0", expected_otel_prior_34) -- cleanup @@ -472,49 +479,200 @@ describe("CP/DP config compat transformations #" .. strategy, function() admin.plugins:remove({ id = response_rl.id }) end) end) + end) - describe("proxy-cache plugin", function() - it("rename age field in response_headers config from age to Age", function() - -- [[ 3.8.x ]] -- - local response_rl = admin.plugins:insert { - name = "proxy-cache", - enabled = true, - config = { - response_code = { 200, 301, 404 }, - request_method = { "GET", "HEAD" }, - content_type = { "text/plain", "application/json" }, - cache_ttl = 300, - strategy = "memory", - cache_control = false, - memory = { - dictionary_name = "kong_db_cache", + describe("ai plugins supported providers", function() + it("[ai-proxy] tries to use unsupported providers on older Kong versions", function() + -- [[ 3.8.x ]] -- + local ai_proxy = admin.plugins:insert { + name = "ai-proxy", + enabled = true, + config = { + response_streaming = "allow", + route_type = "llm/v1/chat", + auth = { + header_name = "header", + header_value = "value", + gcp_service_account_json = '{"service": "account"}', + gcp_use_service_account = true, + }, + model = { + name = "any-model-name", + provider = "gemini", + options = { + max_tokens = 512, + temperature = 0.5, + gemini = { + api_endpoint = "https://gemini.local", + project_id = "kong-gemini", + location_id = "us-east5", + }, }, - -- [[ age field renamed to Age - response_headers = { - ["Age"] = true, - ["X-Cache-Status"] = true, - ["X-Cache-Key"] = true - } - -- ]] - } - } + }, + max_request_body_size = 8192, + }, + } + -- ]] - local expected_response_rl_prior_38 = cycle_aware_deep_copy(response_rl) - expected_response_rl_prior_38.config.response_headers = { - ["age"] = true, - ["X-Cache-Status"] = true, - ["X-Cache-Key"] = true - } + local expected = cycle_aware_deep_copy(ai_proxy) - do_assert(uuid(), "3.7.0", expected_response_rl_prior_38) + -- max body size + expected.config.max_request_body_size = nil - -- cleanup - admin.plugins:remove({ id = response_rl.id }) - end) + -- gemini fields + expected.config.auth.gcp_service_account_json = nil + expected.config.auth.gcp_use_service_account = nil + expected.config.model.options.gemini = nil + + -- bedrock fields + expected.config.auth.aws_access_key_id = nil + expected.config.auth.aws_secret_access_key = nil + expected.config.model.options.bedrock = nil + + -- 'ai fallback' field sets + expected.config.route_type = "preserve" + expected.config.model.provider = "openai" + + do_assert(uuid(), "3.7.0", expected) + + expected.config.response_streaming = nil + expected.config.model.options.upstream_path = nil + expected.config.route_type = "llm/v1/chat" + + do_assert(uuid(), "3.6.0", expected) + + -- cleanup + admin.plugins:remove({ id = ai_proxy.id }) + end) + + it("[ai-request-transformer] tries to use unsupported providers on older Kong versions", function() + -- [[ 3.8.x ]] -- + local ai_request_transformer = admin.plugins:insert { + name = "ai-request-transformer", + enabled = true, + config = { + llm = { + route_type = "llm/v1/chat", + auth = { + header_name = "header", + header_value = "value", + gcp_service_account_json = '{"service": "account"}', + gcp_use_service_account = true, + }, + model = { + name = "any-model-name", + provider = "gemini", + options = { + max_tokens = 512, + temperature = 0.5, + gemini = { + api_endpoint = "https://gemini.local", + project_id = "kong-gemini", + location_id = "us-east5", + }, + }, + }, + }, + max_request_body_size = 8192, + prompt = "anything", + }, + } + -- ]] + + local expected = cycle_aware_deep_copy(ai_request_transformer) + + -- max body size + expected.config.max_request_body_size = nil + + -- gemini fields + expected.config.llm.auth.gcp_service_account_json = nil + expected.config.llm.auth.gcp_use_service_account = nil + expected.config.llm.model.options.gemini = nil + + -- bedrock fields + expected.config.llm.auth.aws_access_key_id = nil + expected.config.llm.auth.aws_secret_access_key = nil + expected.config.llm.model.options.bedrock = nil + + -- 'ai fallback' field sets + expected.config.llm.model.provider = "openai" + + do_assert(uuid(), "3.7.0", expected) + + expected.config.llm.model.options.upstream_path = nil + expected.config.llm.route_type = "llm/v1/chat" + + do_assert(uuid(), "3.6.0", expected) + + -- cleanup + admin.plugins:remove({ id = ai_request_transformer.id }) + end) + + it("[ai-response-transformer] tries to use unsupported providers on older Kong versions", function() + -- [[ 3.8.x ]] -- + local ai_response_transformer = admin.plugins:insert { + name = "ai-response-transformer", + enabled = true, + config = { + llm = { + route_type = "llm/v1/chat", + auth = { + header_name = "header", + header_value = "value", + gcp_service_account_json = '{"service": "account"}', + gcp_use_service_account = true, + }, + model = { + name = "any-model-name", + provider = "gemini", + options = { + max_tokens = 512, + temperature = 0.5, + gemini = { + api_endpoint = "https://gemini.local", + project_id = "kong-gemini", + location_id = "us-east5", + }, + }, + }, + }, + max_request_body_size = 8192, + prompt = "anything", + }, + } + -- ]] + + local expected = cycle_aware_deep_copy(ai_response_transformer) + + -- max body size + expected.config.max_request_body_size = nil + + -- gemini fields + expected.config.llm.auth.gcp_service_account_json = nil + expected.config.llm.auth.gcp_use_service_account = nil + expected.config.llm.model.options.gemini = nil + + -- bedrock fields + expected.config.llm.auth.aws_access_key_id = nil + expected.config.llm.auth.aws_secret_access_key = nil + expected.config.llm.model.options.bedrock = nil + + -- 'ai fallback' field sets + expected.config.llm.model.provider = "openai" + + do_assert(uuid(), "3.7.0", expected) + + expected.config.llm.model.options.upstream_path = nil + expected.config.llm.route_type = "llm/v1/chat" + + do_assert(uuid(), "3.6.0", expected) + + -- cleanup + admin.plugins:remove({ id = ai_response_transformer.id }) end) end) - describe("ai plugins", function() + describe("ai plugins shared options", function() it("[ai-proxy] sets unsupported AI LLM properties to nil or defaults", function() -- [[ 3.7.x ]] -- local ai_proxy = admin.plugins:insert { @@ -536,16 +694,33 @@ describe("CP/DP config compat transformations #" .. strategy, function() upstream_path = "/anywhere", -- becomes nil }, }, + max_request_body_size = 8192, }, } -- ]] - local expected_ai_proxy_prior_37 = cycle_aware_deep_copy(ai_proxy) - expected_ai_proxy_prior_37.config.response_streaming = nil - expected_ai_proxy_prior_37.config.model.options.upstream_path = nil - expected_ai_proxy_prior_37.config.route_type = "llm/v1/chat" + local expected = cycle_aware_deep_copy(ai_proxy) + + -- max body size + expected.config.max_request_body_size = nil + + -- gemini fields + expected.config.auth.gcp_service_account_json = nil + expected.config.auth.gcp_use_service_account = nil + expected.config.model.options.gemini = nil + + -- bedrock fields + expected.config.auth.aws_access_key_id = nil + expected.config.auth.aws_secret_access_key = nil + expected.config.model.options.bedrock = nil - do_assert(uuid(), "3.6.0", expected_ai_proxy_prior_37) + do_assert(uuid(), "3.7.0", expected) + + expected.config.response_streaming = nil + expected.config.model.options.upstream_path = nil + expected.config.route_type = "llm/v1/chat" + + do_assert(uuid(), "3.6.0", expected) -- cleanup admin.plugins:remove({ id = ai_proxy.id }) @@ -577,14 +752,31 @@ describe("CP/DP config compat transformations #" .. strategy, function() }, }, }, + max_request_body_size = 8192, }, } -- ]] - local expected_ai_request_transformer_prior_37 = cycle_aware_deep_copy(ai_request_transformer) - expected_ai_request_transformer_prior_37.config.llm.model.options.upstream_path = nil + local expected = cycle_aware_deep_copy(ai_request_transformer) + + -- max body size + expected.config.max_request_body_size = nil + + -- gemini fields + expected.config.llm.auth.gcp_service_account_json = nil + expected.config.llm.auth.gcp_use_service_account = nil + expected.config.llm.model.options.gemini = nil + + -- bedrock fields + expected.config.llm.auth.aws_access_key_id = nil + expected.config.llm.auth.aws_secret_access_key = nil + expected.config.llm.model.options.bedrock = nil - do_assert(uuid(), "3.6.0", expected_ai_request_transformer_prior_37) + do_assert(uuid(), "3.7.0", expected) + + expected.config.llm.model.options.upstream_path = nil + + do_assert(uuid(), "3.6.0", expected) -- cleanup admin.plugins:remove({ id = ai_request_transformer.id }) @@ -614,18 +806,81 @@ describe("CP/DP config compat transformations #" .. strategy, function() }, }, }, + max_request_body_size = 8192, }, } - -- ]] + --]] + + local expected = cycle_aware_deep_copy(ai_response_transformer) + + -- max body size + expected.config.max_request_body_size = nil - local expected_ai_response_transformer_prior_37 = cycle_aware_deep_copy(ai_response_transformer) - expected_ai_response_transformer_prior_37.config.llm.model.options.upstream_path = nil + -- gemini fields + expected.config.llm.auth.gcp_service_account_json = nil + expected.config.llm.auth.gcp_use_service_account = nil + expected.config.llm.model.options.gemini = nil - do_assert(uuid(), "3.6.0", expected_ai_response_transformer_prior_37) + -- bedrock fields + expected.config.llm.auth.aws_access_key_id = nil + expected.config.llm.auth.aws_secret_access_key = nil + expected.config.llm.model.options.bedrock = nil + + do_assert(uuid(), "3.7.0", expected) + + expected.config.llm.model.options.upstream_path = nil + + do_assert(uuid(), "3.6.0", expected) -- cleanup admin.plugins:remove({ id = ai_response_transformer.id }) end) + + it("[ai-prompt-guard] sets unsupported match_all_roles to nil or defaults", function() + -- [[ 3.8.x ]] -- + local ai_prompt_guard = admin.plugins:insert { + name = "ai-prompt-guard", + enabled = true, + config = { + allow_patterns = { "a" }, + allow_all_conversation_history = false, + match_all_roles = true, + max_request_body_size = 8192, + }, + } + -- ]] + + local expected = cycle_aware_deep_copy(ai_prompt_guard) + expected.config.match_all_roles = nil + expected.config.max_request_body_size = nil + + do_assert(uuid(), "3.7.0", expected) + + -- cleanup + admin.plugins:remove({ id = ai_prompt_guard.id }) + end) + end) + + describe("prometheus plugins", function() + it("[prometheus] remove ai_metrics property for versions below 3.8", function() + -- [[ 3.8.x ]] -- + local prometheus = admin.plugins:insert { + name = "prometheus", + enabled = true, + config = { + ai_metrics = true, -- becomes nil + }, + } + -- ]] + + local expected_prometheus_prior_38 = cycle_aware_deep_copy(prometheus) + expected_prometheus_prior_38.config.ai_metrics = nil + + do_assert(uuid(), "3.7.0", expected_prometheus_prior_38) + + -- cleanup + admin.plugins:remove({ id = prometheus.id }) + end) end) describe("www-authenticate header in plugins (realm config)", function() @@ -762,6 +1017,46 @@ describe("CP/DP config compat transformations #" .. strategy, function() admin.plugins:remove({ id = rt.id }) end) end) + + describe("compatibility test for acl plugin", function() + it("removes `config.always_use_authenticated_groups` before sending them to older(less than 3.8.0.0) DP nodes", function() + local acl = admin.plugins:insert { + name = "acl", + enabled = true, + config = { + allow = { "admin" }, + -- [[ new fields 3.8.0 + always_use_authenticated_groups = true, + -- ]] + } + } + + assert.not_nil(acl.config.always_use_authenticated_groups) + local expected_acl = cycle_aware_deep_copy(acl) + expected_acl.config.always_use_authenticated_groups = nil + do_assert(uuid(), "3.7.0", expected_acl) + + -- cleanup + admin.plugins:remove({ id = acl.id }) + end) + + it("does not remove `config.always_use_authenticated_groups` from DP nodes that are already compatible", function() + local acl = admin.plugins:insert { + name = "acl", + enabled = true, + config = { + allow = { "admin" }, + -- [[ new fields 3.8.0 + always_use_authenticated_groups = true, + -- ]] + } + } + do_assert(uuid(), "3.8.0", acl) + + -- cleanup + admin.plugins:remove({ id = acl.id }) + end) + end) end) end) diff --git a/spec/02-integration/12-stream_api/01-stream_api_endpoint_spec.lua b/spec/02-integration/12-stream_api/01-stream_api_endpoint_spec.lua index 05385fd7397..a60c60c489f 100644 --- a/spec/02-integration/12-stream_api/01-stream_api_endpoint_spec.lua +++ b/spec/02-integration/12-stream_api/01-stream_api_endpoint_spec.lua @@ -13,7 +13,7 @@ describe("Stream module API endpoint", function() plugins = "stream-api-echo", }) - socket_path = "unix:" .. helpers.get_running_conf().prefix .. "/stream_rpc.sock" + socket_path = "unix:" .. helpers.get_running_conf().socket_path .. "/stream_rpc.sock" end) lazy_teardown(function() diff --git a/spec/02-integration/14-tracing/01-instrumentations_spec.lua b/spec/02-integration/14-observability/01-instrumentations_spec.lua similarity index 99% rename from spec/02-integration/14-tracing/01-instrumentations_spec.lua rename to spec/02-integration/14-observability/01-instrumentations_spec.lua index 781c85cd8fb..0d9af192799 100644 --- a/spec/02-integration/14-tracing/01-instrumentations_spec.lua +++ b/spec/02-integration/14-observability/01-instrumentations_spec.lua @@ -524,7 +524,7 @@ for _, strategy in helpers.each_strategy() do -- intentionally trigger a DNS query error local service = bp.services:insert({ name = "inexist-host-service", - host = "really-inexist-host", + host = "really-inexist-host.test", port = 80, }) @@ -558,7 +558,7 @@ for _, strategy in helpers.each_strategy() do local dns_spans = assert_has_spans("kong.dns", spans) local upstream_dns for _, dns_span in ipairs(dns_spans) do - if dns_span.attributes["dns.record.domain"] == "really-inexist-host" then + if dns_span.attributes["dns.record.domain"] == "really-inexist-host.test" then upstream_dns = dns_span break end diff --git a/spec/02-integration/14-tracing/02-propagation_spec.lua b/spec/02-integration/14-observability/02-propagation_spec.lua similarity index 95% rename from spec/02-integration/14-tracing/02-propagation_spec.lua rename to spec/02-integration/14-observability/02-propagation_spec.lua index 5465994d32e..8275998bcfa 100644 --- a/spec/02-integration/14-tracing/02-propagation_spec.lua +++ b/spec/02-integration/14-observability/02-propagation_spec.lua @@ -1,7 +1,7 @@ local helpers = require "spec.helpers" local cjson = require "cjson" local to_hex = require("resty.string").to_hex -local from_hex = require 'kong.tracing.propagation.utils'.from_hex +local from_hex = require 'kong.observability.tracing.propagation.utils'.from_hex local rand_bytes = require("kong.tools.rand").get_rand_bytes @@ -29,8 +29,8 @@ local function generate_function_plugin_config(propagation_config, trace_id, spa return { access = { string.format([[ - local propagation = require 'kong.tracing.propagation' - local from_hex = require 'kong.tracing.propagation.utils'.from_hex + local propagation = require 'kong.observability.tracing.propagation' + local from_hex = require 'kong.observability.tracing.propagation.utils'.from_hex local function transform_bin_id(id, last_byte) if not id then diff --git a/spec/02-integration/14-tracing/03-tracer-pdk_spec.lua b/spec/02-integration/14-observability/03-tracer-pdk_spec.lua similarity index 100% rename from spec/02-integration/14-tracing/03-tracer-pdk_spec.lua rename to spec/02-integration/14-observability/03-tracer-pdk_spec.lua diff --git a/spec/02-integration/14-tracing/04-trace-ids-log_spec.lua b/spec/02-integration/14-observability/04-trace-ids-log_spec.lua similarity index 98% rename from spec/02-integration/14-tracing/04-trace-ids-log_spec.lua rename to spec/02-integration/14-observability/04-trace-ids-log_spec.lua index b17fcbfa59a..fa6b6d02929 100644 --- a/spec/02-integration/14-tracing/04-trace-ids-log_spec.lua +++ b/spec/02-integration/14-observability/04-trace-ids-log_spec.lua @@ -145,7 +145,7 @@ for _, strategy in helpers.each_strategy() do name = "opentelemetry", route = { id = otel_route.id }, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = config_header.type, } }) @@ -154,7 +154,7 @@ for _, strategy in helpers.each_strategy() do name = "opentelemetry", route = { id = otel_zipkin_route.id }, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = config_header.type, } }) @@ -163,7 +163,7 @@ for _, strategy in helpers.each_strategy() do name = "opentelemetry", route = { id = otel_zipkin_route_2.id }, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = "jaeger", } }) diff --git a/spec/02-integration/14-observability/05-logs_spec.lua b/spec/02-integration/14-observability/05-logs_spec.lua new file mode 100644 index 00000000000..3334386bb5d --- /dev/null +++ b/spec/02-integration/14-observability/05-logs_spec.lua @@ -0,0 +1,86 @@ +local helpers = require "spec.helpers" + +for _, strategy in helpers.each_strategy() do + describe("Observability Logs", function () + describe("ngx.log patch", function() + local proxy_client + local post_function_access = [[ + local threads = {} + local n_threads = 100 + + for i = 1, n_threads do + threads[i] = ngx.thread.spawn(function() + ngx.log(ngx.INFO, "thread_" .. i .. " logged") + end) + end + + for i = 1, n_threads do + ngx.thread.wait(threads[i]) + end + ]] + + lazy_setup(function() + local bp = helpers.get_db_utils(strategy, { + "routes", + "services", + "plugins", + }) + + local http_srv = assert(bp.services:insert { + name = "mock-service", + host = helpers.mock_upstream_host, + port = helpers.mock_upstream_port, + }) + + local logs_route = assert(bp.routes:insert({ service = http_srv, + protocols = { "http" }, + paths = { "/logs" }})) + + assert(bp.plugins:insert({ + name = "post-function", + route = logs_route, + config = { + access = { post_function_access }, + }, + })) + + -- only needed to enable the log collection hook + assert(bp.plugins:insert({ + name = "opentelemetry", + route = logs_route, + config = { + logs_endpoint = "http://" .. helpers.mock_upstream_host .. ":" .. helpers.mock_upstream_port, + } + })) + + helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + plugins = "opentelemetry,post-function", + }) + proxy_client = helpers.proxy_client() + end) + + lazy_teardown(function() + if proxy_client then + proxy_client:close() + end + helpers.stop_kong() + end) + + it("does not produce yielding and concurrent executions", function () + local res = assert(proxy_client:send { + method = "GET", + path = "/logs", + }) + assert.res_status(200, res) + + -- plugin produced logs: + assert.logfile().has.line("thread_1 logged", true, 10) + assert.logfile().has.line("thread_100 logged", true, 10) + -- plugin did not produce concurrent accesses to ngx.log: + assert.logfile().has.no.line("[error]", true) + end) + end) + end) +end diff --git a/spec/02-integration/14-observability/06-telemetry-pdk_spec.lua b/spec/02-integration/14-observability/06-telemetry-pdk_spec.lua new file mode 100644 index 00000000000..c59ba5a6b29 --- /dev/null +++ b/spec/02-integration/14-observability/06-telemetry-pdk_spec.lua @@ -0,0 +1,208 @@ +local helpers = require "spec.helpers" +local pb = require "pb" + +local HTTP_SERVER_PORT_LOGS = helpers.get_available_port() + + +for _, strategy in helpers.each_strategy() do + describe("kong.pdk.telemetry #" .. strategy, function() + local bp + local plugin_instance_name = "my-pdk-logger-instance" + + describe("log", function() + describe("with OpenTelemetry", function() + local mock_logs + + lazy_setup(function() + bp, _ = assert(helpers.get_db_utils(strategy, { + "services", + "routes", + "plugins", + }, { "opentelemetry", "pdk-logger" })) + + local http_srv = assert(bp.services:insert { + name = "mock-service", + host = helpers.mock_upstream_host, + port = helpers.mock_upstream_port, + }) + + local logs_route = assert(bp.routes:insert({ + service = http_srv, + protocols = { "http" }, + paths = { "/logs" } + })) + + assert(bp.plugins:insert({ + name = "opentelemetry", + route = logs_route, + config = { + logs_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_LOGS, + queue = { + max_batch_size = 1000, + max_coalescing_delay = 2, + }, + } + })) + + assert(bp.plugins:insert({ + name = "pdk-logger", + route = logs_route, + config = {}, + instance_name = plugin_instance_name, + })) + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + plugins = "opentelemetry,pdk-logger", + })) + + mock_logs = helpers.http_mock(HTTP_SERVER_PORT_LOGS, { timeout = 1 }) + end) + + lazy_teardown(function() + helpers.stop_kong() + if mock_logs then + mock_logs("close", true) + end + end) + + local function assert_find_valid_logs(body, request_id) + local decoded = assert(pb.decode("opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest", body)) + assert.not_nil(decoded) + + local scope_logs = decoded.resource_logs[1].scope_logs + assert.is_true(#scope_logs > 0, scope_logs) + + local found = 0 + for _, scope_log in ipairs(scope_logs) do + local log_records = scope_log.log_records + for _, log_record in ipairs(log_records) do + -- from the pdk-logger plugin: + local plugin_name = "pdk-logger" + local attributes = { + some_key = "some_value", + some_other_key = "some_other_value" + } + local expected_messages_attributes = { + access_phase = { message = "hello, access phase", attributes = attributes}, + header_filter_phase = { message = "hello, header_filter phase", attributes = {}}, + log_phase = { message = "", attributes = attributes}, + log_phase_2 = { message = "", attributes = {}}, + } + + assert.is_table(log_record.attributes) + local found_attrs = {} + for _, attr in ipairs(log_record.attributes) do + found_attrs[attr.key] = attr.value[attr.value.value] + end + + local exp_msg_attr = expected_messages_attributes[found_attrs["message.type"]] + + -- filter the right log lines + if exp_msg_attr then + -- ensure the log is from the current request + if found_attrs["request.id"] == request_id then + local logline = log_record.body and log_record.body.string_value + + assert.equals(exp_msg_attr.message, logline) + assert.partial_match(exp_msg_attr.attributes, found_attrs) + + assert.is_string(found_attrs["plugin.id"]) + assert.is_number(found_attrs["introspection.current.line"]) + assert.matches("pdk%-logger/handler%.lua", found_attrs["introspection.source"]) + assert.equals(plugin_name, found_attrs["plugin.name"]) + assert.equals(plugin_instance_name, found_attrs["plugin.instance.name"]) + + assert.is_number(log_record.time_unix_nano) + assert.is_number(log_record.observed_time_unix_nano) + + found = found + 1 + end + end + end + end + assert.equals(4, found) + end + + it("produces and exports valid logs", function() + local headers, body, request_id + + local cli = helpers.proxy_client() + local res = assert(cli:send { + method = "GET", + path = "/logs", + }) + assert.res_status(200, res) + cli:close() + + request_id = res.headers["X-Kong-Request-Id"] + + helpers.wait_until(function() + local lines + lines, body, headers = mock_logs() + + return lines + end, 10) + + assert.is_string(body) + assert.equals(headers["Content-Type"], "application/x-protobuf") + + assert_find_valid_logs(body, request_id) + assert.logfile().has.no.line("[error]", true) + end) + end) + + describe("without OpenTelemetry", function() + lazy_setup(function() + bp, _ = assert(helpers.get_db_utils(strategy, { + "services", + "routes", + "plugins", + }, { "pdk-logger" })) + + local http_srv = assert(bp.services:insert { + name = "mock-service", + host = helpers.mock_upstream_host, + port = helpers.mock_upstream_port, + }) + + local logs_route = assert(bp.routes:insert({ + service = http_srv, + protocols = { "http" }, + paths = { "/logs" } + })) + + assert(bp.plugins:insert({ + name = "pdk-logger", + route = logs_route, + config = {}, + instance_name = plugin_instance_name, + })) + + assert(helpers.start_kong({ + database = strategy, + nginx_conf = "spec/fixtures/custom_nginx.template", + plugins = "pdk-logger", + })) + end) + + lazy_teardown(function() + helpers.stop_kong() + end) + + it("handles errors correctly", function() + local cli = helpers.proxy_client() + local res = assert(cli:send { + method = "GET", + path = "/logs", + }) + assert.res_status(200, res) + cli:close() + + assert.logfile().has.line("Telemetry logging is disabled", true, 10) + end) + end) + end) + end) +end diff --git a/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua b/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua index ce637af87f6..f2c623128f9 100644 --- a/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua +++ b/spec/02-integration/20-wasm/04-proxy-wasm_spec.lua @@ -1,5 +1,6 @@ local helpers = require "spec.helpers" local cjson = require "cjson" +local meta = require "kong.meta" local HEADER_NAME_PHASE = "X-PW-Phase" @@ -195,7 +196,7 @@ describe("proxy-wasm filters (#wasm) (#" .. strategy .. ")", function() local body = assert.res_status(200, res) local json = cjson.decode(body) - assert.equal("proxy-wasm", json.headers["via"]) + assert.equal("1.1 " .. meta._SERVER_TOKENS, json.headers["via"]) -- TODO: honor case-sensitivity (proxy-wasm-rust-sdk/ngx_wasm_module investigation) -- assert.equal("proxy-wasm", json.headers["Via"]) assert.logfile().has.no.line("[error]", true, 0) @@ -786,7 +787,7 @@ describe("proxy-wasm filters (#wasm) (#" .. strategy .. ")", function() assert.logfile().has.no.line("[crit]", true, 0) end) - pending("resolves DNS hostnames to send an http dispatch, return its response body", function() + it("resolves DNS hostnames to send an http dispatch, return its response body", function() local client = helpers.proxy_client() finally(function() client:close() end) diff --git a/spec/03-plugins/01-legacy_queue_parameter_warning_spec.lua b/spec/03-plugins/01-legacy_queue_parameter_warning_spec.lua index 8390383533d..5c873c7666a 100644 --- a/spec/03-plugins/01-legacy_queue_parameter_warning_spec.lua +++ b/spec/03-plugins/01-legacy_queue_parameter_warning_spec.lua @@ -55,7 +55,7 @@ for _, strategy in helpers.each_strategy() do ["statsd"] = {}, ["datadog"] = {}, ["opentelemetry"] = { - endpoint = "http://example.com/", + traces_endpoint = "http://example.com/", }, } diff --git a/spec/03-plugins/02-legacy_propagation_parameter_warning_spec.lua b/spec/03-plugins/02-legacy_propagation_parameter_warning_spec.lua index 88e8a487ec5..c69e97dc146 100644 --- a/spec/03-plugins/02-legacy_propagation_parameter_warning_spec.lua +++ b/spec/03-plugins/02-legacy_propagation_parameter_warning_spec.lua @@ -53,7 +53,7 @@ for _, strategy in helpers.each_strategy() do http_endpoint = "http://example.com/", }, ["opentelemetry"] = { - endpoint = "http://example.com/", + traces_endpoint = "http://example.com/", }, } diff --git a/spec/03-plugins/13-cors/01-access_spec.lua b/spec/03-plugins/13-cors/01-access_spec.lua index 7bba3a82ce8..cf6dce91817 100644 --- a/spec/03-plugins/13-cors/01-access_spec.lua +++ b/spec/03-plugins/13-cors/01-access_spec.lua @@ -287,6 +287,10 @@ for _, strategy in helpers.each_strategy() do hosts = { "cors13.test" }, }) + local route14 = bp.routes:insert({ + hosts = { "cors14.test" }, + }) + local mock_upstream = bp.services:insert { host = helpers.mock_upstream_hostname, port = helpers.mock_upstream_port, @@ -451,6 +455,15 @@ for _, strategy in helpers.each_strategy() do } } + bp.plugins:insert { + name = "cors", + route = { id = route14.id }, + config = { + preflight_continue = false, + origins = { "foo.bar", "*" } + } + } + bp.plugins:insert { name = "cors", route = { id = route_timeout.id }, @@ -613,7 +626,7 @@ for _, strategy in helpers.each_strategy() do assert.is_nil(res.headers["Vary"]) end) - it("gives appropriate defaults when origin is explicitly set to *", function() + it("gives appropriate defaults when origin is explicitly set to * and config.credentials=true", function() local res = assert(proxy_client:send { method = "OPTIONS", headers = { @@ -633,6 +646,25 @@ for _, strategy in helpers.each_strategy() do assert.is_nil(res.headers["Access-Control-Max-Age"]) end) + it("gives * wildcard when origin has multiple entries and have * included", function() + local res = assert(proxy_client:send { + method = "OPTIONS", + headers = { + ["Host"] = "cors14.test", + ["Origin"] = "http://www.example.net", + ["Access-Control-Request-Method"] = "GET", + } + }) + assert.res_status(200, res) + assert.equal("0", res.headers["Content-Length"]) + assert.equal(CORS_DEFAULT_METHODS, res.headers["Access-Control-Allow-Methods"]) + assert.equal("*", res.headers["Access-Control-Allow-Origin"]) + assert.is_nil(res.headers["Access-Control-Allow-Headers"]) + assert.is_nil(res.headers["Access-Control-Expose-Headers"]) + assert.is_nil(res.headers["Access-Control-Allow-Credentials"]) + assert.is_nil(res.headers["Access-Control-Max-Age"]) + end) + it("accepts config options", function() local res = assert(proxy_client:send { method = "OPTIONS", @@ -1032,7 +1064,7 @@ for _, strategy in helpers.each_strategy() do assert.equal("Origin", res.headers["Vary"]) end) - it("responds with * when config.credentials=false", function() + it("responds with * when origin is explicitly set to * and config.credentials=false", function() local res = assert(proxy_client:send { method = "GET", headers = { @@ -1046,6 +1078,18 @@ for _, strategy in helpers.each_strategy() do assert.is_nil(res.headers["Vary"]) end) + it("responds with * when origin has multiple entries and have * included", function() + local res = assert(proxy_client:send { + method = "GET", + headers = { + ["Host"] = "cors14.test", + ["Origin"] = "http://www.example.net" + } + }) + assert.res_status(200, res) + assert.equals("*", res.headers["Access-Control-Allow-Origin"]) + end) + it("removes upstream ACAO header when no match is found", function() local res = proxy_client:get("/response-headers", { query = ngx.encode_args({ diff --git a/spec/03-plugins/18-acl/02-access_spec.lua b/spec/03-plugins/18-acl/02-access_spec.lua index 157fc2afcf7..8b69f0f2434 100644 --- a/spec/03-plugins/18-acl/02-access_spec.lua +++ b/spec/03-plugins/18-acl/02-access_spec.lua @@ -80,6 +80,20 @@ for _, strategy in helpers.each_strategy() do consumer = { id = consumer4.id }, } + local consumer5 = bp.consumers:insert { + username = "consumer5" + } + + bp.keyauth_credentials:insert { + key = "apikey127", + consumer = { id = consumer5.id }, + } + + bp.acls:insert { + group = "acl_group1", + consumer = { id = consumer5.id }, + } + local anonymous = bp.consumers:insert { username = "anonymous" } @@ -739,6 +753,86 @@ for _, strategy in helpers.each_strategy() do } } + local route15 = bp.routes:insert({ + hosts = { "acl15.test" } + }) + + bp.plugins:insert { + name = "acl", + route = { id = route15.id }, + config = { + allow = { "auth_group1" }, + hide_groups_header = false, + always_use_authenticated_groups = true, + } + } + + bp.plugins:insert { + name = "key-auth", + route = { id = route15.id }, + config = {} + } + + bp.plugins:insert { + name = "ctx-checker", + route = { id = route15.id }, + config = { + ctx_kind = "kong.ctx.shared", + ctx_set_field = "authenticated_groups", + ctx_set_array = { "auth_group1" }, + } + } + + local route16 = bp.routes:insert({ + hosts = { "acl16.test" } + }) + + bp.plugins:insert { + name = "acl", + route = { id = route16.id }, + config = { + allow = { "auth_group1" }, + hide_groups_header = false, + always_use_authenticated_groups = true, + } + } + + bp.plugins:insert { + name = "key-auth", + route = { id = route16.id }, + config = {} + } + + bp.plugins:insert { + name = "ctx-checker", + route = { id = route16.id }, + config = { + ctx_kind = "kong.ctx.shared", + ctx_set_field = "authenticated_groups", + ctx_set_array = { "auth_group2" }, + } + } + + local route17 = bp.routes:insert({ + hosts = { "acl17.test" } + }) + + bp.plugins:insert { + name = "acl", + route = { id = route17.id }, + config = { + allow = { "acl_group1" }, + hide_groups_header = false, + always_use_authenticated_groups = true, + } + } + + bp.plugins:insert { + name = "key-auth", + route = { id = route17.id }, + config = {} + } + assert(helpers.start_kong({ plugins = "bundled, ctx-checker", database = strategy, @@ -1380,6 +1474,46 @@ for _, strategy in helpers.each_strategy() do assert.res_status(200, res) end) end) + + describe("always_use_authenticated_groups", function() + it("if authenticated_groups is set, it'll be used", function() + local res = assert(proxy_client:get("/request?apikey=apikey127", { + headers = { + ["Host"] = "acl15.test" + } + })) + local body = assert(cjson.decode(assert.res_status(200, res))) + assert.equal("auth_group1", body.headers["x-authenticated-groups"]) + assert.equal(nil, body.headers["x-consumer-groups"]) + + res = assert(proxy_client:get("/request?apikey=apikey127", { + headers = { + ["Host"] = "acl16.test" + } + })) + body = assert(cjson.decode(assert.res_status(403, res))) + assert.matches("You cannot consume this service", body.message) + end) + + it("if authenticated_groups is not set, fallback to use acl groups", function() + local res = assert(proxy_client:get("/request?apikey=apikey127", { + headers = { + ["Host"] = "acl17.test" + } + })) + local body = assert(cjson.decode(assert.res_status(200, res))) + assert.equal(nil, body.headers["x-authenticated-groups"]) + assert.equal("acl_group1", body.headers["x-consumer-groups"]) + + local res = assert(proxy_client:get("/request?apikey=apikey126", { + headers = { + ["Host"] = "acl17.test" + } + })) + body = assert(cjson.decode(assert.res_status(403, res))) + assert.matches("You cannot consume this service", body.message) + end) + end) end) diff --git a/spec/03-plugins/26-prometheus/02-access_spec.lua b/spec/03-plugins/26-prometheus/02-access_spec.lua index 9138637d2f2..5292b101049 100644 --- a/spec/03-plugins/26-prometheus/02-access_spec.lua +++ b/spec/03-plugins/26-prometheus/02-access_spec.lua @@ -768,6 +768,7 @@ describe("Plugin: prometheus (access) AI metrics", function() assert.not_match('ai_llm_requests_total', body, nil, true) assert.not_match('ai_llm_cost_total', body, nil, true) assert.not_match('ai_llm_tokens_total', body, nil, true) + assert.not_match('ai_llm_provider_latency_ms_bucket', body, nil, true) end) it("update prometheus plugin config", function() @@ -829,6 +830,8 @@ describe("Plugin: prometheus (access) AI metrics", function() assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00037', body, nil, true) + assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 1', body, nil, true) + assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="completion_tokens",workspace="default"} 12', body, nil, true) assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="prompt_tokens",workspace="default"} 25', body, nil, true) assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="total_tokens",workspace="default"} 37', body, nil, true) @@ -865,6 +868,8 @@ describe("Plugin: prometheus (access) AI metrics", function() assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00074', body, nil, true) + assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2', body, nil, true) + assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="completion_tokens",workspace="default"} 24', body, nil, true) assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="prompt_tokens",workspace="default"} 50', body, nil, true) assert.matches('ai_llm_tokens_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",token_type="total_tokens",workspace="default"} 74', body, nil, true) @@ -895,5 +900,6 @@ describe("Plugin: prometheus (access) AI metrics", function() assert.matches('ai_llm_requests_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 2', body, nil, true) assert.matches('ai_llm_cost_total{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default"} 0.00074', body, nil, true) + assert.matches('ai_llm_provider_latency_ms_bucket{ai_provider="openai",ai_model="gpt-3.5-turbo",cache_status="",vector_db="",embeddings_provider="",embeddings_model="",workspace="default",le="+Inf"} 2', body, nil, true) end) end) \ No newline at end of file diff --git a/spec/03-plugins/27-aws-lambda/05-aws-serializer_spec.lua b/spec/03-plugins/27-aws-lambda/05-aws-serializer_spec.lua index d8993656adb..b4e90ac8003 100644 --- a/spec/03-plugins/27-aws-lambda/05-aws-serializer_spec.lua +++ b/spec/03-plugins/27-aws-lambda/05-aws-serializer_spec.lua @@ -10,7 +10,7 @@ describe("[AWS Lambda] aws-gateway input", function() local function reload_module() -- make sure to reload the module - package.loaded["kong.tracing.request_id"] = nil + package.loaded["kong.observability.tracing.request_id"] = nil package.loaded["kong.plugins.aws-lambda.request-util"] = nil aws_serialize = require "kong.plugins.aws-lambda.request-util".aws_serializer end diff --git a/spec/03-plugins/27-aws-lambda/99-access_spec.lua b/spec/03-plugins/27-aws-lambda/99-access_spec.lua index 7f29aa90404..76b79c64c66 100644 --- a/spec/03-plugins/27-aws-lambda/99-access_spec.lua +++ b/spec/03-plugins/27-aws-lambda/99-access_spec.lua @@ -188,6 +188,12 @@ for _, strategy in helpers.each_strategy() do service = null, } + local route28 = bp.routes:insert { + hosts = { "lambda28.test" }, + protocols = { "http", "https" }, + service = null, + } + bp.plugins:insert { name = "aws-lambda", route = { id = route1.id }, @@ -560,6 +566,20 @@ for _, strategy in helpers.each_strategy() do } } + bp.plugins:insert { + name = "aws-lambda", + route = { id = route28.id }, + config = { + port = 10001, + aws_key = "mock-key", + aws_secret = "mock-secret", + aws_region = "us-east-1", + function_name = "functionWithArrayCTypeInMVHAndEmptyArray", + empty_arrays_mode = "legacy", + is_proxy_integration = true, + } + } + fixtures.dns_mock:A({ name = "custom.lambda.endpoint", address = "127.0.0.1", @@ -928,7 +948,7 @@ for _, strategy in helpers.each_strategy() do }) if server_tokens then - assert.equal(server_tokens, res.headers["Via"]) + assert.equal("2 " .. server_tokens, res.headers["Via"]) end end) @@ -985,6 +1005,19 @@ for _, strategy in helpers.each_strategy() do assert.matches("\"testbody\":%[%]", body) end) + it("invokes a Lambda function with legacy empty array mode and mutlivalueheaders", function() + local res = assert(proxy_client:send { + method = "GET", + path = "/get", + headers = { + ["Host"] = "lambda28.test" + } + }) + + local _ = assert.res_status(200, res) + assert.equal("application/json+test", res.headers["Content-Type"]) + end) + describe("config.is_proxy_integration = true", function() diff --git a/spec/03-plugins/31-proxy-cache/02-access_spec.lua b/spec/03-plugins/31-proxy-cache/02-access_spec.lua index 665e23fade0..250b07a7830 100644 --- a/spec/03-plugins/31-proxy-cache/02-access_spec.lua +++ b/spec/03-plugins/31-proxy-cache/02-access_spec.lua @@ -102,6 +102,12 @@ do local route22 = assert(bp.routes:insert({ hosts = { "route-22.test" }, })) + local route23 = assert(bp.routes:insert({ + hosts = { "route-23.test" }, + })) + local route24 = assert(bp.routes:insert({ + hosts = { "route-24.test" }, + })) local consumer1 = assert(bp.consumers:insert { username = "bob", @@ -329,13 +335,39 @@ do content_type = { "text/plain", "application/json" }, [policy] = policy_config, response_headers = { - ["Age"] = false, + age = false, ["X-Cache-Status"] = false, ["X-Cache-Key"] = false }, }, }) + assert(bp.plugins:insert { + name = "proxy-cache", + route = { id = route23.id }, + config = { + strategy = policy, + content_type = { "text/plain", "application/json" }, + [policy] = policy_config, + response_headers = { + age = true, + ["X-Cache-Status"] = true, + ["X-Cache-Key"] = true + }, + }, + }) + + assert(bp.plugins:insert { + name = "proxy-cache", + route = { id = route24.id }, + config = { + strategy = policy, + content_type = { "text/plain", "application/json" }, + [policy] = policy_config, + -- leave reponse_header to default values + }, + }) + assert(helpers.start_kong({ plugins = "bundled", nginx_conf = "spec/fixtures/custom_nginx.template", @@ -416,6 +448,56 @@ do assert.is_not_nil(res.headers["X-Cache-Key"]) end) + it("response_headers headers on the response when configured", function() + -- Initial query to set cache + local res = assert(client:get("/get", { + headers = { + Host = "route-23.test", + }, + })) + -- Cache should be Miss + assert.res_status(200, res) + assert.is_same("Miss", res.headers["X-Cache-Status"]) + assert.is_not_nil(res.headers["X-Cache-Key"]) + assert.is_nil(res.headers["Age"]) + -- Cache should be HIT + res = assert(client:get("/get", { + headers = { + Host = "route-23.test", + }, + })) + assert.res_status(200, res) + assert.same("Hit", res.headers["X-Cache-Status"]) + -- response_headers are configured + assert.is_not_nil(res.headers["Age"]) + assert.is_not_nil(res.headers["X-Cache-Key"]) + end) + + it("response_headers headers on the response when set to default", function() + -- Initial query to set cache + local res = assert(client:get("/get", { + headers = { + Host = "route-24.test", + }, + })) + -- Cache should be Miss + assert.res_status(200, res) + assert.is_same("Miss", res.headers["X-Cache-Status"]) + assert.is_not_nil(res.headers["X-Cache-Key"]) + assert.is_nil(res.headers["Age"]) + res = assert(client:get("/get", { + headers = { + Host = "route-24.test", + }, + })) + -- Cache should be Hit + assert.res_status(200, res) + assert.same("Hit", res.headers["X-Cache-Status"]) + -- response_headers are on by default + assert.is_not_nil(res.headers["Age"]) + assert.is_not_nil(res.headers["X-Cache-Key"]) + end) + it("respects cache ttl", function() local res = assert(get(client, "route-6.test")) diff --git a/spec/03-plugins/35-azure-functions/01-access_spec.lua b/spec/03-plugins/35-azure-functions/01-access_spec.lua index ca5125fe1fa..05f5598aec8 100644 --- a/spec/03-plugins/35-azure-functions/01-access_spec.lua +++ b/spec/03-plugins/35-azure-functions/01-access_spec.lua @@ -110,7 +110,7 @@ for _, strategy in helpers.each_strategy() do } ), } - + -- this plugin definition results in an upstream url to -- http://mockbin.org/request -- which will echo the request for inspection @@ -257,7 +257,7 @@ for _, strategy in helpers.each_strategy() do } }) - assert.equal(server_tokens, res.headers["Via"]) + assert.equal("2 " .. server_tokens, res.headers["Via"]) end) it("returns Content-Length header", function() diff --git a/spec/03-plugins/37-opentelemetry/01-otlp_spec.lua b/spec/03-plugins/37-opentelemetry/01-otlp_spec.lua index db243147b74..07add4f743c 100644 --- a/spec/03-plugins/37-opentelemetry/01-otlp_spec.lua +++ b/spec/03-plugins/37-opentelemetry/01-otlp_spec.lua @@ -43,6 +43,14 @@ local pb_decode_span = function(data) return pb.decode("opentelemetry.proto.trace.v1.Span", data) end +local pb_encode_log = function(data) + return pb.encode("opentelemetry.proto.logs.v1.LogRecord", data) +end + +local pb_decode_log = function(data) + return pb.decode("opentelemetry.proto.logs.v1.LogRecord", data) +end + describe("Plugin: opentelemetry (otlp)", function() local old_ngx_get_phase @@ -66,7 +74,7 @@ describe("Plugin: opentelemetry (otlp)", function() ngx.ctx.KONG_SPANS = nil end) - it("encode/decode pb", function () + it("encode/decode pb (traces)", function () local N = 10000 local test_spans = { @@ -125,6 +133,41 @@ describe("Plugin: opentelemetry (otlp)", function() end end) + it("encode/decode pb (logs)", function () + local N = 10000 + + local test_logs = {} + + for _ = 1, N do + local now_ns = time_ns() + + local log = { + time_unix_nano = now_ns, + observed_time_unix_nano = now_ns, + log_level = ngx.INFO, + span_id = rand_bytes(8), + body = "log line", + attributes = { + foo = "bar", + test = true, + version = 0.1, + }, + } + insert(test_logs, log) + end + + local trace_id = rand_bytes(16) + local flags = tonumber(rand_bytes(1)) + local prepared_logs = otlp.prepare_logs(test_logs, trace_id, flags) + + for _, prepared_log in ipairs(prepared_logs) do + local decoded_log = pb_decode_log(pb_encode_log(prepared_log)) + + local ok, err = table_compare(prepared_log, decoded_log) + assert.is_true(ok, err) + end + end) + it("check lengths of trace_id and span_id ", function () local TRACE_ID_LEN, PARENT_SPAN_ID_LEN = 16, 8 local default_span = { diff --git a/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua b/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua index 514a3069cc3..dd34df4f151 100644 --- a/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua +++ b/spec/03-plugins/37-opentelemetry/03-propagation_spec.lua @@ -73,7 +73,7 @@ local function setup_otel_old_propagation(bp, service) }).id}, config = { -- fake endpoint, request to backend will sliently fail - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", } }) @@ -84,7 +84,7 @@ local function setup_otel_old_propagation(bp, service) hosts = { http_route_ignore_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = "ignore", } }) @@ -96,7 +96,7 @@ local function setup_otel_old_propagation(bp, service) hosts = { http_route_w3c_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = "w3c", } }) @@ -108,7 +108,7 @@ local function setup_otel_old_propagation(bp, service) hosts = { http_route_dd_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = "datadog", } }) @@ -120,7 +120,7 @@ local function setup_otel_old_propagation(bp, service) hosts = { http_route_b3_single_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", header_type = "b3-single", } }) @@ -136,7 +136,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, inject = { "preserve" }, @@ -152,7 +152,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_ignore_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { }, inject = { "preserve" }, @@ -168,7 +168,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_w3c_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, inject = { "preserve", "w3c" }, @@ -184,7 +184,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_dd_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, inject = { "preserve", "datadog" }, @@ -200,7 +200,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_b3_single_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { "b3", "w3c", "jaeger", "ot", "datadog", "aws", "gcp" }, inject = { "preserve", "b3-single" }, @@ -218,7 +218,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_no_preserve_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", -- old configuration ignored when new propagation configuration is provided header_type = "preserve", propagation = { @@ -237,7 +237,7 @@ local function setup_otel_new_propagation(bp, service) hosts = { http_route_clear_host }, }).id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", propagation = { extract = { "w3c", "ot" }, inject = { "preserve" }, @@ -621,7 +621,7 @@ for _, sampling_rate in ipairs({1, 0, 0.5}) do }).id}, config = { -- fake endpoint, request to backend will sliently fail - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", sampling_rate = sampling_rate, } }) @@ -776,7 +776,7 @@ describe("propagation tests with enabled " .. instrumentation .. " instrumentati name = "opentelemetry", route = {id = route.id}, config = { - endpoint = "http://localhost:8080/v1/traces", + traces_endpoint = "http://localhost:8080/v1/traces", } }) diff --git a/spec/03-plugins/37-opentelemetry/04-exporter_spec.lua b/spec/03-plugins/37-opentelemetry/04-exporter_spec.lua index aa65233e123..b01e717fe6b 100644 --- a/spec/03-plugins/37-opentelemetry/04-exporter_spec.lua +++ b/spec/03-plugins/37-opentelemetry/04-exporter_spec.lua @@ -26,9 +26,14 @@ local function sort_by_key(tbl) end) end -local HTTP_SERVER_PORT = helpers.get_available_port() +local HTTP_SERVER_PORT_TRACES = helpers.get_available_port() +local HTTP_SERVER_PORT_LOGS = helpers.get_available_port() local PROXY_PORT = 9000 +local post_function_access_body = + [[kong.log.info("this is a log from kong.log"); + ngx.log(ngx.INFO, "this is a log from ngx.log")]] + for _, strategy in helpers.each_strategy() do describe("opentelemetry exporter #" .. strategy, function() local bp @@ -63,6 +68,14 @@ for _, strategy in helpers.each_strategy() do protocols = { "http" }, paths = { "/" }})) + local logs_route = assert(bp.routes:insert({ service = http_srv, + protocols = { "http" }, + paths = { "/logs" }})) + + local logs_traces_route = assert(bp.routes:insert({ service = http_srv, + protocols = { "http" }, + paths = { "/traces_logs" }})) + assert(bp.routes:insert({ service = http_srv2, protocols = { "http" }, paths = { "/no_plugin" }})) @@ -72,16 +85,57 @@ for _, strategy in helpers.each_strategy() do route = router_scoped and route, service = service_scoped and http_srv, config = table_merge({ - endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT, + traces_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_TRACES, batch_flush_delay = 0, -- report immediately }, config) })) + assert(bp.plugins:insert({ + name = "opentelemetry", + route = logs_traces_route, + config = table_merge({ + traces_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_TRACES, + logs_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_LOGS, + queue = { + max_batch_size = 1000, + max_coalescing_delay = 2, + }, + }, config) + })) + + assert(bp.plugins:insert({ + name = "opentelemetry", + route = logs_route, + config = table_merge({ + logs_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_LOGS, + queue = { + max_batch_size = 1000, + max_coalescing_delay = 2, + }, + }, config) + })) + + assert(bp.plugins:insert({ + name = "post-function", + route = logs_traces_route, + config = { + access = { post_function_access_body }, + }, + })) + + assert(bp.plugins:insert({ + name = "post-function", + route = logs_route, + config = { + access = { post_function_access_body }, + }, + })) + if another_global then assert(bp.plugins:insert({ name = "opentelemetry", config = table_merge({ - endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT, + traces_endpoint = "http://127.0.0.1:" .. HTTP_SERVER_PORT_TRACES, batch_flush_delay = 0, -- report immediately }, config) })) @@ -91,14 +145,14 @@ for _, strategy in helpers.each_strategy() do proxy_listen = "0.0.0.0:" .. PROXY_PORT, database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", - plugins = "opentelemetry", + plugins = "opentelemetry,post-function", tracing_instrumentations = types, tracing_sampling_rate = global_sampling_rate or 1, }, nil, nil, fixtures)) end describe("valid #http request", function () - local mock + local mock_traces, mock_logs lazy_setup(function() bp, _ = assert(helpers.get_db_utils(strategy, { "services", @@ -111,17 +165,21 @@ for _, strategy in helpers.each_strategy() do ["X-Access-Token"] = "token", }, }) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock_traces = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) + mock_logs = helpers.http_mock(HTTP_SERVER_PORT_LOGS, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() helpers.stop_kong() - if mock then - mock("close", true) + if mock_traces then + mock_traces("close", true) + end + if mock_logs then + mock_logs("close", true) end end) - it("works", function () + it("exports valid traces", function () local headers, body helpers.wait_until(function() local cli = helpers.proxy_client(7000, PROXY_PORT) @@ -134,7 +192,7 @@ for _, strategy in helpers.each_strategy() do cli:close() local lines - lines, body, headers = mock() + lines, body, headers = mock_traces() return lines end, 10) @@ -162,6 +220,127 @@ for _, strategy in helpers.each_strategy() do local scope_spans = decoded.resource_spans[1].scope_spans assert.is_true(#scope_spans > 0, scope_spans) end) + + local function assert_find_valid_logs(body, request_id, trace_id) + local decoded = assert(pb.decode("opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest", body)) + assert.not_nil(decoded) + + -- array is unstable + local res_attr = decoded.resource_logs[1].resource.attributes + sort_by_key(res_attr) + -- default resource attributes + assert.same("service.instance.id", res_attr[1].key) + assert.same("service.name", res_attr[2].key) + assert.same({string_value = "kong", value = "string_value"}, res_attr[2].value) + assert.same("service.version", res_attr[3].key) + assert.same({string_value = kong.version, value = "string_value"}, res_attr[3].value) + + local scope_logs = decoded.resource_logs[1].scope_logs + assert.is_true(#scope_logs > 0, scope_logs) + + local found = 0 + for _, scope_log in ipairs(scope_logs) do + local log_records = scope_log.log_records + for _, log_record in ipairs(log_records) do + local logline = log_record.body.string_value + + -- filter the right log lines + if string.find(logline, "this is a log") then + assert(logline:sub(-7) == "ngx.log" or logline:sub(-8) == "kong.log", logline) + + assert.is_table(log_record.attributes) + local found_attrs = {} + for _, attr in ipairs(log_record.attributes) do + found_attrs[attr.key] = attr.value[attr.value.value] + end + + -- ensure the log is from the current request + if found_attrs["request.id"] == request_id then + local expected_line + if logline:sub(-8) == "kong.log" then + expected_line = 1 + else + expected_line = 2 + end + + assert.is_number(log_record.time_unix_nano) + assert.is_number(log_record.observed_time_unix_nano) + assert.equals(post_function_access_body, found_attrs["introspection.source"]) + assert.equals(expected_line, found_attrs["introspection.current.line"]) + assert.equals(log_record.severity_number, 9) + assert.equals(log_record.severity_text, "INFO") + if trace_id then + assert.equals(trace_id, to_hex(log_record.trace_id)) + assert.is_string(log_record.span_id) + assert.is_number(log_record.flags) + end + + found = found + 1 + if found == 2 then + break + end + end + end + end + end + assert.equals(2, found) + end + + it("exports valid logs with tracing", function () + local trace_id = gen_trace_id() + + local headers, body, request_id + + local cli = helpers.proxy_client(7000, PROXY_PORT) + local res = assert(cli:send { + method = "GET", + path = "/traces_logs", + headers = { + traceparent = fmt("00-%s-0123456789abcdef-01", trace_id), + }, + }) + assert.res_status(200, res) + cli:close() + + request_id = res.headers["X-Kong-Request-Id"] + + helpers.wait_until(function() + local lines + lines, body, headers = mock_logs() + + return lines + end, 10) + + assert.is_string(body) + assert.equals(headers["Content-Type"], "application/x-protobuf") + assert_find_valid_logs(body, request_id, trace_id) + end) + + it("exports valid logs without tracing", function () + local headers, body, request_id + + local cli = helpers.proxy_client(7000, PROXY_PORT) + local res = assert(cli:send { + method = "GET", + path = "/logs", + }) + assert.res_status(200, res) + cli:close() + + request_id = res.headers["X-Kong-Request-Id"] + + helpers.wait_until(function() + local lines + lines, body, headers = mock_logs() + + return lines + end, 10) + + assert.is_string(body) + assert.equals(headers["Content-Type"], "application/x-protobuf") + + assert_find_valid_logs(body, request_id) + end) end) -- this test is not meant to check that the sampling rate is applied @@ -187,7 +366,7 @@ for _, strategy in helpers.each_strategy() do setup_instrumentations("all", { sampling_rate = sampling_rate, }, nil, nil, nil, nil, global_sampling_rate) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() @@ -260,7 +439,7 @@ for _, strategy in helpers.each_strategy() do }, { "opentelemetry" })) setup_instrumentations("all", {}, nil, nil, nil, nil, sampling_rate) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() @@ -342,7 +521,7 @@ for _, strategy in helpers.each_strategy() do ["X-Access-Token"] = "token", }, }, nil, case[1], case[2], case[3]) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() @@ -390,7 +569,7 @@ for _, strategy in helpers.each_strategy() do ["os.version"] = "debian", } }) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() @@ -459,7 +638,7 @@ for _, strategy in helpers.each_strategy() do fixtures.http_mock.my_server_block = [[ server { server_name myserver; - listen ]] .. HTTP_SERVER_PORT .. [[; + listen ]] .. HTTP_SERVER_PORT_TRACES .. [[; client_body_buffer_size 1024k; location / { @@ -550,7 +729,7 @@ for _, strategy in helpers.each_strategy() do }, { "opentelemetry" })) setup_instrumentations("request") - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() @@ -618,7 +797,7 @@ for _, strategy in helpers.each_strategy() do describe("#referenceable fields", function () local mock lazy_setup(function() - helpers.setenv("TEST_OTEL_ENDPOINT", "http://127.0.0.1:" .. HTTP_SERVER_PORT) + helpers.setenv("TEST_OTEL_ENDPOINT", "http://127.0.0.1:" .. HTTP_SERVER_PORT_TRACES) helpers.setenv("TEST_OTEL_ACCESS_KEY", "secret-1") helpers.setenv("TEST_OTEL_ACCESS_SECRET", "secret-2") @@ -635,7 +814,7 @@ for _, strategy in helpers.each_strategy() do ["X-Access-Secret"] = "{vault://env/test_otel_access_secret}", }, }) - mock = helpers.http_mock(HTTP_SERVER_PORT, { timeout = HTTP_MOCK_TIMEOUT }) + mock = helpers.http_mock(HTTP_SERVER_PORT_TRACES, { timeout = HTTP_MOCK_TIMEOUT }) end) lazy_teardown(function() diff --git a/spec/03-plugins/37-opentelemetry/05-otelcol_spec.lua b/spec/03-plugins/37-opentelemetry/05-otelcol_spec.lua index 6851cc7948d..41e36f8a180 100644 --- a/spec/03-plugins/37-opentelemetry/05-otelcol_spec.lua +++ b/spec/03-plugins/37-opentelemetry/05-otelcol_spec.lua @@ -3,7 +3,7 @@ local helpers = require "spec.helpers" local kong_table = require "kong.tools.table" local ngx_re = require "ngx.re" local http = require "resty.http" - +local cjson = require "cjson.safe" local fmt = string.format local table_merge = kong_table.table_merge @@ -32,9 +32,13 @@ for _, strategy in helpers.each_strategy() do port = helpers.mock_upstream_port, }) - bp.routes:insert({ service = http_srv, - protocols = { "http" }, - paths = { "/" }}) + local traces_route = bp.routes:insert({ service = http_srv, + protocols = { "http" }, + paths = { "/traces" }}) + + local logs_route = bp.routes:insert({ service = http_srv, + protocols = { "http" }, + paths = { "/logs" }}) local route_traceid = bp.routes:insert({ service = http_srv, protocols = { "http" }, @@ -42,17 +46,40 @@ for _, strategy in helpers.each_strategy() do bp.plugins:insert({ name = "opentelemetry", + route = { id = traces_route.id }, config = table_merge({ - endpoint = fmt("http://%s:%s/v1/traces", OTELCOL_HOST, OTELCOL_HTTP_PORT), + traces_endpoint = fmt("http://%s:%s/v1/traces", OTELCOL_HOST, OTELCOL_HTTP_PORT), batch_flush_delay = 0, -- report immediately }, config) }) + bp.plugins:insert({ + name = "opentelemetry", + route = { id = logs_route.id }, + config = table_merge({ + logs_endpoint = fmt("http://%s:%s/v1/logs", OTELCOL_HOST, OTELCOL_HTTP_PORT), + queue = { + max_batch_size = 1000, + max_coalescing_delay = 2, + }, + }, config) + }) + + bp.plugins:insert({ + name = "post-function", + route = logs_route, + config = { + access = {[[ + ngx.log(ngx.WARN, "this is a log") + ]]}, + }, + }) + bp.plugins:insert({ name = "opentelemetry", route = { id = route_traceid.id }, config = table_merge({ - endpoint = fmt("http://%s:%s/v1/traces", OTELCOL_HOST, OTELCOL_HTTP_PORT), + traces_endpoint = fmt("http://%s:%s/v1/traces", OTELCOL_HOST, OTELCOL_HTTP_PORT), batch_flush_delay = 0, -- report immediately http_response_header_for_traceid = "x-trace-id", }, config) @@ -61,7 +88,8 @@ for _, strategy in helpers.each_strategy() do assert(helpers.start_kong { database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", - plugins = "opentelemetry", + plugins = "opentelemetry, post-function", + log_level = "warn", tracing_instrumentations = types, tracing_sampling_rate = 1, }) @@ -88,7 +116,7 @@ for _, strategy in helpers.each_strategy() do it("send traces", function() local httpc = http.new() for i = 1, LIMIT do - local res, err = httpc:request_uri(proxy_url) + local res, err = httpc:request_uri(proxy_url .. "/traces") assert.is_nil(err) assert.same(200, res.status) end @@ -125,7 +153,7 @@ for _, strategy in helpers.each_strategy() do assert(helpers.restart_kong { database = strategy, nginx_conf = "spec/fixtures/custom_nginx.template", - plugins = "opentelemetry", + plugins = "opentelemetry, post-function", tracing_instrumentations = "all", tracing_sampling_rate = 0.00005, }) @@ -147,8 +175,82 @@ for _, strategy in helpers.each_strategy() do end httpc:close() end) - end) + describe("otelcol receives logs #http", function() + local REQUESTS = 100 + + lazy_setup(function() + -- clear file + local shell = require "resty.shell" + shell.run("mkdir -p $(dirname " .. OTELCOL_FILE_EXPORTER_PATH .. ")", nil, 0) + shell.run("cat /dev/null > " .. OTELCOL_FILE_EXPORTER_PATH, nil, 0) + setup_instrumentations("all") + end) + + lazy_teardown(function() + helpers.stop_kong() + end) + + it("send valid logs", function() + local httpc = http.new() + for i = 1, REQUESTS do + local res, err = httpc:request_uri(proxy_url .. "/logs") + assert.is_nil(err) + assert.same(200, res.status) + end + httpc:close() + + local parts + helpers.wait_until(function() + local f = assert(io.open(OTELCOL_FILE_EXPORTER_PATH, "rb")) + local raw_content = f:read("*all") + f:close() + + parts = split(raw_content, "\n", "jo") + return #parts > 0 + end, 10) + + local contents = {} + for _, p in ipairs(parts) do + -- after the file is truncated the collector + -- may continue exporting partial json objects + local trimmed = string.match(p, "({.*)") + local decoded = cjson.decode(trimmed) + if decoded then + table.insert(contents, decoded) + end + end + + local count = 0 + for _, content in ipairs(contents) do + if not content.resourceLogs then + goto continue + end + + local scope_logs = content.resourceLogs[1].scopeLogs + assert.is_true(#scope_logs > 0, scope_logs) + + for _, scope_log in ipairs(scope_logs) do + local log_records = scope_log.logRecords + for _, log_record in ipairs(log_records) do + if log_record.body.stringValue == "this is a log" then + count = count + 1 + + assert.not_nil(log_record.observedTimeUnixNano) + assert.not_nil(log_record.timeUnixNano) + assert.equals("SEVERITY_NUMBER_WARN", log_record.severityNumber) + assert.equals("WARN", log_record.severityText) + assert.not_nil(log_record.attributes) + end + end + end + + ::continue:: + end + + assert.equals(REQUESTS, count) + end) + end) end) end diff --git a/spec/03-plugins/37-opentelemetry/06-regression_spec.lua b/spec/03-plugins/37-opentelemetry/06-regression_spec.lua index dfa212a7d7e..7c1c661cad5 100644 --- a/spec/03-plugins/37-opentelemetry/06-regression_spec.lua +++ b/spec/03-plugins/37-opentelemetry/06-regression_spec.lua @@ -37,7 +37,7 @@ for _, strategy in helpers.each_strategy() do route = route, service = http_srv, config = { - endpoint = "http://127.0.0.1:" .. mock_port1, + traces_endpoint = "http://127.0.0.1:" .. mock_port1, batch_flush_delay = 0, -- report immediately } }) diff --git a/spec/03-plugins/38-ai-proxy/01-unit_spec.lua b/spec/03-plugins/38-ai-proxy/01-unit_spec.lua index c1dfadfb4ac..009f079195d 100644 --- a/spec/03-plugins/38-ai-proxy/01-unit_spec.lua +++ b/spec/03-plugins/38-ai-proxy/01-unit_spec.lua @@ -223,6 +223,34 @@ local FORMATS = { }, }, }, + gemini = { + ["llm/v1/chat"] = { + config = { + name = "gemini-pro", + provider = "gemini", + options = { + max_tokens = 8192, + temperature = 0.8, + top_k = 1, + top_p = 0.6, + }, + }, + }, + }, + bedrock = { + ["llm/v1/chat"] = { + config = { + name = "bedrock", + provider = "bedrock", + options = { + max_tokens = 8192, + temperature = 0.8, + top_k = 1, + top_p = 0.6, + }, + }, + }, + }, } local STREAMS = { @@ -646,5 +674,62 @@ describe(PLUGIN_NAME .. ": (unit)", function() }, formatted) end) + describe("streaming transformer tests", function() + + it("transforms truncated-json type (beginning of stream)", function() + local input = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/input.bin")) + local events = ai_shared.frame_to_events(input, "gemini") + + local expected = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/expected-output.json")) + local expected_events = cjson.decode(expected) + + assert.same(events, expected_events, true) + end) + + it("transforms truncated-json type (end of stream)", function() + local input = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/input.bin")) + local events = ai_shared.frame_to_events(input, "gemini") + + local expected = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/expected-output.json")) + local expected_events = cjson.decode(expected) + + assert.same(events, expected_events, true) + end) + + it("transforms complete-json type", function() + local input = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/input.bin")) + local events = ai_shared.frame_to_events(input, "cohere") -- not "truncated json mode" like Gemini + + local expected = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/expected-output.json")) + local expected_events = cjson.decode(expected) + + assert.same(events, expected_events) + end) + + it("transforms text/event-stream type", function() + local input = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/input.bin")) + local events = ai_shared.frame_to_events(input, "openai") -- not "truncated json mode" like Gemini + + local expected = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/expected-output.json")) + local expected_events = cjson.decode(expected) + + assert.same(events, expected_events) + end) + + it("transforms application/vnd.amazon.eventstream (AWS) type", function() + local input = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/input.bin")) + local events = ai_shared.frame_to_events(input, "bedrock") + + local expected = pl_file.read(fmt("spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/expected-output.json")) + local expected_events = cjson.decode(expected) + + assert.equal(#events, #expected_events) + for i, _ in ipairs(expected_events) do + -- tables are random ordered, so we need to compare each serialized event + assert.same(cjson.decode(events[i].data), cjson.decode(expected_events[i].data)) + end + end) + + end) end) diff --git a/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua b/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua index b67d815fa07..0b5468e2e88 100644 --- a/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua +++ b/spec/03-plugins/38-ai-proxy/02-openai_integration_spec.lua @@ -44,11 +44,13 @@ local _EXPECTED_CHAT_STATS = { provider_name = 'openai', request_model = 'gpt-3.5-turbo', response_model = 'gpt-3.5-turbo-0613', + llm_latency = 1 }, usage = { prompt_tokens = 25, completion_tokens = 12, total_tokens = 37, + time_per_token = 1, cost = 0.00037, }, cache = {} @@ -713,7 +715,20 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.is_number(log_message.response.size) -- test ai-proxy stats - assert.same(_EXPECTED_CHAT_STATS, log_message.ai) + -- TODO: as we are reusing this test for ai-proxy and ai-proxy-advanced + -- we are currently stripping the top level key and comparing values directly + local _, first_expected = next(_EXPECTED_CHAT_STATS) + local _, first_got = next(log_message.ai) + local actual_llm_latency = first_got.meta.llm_latency + local actual_time_per_token = first_got.usage.time_per_token + local time_per_token = math.floor(actual_llm_latency / first_got.usage.completion_tokens) + + first_got.meta.llm_latency = 1 + first_got.usage.time_per_token = 1 + + assert.same(first_expected, first_got) + assert.is_true(actual_llm_latency > 0) + assert.same(actual_time_per_token, time_per_token) end) it("does not log statistics", function() @@ -758,7 +773,7 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }, body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"), }) - + -- validate that the request succeeded, response status 200 local body = assert.res_status(200 , r) local json = cjson.decode(body) @@ -780,14 +795,18 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.is_number(log_message.request.size) assert.is_number(log_message.response.size) + -- TODO: as we are reusing this test for ai-proxy and ai-proxy-advanced + -- we are currently stripping the top level key and comparing values directly + local _, message = next(log_message.ai) + -- test request bodies - assert.matches('"content": "What is 1 + 1?"', log_message.ai['ai-proxy'].payload.request, nil, true) - assert.matches('"role": "user"', log_message.ai['ai-proxy'].payload.request, nil, true) + assert.matches('"content": "What is 1 + 1?"', message.payload.request, nil, true) + assert.matches('"role": "user"', message.payload.request, nil, true) -- test response bodies - assert.matches('"content": "The sum of 1 + 1 is 2.",', log_message.ai["ai-proxy"].payload.response, nil, true) - assert.matches('"role": "assistant"', log_message.ai["ai-proxy"].payload.response, nil, true) - assert.matches('"id": "chatcmpl-8T6YwgvjQVVnGbJ2w8hpOA17SeNy2"', log_message.ai["ai-proxy"].payload.response, nil, true) + assert.matches('"content": "The sum of 1 + 1 is 2.",', message.payload.response, nil, true) + assert.matches('"role": "assistant"', message.payload.response, nil, true) + assert.matches('"id": "chatcmpl-8T6YwgvjQVVnGbJ2w8hpOA17SeNy2"', message.payload.response, nil, true) end) it("internal_server_error request", function() @@ -902,12 +921,12 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then }, body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"), }) - + -- check we got internal server error local body = assert.res_status(500 , r) local json = cjson.decode(body) assert.is_truthy(json.error) - assert.equals(json.error.message, "transformation failed from type openai://llm/v1/chat: 'choices' not in llm/v1/chat response") + assert.same(json.error.message, "transformation failed from type openai://llm/v1/chat: 'choices' not in llm/v1/chat response") end) it("bad request", function() diff --git a/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua b/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua index 0e8014dc5fe..0b051da1479 100644 --- a/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua +++ b/spec/03-plugins/39-ai-request-transformer/02-integration_spec.lua @@ -124,11 +124,13 @@ local _EXPECTED_CHAT_STATS = { provider_name = 'openai', request_model = 'gpt-4', response_model = 'gpt-3.5-turbo-0613', + llm_latency = 1 }, usage = { prompt_tokens = 25, completion_tokens = 12, total_tokens = 37, + time_per_token = 1, cost = 0.00037, }, cache = {} @@ -295,8 +297,18 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.is_number(log_message.request.size) assert.is_number(log_message.response.size) - -- test ai-proxy stats + -- test ai-request-transformer stats + local actual_chat_stats = log_message.ai + local actual_llm_latency = actual_chat_stats["ai-request-transformer"].meta.llm_latency + local actual_time_per_token = actual_chat_stats["ai-request-transformer"].usage.time_per_token + local time_per_token = math.floor(actual_llm_latency / actual_chat_stats["ai-request-transformer"].usage.completion_tokens) + + log_message.ai["ai-request-transformer"].meta.llm_latency = 1 + log_message.ai["ai-request-transformer"].usage.time_per_token = 1 + assert.same(_EXPECTED_CHAT_STATS, log_message.ai) + assert.is_true(actual_llm_latency > 0) + assert.same(actual_time_per_token, time_per_token) end) it("bad request from LLM", function() diff --git a/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua b/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua index 34f5afab3b6..29ed47e41ea 100644 --- a/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua +++ b/spec/03-plugins/40-ai-response-transformer/02-integration_spec.lua @@ -181,11 +181,13 @@ local _EXPECTED_CHAT_STATS = { provider_name = 'openai', request_model = 'gpt-4', response_model = 'gpt-3.5-turbo-0613', + llm_latency = 1 }, usage = { prompt_tokens = 25, completion_tokens = 12, total_tokens = 37, + time_per_token = 1, cost = 0.00037, }, cache = {} @@ -424,8 +426,18 @@ for _, strategy in helpers.all_strategies() do if strategy ~= "cassandra" then assert.is_number(log_message.request.size) assert.is_number(log_message.response.size) - -- test ai-proxy stats + -- test ai-response-transformer stats + local actual_chat_stats = log_message.ai + local actual_llm_latency = actual_chat_stats["ai-response-transformer"].meta.llm_latency + local actual_time_per_token = actual_chat_stats["ai-response-transformer"].usage.time_per_token + local time_per_token = math.floor(actual_llm_latency / actual_chat_stats["ai-response-transformer"].usage.completion_tokens) + + log_message.ai["ai-response-transformer"].meta.llm_latency = 1 + log_message.ai["ai-response-transformer"].usage.time_per_token = 1 + assert.same(_EXPECTED_CHAT_STATS, log_message.ai) + assert.is_true(actual_llm_latency > 0) + assert.same(actual_time_per_token, time_per_token) end) it("fails properly when json instructions are bad", function() diff --git a/spec/03-plugins/42-ai-prompt-guard/00-config_spec.lua b/spec/03-plugins/42-ai-prompt-guard/00-config_spec.lua index 103ed45840a..7bc8169e157 100644 --- a/spec/03-plugins/42-ai-prompt-guard/00-config_spec.lua +++ b/spec/03-plugins/42-ai-prompt-guard/00-config_spec.lua @@ -84,4 +84,22 @@ describe(PLUGIN_NAME .. ": (schema)", function() assert.same({ config = {allow_patterns = "length must be at most 10" }}, err) end) + it("allow_all_conversation_history needs to be false if match_all_roles is set to true", function() + local config = { + allow_patterns = { "wat" }, + allow_all_conversation_history = true, + match_all_roles = true, + } + + local ok, err = validate(config) + + assert.is_falsy(ok) + assert.not_nil(err) + assert.same({ + ["@entity"] = { + [1] = 'failed conditional validation given value of field \'config.match_all_roles\'' }, + ["config"] = { + ["allow_all_conversation_history"] = 'value must be false' }}, err) + end) + end) diff --git a/spec/03-plugins/42-ai-prompt-guard/01-unit_spec.lua b/spec/03-plugins/42-ai-prompt-guard/01-unit_spec.lua index 9007376fcf0..eab961081e6 100644 --- a/spec/03-plugins/42-ai-prompt-guard/01-unit_spec.lua +++ b/spec/03-plugins/42-ai-prompt-guard/01-unit_spec.lua @@ -1,119 +1,57 @@ local PLUGIN_NAME = "ai-prompt-guard" - - -local general_chat_request = { - messages = { - [1] = { - role = "system", - content = "You are a mathematician." - }, - [2] = { - role = "user", - content = "What is 1 + 1?" - }, - }, -} - -local general_chat_request_with_history = { - messages = { - [1] = { - role = "system", - content = "You are a mathematician." - }, - [2] = { - role = "user", - content = "What is 12 + 1?" - }, - [3] = { - role = "assistant", - content = "The answer is 13.", - }, - [4] = { - role = "user", - content = "Now double the previous answer.", - }, - }, +local message_fixtures = { + user = "this is a user request", + system = "this is a system message", + assistant = "this is an assistant reply", } -local denied_chat_request = { - messages = { - [1] = { +local _M = {} +local function create_request(typ) + local messages = { + { role = "system", - content = "You are a mathematician." - }, - [2] = { - role = "user", - content = "What is 22 + 1?" - }, - }, -} - -local neither_allowed_nor_denied_chat_request = { - messages = { - [1] = { - role = "system", - content = "You are a mathematician." - }, - [2] = { - role = "user", - content = "What is 55 + 55?" - }, - }, -} - - -local general_completions_request = { - prompt = "You are a mathematician. What is 1 + 1?" -} - - -local denied_completions_request = { - prompt = "You are a mathematician. What is 22 + 1?" -} - -local neither_allowed_nor_denied_completions_request = { - prompt = "You are a mathematician. What is 55 + 55?" -} - -local allow_patterns_no_history = { - allow_patterns = { - [1] = ".*1 \\+ 1.*" - }, - allow_all_conversation_history = true, -} - -local allow_patterns_with_history = { - allow_patterns = { - [1] = ".*1 \\+ 1.*" - }, - allow_all_conversation_history = false, -} - -local deny_patterns_with_history = { - deny_patterns = { - [1] = ".*12 \\+ 1.*" - }, - allow_all_conversation_history = false, -} - -local deny_patterns_no_history = { - deny_patterns = { - [1] = ".*22 \\+ 1.*" - }, - allow_all_conversation_history = true, -} - -local both_patterns_no_history = { - allow_patterns = { - [1] = ".*1 \\+ 1.*" - }, - deny_patterns = { - [1] = ".*99 \\+ 99.*" - }, - allow_all_conversation_history = true, -} - + content = message_fixtures.system, + } + } + + if typ ~= "chat" and typ ~= "completions" then + error("type must be one of 'chat' or 'completions'", 2) + end + + return setmetatable({ + messages = messages, + type = typ, + }, { + __index = _M, + }) +end + +function _M:append_message(role, custom) + if not message_fixtures[role] then + assert("role must be one of: user, system or assistant") + end + + if self.type == "completion" then + self.prompt = "this is a completions request" + if custom then + self.prompt = self.prompt .. " with custom content " .. custom + end + return + end + + local message = message_fixtures[role] + if custom then + message = message .. " with custom content " .. custom + end + + self.messages[#self.messages+1] = { + role = "user", + content = message + } + + return self +end describe(PLUGIN_NAME .. ": (unit)", function() @@ -132,115 +70,133 @@ describe(PLUGIN_NAME .. ": (unit)", function() - describe("chat operations", function() - - it("allows request when only conf.allow_patterns is set", function() - local ok, err = access_handler._execute(general_chat_request, allow_patterns_no_history) - - assert.is_truthy(ok) - assert.is_nil(err) - end) - - - it("allows request when only conf.deny_patterns is set, and pattern should not match", function() - local ok, err = access_handler._execute(general_chat_request, deny_patterns_no_history) - - assert.is_truthy(ok) - assert.is_nil(err) - end) - - - it("denies request when only conf.allow_patterns is set, and pattern should not match", function() - local ok, err = access_handler._execute(denied_chat_request, allow_patterns_no_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt doesn't match any allowed pattern") - end) - - - it("denies request when only conf.deny_patterns is set, and pattern should match", function() - local ok, err = access_handler._execute(denied_chat_request, deny_patterns_no_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt pattern is blocked") - end) - - - it("allows request when both conf.allow_patterns and conf.deny_patterns are set, and pattern matches allow", function() - local ok, err = access_handler._execute(general_chat_request, both_patterns_no_history) - - assert.is_truthy(ok) - assert.is_nil(err) - end) - - - it("denies request when both conf.allow_patterns and conf.deny_patterns are set, and pattern matches neither", function() - local ok, err = access_handler._execute(neither_allowed_nor_denied_chat_request, both_patterns_no_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt doesn't match any allowed pattern") - end) - - - it("denies request when only conf.allow_patterns is set and previous chat history should not match", function() - local ok, err = access_handler._execute(general_chat_request_with_history, allow_patterns_with_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt doesn't match any allowed pattern") - end) - - - it("denies request when only conf.deny_patterns is set and previous chat history should match", function() - local ok, err = access_handler._execute(general_chat_request_with_history, deny_patterns_with_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt pattern is blocked") - end) - - end) - - - describe("completions operations", function() - - it("allows request when only conf.allow_patterns is set", function() - local ok, err = access_handler._execute(general_completions_request, allow_patterns_no_history) - - assert.is_truthy(ok) - assert.is_nil(err) - end) - - - it("allows request when only conf.deny_patterns is set, and pattern should not match", function() - local ok, err = access_handler._execute(general_completions_request, deny_patterns_no_history) - - assert.is_truthy(ok) - assert.is_nil(err) - end) - - - it("denies request when only conf.allow_patterns is set, and pattern should not match", function() - local ok, err = access_handler._execute(denied_completions_request, allow_patterns_no_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt doesn't match any allowed pattern") + for _, request_type in ipairs({"chat", "completions"}) do + + describe(request_type .. " operations", function() + it("allows a user request when nothing is set", function() + -- deny_pattern in this case should be made to have no effect + local ctx = create_request(request_type):append_message("user", "pattern") + local ok, err = access_handler._execute(ctx, { + }) + + assert.is_truthy(ok) + assert.is_nil(err) + end) + + -- only chat has history + -- match_all_roles require history + for _, has_history in ipairs({false, request_type == "chat" and true or nil}) do + for _, match_all_roles in ipairs({false, has_history and true or nil}) do + + -- we only have user or not user, so testing "assistant" is not necessary + local role = match_all_roles and "system" or "user" + + describe("conf.allow_patterns is set", function() + for _, has_deny_patterns in ipairs({true, false}) do + + local test_description = has_history and " in history" or " only the last" + test_description = test_description .. (has_deny_patterns and ", conf.deny_patterns is also set" or "") + + it("allows a matching user request" .. test_description, function() + -- deny_pattern in this case should be made to have no effect + local ctx = create_request(request_type):append_message(role, "pattern") + + if has_history then + ctx:append_message("user", "no match") + end + local ok, err = access_handler._execute(ctx, { + allow_patterns = { + "pa..ern" + }, + deny_patterns = has_deny_patterns and {"deny match"} or nil, + allow_all_conversation_history = not has_history, + match_all_roles = match_all_roles, + }) + + assert.is_truthy(ok) + assert.is_nil(err) + end) + + it("denies an unmatched user request" .. test_description, function() + -- deny_pattern in this case should be made to have no effect + local ctx = create_request(request_type):append_message("user", "no match") + + if has_history then + ctx:append_message("user", "no match") + else + -- if we are ignoring history, actually put a matched message in history to test edge case + ctx:append_message(role, "pattern"):append_message("user", "no match") + end + + local ok, err = access_handler._execute(ctx, { + allow_patterns = { + "pa..ern" + }, + deny_patterns = has_deny_patterns and {"deny match"} or nil, + allow_all_conversation_history = not has_history, + match_all_roles = match_all_roles, + }) + + assert.is_falsy(ok) + assert.equal("prompt doesn't match any allowed pattern", err) + end) + + end -- for _, has_deny_patterns in ipairs({true, false}) do + end) + + describe("conf.deny_patterns is set", function() + for _, has_allow_patterns in ipairs({true, false}) do + + local test_description = has_history and " in history" or " only the last" + test_description = test_description .. (has_allow_patterns and ", conf.allow_patterns is also set" or "") + + it("denies a matching user request" .. test_description, function() + -- allow_pattern in this case should be made to have no effect + local ctx = create_request(request_type):append_message(role, "pattern") + + if has_history then + ctx:append_message("user", "no match") + end + local ok, err = access_handler._execute(ctx, { + deny_patterns = { + "pa..ern" + }, + allow_patterns = has_allow_patterns and {"allow match"} or nil, + allow_all_conversation_history = not has_history, + }) + + assert.is_falsy(ok) + assert.equal("prompt pattern is blocked", err) + end) + + it("allows unmatched user request" .. test_description, function() + -- allow_pattern in this case should be made to have no effect + local ctx = create_request(request_type):append_message(role, "allow match") + + if has_history then + ctx:append_message("user", "no match") + else + -- if we are ignoring history, actually put a matched message in history to test edge case + ctx:append_message(role, "pattern"):append_message(role, "allow match") + end + + local ok, err = access_handler._execute(ctx, { + deny_patterns = { + "pa..ern" + }, + allow_patterns = has_allow_patterns and {"allow match"} or nil, + allow_all_conversation_history = not has_history, + }) + + assert.is_truthy(ok) + assert.is_nil(err) + end) + end -- for for _, has_allow_patterns in ipairs({true, false}) do + end) + + end -- for _, match_all_role in ipairs(false, true)) do + end -- for _, has_history in ipairs({true, false}) do end) - - - it("denies request when only conf.deny_patterns is set, and pattern should match", function() - local ok, err = access_handler._execute(denied_completions_request, deny_patterns_no_history) - - assert.is_falsy(ok) - assert.equal("prompt pattern is blocked", err) - end) - - - it("denies request when both conf.allow_patterns and conf.deny_patterns are set, and pattern matches neither", function() - local ok, err = access_handler._execute(neither_allowed_nor_denied_completions_request, both_patterns_no_history) - - assert.is_falsy(ok) - assert.equal(err, "prompt doesn't match any allowed pattern") - end) - - end) + end -- for _, request_type in ipairs({"chat", "completions"}) do end) diff --git a/spec/05-migration/plugins/opentelemetry/migrations/001_331_to_332_spec.lua b/spec/05-migration/plugins/opentelemetry/migrations/001_331_to_332_spec.lua index 98ac32422df..096cd2cdbab 100644 --- a/spec/05-migration/plugins/opentelemetry/migrations/001_331_to_332_spec.lua +++ b/spec/05-migration/plugins/opentelemetry/migrations/001_331_to_332_spec.lua @@ -4,7 +4,56 @@ local uh = require "spec.upgrade_helpers" if uh.database_type() == 'postgres' then - local handler = uh.get_busted_handler("3.3.0", "3.6.0") + local handler = uh.get_busted_handler("3.0.0", "3.2.0") + handler("opentelemetry plugin migration", function() + lazy_setup(function() + assert(uh.start_kong()) + end) + + lazy_teardown(function () + assert(uh.stop_kong()) + end) + + uh.setup(function () + local admin_client = assert(uh.admin_client()) + + local res = assert(admin_client:send { + method = "POST", + path = "/plugins/", + body = { + name = "opentelemetry", + config = { + endpoint = "http://localhost:8080/v1/traces", + } + }, + headers = { + ["Content-Type"] = "application/json" + } + }) + assert.res_status(201, res) + admin_client:close() + end) + + uh.new_after_finish("has opentelemetry queue configuration", function () + local admin_client = assert(uh.admin_client()) + local res = assert(admin_client:send { + method = "GET", + path = "/plugins/" + }) + local body = cjson.decode(assert.res_status(200, res)) + assert.equal(1, #body.data) + assert.equal("opentelemetry", body.data[1].name) + local expected_config = { + queue = { + max_batch_size = 200 + }, + } + assert.partial_match(expected_config, body.data[1].config) + admin_client:close() + end) + end) + + handler = uh.get_busted_handler("3.3.0", "3.6.0") handler("opentelemetry plugin migration", function() lazy_setup(function() assert(uh.start_kong()) @@ -47,7 +96,6 @@ if uh.database_type() == 'postgres' then assert.equal(1, #body.data) assert.equal("opentelemetry", body.data[1].name) local expected_config = { - endpoint = "http://localhost:8080/v1/traces", queue = { max_batch_size = 200 }, diff --git a/spec/06-third-party/01-deck/01-deck-integration_spec.lua b/spec/06-third-party/01-deck/01-deck-integration_spec.lua index 3297bee2e32..21bdf4b58e5 100644 --- a/spec/06-third-party/01-deck/01-deck-integration_spec.lua +++ b/spec/06-third-party/01-deck/01-deck-integration_spec.lua @@ -131,7 +131,7 @@ local function get_plugins_configs(service) ["opentelemetry"] = { name = "opentelemetry", config = { - endpoint = "http://test.test" + traces_endpoint = "http://test.test" }, }, ["loggly"] = { diff --git a/spec/fixtures/ai-proxy/unit/expected-requests/bedrock/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/expected-requests/bedrock/llm-v1-chat.json new file mode 100644 index 00000000000..ad68f6b2833 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/expected-requests/bedrock/llm-v1-chat.json @@ -0,0 +1,55 @@ +{ + "system": [ + { + "text": "You are a mathematician." + } + ], + "messages": [ + { + "content": [ + { + "text": "What is 1 + 2?" + } + ], + "role": "user" + }, + { + "content": [ + { + "text": "The sum of 1 + 2 is 3. If you have any more math questions or if there's anything else I can help you with, feel free to ask!" + } + ], + "role": "assistant" + }, + { + "content": [ + { + "text": "Multiply that by 2" + } + ], + "role": "user" + }, + { + "content": [ + { + "text": "Certainly! If you multiply 3 by 2, the result is 6. If you have any more questions or if there's anything else I can help you with, feel free to ask!" + } + ], + "role": "assistant" + }, + { + "content": [ + { + "text": "Why can't you divide by zero?" + } + ], + "role": "user" + } + ], + "inferenceConfig": { + "maxTokens": 8192, + "temperature": 0.8, + "topP": 0.6 + }, + "anthropic_version": "bedrock-2023-05-31" +} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/expected-requests/gemini/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/expected-requests/gemini/llm-v1-chat.json new file mode 100644 index 00000000000..f236df678a4 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/expected-requests/gemini/llm-v1-chat.json @@ -0,0 +1,57 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "What is 1 + 2?" + } + ] + }, + { + "role": "model", + "parts": [ + { + "text": "The sum of 1 + 2 is 3. If you have any more math questions or if there's anything else I can help you with, feel free to ask!" + } + ] + }, + { + "role": "user", + "parts": [ + { + "text": "Multiply that by 2" + } + ] + }, + { + "role": "model", + "parts": [ + { + "text": "Certainly! If you multiply 3 by 2, the result is 6. If you have any more questions or if there's anything else I can help you with, feel free to ask!" + } + ] + }, + { + "role": "user", + "parts": [ + { + "text": "Why can't you divide by zero?" + } + ] + } + ], + "generationConfig": { + "temperature": 0.8, + "topK": 1, + "topP": 0.6, + "maxOutputTokens": 8192 + }, + "systemInstruction": { + "parts": [ + { + "text": "You are a mathematician." + } + ] + } +} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/expected-responses/bedrock/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/expected-responses/bedrock/llm-v1-chat.json new file mode 100644 index 00000000000..948d3fb4746 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/expected-responses/bedrock/llm-v1-chat.json @@ -0,0 +1,19 @@ +{ + "choices": [ + { + "finish_reason": "end_turn", + "index": 0, + "message": { + "content": "You cannot divide by zero because it is not a valid operation in mathematics.", + "role": "assistant" + } + } + ], + "object": "chat.completion", + "usage": { + "completion_tokens": 119, + "prompt_tokens": 19, + "total_tokens": 138 + }, + "model": "bedrock" +} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/expected-responses/gemini/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/expected-responses/gemini/llm-v1-chat.json new file mode 100644 index 00000000000..90a1656d2a3 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/expected-responses/gemini/llm-v1-chat.json @@ -0,0 +1,14 @@ +{ + "choices": [ + { + "finish_reason": "stop", + "index": 0, + "message": { + "content": "Ah, vous voulez savoir le double de ce résultat ? Eh bien, le double de 2 est **4**. \n", + "role": "assistant" + } + } + ], + "model": "gemini-pro", + "object": "chat.completion" +} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/real-responses/bedrock/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/real-responses/bedrock/llm-v1-chat.json new file mode 100644 index 00000000000..e995bbd984d --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/real-responses/bedrock/llm-v1-chat.json @@ -0,0 +1,21 @@ +{ + "metrics": { + "latencyMs": 14767 + }, + "output": { + "message": { + "content": [ + { + "text": "You cannot divide by zero because it is not a valid operation in mathematics." + } + ], + "role": "assistant" + } + }, + "stopReason": "end_turn", + "usage": { + "completion_tokens": 119, + "prompt_tokens": 19, + "total_tokens": 138 + } +} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/real-responses/gemini/llm-v1-chat.json b/spec/fixtures/ai-proxy/unit/real-responses/gemini/llm-v1-chat.json new file mode 100644 index 00000000000..96933d9835e --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/real-responses/gemini/llm-v1-chat.json @@ -0,0 +1,39 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "Ah, vous voulez savoir le double de ce résultat ? Eh bien, le double de 2 est **4**. \n" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 14, + "candidatesTokenCount": 128, + "totalTokenCount": 142 + } +} diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/expected-output.json b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/expected-output.json new file mode 100644 index 00000000000..8761c559360 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/expected-output.json @@ -0,0 +1,20 @@ +[ + { + "data": "{\"body\":\"{\\\"p\\\":\\\"abcdefghijkl\\\",\\\"role\\\":\\\"assistant\\\"}\",\"headers\":{\":event-type\":\"messageStart\",\":content-type\":\"application\/json\",\":message-type\":\"event\"}}" + }, + { + "data": "{\"body\":\"{\\\"contentBlockIndex\\\":0,\\\"delta\\\":{\\\"text\\\":\\\"Hello! Relativity is a set of physical theories that are collectively known as special relativity and general relativity, proposed by Albert Einstein. These theories revolutionized our understanding of space, time, and gravity, and have had far-reach\\\"},\\\"p\\\":\\\"abcd\\\"}\",\"headers\":{\":event-type\":\"contentBlockDelta\",\":content-type\":\"application\\/json\",\":message-type\":\"event\"}}" + }, + { + "data": "{\"headers\":{\":event-type\":\"contentBlockDelta\",\":message-type\":\"event\",\":content-type\":\"application\\/json\"},\"body\":\"{\\\"contentBlockIndex\\\":0,\\\"delta\\\":{\\\"text\\\":\\\"ing implications in various scientific and technological fields. Special relativity applies to all physical phenomena in the absence of gravity, while general relativity explains the law of gravity and its effects on the nature of space, time, and matter.\\\"},\\\"p\\\":\\\"abcdefghijk\\\"}\"}" + }, + { + "data": "{\"body\":\"{\\\"contentBlockIndex\\\":0,\\\"p\\\":\\\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR\\\"}\",\"headers\":{\":content-type\":\"application\\/json\",\":event-type\":\"contentBlockStop\",\":message-type\":\"event\"}}" + }, + { + "data": "{\"body\":\"{\\\"p\\\":\\\"abcdefghijklm\\\",\\\"stopReason\\\":\\\"end_turn\\\"}\",\"headers\":{\":message-type\":\"event\",\":content-type\":\"application\\/json\",\":event-type\":\"messageStop\"}}" + }, + { + "data": "{\"headers\":{\":message-type\":\"event\",\":content-type\":\"application\\/json\",\":event-type\":\"metadata\"},\"body\":\"{\\\"metrics\\\":{\\\"latencyMs\\\":2613},\\\"p\\\":\\\"abcdefghijklmnopqrstuvwxyzABCDEF\\\",\\\"usage\\\":{\\\"inputTokens\\\":9,\\\"outputTokens\\\":97,\\\"totalTokens\\\":106}}\"}" + } +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/input.bin b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/input.bin new file mode 100644 index 00000000000..8f9d03b4f7e Binary files /dev/null and b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/aws/input.bin differ diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/expected-output.json b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/expected-output.json new file mode 100644 index 00000000000..b08549afbf4 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/expected-output.json @@ -0,0 +1,8 @@ +[ + { + "data": "{\"is_finished\":false,\"event_type\":\"stream-start\",\"generation_id\":\"10f31c2f-1a4c-48cf-b500-dc8141a25ae5\"}" + }, + { + "data": "{\"is_finished\":false,\"event_type\":\"text-generation\",\"text\":\"2\"}" + } +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/input.bin b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/input.bin new file mode 100644 index 00000000000..af13220a423 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/complete-json/input.bin @@ -0,0 +1,2 @@ +{"is_finished":false,"event_type":"stream-start","generation_id":"10f31c2f-1a4c-48cf-b500-dc8141a25ae5"} +{"is_finished":false,"event_type":"text-generation","text":"2"} \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/expected-output.json b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/expected-output.json new file mode 100644 index 00000000000..5f3b0afa51d --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/expected-output.json @@ -0,0 +1,14 @@ +[ + { + "data": "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \"The\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 1,\n \"totalTokenCount\": 7\n }\n}" + }, + { + "data": "\n{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \" theory of relativity is actually two theories by Albert Einstein: **special relativity** and\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \"safetyRatings\": [\n {\n \"category\": \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HATE_SPEECH\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HARASSMENT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n \"probability\": \"NEGLIGIBLE\"\n }\n ]\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 17,\n \"totalTokenCount\": 23\n }\n}" + }, + { + "data": "\n{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \" **general relativity**. Here's a simplified breakdown:\\n\\n**Special Relativity (\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \"safetyRatings\": [\n {\n \"category\": \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HATE_SPEECH\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HARASSMENT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n \"probability\": \"NEGLIGIBLE\"\n }\n ]\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 33,\n \"totalTokenCount\": 39\n }\n}" + }, + { + "data": "\n{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \"1905):**\\n\\n* **Focus:** The relationship between space and time.\\n* **Key ideas:**\\n * **Speed of light\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \"safetyRatings\": [\n {\n \"category\": \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HATE_SPEECH\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HARASSMENT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n \"probability\": \"NEGLIGIBLE\"\n }\n ]\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 65,\n \"totalTokenCount\": 71\n }\n}\n" + } +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/input.bin b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/input.bin new file mode 100644 index 00000000000..8cef2a01fa8 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-beginning/input.bin @@ -0,0 +1,141 @@ +[{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "The" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 1, + "totalTokenCount": 7 + } +} +, +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": " theory of relativity is actually two theories by Albert Einstein: **special relativity** and" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 17, + "totalTokenCount": 23 + } +} +, +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": " **general relativity**. Here's a simplified breakdown:\n\n**Special Relativity (" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 33, + "totalTokenCount": 39 + } +} +, +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "1905):**\n\n* **Focus:** The relationship between space and time.\n* **Key ideas:**\n * **Speed of light" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 65, + "totalTokenCount": 71 + } +} diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/expected-output.json b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/expected-output.json new file mode 100644 index 00000000000..f35aaf6f9db --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/expected-output.json @@ -0,0 +1,11 @@ +[ + { + "data": "{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \" is constant:** No matter how fast you are moving, light always travels at the same speed (approximately 299,792,458\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \"safetyRatings\": [\n {\n \"category\": \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HATE_SPEECH\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HARASSMENT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n \"probability\": \"NEGLIGIBLE\"\n }\n ]\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 97,\n \"totalTokenCount\": 103\n }\n}" + }, + { + "data": "\n{\n \"candidates\": [\n {\n \"content\": {\n \"parts\": [\n {\n \"text\": \" not a limit.\\n\\nIf you're interested in learning more about relativity, I encourage you to explore further resources online or in books. There are many excellent introductory materials available. \\n\"\n }\n ],\n \"role\": \"model\"\n },\n \"finishReason\": \"STOP\",\n \"index\": 0,\n \"safetyRatings\": [\n {\n \"category\": \"HARM_CATEGORY_SEXUALLY_EXPLICIT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HATE_SPEECH\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_HARASSMENT\",\n \"probability\": \"NEGLIGIBLE\"\n },\n {\n \"category\": \"HARM_CATEGORY_DANGEROUS_CONTENT\",\n \"probability\": \"NEGLIGIBLE\"\n }\n ]\n }\n ],\n \"usageMetadata\": {\n \"promptTokenCount\": 6,\n \"candidatesTokenCount\": 547,\n \"totalTokenCount\": 553\n }\n}\n" + }, + { + "data": "[DONE]" + } +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/input.bin b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/input.bin new file mode 100644 index 00000000000..d6489e74d19 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/partial-json-end/input.bin @@ -0,0 +1,80 @@ +,{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": " is constant:** No matter how fast you are moving, light always travels at the same speed (approximately 299,792,458" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 97, + "totalTokenCount": 103 + } +} +, +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": " not a limit.\n\nIf you're interested in learning more about relativity, I encourage you to explore further resources online or in books. There are many excellent introductory materials available. \n" + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0, + "safetyRatings": [ + { + "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HATE_SPEECH", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_HARASSMENT", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS_CONTENT", + "probability": "NEGLIGIBLE" + } + ] + } + ], + "usageMetadata": { + "promptTokenCount": 6, + "candidatesTokenCount": 547, + "totalTokenCount": 553 + } +} +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/expected-output.json b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/expected-output.json new file mode 100644 index 00000000000..f515516c7ec --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/expected-output.json @@ -0,0 +1,11 @@ +[ + { + "data": "{ \"choices\": [ { \"delta\": { \"content\": \"\", \"role\": \"assistant\" }, \"finish_reason\": null, \"index\": 0, \"logprobs\": null } ], \"created\": 1720136012, \"id\": \"chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY\", \"model\": \"gpt-4-0613\", \"object\": \"chat.completion.chunk\", \"system_fingerprint\": null}" + }, + { + "data": "{ \"choices\": [ { \"delta\": { \"content\": \"2\" }, \"finish_reason\": null, \"index\": 0, \"logprobs\": null } ], \"created\": 1720136012, \"id\": \"chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY\", \"model\": \"gpt-4-0613\", \"object\": \"chat.completion.chunk\", \"system_fingerprint\": null}" + }, + { + "data": "{ \"choices\": [ { \"delta\": {}, \"finish_reason\": \"stop\", \"index\": 0, \"logprobs\": null } ], \"created\": 1720136012, \"id\": \"chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY\", \"model\": \"gpt-4-0613\", \"object\": \"chat.completion.chunk\", \"system_fingerprint\": null}" + } +] \ No newline at end of file diff --git a/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/input.bin b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/input.bin new file mode 100644 index 00000000000..efe2ad50c65 --- /dev/null +++ b/spec/fixtures/ai-proxy/unit/streaming-chunk-formats/text-event-stream/input.bin @@ -0,0 +1,7 @@ +data: { "choices": [ { "delta": { "content": "", "role": "assistant" }, "finish_reason": null, "index": 0, "logprobs": null } ], "created": 1720136012, "id": "chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY", "model": "gpt-4-0613", "object": "chat.completion.chunk", "system_fingerprint": null} + +data: { "choices": [ { "delta": { "content": "2" }, "finish_reason": null, "index": 0, "logprobs": null } ], "created": 1720136012, "id": "chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY", "model": "gpt-4-0613", "object": "chat.completion.chunk", "system_fingerprint": null} + +data: { "choices": [ { "delta": {}, "finish_reason": "stop", "index": 0, "logprobs": null } ], "created": 1720136012, "id": "chatcmpl-9hQFArK1oMZcRwaIa86RGwrjVNmeY", "model": "gpt-4-0613", "object": "chat.completion.chunk", "system_fingerprint": null} + +data: [DONE] \ No newline at end of file diff --git a/spec/fixtures/aws-lambda.lua b/spec/fixtures/aws-lambda.lua index 3ab5b0ac0fa..e74aa840a79 100644 --- a/spec/fixtures/aws-lambda.lua +++ b/spec/fixtures/aws-lambda.lua @@ -70,6 +70,10 @@ local fixtures = { local str = "{\"statusCode\": 200, \"testbody\": [], \"isBase64Encoded\": false}" ngx.say(str) + elseif string.match(ngx.var.uri, "functionWithArrayCTypeInMVHAndEmptyArray") then + ngx.header["Content-Type"] = "application/json" + ngx.say("{\"statusCode\": 200, \"isBase64Encoded\": true, \"body\": \"eyJrZXkiOiAidmFsdWUiLCAia2V5MiI6IFtdfQ==\", \"headers\": {}, \"multiValueHeaders\": {\"Content-Type\": [\"application/json+test\"]}}") + elseif type(res) == 'string' then ngx.header["Content-Length"] = #res + 1 ngx.say(res) diff --git a/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/handler.lua b/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/handler.lua new file mode 100644 index 00000000000..ba9d3a4f38f --- /dev/null +++ b/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/handler.lua @@ -0,0 +1,74 @@ +-- The test case 04-client_ipc_spec.lua will load this plugin and check its +-- generated error logs. + +local DnsClientTestHandler = { + VERSION = "1.0", + PRIORITY = 1000, +} + + +local log = ngx.log +local ERR = ngx.ERR +local PRE = "dns-client-test:" + + +local function test() + local phase = "" + local host = "ipc.test" + + -- inject resolver.query + require("resty.dns.resolver").query = function(self, name, opts) + log(ERR, PRE, phase, "query:", name) + return {{ + type = opts.qtype, + address = "1.2.3.4", + target = "1.2.3.4", + class = 1, + name = name, + ttl = 0.1, + }} + end + + local dns_client = require("kong.tools.dns")() + local cli = dns_client.new({}) + + -- inject broadcast + local orig_broadcast = cli.cache.broadcast + cli.cache.broadcast = function(channel, data) + log(ERR, PRE, phase, "broadcast:", data) + orig_broadcast(channel, data) + end + + -- inject lrucahce.delete + local orig_delete = cli.cache.lru.delete + cli.cache.lru.delete = function(self, key) + log(ERR, PRE, phase, "lru delete:", key) + orig_delete(self, key) + end + + -- phase 1: two processes try to get answers and trigger only one query + phase = "first:" + local answers = cli:_resolve(host) + log(ERR, PRE, phase, "answers:", answers[1].address) + + -- wait records to be stale + ngx.sleep(0.5) + + -- phase 2: get the stale record and trigger only one stale-updating task, + -- the stale-updating task will update the record and broadcast + -- the lru cache invalidation event to other workers + phase = "stale:" + local answers = cli:_resolve(host) + log(ERR, PRE, phase, "answers:", answers[1].address) + + -- tests end + log(ERR, PRE, "DNS query completed") +end + + +function DnsClientTestHandler:init_worker() + ngx.timer.at(0, test) +end + + +return DnsClientTestHandler diff --git a/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/schema.lua b/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/schema.lua new file mode 100644 index 00000000000..8b6c80ad59e --- /dev/null +++ b/spec/fixtures/custom_plugins/kong/plugins/dns-client-test/schema.lua @@ -0,0 +1,12 @@ +return { + name = "dns-client-test", + fields = { + { + config = { + type = "record", + fields = { + }, + }, + }, + }, +} diff --git a/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/handler.lua b/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/handler.lua new file mode 100644 index 00000000000..7a5d4d4bc4b --- /dev/null +++ b/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/handler.lua @@ -0,0 +1,49 @@ +local PDKLoggerHandler = { + VERSION = "0.1-t", + PRIORITY = 1000, +} + +local plugin_name = "pdk-logger" +local attributes = { some_key = "some_value", some_other_key = "some_other_value"} + + +function PDKLoggerHandler:access(conf) + local message_type = "access_phase" + local message = "hello, access phase" + -- pass both optional arguments (message and attributes) + local ok, err = kong.telemetry.log(plugin_name, conf, message_type, message, attributes) + if not ok then + kong.log.err(err) + end +end + + +function PDKLoggerHandler:header_filter(conf) + local message_type = "header_filter_phase" + local message = "hello, header_filter phase" + -- no attributes + local ok, err = kong.telemetry.log(plugin_name, conf, message_type, message, nil) + if not ok then + kong.log.err(err) + end +end + + +function PDKLoggerHandler:log(conf) + local message_type = "log_phase" + -- no message + local ok, err = kong.telemetry.log(plugin_name, conf, message_type, nil, attributes) + if not ok then + kong.log.err(err) + end + + message_type = "log_phase_2" + -- no attributes and no message + ok, err = kong.telemetry.log(plugin_name, conf, message_type, nil, nil) + if not ok then + kong.log.err(err) + end +end + + +return PDKLoggerHandler diff --git a/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/schema.lua b/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/schema.lua new file mode 100644 index 00000000000..cbdafd0a09e --- /dev/null +++ b/spec/fixtures/custom_plugins/kong/plugins/pdk-logger/schema.lua @@ -0,0 +1,18 @@ +local typedefs = require "kong.db.schema.typedefs" + + +return { + name = "pdk-logger", + fields = { + { + protocols = typedefs.protocols { default = { "http", "https", "tcp", "tls", "grpc", "grpcs" } }, + }, + { + config = { + type = "record", + fields = { + }, + }, + }, + }, +} diff --git a/spec/fixtures/opentelemetry/otelcol.yaml b/spec/fixtures/opentelemetry/otelcol.yaml index 9cd430d1fc9..a15acde86bf 100644 --- a/spec/fixtures/opentelemetry/otelcol.yaml +++ b/spec/fixtures/opentelemetry/otelcol.yaml @@ -26,3 +26,7 @@ service: receivers: [otlp] processors: [batch] exporters: [logging, file] + logs: + receivers: [otlp] + processors: [batch] + exporters: [logging, file] diff --git a/spec/fixtures/shared_dict.lua b/spec/fixtures/shared_dict.lua index c552376ecaf..fe0691d0a13 100644 --- a/spec/fixtures/shared_dict.lua +++ b/spec/fixtures/shared_dict.lua @@ -13,6 +13,7 @@ local dicts = { "kong_db_cache_2 16m", "kong_db_cache_miss 12m", "kong_db_cache_miss_2 12m", + "kong_dns_cache 5m", "kong_mock_upstream_loggers 10m", "kong_secrets 5m", "test_vault 5m", diff --git a/spec/fixtures/template_inject/nginx_kong_test_custom_inject_stream.lua b/spec/fixtures/template_inject/nginx_kong_test_custom_inject_stream.lua index 7d43af7446c..5306bd8803e 100644 --- a/spec/fixtures/template_inject/nginx_kong_test_custom_inject_stream.lua +++ b/spec/fixtures/template_inject/nginx_kong_test_custom_inject_stream.lua @@ -33,7 +33,7 @@ include '*.stream_mock'; > if cluster_ssl_tunnel then server { - listen unix:${{PREFIX}}/cluster_proxy_ssl_terminator.sock; + listen unix:${{SOCKET_PATH}}/cluster_proxy_ssl_terminator.sock; proxy_pass ${{cluster_ssl_tunnel}}; proxy_ssl on; diff --git a/spec/helpers.lua b/spec/helpers.lua index 7292b55e06c..5fb537e8c6e 100644 --- a/spec/helpers.lua +++ b/spec/helpers.lua @@ -545,7 +545,7 @@ end -- @param db the database object -- @return ml_cache instance local function get_cache(db) - local worker_events = assert(kong_global.init_worker_events()) + local worker_events = assert(kong_global.init_worker_events(conf)) local cluster_events = assert(kong_global.init_cluster_events(conf, db)) local cache = assert(kong_global.init_cache(conf, cluster_events, @@ -1210,6 +1210,7 @@ local function grpc_client(host, port, opts) __call = function(t, args) local service = assert(args.service) local body = args.body + local arg_opts = args.opts or {} local t_body = type(body) if t_body ~= "nil" then @@ -1217,11 +1218,12 @@ local function grpc_client(host, port, opts) body = cjson.encode(body) end - args.opts["-d"] = string.format("'%s'", body) + arg_opts["-d"] = string.format("'%s'", body) end - local opts = gen_grpcurl_opts(pl_tablex.merge(t.opts, args.opts, true)) - local ok, _, out, err = exec(string.format(t.cmd_template, opts, service), true) + local cmd_opts = gen_grpcurl_opts(pl_tablex.merge(t.opts, arg_opts, true)) + local cmd = string.format(t.cmd_template, cmd_opts, service) + local ok, _, out, err = exec(cmd, true) if ok then return ok, ("%s%s"):format(out or "", err or "") @@ -2459,6 +2461,11 @@ local deep_sort do return deep_compare(a[1], b[1]) end + -- compare cjson.null or ngx.null + if type(a) == "userdata" and type(b) == "userdata" then + return false + end + return a < b end @@ -3836,10 +3843,13 @@ end -- @param preserve_dc ??? local function cleanup_kong(prefix, preserve_prefix, preserve_dc) -- remove socket files to ensure `pl.dir.rmtree()` ok - local socks = { "/worker_events.sock", "/stream_worker_events.sock", } - for _, name in ipairs(socks) do - local sock_file = (prefix or conf.prefix) .. name - os.remove(sock_file) + prefix = prefix or conf.prefix + local socket_path = pl_path.join(prefix, constants.SOCKET_DIRECTORY) + for child in lfs.dir(socket_path) do + if child:sub(-5) == ".sock" then + local path = pl_path.join(socket_path, child) + os.remove(path) + end end -- note: set env var "KONG_TEST_DONT_CLEAN" !! the "_TEST" will be dropped diff --git a/spec/helpers/dns.lua b/spec/helpers/dns.lua index 4f8bf45333e..68fdbfbcf2b 100644 --- a/spec/helpers/dns.lua +++ b/spec/helpers/dns.lua @@ -37,7 +37,10 @@ end --- Expires a record now. -- @param record a DNS record previously created -function _M.dnsExpire(record) +function _M.dnsExpire(client, record) + local dnscache = client.getcache() + dnscache:delete(record[1].name .. ":" .. record[1].type) + dnscache:delete(record[1].name .. ":-1") -- A/AAAA record.expire = gettime() - 1 end @@ -76,12 +79,13 @@ function _M.dnsSRV(client, records, staleTtl) -- set timeouts records.touch = gettime() records.expire = gettime() + records[1].ttl + records.ttl = records[1].ttl -- create key, and insert it - local key = records[1].type..":"..records[1].name + local key = records[1].name..":"..records[1].type + dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) + key = records[1].name..":-1" -- A/AAAA dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) - -- insert last-succesful lookup type - dnscache:set(records[1].name, records[1].type) return records end @@ -117,12 +121,13 @@ function _M.dnsA(client, records, staleTtl) -- set timeouts records.touch = gettime() records.expire = gettime() + records[1].ttl + records.ttl = records[1].ttl -- create key, and insert it - local key = records[1].type..":"..records[1].name - dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) - -- insert last-succesful lookup type - dnscache:set(records[1].name, records[1].type) + local key = records[1].name..":"..records[1].type + dnscache:set(key, records, records[1].ttl) + key = records[1].name..":-1" -- A/AAAA + dnscache:set(key, records, records[1].ttl) return records end @@ -157,12 +162,13 @@ function _M.dnsAAAA(client, records, staleTtl) -- set timeouts records.touch = gettime() records.expire = gettime() + records[1].ttl + records.ttl = records[1].ttl -- create key, and insert it - local key = records[1].type..":"..records[1].name + local key = records[1].name..":"..records[1].type + dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) + key = records[1].name..":-1" -- A/AAAA dnscache:set(key, records, records[1].ttl + (staleTtl or 4)) - -- insert last-succesful lookup type - dnscache:set(records[1].name, records[1].type) return records end diff --git a/t/01-pdk/04-request/15-get_raw_body.t b/t/01-pdk/04-request/15-get_raw_body.t index 216b94096f7..2e47aeb461d 100644 --- a/t/01-pdk/04-request/15-get_raw_body.t +++ b/t/01-pdk/04-request/15-get_raw_body.t @@ -119,3 +119,81 @@ body: 'potato' body err: request body did not fit into client body buffer, consider raising 'client_body_buffer_size' --- no_error_log [error] + + + +=== TEST 6: request.get_raw_body() returns correctly if max_allowed_file_size is larger than request +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + access_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local body, err = pdk.request.get_raw_body(20000) + if body then + ngx.say("body length: ", #body) + + else + ngx.say("body err: ", err) + end + } + } +--- request eval +"GET /t\r\n" . ("a" x 20000) +--- response_body +body length: 20000 +--- no_error_log +[error] + + + +=== TEST 7: request.get_raw_body() returns error if max_allowed_file_size is smaller than request +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + access_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local body, err = pdk.request.get_raw_body(19999) + if body then + ngx.say("body length: ", #body) + + else + ngx.say("body err: ", err) + end + } + } +--- request eval +"GET /t\r\n" . ("a" x 20000) +--- response_body +body err: request body file too big: 20000 > 19999 +--- no_error_log +[error] + + + +=== TEST 8: request.get_raw_body() returns correctly if max_allowed_file_size is equal to 0 +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + access_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local body, err = pdk.request.get_raw_body(0) + if body then + ngx.say("body length: ", #body) + + else + ngx.say("body err: ", err) + end + } + } +--- request eval +"GET /t\r\n" . ("a" x 20000) +--- response_body +body length: 20000 +--- no_error_log +[error] diff --git a/t/01-pdk/04-request/16-get_body.t b/t/01-pdk/04-request/16-get_body.t index ead3f535dd1..39284e1205d 100644 --- a/t/01-pdk/04-request/16-get_body.t +++ b/t/01-pdk/04-request/16-get_body.t @@ -704,3 +704,35 @@ test=data mime=multipart/form-data --- no_error_log [error] + + + +=== TEST 26: request.get_body() with application/json returns ok if max_allowed_file_size is set +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + access_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local args, err = pdk.request.get_body("application/json") + ngx.say("error: ", err) + + local args, err = pdk.request.get_body("application/json", nil, 20008) + if err then + ngx.say("error: ", err) + else + ngx.say("parsed ok") + end + + } + } +--- request eval +"POST /t\r\n{\"b\":\"" . ("a" x 20000) . "\"}" +--- more_headers +Content-Type: application/json +--- response_body +error: request body did not fit into client body buffer, consider raising 'client_body_buffer_size' +parsed ok +--- no_error_log +[error] diff --git a/t/01-pdk/06-service-request/00-phase_checks.t b/t/01-pdk/06-service-request/00-phase_checks.t index 80a57cbce4b..d459338a9b2 100644 --- a/t/01-pdk/06-service-request/00-phase_checks.t +++ b/t/01-pdk/06-service-request/00-phase_checks.t @@ -47,7 +47,7 @@ qq{ args = { "http" }, init_worker = false, certificate = "pending", - rewrite = "forced false", + rewrite = true, access = true, response = "forced false", header_filter = "forced false", @@ -71,7 +71,7 @@ qq{ args = { "/" }, init_worker = false, certificate = "pending", - rewrite = "forced false", + rewrite = true, access = true, response = "forced false", header_filter = "forced false", diff --git a/t/01-pdk/09-service/00-phase_checks.t b/t/01-pdk/09-service/00-phase_checks.t index 2be48fef1b9..c19d9a824b7 100644 --- a/t/01-pdk/09-service/00-phase_checks.t +++ b/t/01-pdk/09-service/00-phase_checks.t @@ -69,6 +69,42 @@ qq{ args = { "example.com", 8000 }, init_worker = false, certificate = "pending", + rewrite = true, + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "forced false", + admin_api = "forced false", + }, { + method = "set_retries", + args = { 3, }, + init_worker = "forced false", + certificate = "pending", + rewrite = "forced false", + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "forced false", + admin_api = "forced false", + }, { + method = "set_target_retry_callback", + args = { function() end }, + init_worker = "forced false", + certificate = "pending", + rewrite = "forced false", + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "forced false", + admin_api = "forced false", + }, { + method = "set_timeouts", + args = { 1, 2, 3}, + init_worker = "forced false", + certificate = "pending", rewrite = "forced false", access = true, response = "forced false", @@ -219,6 +255,19 @@ qq{ args = { "example.com", 8000 }, init_worker = false, certificate = "pending", + rewrite = true, + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "pending", + admin_api = "forced false", + preread = "pending", + }, { + method = "set_retries", + args = { 3, }, + init_worker = "forced false", + certificate = "pending", rewrite = "forced false", access = true, response = "forced false", @@ -227,8 +276,33 @@ qq{ log = "pending", admin_api = "forced false", preread = "pending", - }, - { + }, { + method = "set_target_retry_callback", + args = { function() end }, + init_worker = "forced false", + certificate = "pending", + rewrite = "forced false", + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "pending", + admin_api = "forced false", + preread = "pending", + }, { + method = "set_timeouts", + args = { 1, 2, 3}, + init_worker = "forced false", + certificate = "pending", + rewrite = "forced false", + access = true, + response = "forced false", + header_filter = "forced false", + body_filter = "forced false", + log = "pending", + admin_api = "forced false", + preread = "pending", + }, { method = "set_tls_cert_key", args = { chain, key, }, init_worker = false, diff --git a/t/01-pdk/09-service/04-set-retries.t b/t/01-pdk/09-service/04-set-retries.t new file mode 100644 index 00000000000..e6687a310a4 --- /dev/null +++ b/t/01-pdk/09-service/04-set-retries.t @@ -0,0 +1,107 @@ +use strict; +use warnings FATAL => 'all'; +use Test::Nginx::Socket::Lua; +do "./t/Util.pm"; + +plan tests => repeat_each() * (blocks() * 3); + +run_tests(); + +__DATA__ + +=== TEST 1: service.set_retries() errors if port is not a number +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_retries, "2") + ngx.say(err) + } + } +--- request +GET /t +--- response_body +retries must be an integer +--- no_error_log +[error] + + + +=== TEST 1: service.set_retries() errors if port is not an integer +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_retries, 1.23) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +retries must be an integer +--- no_error_log +[error] + + + +=== TEST 3: service.set_target() errors if port is out of range +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_retries, -1) + ngx.say(err) + local pok, err = pcall(pdk.service.set_retries, 32768) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +port must be an integer between 0 and 32767: given -1 +port must be an integer between 0 and 32767: given 32768 +--- no_error_log +[error] + + + +=== TEST 4: service.set_retries() sets ngx.ctx.balancer_data.retries +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + + set $upstream_host ''; + + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + ngx.ctx.balancer_data = { + retries = 1 + } + + local ok = pdk.service.set_retries(123) + + ngx.say(tostring(ok)) + ngx.say("retries: ", ngx.ctx.balancer_data.retries) + } + } +--- request +GET /t +--- response_body +nil +retries: 123 +--- no_error_log +[error] + + diff --git a/t/01-pdk/09-service/05-set-timeouts.t b/t/01-pdk/09-service/05-set-timeouts.t new file mode 100644 index 00000000000..bcf366f84e1 --- /dev/null +++ b/t/01-pdk/09-service/05-set-timeouts.t @@ -0,0 +1,178 @@ +use strict; +use warnings FATAL => 'all'; +use Test::Nginx::Socket::Lua; +do "./t/Util.pm"; + +plan tests => repeat_each() * (blocks() * 3); + +run_tests(); + +__DATA__ + +=== TEST 1: service.set_timeouts() errors if connect_timeout is not a number +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, "2", 1, 1) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +connect_timeout must be an integer +--- no_error_log +[error] + + + +=== TEST 2: service.set_timeouts() errors if write_timeout is not a number +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, 2, "1", 1) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +write_timeout must be an integer +--- no_error_log +[error] + + + +=== TEST 3: service.set_timeouts() errors if read_timeout is not a number +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, 2, 1, "1") + ngx.say(err) + } + } +--- request +GET /t +--- response_body +read_timeout must be an integer +--- no_error_log +[error] + + + +=== TEST 4: service.set_timeouts() errors if connect_timeout is out of range +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, -1, 1, 1) + ngx.say(err) + local pok, err = pcall(pdk.service.set_timeouts, 2147483647, 1, 1) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +connect_timeout must be an integer between 1 and 2147483646: given -1 +connect_timeout must be an integer between 1 and 2147483646: given 2147483647 +--- no_error_log +[error] + + + +=== TEST 5: service.set_timeouts() errors if write_timeout is out of range +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, 2, -1, 1) + ngx.say(err) + local pok, err = pcall(pdk.service.set_timeouts, 2, 2147483647, 1) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +write_timeout must be an integer between 1 and 2147483646: given -1 +write_timeout must be an integer between 1 and 2147483646: given 2147483647 +--- no_error_log +[error] + + + +=== TEST 6: service.set_timeouts() errors if read_timeout is out of range +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + local pok, err = pcall(pdk.service.set_timeouts, 2, 1, -1) + ngx.say(err) + local pok, err = pcall(pdk.service.set_timeouts, 2, 1, 2147483647) + ngx.say(err) + } + } +--- request +GET /t +--- response_body +read_timeout must be an integer between 1 and 2147483646: given -1 +read_timeout must be an integer between 1 and 2147483646: given 2147483647 +--- no_error_log +[error] + + + +=== TEST 7: service.set_timeouts() sets the timeouts +--- http_config eval: $t::Util::HttpConfig +--- config + location = /t { + content_by_lua_block { + local PDK = require "kong.pdk" + local pdk = PDK.new() + + ngx.ctx.balancer_data = { + connect_timeout = 1, + write_timeout = 1, + read_timeout = 1, + } + + local ok = pdk.service.set_timeouts(2, 3, 4) + ngx.say(tostring(ok)) + ngx.say(ngx.ctx.balancer_data.connect_timeout) + ngx.say(ngx.ctx.balancer_data.write_timeout) + ngx.say(ngx.ctx.balancer_data.read_timeout) + } + } +--- request +GET /t +--- response_body +nil +2 +3 +4 +--- no_error_log +[error] + + diff --git a/t/04-patch/03-fix-ngx-send-header-filter-finalize-ctx.t b/t/04-patch/03-fix-ngx-send-header-filter-finalize-ctx.t new file mode 100644 index 00000000000..a4cc5c1644c --- /dev/null +++ b/t/04-patch/03-fix-ngx-send-header-filter-finalize-ctx.t @@ -0,0 +1,39 @@ +# vim:set ft= ts=4 sw=4 et fdm=marker: + +use Test::Nginx::Socket::Lua; + +#worker_connections(1014); +#master_on(); +#workers(2); +#log_level('warn'); + +repeat_each(2); +#repeat_each(1); + +plan tests => repeat_each() * (blocks() * 2); + +#no_diff(); +#no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: send_header trigger filter finalize does not clear the ctx +--- config + location /lua { + content_by_lua_block { + ngx.header["Last-Modified"] = ngx.http_time(ngx.time()) + ngx.send_headers() + local phase = ngx.get_phase() + } + header_filter_by_lua_block { + ngx.header["X-Hello-World"] = "Hello World" + } + } +--- request +GET /lua +--- more_headers +If-Unmodified-Since: Wed, 01 Jan 2020 07:28:00 GMT +--- error_code: 412 +--- no_error_log +unknown phase: 0 diff --git a/t/Util.pm b/t/Util.pm index 7d60e2eb144..b685c5cfad0 100644 --- a/t/Util.pm +++ b/t/Util.pm @@ -136,6 +136,7 @@ our $InitByLuaBlockConfig = <<_EOC_; if not forced_false and ok1 == false + and err1 and not err1:match("attempt to index field ") and not err1:match("API disabled in the ") and not err1:match("headers have already been sent") then @@ -170,7 +171,7 @@ our $InitByLuaBlockConfig = <<_EOC_; end -- if failed with OpenResty phase error - if err1:match("API disabled in the ") then + if err1 and err1:match("API disabled in the ") then -- should replace with a Kong error if not err2:match("function cannot be called") then log(ERR, msg, "a Kong-generated error; got: ", (err2:gsub(",", ";")))