From 924a30db36268ee1d7fab255ddebfc23a7110314 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E6=B3=BD=E8=BD=A9?= <spacewanderlzx@gmail.com> Date: Fri, 18 Dec 2020 15:05:09 +0800 Subject: [PATCH] feat: add control API (#3048) Signed-off-by: spacewander <spacewanderlzx@gmail.com> Co-authored-by: John Bampton <jbampton@users.noreply.github.com --- .travis/apisix_cli_test/common.sh | 31 +++++ .../test_ci_only.sh} | 14 +-- .travis/apisix_cli_test/test_control.sh | 118 +++++++++++++++++ .../test_main.sh} | 14 +-- .../linux_apisix_current_luarocks_runner.sh | 5 +- apisix/cli/ngx_tpl.lua | 14 +++ apisix/cli/ops.lua | 19 +++ apisix/control/router.lua | 119 ++++++++++++++++++ apisix/control/v1.lua | 33 +++++ apisix/init.lua | 12 ++ apisix/plugins/example-plugin.lua | 25 ++++ conf/config-default.yaml | 4 + doc/README.md | 1 + doc/control-api.md | 51 ++++++++ t/APISIX.pm | 6 + t/control/v1.t | 55 ++++++++ 16 files changed, 497 insertions(+), 24 deletions(-) create mode 100755 .travis/apisix_cli_test/common.sh rename .travis/{apisix_cli_test_in_ci.sh => apisix_cli_test/test_ci_only.sh} (85%) create mode 100755 .travis/apisix_cli_test/test_control.sh rename .travis/{apisix_cli_test.sh => apisix_cli_test/test_main.sh} (99%) create mode 100644 apisix/control/router.lua create mode 100644 apisix/control/v1.lua create mode 100644 doc/control-api.md create mode 100644 t/control/v1.t diff --git a/.travis/apisix_cli_test/common.sh b/.travis/apisix_cli_test/common.sh new file mode 100755 index 000000000000..23190ba61731 --- /dev/null +++ b/.travis/apisix_cli_test/common.sh @@ -0,0 +1,31 @@ +# +# 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. +# + +# 'make init' operates scripts and related configuration files in the current directory +# The 'apisix' command is a command in the /usr/local/apisix, +# and the configuration file for the operation is in the /usr/local/apisix/conf + +set -ex + +clean_up() { + make stop || true + git checkout conf/config.yaml +} + +trap clean_up EXIT + +unset APISIX_PROFILE diff --git a/.travis/apisix_cli_test_in_ci.sh b/.travis/apisix_cli_test/test_ci_only.sh similarity index 85% rename from .travis/apisix_cli_test_in_ci.sh rename to .travis/apisix_cli_test/test_ci_only.sh index 87410fea0166..6b064a040901 100755 --- a/.travis/apisix_cli_test_in_ci.sh +++ b/.travis/apisix_cli_test/test_ci_only.sh @@ -17,18 +17,10 @@ # limitations under the License. # -# This file is like apisix_cli_test.sh, but requires extra dependencies which -# you don't need them in daily development. +# This file is like other test_*.sh, but requires extra dependencies which +# you don't need in daily development. -set -ex - -clean_up() { - git checkout conf/config.yaml -} - -trap clean_up EXIT - -unset APISIX_PROFILE +. ./.travis/apisix_cli_test/common.sh # check error handling when connecting to old etcd git checkout conf/config.yaml diff --git a/.travis/apisix_cli_test/test_control.sh b/.travis/apisix_cli_test/test_control.sh new file mode 100755 index 000000000000..816dd695174f --- /dev/null +++ b/.travis/apisix_cli_test/test_control.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash + +# +# 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. +# + +. ./.travis/apisix_cli_test/common.sh + +# control server +echo ' +apisix: + enable_control: true +' > conf/config.yaml + +make init + +if ! grep "listen 127.0.0.1:9090;" conf/nginx.conf > /dev/null; then + echo "failed: find default address for control server" + exit 1 +fi + +make run + +sleep 0.1 +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9090/v1/schema) + +if [ ! $code -eq 200 ]; then + echo "failed: access control server" + exit 1 +fi + +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9090/v0/schema) + +if [ ! $code -eq 404 ]; then + echo "failed: handle route not found" + exit 1 +fi + +make stop + +echo ' +apisix: + enable_control: true + control: + ip: 127.0.0.2 +' > conf/config.yaml + +make init + +if ! grep "listen 127.0.0.2:9090;" conf/nginx.conf > /dev/null; then + echo "failed: customize address for control server" + exit 1 +fi + +make run + +sleep 0.1 +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.2:9090/v1/schema) + +if [ ! $code -eq 200 ]; then + echo "failed: access control server" + exit 1 +fi + +make stop + +echo ' +apisix: + enable_control: true + control: + port: 9091 +' > conf/config.yaml + +make init + +if ! grep "listen 127.0.0.1:9091;" conf/nginx.conf > /dev/null; then + echo "failed: customize address for control server" + exit 1 +fi + +make run + +sleep 0.1 +code=$(curl -v -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9091/v1/schema) + +if [ ! $code -eq 200 ]; then + echo "failed: access control server" + exit 1 +fi + +make stop + +echo ' +apisix: + enable_control: false +' > conf/config.yaml + +make init + +if grep "listen 127.0.0.1:9090;" conf/nginx.conf > /dev/null; then + echo "failed: disable control server" + exit 1 +fi + +echo "pass: access control server" diff --git a/.travis/apisix_cli_test.sh b/.travis/apisix_cli_test/test_main.sh similarity index 99% rename from .travis/apisix_cli_test.sh rename to .travis/apisix_cli_test/test_main.sh index 5b43330bf8b1..a5ba0abe7043 100755 --- a/.travis/apisix_cli_test.sh +++ b/.travis/apisix_cli_test/test_main.sh @@ -21,15 +21,7 @@ # The 'apisix' command is a command in the /usr/local/apisix, # and the configuration file for the operation is in the /usr/local/apisix/conf -set -ex - -clean_up() { - git checkout conf/config.yaml -} - -trap clean_up EXIT - -unset APISIX_PROFILE +. ./.travis/apisix_cli_test/common.sh git checkout conf/config.yaml @@ -546,7 +538,7 @@ if [ $count_test_access_log -eq 0 ]; then fi count_access_log_off=`grep -c "access_log off;" conf/nginx.conf || true` -if [ $count_access_log_off -eq 2 ]; then +if [ $count_access_log_off -eq 3 ]; then echo "failed: nginx.conf file find access_log off; when enable access log" exit 1 fi @@ -581,7 +573,7 @@ if [ $count_test_access_log -eq 1 ]; then fi count_access_log_off=`grep -c "access_log off;" conf/nginx.conf || true` -if [ $count_access_log_off -ne 2 ]; then +if [ $count_access_log_off -ne 3 ]; then echo "failed: nginx.conf file doesn't find access_log off; when disable access log" exit 1 fi diff --git a/.travis/linux_apisix_current_luarocks_runner.sh b/.travis/linux_apisix_current_luarocks_runner.sh index c3c64faef7f3..70d2866ba362 100755 --- a/.travis/linux_apisix_current_luarocks_runner.sh +++ b/.travis/linux_apisix_current_luarocks_runner.sh @@ -54,8 +54,9 @@ script() { cd .. # apisix cli test - sudo PATH=$PATH .travis/apisix_cli_test.sh - sudo PATH=$PATH .travis/apisix_cli_test_in_ci.sh + for f in ./.travis/apisix_cli_test/test_*.sh; do + sudo PATH="$PATH" "$f" + done } case_opt=$1 diff --git a/apisix/cli/ngx_tpl.lua b/apisix/cli/ngx_tpl.lua index ed693d053329..3dabcf0e30e4 100644 --- a/apisix/cli/ngx_tpl.lua +++ b/apisix/cli/ngx_tpl.lua @@ -255,6 +255,20 @@ http { apisix.http_init_worker() } + {% if enable_control then %} + server { + listen {* control_server_addr *}; + + access_log off; + + location / { + content_by_lua_block { + apisix.http_control() + } + } + } + {% end %} + {% if enable_admin and port_admin then %} server { {%if https_admin then%} diff --git a/apisix/cli/ops.lua b/apisix/cli/ops.lua index fe663b5efaad..76d8442236fa 100644 --- a/apisix/cli/ops.lua +++ b/apisix/cli/ops.lua @@ -265,6 +265,25 @@ Please modify "admin_key" in conf/config.yaml . sys_conf[k] = v end + if yaml_conf.apisix.enable_control then + if not yaml_conf.apisix.control then + sys_conf.control_server_addr = "127.0.0.1:9090" + else + local ip = yaml_conf.apisix.control.ip + local port = tonumber(yaml_conf.apisix.control.port) + + if ip == nil then + ip = "127.0.0.1" + end + + if not port then + port = 9090 + end + + sys_conf.control_server_addr = ip .. ":" .. port + end + end + local wrn = sys_conf["worker_rlimit_nofile"] local wc = sys_conf["event"]["worker_connections"] if not wrn or wrn <= wc then diff --git a/apisix/control/router.lua b/apisix/control/router.lua new file mode 100644 index 000000000000..221d2234bdac --- /dev/null +++ b/apisix/control/router.lua @@ -0,0 +1,119 @@ +-- +-- 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 require = require +local router = require("resty.radixtree") +local builtin_v1_routes = require("apisix.control.v1") +local plugin_mod = require("apisix.plugin") +local core = require("apisix.core") +local str_sub = string.sub +local ipairs = ipairs +local type = type +local ngx = ngx +local get_method = ngx.req.get_method + + +local _M = {} +local current_version = 1 + + +local fetch_control_api_router +do + local function register_api_routes(routes, api_routes) + for _, route in ipairs(api_routes) do + core.table.insert(routes, { + methods = route.methods, + -- note that it is 'uris' for control API, which is an array of strings + paths = route.uris, + handler = function (api_ctx) + local code, body = route.handler(api_ctx) + if code or body then + if type(body) == "table" and ngx.header["Content-Type"] == nil then + core.response.set_header("Content-Type", "application/json") + end + + core.response.exit(code, body) + end + end + }) + end + end + + local routes = {} + local v1_routes = {} + local function empty_func() end + +function fetch_control_api_router() + core.table.clear(v1_routes) + + register_api_routes(v1_routes, builtin_v1_routes) + + for _, plugin in ipairs(plugin_mod.plugins) do + local api_fun = plugin.control_api + if api_fun then + local api_routes = api_fun(current_version) + register_api_routes(v1_routes, api_routes) + end + end + + local v1_router, err = router.new(v1_routes) + if not v1_router then + return nil, err + end + + core.table.clear(routes) + core.table.insert(routes, { + paths = {"/v1/*"}, + filter_fun = function(vars, opts, ...) + local uri = str_sub(vars.uri, #"/v1" + 1) + return v1_router:dispatch(uri, opts, ...) + end, + handler = empty_func, + }) + + return router.new(routes) +end + +end -- do + + +do + local match_opts = {} + local cached_version + local router + +function _M.match(uri) + if cached_version ~= plugin_mod.load_times then + local err + router, err = fetch_control_api_router() + if router == nil then + core.log.error("failed to fetch valid api router: ", err) + return false + end + + cached_version = plugin_mod.load_times + end + + core.table.clear(match_opts) + match_opts.method = get_method() + + return router:dispatch(uri, match_opts) +end + +end -- do + + +return _M diff --git a/apisix/control/v1.lua b/apisix/control/v1.lua new file mode 100644 index 000000000000..7ad795e47bd0 --- /dev/null +++ b/apisix/control/v1.lua @@ -0,0 +1,33 @@ +-- +-- 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 _M = {} + + +function _M.schema() + -- stub for test yet, fill it in the next PR + return 200, {} +end + + +return { + -- /v1/schema + { + methods = {"GET"}, + uris = {"/schema"}, + handler = _M.schema, + } +} diff --git a/apisix/init.lua b/apisix/init.lua index 1b9c62c85132..01dc7729d3c4 100644 --- a/apisix/init.lua +++ b/apisix/init.lua @@ -37,6 +37,10 @@ local ngx_now = ngx.now local str_byte = string.byte local str_sub = string.sub local tonumber = tonumber +local control_api_router +if ngx.config.subsystem == "http" then + control_api_router = require("apisix.control.router") +end local load_balancer local local_conf local dns_resolver @@ -810,6 +814,14 @@ end end -- do +function _M.http_control() + local ok = control_api_router.match(get_var("uri")) + if not ok then + ngx_exit(404) + end +end + + function _M.stream_init() core.log.info("enter stream_init") end diff --git a/apisix/plugins/example-plugin.lua b/apisix/plugins/example-plugin.lua index 0a58efd03823..c94a7a4e8b23 100644 --- a/apisix/plugins/example-plugin.lua +++ b/apisix/plugins/example-plugin.lua @@ -14,6 +14,7 @@ -- See the License for the specific language governing permissions and -- limitations under the License. -- +local ngx = ngx local core = require("apisix.core") local plugin = require("apisix.plugin") local upstream = require("apisix.upstream") @@ -109,4 +110,28 @@ function _M.access(conf, ctx) end +local function hello() + local args = ngx.req.get_uri_args() + if args["json"] then + return 200, {msg = "world"} + else + return 200, "world\n" + end +end + + +function _M.control_api(ver) + if ver == 1 then + return { + -- /v1/plugin/example-plugin/hello + { + methods = {"GET"}, + uris = {"/plugin/example-plugin/hello"}, + handler = hello, + } + } + end +end + + return _M diff --git a/conf/config-default.yaml b/conf/config-default.yaml index 6f3f2e1b0778..dd8a931ed249 100644 --- a/conf/config-default.yaml +++ b/conf/config-default.yaml @@ -115,6 +115,10 @@ apisix: key_encrypt_salt: "edd1c9f0985e76a2" # If not set, will save origin ssl key into etcd. # If set this, must be a string of length 16. And it will encrypt ssl key with AES-128-CBC # !!! So do not change it after saving your ssl, it can't decrypt the ssl keys have be saved if you change !! + enable_control: true + # control: + # ip: "127.0.0.1" + # port: 9090 nginx_config: # config for render the template to generate nginx.conf error_log: "logs/error.log" diff --git a/doc/README.md b/doc/README.md index 9961f6a97cc8..24fa72404224 100644 --- a/doc/README.md +++ b/doc/README.md @@ -26,6 +26,7 @@ * [Getting Started Guide](getting-started.md) * [How to build Apache APISIX](how-to-build.md) * [Admin API](admin-api.md) +* [Control API](control-api.md) * [Health Check](health-check.md): Enable health check on the upstream node, and will automatically filter unhealthy nodes during load balancing to ensure system stability. * [Router radixtree](router-radixtree.md) * [Stand Alone Model](stand-alone.md): Supports to load route rules from local yaml file, it is more friendly such as under the kubernetes(k8s). diff --git a/doc/control-api.md b/doc/control-api.md new file mode 100644 index 000000000000..c43ec7afea11 --- /dev/null +++ b/doc/control-api.md @@ -0,0 +1,51 @@ +<!-- +# +# 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. +# +--> + +The control API can be used to +* expose APISIX internal state +* control the behavior of a single isolate APISIX data panel + +By default, the control API server is enabled and listens to `127.0.0.1:9090`. You can change it via +the `control` section under `apisix` in `conf/config.yaml`: + +```yaml +apisix: + ... + enable_control: true + control: + ip: "127.0.0.1" + port: 9090 +``` + +Note that the control API server should not be configured to listen to the public traffic! + +## Control API Added via plugin + +Plugin can add its control API when it is enabled. +If a plugin adds such a control API, please refer to each plugin's documentation for those APIs. + +## Plugin independent Control API + +Here is the supported API: + +### GET /v1/schema + +Introduced since `v2.2`. + +Return the jsonschema used by this APISIX instance. diff --git a/t/APISIX.pm b/t/APISIX.pm index 78f2ddd4d87e..2634eea45b2a 100644 --- a/t/APISIX.pm +++ b/t/APISIX.pm @@ -375,6 +375,12 @@ _EOC_ } } + location /v1/ { + content_by_lua_block { + apisix.http_control() + } + } + location / { set \$upstream_mirror_host ''; set \$upstream_upgrade ''; diff --git a/t/control/v1.t b/t/control/v1.t new file mode 100644 index 000000000000..d1e4a1735cc3 --- /dev/null +++ b/t/control/v1.t @@ -0,0 +1,55 @@ +# +# 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. +# +use t::APISIX 'no_plan'; + +repeat_each(1); +no_long_string(); +no_root_location(); +no_shuffle(); +log_level("info"); + +add_block_preprocessor(sub { + my ($block) = @_; + + if (!$block->request) { + $block->set_value("request", "GET /t"); + } + + if (!$block->no_error_log) { + $block->set_value("no_error_log", "[error]\n[alert]"); + } +}); + +run_tests; + +__DATA__ + +=== TEST 1: sanity +--- request +GET /v1/plugin/example-plugin/hello +--- response_body +world + + + +=== TEST 2: set Content-Type for table response +--- request +GET /v1/plugin/example-plugin/hello?json +--- response_body +{"msg":"world"} +--- response_headers +Content-Type: application/json