From 32867226c79a4e6449fec91c1b496d5547765e97 Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Tue, 8 Apr 2025 16:01:45 +0330 Subject: [PATCH 1/7] Add proxy-chain plugin with documentation --- apisix/plugins/proxy-chain.lua | 147 +++++++++++++++++++++++++++++++++ docs/en/latest/proxy-chain.md | 0 2 files changed, 147 insertions(+) create mode 100644 apisix/plugins/proxy-chain.lua create mode 100644 docs/en/latest/proxy-chain.md diff --git a/apisix/plugins/proxy-chain.lua b/apisix/plugins/proxy-chain.lua new file mode 100644 index 000000000000..012a7fbfe65d --- /dev/null +++ b/apisix/plugins/proxy-chain.lua @@ -0,0 +1,147 @@ +local core = require("apisix.core") +local http = require("resty.http") +local cjson = require("cjson") + +local plugin_name = "proxy-chain" + + +local schema = { + type = "object", + properties = { + services = { + type = "array", + items = { + type = "object", + properties = { + uri = { type = "string", minLength = 1 }, + method = { type = "string", enum = {"GET", "POST", "PUT", "DELETE"}, default = "POST" } + }, + required = {"uri"} + }, + minItems = 1 + }, + token_header = { type = "string" } + }, + required = {"services"} +} + +local _M = { + version = 0.1, + priority = 1000, + name = plugin_name, + schema = schema, + description = "A plugin to chain multiple service requests and merge their responses." +} + +function _M.check_schema(conf) + return core.schema.check(schema, conf) +end + +function _M.access(conf, ctx) + + ngx.req.read_body() + local original_body = ngx.req.get_body_data() + local original_data = {} + + core.log.info("Original body: ", original_body or "nil") + if original_body and original_body ~= "" then + local success, decoded = pcall(cjson.decode, original_body) + if success then + original_data = decoded + else + core.log.warn("Invalid JSON in original body: ", original_body) + end + end + + local uri_args = ngx.req.get_uri_args() + for k, v in pairs(uri_args) do + original_data[k] = v + end + + + local headers = ngx.req.get_headers() + local auth_header + if conf.token_header then + local token = headers[conf.token_header] or headers[conf.token_header:lower()] or "" + if token == "" then + core.log.info("No token found in header: ", conf.token_header, ", falling back to Authorization") + token = headers["Authorization"] or headers["authorization"] or "" + if token ~= "" then + token = token:gsub("^Bearer%s+", "") + end + end + if token ~= "" then + core.log.info("Token extracted from ", conf.token_header, ": ", token) + auth_header = "Bearer " .. token + else + core.log.info("No token provided in ", conf.token_header, " or Authorization, proceeding without auth") + end + else + local token = headers["Authorization"] or headers["authorization"] or "" + if token ~= "" then + token = token:gsub("^Bearer%s+", "") + core.log.info("Token extracted from Authorization: ", token) + auth_header = "Bearer " .. token + else + core.log.info("No token_header specified and no Authorization provided, proceeding without auth") + end + end + + local merged_data = core.table.deepcopy(original_data) + + + for i, service in ipairs(conf.services) do + local httpc = http.new() + local service_headers = { + ["Content-Type"] = "application/json", + ["Accept"] = "*/*" + } + if auth_header then + service_headers["Authorization"] = auth_header + end + + local res, err = httpc:request_uri(service.uri, { + method = service.method, + body = cjson.encode(merged_data), + headers = service_headers + }) + + if not res then + core.log.error("Failed to call service ", service.uri, ": ", err) + return 500, { error = "Failed to call service: " .. service.uri } + end + + if res.status ~= 200 then + core.log.error("Service ", service.uri, " returned non-200 status: ", res.status, " body: ", res.body or "nil") + return res.status, { error = "Service error", body = res.body } + end + + core.log.info("Response from ", service.uri, ": ", res.body or "nil") + + local service_data = {} + if res.body and res.body ~= "" then + local success, decoded = pcall(cjson.decode, res.body) + if success then + service_data = decoded + else + core.log.error("Invalid JSON in response from ", service.uri, ": ", res.body) + return 500, { error = "Invalid JSON in response from " .. service.uri } + end + end + + for k, v in pairs(service_data) do + merged_data[k] = v + end + end + + local new_body = cjson.encode(merged_data) + core.log.info("Merged data sent to upstream: ", new_body) + + ctx.proxy_chain_response = merged_data + ngx.req.set_body_data(new_body) + if auth_header then + ngx.req.set_header("Authorization", auth_header) + end +end + +return _M \ No newline at end of file diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md new file mode 100644 index 000000000000..e69de29bb2d1 From ded292ddb2129f4b8964602dc9c91b6b7bae3f28 Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Wed, 9 Apr 2025 21:27:31 +0330 Subject: [PATCH 2/7] Add proxy-chain plugin with tests and CI workflow --- apisix/plugins/proxy-chain.lua | 63 +++++--- docs/en/latest/proxy-chain.md | 253 +++++++++++++++++++++++++++++++++ t/plugin/proxy-chain.t | 159 +++++++++++++++++++++ 3 files changed, 453 insertions(+), 22 deletions(-) create mode 100644 t/plugin/proxy-chain.t diff --git a/apisix/plugins/proxy-chain.lua b/apisix/plugins/proxy-chain.lua index 012a7fbfe65d..de89fa6f3bd0 100644 --- a/apisix/plugins/proxy-chain.lua +++ b/apisix/plugins/proxy-chain.lua @@ -1,10 +1,16 @@ -local core = require("apisix.core") -local http = require("resty.http") -local cjson = require("cjson") +-- Proxy Chain Plugin for APISIX +-- Author: Vahid Aghazadeh v.opsource@gmail.com +-- Description: This plugin chains multiple upstream service requests, merging their responses into a single payload. +-- It supports passing a custom token header (e.g., Authorization) between services for authentication purposes. +-- License: Apache License 2.0 -local plugin_name = "proxy-chain" +local core = require("apisix.core") -- Core APISIX utilities +local http = require("resty.http") -- HTTP client for making service calls +local cjson = require("cjson") -- JSON encoding/decoding library +local plugin_name = "proxy-chain" +-- Schema definition for plugin configuration local schema = { type = "object", properties = { @@ -13,61 +19,67 @@ local schema = { items = { type = "object", properties = { - uri = { type = "string", minLength = 1 }, - method = { type = "string", enum = {"GET", "POST", "PUT", "DELETE"}, default = "POST" } + uri = { type = "string", minLength = 1 }, -- URI of the service to call + method = { type = "string", enum = {"GET", "POST", "PUT", "DELETE"}, default = "POST" } -- HTTP method }, - required = {"uri"} + required = {"uri"} -- URI is mandatory }, - minItems = 1 + minItems = 1 -- At least one service must be specified }, - token_header = { type = "string" } + token_header = { type = "string" } -- Optional header name for passing a token }, - required = {"services"} + required = {"services"} -- Services array is mandatory } +-- Plugin metadata local _M = { - version = 0.1, - priority = 1000, - name = plugin_name, - schema = schema, + version = 0.1, -- Plugin version + priority = 1000, -- Execution priority (higher runs earlier) + name = plugin_name, -- Plugin name + schema = schema, -- Configuration schema description = "A plugin to chain multiple service requests and merge their responses." } +-- Validate the plugin configuration against the schema function _M.check_schema(conf) return core.schema.check(schema, conf) end +-- Access phase: Chain service calls and merge responses function _M.access(conf, ctx) - + -- Read the incoming request body ngx.req.read_body() local original_body = ngx.req.get_body_data() local original_data = {} + -- Log the original request body core.log.info("Original body: ", original_body or "nil") if original_body and original_body ~= "" then local success, decoded = pcall(cjson.decode, original_body) if success then - original_data = decoded + original_data = decoded -- Parse JSON body if valid else core.log.warn("Invalid JSON in original body: ", original_body) end end + -- Merge URI arguments into the original data local uri_args = ngx.req.get_uri_args() for k, v in pairs(uri_args) do original_data[k] = v end - + -- Extract authentication token from headers local headers = ngx.req.get_headers() local auth_header if conf.token_header then + -- Check custom token header (case-insensitive) local token = headers[conf.token_header] or headers[conf.token_header:lower()] or "" if token == "" then core.log.info("No token found in header: ", conf.token_header, ", falling back to Authorization") token = headers["Authorization"] or headers["authorization"] or "" if token ~= "" then - token = token:gsub("^Bearer%s+", "") + token = token:gsub("^Bearer%s+", "") -- Remove "Bearer " prefix end end if token ~= "" then @@ -77,6 +89,7 @@ function _M.access(conf, ctx) core.log.info("No token provided in ", conf.token_header, " or Authorization, proceeding without auth") end else + -- Fallback to Authorization header if no token_header is specified local token = headers["Authorization"] or headers["authorization"] or "" if token ~= "" then token = token:gsub("^Bearer%s+", "") @@ -87,9 +100,10 @@ function _M.access(conf, ctx) end end + -- Initialize merged data with original request data local merged_data = core.table.deepcopy(original_data) - + -- Iterate through each service in the chain for i, service in ipairs(conf.services) do local httpc = http.new() local service_headers = { @@ -97,9 +111,10 @@ function _M.access(conf, ctx) ["Accept"] = "*/*" } if auth_header then - service_headers["Authorization"] = auth_header + service_headers["Authorization"] = auth_header -- Add auth token to headers end + -- Make the HTTP request to the service local res, err = httpc:request_uri(service.uri, { method = service.method, body = cjson.encode(merged_data), @@ -118,6 +133,7 @@ function _M.access(conf, ctx) core.log.info("Response from ", service.uri, ": ", res.body or "nil") + -- Parse the service response local service_data = {} if res.body and res.body ~= "" then local success, decoded = pcall(cjson.decode, res.body) @@ -129,19 +145,22 @@ function _M.access(conf, ctx) end end + -- Merge service response into the cumulative data for k, v in pairs(service_data) do merged_data[k] = v end end + -- Prepare the final body to send to the upstream local new_body = cjson.encode(merged_data) core.log.info("Merged data sent to upstream: ", new_body) + -- Store the merged response in context and update the request ctx.proxy_chain_response = merged_data ngx.req.set_body_data(new_body) if auth_header then - ngx.req.set_header("Authorization", auth_header) + ngx.req.set_header("Authorization", auth_header) -- Pass token to upstream end end -return _M \ No newline at end of file +return _M diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md index e69de29bb2d1..c3b54fc761f6 100644 --- a/docs/en/latest/proxy-chain.md +++ b/docs/en/latest/proxy-chain.md @@ -0,0 +1,253 @@ +# Proxy Chain Plugin for APISIX + +The `proxy-chain` plugin for APISIX allows you to chain multiple upstream service calls in a sequence, passing data between them as needed. This is particularly useful for workflows where a request needs to interact with multiple services before returning a final response to the client. + +## Features +- Chain multiple upstream service calls in a defined order. +- Pass custom headers (e.g., authentication tokens) between services. +- Flexible configuration for service endpoints and HTTP methods. + +--- + +## Installation + +### Docker + +#### Prerequisites +- Docker installed on your system. +- APISIX version 3.0 or higher. + +#### Steps +1. **Prepare the Plugin File**: + - Place the `proxy-chain.lua` file in a local directory, e.g., `./plugins/`. + +2. **Create a Dockerfile**: + - Create a `Dockerfile` in your project directory: + ```Dockerfile + FROM apache/apisix:3.11.0-debian + USER root + COPY ./plugins/proxy-chain.lua /usr/local/apisix/apisix/plugins/proxy-chain.lua + RUN chown -R apisix:apisix /usr/local/apisix/apisix/plugins/proxy-chain.lua + CMD ["apisix", "start"] + ``` + +3. **Build and Run**: + - Build the Docker image and run it using `docker-compose` or directly: + ```bash + docker build -t apisix-with-proxy-chain . + docker run -d -p 9080:9080 -p 9180:9180 apisix-with-proxy-chain + ``` + - Alternatively, use a `docker-compose.yml`: + ```yaml + version: "3" + services: + apisix: + image: apisix-with-proxy-chain + build: + context: . + dockerfile: Dockerfile + ports: + - "9080:9080" + - "9180:9180" + ``` + ```bash + docker-compose up -d --build + ``` + +4. **Reload APISIX**: + - Ensure the plugin is loaded: + ```bash + docker exec apisix reload + ``` + +### Kubernetes + +#### Prerequisites +- A Kubernetes cluster (e.g., Minikube, GKE, EKS). +- `kubectl` configured to interact with your cluster. +- Helm (optional, for easier deployment). + +#### Steps +1. **Prepare the Plugin File**: + - Place `proxy-chain.lua` in a local directory, e.g., `./plugins/`. + +2. **Create a ConfigMap**: + - Define a ConfigMap to include the plugin file: + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: apisix-plugins + data: + proxy-chain.lua: | + -- Content of proxy-chain.lua goes here + -- (Paste the entire Lua code here) + ``` + +3. **Deploy APISIX with Custom Plugin**: + - Use a Helm chart or a custom manifest. Here’s an example with a manifest: + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: apisix + spec: + replicas: 1 + selector: + matchLabels: + app: apisix + template: + metadata: + labels: + app: apisix + spec: + containers: + - name: apisix + image: apache/apisix:3.11.0-debian + ports: + - containerPort: 9080 + - containerPort: 9180 + volumeMounts: + - name: plugins-volume + mountPath: /usr/local/apisix/apisix/plugins/proxy-chain.lua + subPath: proxy-chain.lua + volumes: + - name: plugins-volume + configMap: + name: apisix-plugins + --- + apiVersion: v1 + kind: Service + metadata: + name: apisix-service + spec: + ports: + - port: 9080 + targetPort: 9080 + name: gateway + - port: 9180 + targetPort: 9180 + name: admin + selector: + app: apisix + type: LoadBalancer + ``` + - Apply the manifests: + ```bash + kubectl apply -f configmap.yaml + kubectl apply -f apisix-deployment.yaml + ``` + +4. **Reload APISIX**: + - Access the APISIX Admin API to reload: + ```bash + kubectl exec -it -- apisix reload + ``` + +--- + +## Configuration + +### Docker + +#### Configuration Steps +1. **Add to Route**: + - Use the APISIX Admin API to configure a route: + ```bash + curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/24 \ + -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/api/v1/checkout", + "methods": ["POST"], + "plugins": { + "proxy-chain": { + "services": [ + { + "uri": "http://customer_service/api/v1/user", + "method": "POST" + } + ] + } + }, + "upstream_id": "550932803756229477" + }' + ``` + +2. **Verify**: + - Test the endpoint: + ```bash + curl -X POST http:///v1/checkout + ``` + +### Kubernetes + +#### Configuration Steps +1. **Add to Route**: + - Assuming APISIX Ingress Controller is installed, use a custom resource (CRD) or Admin API: + ```yaml + apiVersion: apisix.apache.org/v2 + kind: ApisixRoute + metadata: + name: checkout-route + spec: + http: + - name: checkout + match: + paths: + - /v1/checkout + methods: + - POST + backends: + - serviceName: upstream-service + servicePort: 80 + plugins: + - name: proxy-chain + enable: true + config: + services: + - uri: "http://customer_service/api/v1/user" + method: "POST" + ``` + - Apply the CRD: + ```bash + kubectl apply -f route.yaml + ``` + - Alternatively, use the Admin API via port-forwarding: + ```bash + kubectl port-forward service/apisix-service 9180:9180 + curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/24 \ + -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \ + -H 'Content-Type: application/json' \ + -d '{ + "uri": "/offl/v1/checkout", + "methods": ["POST"], + "plugins": { + "proxy-chain": { + "services": [ + { + "uri": "http://customer_service/api/v1/user", + "method": "POST" + } + ], + } + }, + "upstream_id": "550932803756229477" + }' + ``` + +2. **Verify**: + - Test the endpoint (assuming a LoadBalancer or Ingress): + ```bash + curl -X POST http:///v1/checkout + ``` + +--- + +## Attributes +| Name | Type | Required | Default | Description | +|----------------|--------|----------|---------|--------------------------------------------------| +| services | array | Yes | - | List of upstream services to chain. | +| services.uri | string | Yes | - | URI of the upstream service. | +| services.method| string | Yes | - | HTTP method (e.g., "GET", "POST"). | +| token_header | string | No | - | Custom header to pass a token between services. | diff --git a/t/plugin/proxy-chain.t b/t/plugin/proxy-chain.t new file mode 100644 index 000000000000..75c95a78cbce --- /dev/null +++ b/t/plugin/proxy-chain.t @@ -0,0 +1,159 @@ +use Test::Nginx::Socket::Lua; +use Cwd qw(cwd); + +# Repeat each test case 1 time (for consistency) +repeat_each(1); + +# Set timeout for tests +plan tests => repeat_each() * (3 * blocks()); + +# Run tests with APISIX Lua module +my $pwd = cwd(); +our $HttpConfig = qq{ + lua_package_path "$pwd/?.lua;;"; + lua_package_cpath "$pwd/?.so;;"; +}; + +# Enable APISIX test helpers +no_long_string(); +run_tests(); + +__DATA__ + +=== TEST 1: Sanity check - plugin schema validation +--- config + location /t { + content_by_lua_block { + local plugin = require("apisix.plugins.proxy-chain") + local ok, err = plugin.check_schema({ + services = { + { uri = "http://127.0.0.1:1980/test", method = "POST" } + }, + token_header = "Token" + }) + if not ok then + ngx.say("failed to check schema: ", err) + else + ngx.say("schema check passed") + end + } + } +--- request +GET /t +--- response_body +schema check passed +--- no_error_log +[error] + +=== TEST 2: Successful chaining - single service with token +--- config + location /t { + access_by_lua_block { + local plugin = require("apisix.plugins.proxy-chain") + local core = require("apisix.core") + local ctx = { var = { method = "POST" } } + ctx.var.request_body = '{"order_id": "12345"}' + ctx.var.Token = "my-auth-token" + + local conf = { + services = { + { uri = "http://127.0.0.1:1980/test", method = "POST" } + }, + token_header = "Token" + } + + local code, body = plugin.access(conf, ctx) + if code then + ngx.status = code + ngx.say(body.error) + else + ngx.say(ctx.var.request_body) + end + } + } + location /test { + content_by_lua_block { + ngx.say('{"user_id": "67890"}') + } + } +--- request +POST /t +{"order_id": "12345"} +--- response_body +{"order_id":"12345","user_id":"67890"} +--- no_error_log +[error] + +=== TEST 3: Multiple services chaining +--- config + location /t { + access_by_lua_block { + local plugin = require("apisix.plugins.proxy-chain") + local core = require("apisix.core") + local ctx = { var = { method = "POST" } } + ctx.var.request_body = '{"order_id": "12345"}' + ctx.var.Token = "my-auth-token" + + local conf = { + services = { + { uri = "http://127.0.0.1:1980/test1", method = "POST" }, + { uri = "http://127.0.0.1:1980/test2", method = "POST" } + }, + token_header = "Token" + } + + local code, body = plugin.access(conf, ctx) + if code then + ngx.status = code + ngx.say(body.error) + else + ngx.say(ctx.var.request_body) + end + } + } + location /test1 { + content_by_lua_block { + ngx.say('{"user_id": "67890"}') + } + } + location /test2 { + content_by_lua_block { + ngx.say('{"status": "valid"}') + } + } +--- request +POST /t +{"order_id": "12345"} +--- response_body +{"order_id":"12345","user_id":"67890","status":"valid"} +--- no_error_log +[error] + +=== TEST 4: Error handling - service failure +--- config + location /t { + access_by_lua_block { + local plugin = require("apisix.plugins.proxy-chain") + local core = require("apisix.core") + local ctx = { var = { method = "POST" } } + ctx.var.request_body = '{"order_id": "12345"}' + + local conf = { + services = { + { uri = "http://127.0.0.1:1999/nonexistent", method = "POST" } + } + } + + local code, body = plugin.access(conf, ctx) + ngx.status = code + ngx.say(body.error) + } + } +--- request +POST /t +{"order_id": "12345"} +--- response_body +Failed to call service: http://127.0.0.1:1999/nonexistent +--- error_code: 500 +--- error_log +Failed to call service http://127.0.0.1:1999/nonexistent From 1ca95b5108769574cb038d94e0b2700cfd1e8215 Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Sat, 3 May 2025 01:44:52 +0330 Subject: [PATCH 3/7] Documentation Structure --- apisix/plugins/proxy-chain.lua | 21 ++++++++++++++++----- docs/en/latest/proxy-chain.md | 24 ++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/apisix/plugins/proxy-chain.lua b/apisix/plugins/proxy-chain.lua index de89fa6f3bd0..1dd6777aa423 100644 --- a/apisix/plugins/proxy-chain.lua +++ b/apisix/plugins/proxy-chain.lua @@ -1,8 +1,19 @@ --- Proxy Chain Plugin for APISIX --- Author: Vahid Aghazadeh v.opsource@gmail.com --- Description: This plugin chains multiple upstream service requests, merging their responses into a single payload. --- It supports passing a custom token header (e.g., Authorization) between services for authentication purposes. --- License: Apache License 2.0 +-- +-- Licensed to the Apache Software Foundation (ASF) under one or more +-- contributor license agreements. See the NOTICE file distributed with +-- this work for additional information regarding copyright ownership. +-- The ASF licenses this file to You 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. +-- local core = require("apisix.core") -- Core APISIX utilities local http = require("resty.http") -- HTTP client for making service calls diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md index c3b54fc761f6..aaa031fd99a4 100644 --- a/docs/en/latest/proxy-chain.md +++ b/docs/en/latest/proxy-chain.md @@ -1,6 +1,26 @@ -# Proxy Chain Plugin for APISIX +--- +title: Proxy Chain Plugin for APISIX +--- -The `proxy-chain` plugin for APISIX allows you to chain multiple upstream service calls in a sequence, passing data between them as needed. This is particularly useful for workflows where a request needs to interact with multiple services before returning a final response to the client. + +[proxy-chain](https://github.com/apache/apisix) is a plugin for [APISIX](https://github.com/apache/apisix) that allows you to chain multiple upstream service calls in sequence, passing data between them as needed. This is particularly useful for workflows where a request must interact with several services before returning a final response to the client. ## Features - Chain multiple upstream service calls in a defined order. From 16caaf3ca96d2f86c344165a695167fba3d94ff1 Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Fri, 30 May 2025 01:34:14 +0330 Subject: [PATCH 4/7] fixed test file --- t/plugin/proxy-chain.t | 88 +++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 19 deletions(-) diff --git a/t/plugin/proxy-chain.t b/t/plugin/proxy-chain.t index 75c95a78cbce..77317b009650 100644 --- a/t/plugin/proxy-chain.t +++ b/t/plugin/proxy-chain.t @@ -1,22 +1,33 @@ -use Test::Nginx::Socket::Lua; -use Cwd qw(cwd); +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# -# Repeat each test case 1 time (for consistency) -repeat_each(1); +use t::APISIX 'no_plan'; -# Set timeout for tests -plan tests => repeat_each() * (3 * blocks()); +add_block_preprocessor(sub { + my ($block) = @_; + $block->set_value("no_error_log", "[error]"); + $block; +}); -# Run tests with APISIX Lua module -my $pwd = cwd(); -our $HttpConfig = qq{ - lua_package_path "$pwd/?.lua;;"; - lua_package_cpath "$pwd/?.so;;"; -}; - -# Enable APISIX test helpers no_long_string(); -run_tests(); +no_shuffle(); +no_root_location(); + +run_tests; __DATA__ @@ -25,9 +36,10 @@ __DATA__ location /t { content_by_lua_block { local plugin = require("apisix.plugins.proxy-chain") + local core = require("apisix.core") local ok, err = plugin.check_schema({ services = { - { uri = "http://127.0.0.1:1980/test", method = "POST" } + { uri = "http://127.0.0.1:${TEST_NGINX_SERVER_PORT}/test", method = "POST" } }, token_header = "Token" }) @@ -57,7 +69,7 @@ schema check passed local conf = { services = { - { uri = "http://127.0.0.1:1980/test", method = "POST" } + { uri = "http://127.0.0.1:${TEST_NGINX_SERVER_PORT}/test", method = "POST" } }, token_header = "Token" } @@ -96,8 +108,8 @@ POST /t local conf = { services = { - { uri = "http://127.0.0.1:1980/test1", method = "POST" }, - { uri = "http://127.0.0.1:1980/test2", method = "POST" } + { uri = "http://127.0.0.1:${TEST_NGINX_SERVER_PORT}/test1", method = "POST" }, + { uri = "http://127.0.0.1:${TEST_NGINX_SERVER_PORT}/test2", method = "POST" } }, token_header = "Token" } @@ -157,3 +169,41 @@ Failed to call service: http://127.0.0.1:1999/nonexistent --- error_code: 500 --- error_log Failed to call service http://127.0.0.1:1999/nonexistent + +=== TEST 5: Handle missing token +--- config + location /t { + access_by_lua_block { + local plugin = require("apisix.plugins.proxy-chain") + local core = require("apisix.core") + local ctx = { var = { method = "POST" } } + ctx.var.request_body = '{"order_id": "12345"}' + + local conf = { + services = { + { uri = "http://127.0.0.1:${TEST_NGINX_SERVER_PORT}/test", method = "POST" } + }, + token_header = "Token" + } + + local code, body = plugin.access(conf, ctx) + if code then + ngx.status = code + ngx.say(body.error) + else + ngx.say(ctx.var.request_body) + end + } + } + location /test { + content_by_lua_block { + ngx.say('{"user_id": "67890"}') + } + } +--- request +POST /t +{"order_id": "12345"} +--- response_body +{"order_id":"12345","user_id":"67890"} +--- no_error_log +[error] From e10a54010a1bbe0b448683874b2828193c667f0a Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Thu, 5 Jun 2025 21:45:36 +0330 Subject: [PATCH 5/7] docs: fix formatting issues in proxy-chain.md to comply with markdownlint --- docs/en/latest/proxy-chain.md | 54 +++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md index aaa031fd99a4..33894339e906 100644 --- a/docs/en/latest/proxy-chain.md +++ b/docs/en/latest/proxy-chain.md @@ -23,6 +23,7 @@ title: Proxy Chain Plugin for APISIX [proxy-chain](https://github.com/apache/apisix) is a plugin for [APISIX](https://github.com/apache/apisix) that allows you to chain multiple upstream service calls in sequence, passing data between them as needed. This is particularly useful for workflows where a request must interact with several services before returning a final response to the client. ## Features + - Chain multiple upstream service calls in a defined order. - Pass custom headers (e.g., authentication tokens) between services. - Flexible configuration for service endpoints and HTTP methods. @@ -34,16 +35,20 @@ title: Proxy Chain Plugin for APISIX ### Docker #### Prerequisites + - Docker installed on your system. - APISIX version 3.0 or higher. #### Steps + 1. **Prepare the Plugin File**: - Place the `proxy-chain.lua` file in a local directory, e.g., `./plugins/`. 2. **Create a Dockerfile**: - Create a `Dockerfile` in your project directory: + ```Dockerfile + FROM apache/apisix:3.11.0-debian USER root COPY ./plugins/proxy-chain.lua /usr/local/apisix/apisix/plugins/proxy-chain.lua @@ -53,12 +58,18 @@ title: Proxy Chain Plugin for APISIX 3. **Build and Run**: - Build the Docker image and run it using `docker-compose` or directly: + ```bash + docker build -t apisix-with-proxy-chain . docker run -d -p 9080:9080 -p 9180:9180 apisix-with-proxy-chain + ``` + - Alternatively, use a `docker-compose.yml`: + ```yaml + version: "3" services: apisix: @@ -69,31 +80,42 @@ title: Proxy Chain Plugin for APISIX ports: - "9080:9080" - "9180:9180" + ``` + ```bash + docker-compose up -d --build + ``` 4. **Reload APISIX**: - Ensure the plugin is loaded: + ```bash + docker exec apisix reload + ``` ### Kubernetes #### Prerequisites + - A Kubernetes cluster (e.g., Minikube, GKE, EKS). - `kubectl` configured to interact with your cluster. - Helm (optional, for easier deployment). #### Steps + 1. **Prepare the Plugin File**: - Place `proxy-chain.lua` in a local directory, e.g., `./plugins/`. 2. **Create a ConfigMap**: - Define a ConfigMap to include the plugin file: + ```yaml + apiVersion: v1 kind: ConfigMap metadata: @@ -102,11 +124,14 @@ title: Proxy Chain Plugin for APISIX proxy-chain.lua: | -- Content of proxy-chain.lua goes here -- (Paste the entire Lua code here) + ``` 3. **Deploy APISIX with Custom Plugin**: - Use a Helm chart or a custom manifest. Here’s an example with a manifest: + ```yaml + apiVersion: apps/v1 kind: Deployment metadata: @@ -151,17 +176,25 @@ title: Proxy Chain Plugin for APISIX selector: app: apisix type: LoadBalancer + ``` + - Apply the manifests: + ```bash + kubectl apply -f configmap.yaml kubectl apply -f apisix-deployment.yaml + ``` 4. **Reload APISIX**: - Access the APISIX Admin API to reload: + ```bash + kubectl exec -it -- apisix reload + ``` --- @@ -171,9 +204,12 @@ title: Proxy Chain Plugin for APISIX ### Docker #### Configuration Steps + 1. **Add to Route**: - Use the APISIX Admin API to configure a route: + ```bash + curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/24 \ -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \ -H 'Content-Type: application/json' \ @@ -192,20 +228,27 @@ title: Proxy Chain Plugin for APISIX }, "upstream_id": "550932803756229477" }' + ``` 2. **Verify**: - Test the endpoint: + ```bash + curl -X POST http:///v1/checkout + ``` ### Kubernetes #### Configuration Steps + 1. **Add to Route**: - Assuming APISIX Ingress Controller is installed, use a custom resource (CRD) or Admin API: + ```yaml + apiVersion: apisix.apache.org/v2 kind: ApisixRoute metadata: @@ -228,13 +271,19 @@ title: Proxy Chain Plugin for APISIX services: - uri: "http://customer_service/api/v1/user" method: "POST" + ``` - Apply the CRD: + ```bash + kubectl apply -f route.yaml + ``` - Alternatively, use the Admin API via port-forwarding: + ```bash + kubectl port-forward service/apisix-service 9180:9180 curl -X PUT http://127.0.0.1:9180/apisix/admin/routes/24 \ -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" \ @@ -254,17 +303,22 @@ title: Proxy Chain Plugin for APISIX }, "upstream_id": "550932803756229477" }' + ``` 2. **Verify**: - Test the endpoint (assuming a LoadBalancer or Ingress): + ```bash + curl -X POST http:///v1/checkout + ``` --- ## Attributes + | Name | Type | Required | Default | Description | |----------------|--------|----------|---------|--------------------------------------------------| | services | array | Yes | - | List of upstream services to chain. | From b15601e5360abdf91243822965f1f0a01ec90723 Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Sat, 7 Jun 2025 02:18:19 +0330 Subject: [PATCH 6/7] Fix markdown blanks-around-fences --- docs/en/latest/proxy-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md index 33894339e906..37348bfb03f1 100644 --- a/docs/en/latest/proxy-chain.md +++ b/docs/en/latest/proxy-chain.md @@ -272,7 +272,7 @@ title: Proxy Chain Plugin for APISIX - uri: "http://customer_service/api/v1/user" method: "POST" - ``` + ``` - Apply the CRD: ```bash From 2a5540905fc5d82965a2be9dd122698500b7215f Mon Sep 17 00:00:00 2001 From: vahid aghazade Date: Mon, 9 Jun 2025 11:14:37 +0330 Subject: [PATCH 7/7] Fix markdown blanks-around-fences --- docs/en/latest/proxy-chain.md | 85 ++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/docs/en/latest/proxy-chain.md b/docs/en/latest/proxy-chain.md index 37348bfb03f1..3e279494e7b5 100644 --- a/docs/en/latest/proxy-chain.md +++ b/docs/en/latest/proxy-chain.md @@ -54,6 +54,7 @@ title: Proxy Chain Plugin for APISIX COPY ./plugins/proxy-chain.lua /usr/local/apisix/apisix/plugins/proxy-chain.lua RUN chown -R apisix:apisix /usr/local/apisix/apisix/plugins/proxy-chain.lua CMD ["apisix", "start"] + ``` 3. **Build and Run**: @@ -132,50 +133,50 @@ title: Proxy Chain Plugin for APISIX ```yaml - apiVersion: apps/v1 - kind: Deployment - metadata: - name: apisix - spec: - replicas: 1 - selector: - matchLabels: - app: apisix - template: + apiVersion: apps/v1 + kind: Deployment metadata: - labels: - app: apisix + name: apisix spec: - containers: - - name: apisix - image: apache/apisix:3.11.0-debian - ports: - - containerPort: 9080 - - containerPort: 9180 - volumeMounts: - - name: plugins-volume - mountPath: /usr/local/apisix/apisix/plugins/proxy-chain.lua - subPath: proxy-chain.lua - volumes: - - name: plugins-volume - configMap: - name: apisix-plugins + replicas: 1 + selector: + matchLabels: + app: apisix + template: + metadata: + labels: + app: apisix + spec: + containers: + - name: apisix + image: apache/apisix:3.11.0-debian + ports: + - containerPort: 9080 + - containerPort: 9180 + volumeMounts: + - name: plugins-volume + mountPath: /usr/local/apisix/apisix/plugins/proxy-chain.lua + subPath: proxy-chain.lua + volumes: + - name: plugins-volume + configMap: + name: apisix-plugins --- - apiVersion: v1 - kind: Service - metadata: - name: apisix-service - spec: - ports: - - port: 9080 - targetPort: 9080 - name: gateway - - port: 9180 - targetPort: 9180 - name: admin - selector: - app: apisix - type: LoadBalancer + apiVersion: v1 + kind: Service + metadata: + name: apisix-service + spec: + ports: + - port: 9080 + targetPort: 9080 + name: gateway + - port: 9180 + targetPort: 9180 + name: admin + selector: + app: apisix + type: LoadBalancer ``` @@ -273,6 +274,7 @@ title: Proxy Chain Plugin for APISIX method: "POST" ``` + - Apply the CRD: ```bash @@ -280,6 +282,7 @@ title: Proxy Chain Plugin for APISIX kubectl apply -f route.yaml ``` + - Alternatively, use the Admin API via port-forwarding: ```bash