簡介
在進(jìn)一步討論之前诈茧,這里先有必要簡要闡述一下 Kong 是如何構(gòu)建的,特別是它如何與Nginx集成箕别,以及它與Lua腳本之間的關(guān)系
使用 lua-nginx-module 模塊可以在 Nginx 中啟用 Lua 腳本功能测萎,Kong 與 OpenResty 一起發(fā)布,OpenResty 中已經(jīng)包含了 lua-nginx-module 模塊疚顷,OpenResty 不是 Nginx 的分支,而是一組擴(kuò)展 Nginx 功能的模塊
因此禁偎,Kong 是一個 Lua 應(yīng)用程序腿堤,旨在加載和執(zhí)行 Lua 模塊(我們通常稱之為“插件”),并且Kong還為此提供了整套開發(fā)環(huán)境届垫,包括 SDK释液,數(shù)據(jù)庫抽象、數(shù)據(jù)遷移等等
插件由 Lua 模塊組成装处,用戶可以使用插件開發(fā)包(又稱PDK),通過調(diào)用請求響應(yīng)或者流交互實現(xiàn)各種功能 浸船,PDK 是一組 Lua 方法妄迁,插件可以使用它來促進(jìn) Kong 核心模塊(或其他組件)與插件本身的交互
這篇向?qū)⒃敿?xì)描述插件的結(jié)構(gòu),它們的擴(kuò)展點李命,以及如何發(fā)布和安裝它們登淘,有關(guān)PDK的詳情,請查閱插件開發(fā)工具包向?qū)?/p>
文件結(jié)構(gòu)
簡介
插件其實是一組 Lua 模塊封字,本章中描述的每個文件都可以視為一個單獨的模塊黔州,如果它們的命名遵循某個約定,Kong 就會檢測并加載插件模塊:
kong.plugins.<plugin_name>.<module_name>
用戶定義的插件模塊需要通過 package.path 變量訪問到阔籽,用戶可以更改 lua_package_path 配置調(diào)整這個值流妻,然而,安裝插件的首選方法是通過 LuaRocks笆制,它與 Kong 天然集成绅这,有關(guān) LuaRocks 安裝插件的詳情,請參考后面的章節(jié)
為了讓 Kong 意識到哪些插件需要安裝在辆,用戶必須將它們添加到配置文件中的 plugins 屬性中证薇,格式是以逗號分隔的列表,例如:
plugins = bundled,my-custom-plugin # your plugin name here
或者匆篓,用戶不想加載任何預(yù)捆綁的插件:
plugins = my-custom-plugin # your plugin name here
現(xiàn)在浑度,Kong會試圖從下列命名空間中加載Lua模塊
kong.plugins.my-custom-plugin.<module_name>
其中一些模塊是必需的(例如 handler.lua),有些是可選的鸦概,以允許插件實現(xiàn)一些額外的功能(例如 api.lua 可以擴(kuò)展 Admin API 端點)
現(xiàn)在我們將詳細(xì)描述用戶可以實現(xiàn)的模塊以及它們的用途
基礎(chǔ)插件模塊
最基礎(chǔ)的插件箩张,必須包含兩個模塊:
simple-plugin
├── handler.lua
└── schema.lua
- handler.lua:插件的核心,它是一個需要實現(xiàn)的接口,其中每個方法會在請求/連接的生命周期中運行
- schema.lua:插件可能需要保留一些用戶輸入的配置伏钠,此模塊定義一些規(guī)則保存配置的模式横漏,以便用戶只能輸入有效的配置項
高級插件模塊
有些插件與 Kong 之間有更深入地集成,比如在數(shù)據(jù)庫中存數(shù)據(jù)熟掂,在 Admin API 中公開端點等等缎浇,每個插件都可以通過向插件添加新模塊來完成,插件的結(jié)構(gòu)大致如下:
complete-plugin
├── api.lua
├── daos.lua
├── handler.lua
├── migrations
│ ├── init.lua
│ └── 000_base_complete_plugin.lua
└── schema.lua
以下是完整的模塊列表赴肚,以及其簡要說明:
模塊名 | 是否必須 | 描述 |
---|---|---|
api.lua | 否 | 定義 Admin API 中也用的端點列表素跺,與插件自定義的實體進(jìn)行交互 |
daos.lua | 否 | 定義數(shù)據(jù)庫訪問對象列表 |
handler.lua | 是 | 一個需要實現(xiàn)的接口,其中每個方法會在請求/連接的生命周期中運行 |
migrations/*.lua | 否 | 數(shù)據(jù)源遷移誉券,只有當(dāng)用戶的插件有自定義實體時才需要 |
schema.lua | 是 | 保存插件的配置項指厌,以便用戶只能輸入有效的配置值 |
Key-Auth 插件實現(xiàn)了整套完整的插件接口,可以查看源碼了解細(xì)節(jié)
實現(xiàn)自定義邏輯
簡介
Kong 的插件允許用戶在整個生命周期的幾個切點加入自定義邏輯踊跟,為此踩验,必須實現(xiàn) base_plugin.lua
接口中的一些方法,這些方法在 kong.plugins.<plugin_name>.handler
模塊中實現(xiàn)
模塊
kong.plugins.<plugin_name>.handler
可用的上下文
插件接口允許用戶覆蓋 handler.lua 文件中的以下任何方法商玫,在Kong的執(zhí)行生命周期的各個切點實現(xiàn)自定義邏輯:
- HTTP Module:為 HTTP / HTTPS 請求編寫的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() |
init_worker | 每個 Nginx worker 進(jìn)程啟動時執(zhí)行 |
:certificate() |
ssl_certificate | 在 SSL 握手提供證書時執(zhí)行 |
:rewrite() |
rewrite | 從客戶端接收到請求箕憾,進(jìn)入 rewrite 段執(zhí)行,注意拳昌,在這個階段沒有識別服務(wù)袭异,也沒有消費者介入,只有配置成全局插件才會執(zhí)行此處理程序 |
:access() |
access | 從客戶端接收到請求到被代理到 upstream service 之前執(zhí)行 |
:header_filter() |
header_filter | 從 upstream service 接收到所有響應(yīng)頭時執(zhí)行 |
:body_filter() |
body_filter | 針對從 upstream service 接收到的響應(yīng)體塊執(zhí)行炬藤,由于響應(yīng)以流的形式返回給客戶端御铃,超過緩沖區(qū)大小的按塊進(jìn)行傳輸,因此沈矿,如果響應(yīng)體很大上真,會多次調(diào)用這個方法 |
:log() |
log | 最后一個響應(yīng)字節(jié)發(fā)送到客戶端時執(zhí)行 |
- Stream Module:為 TCP 流連接編寫的插件
方法名 | 段信息 | 描述 |
---|---|---|
:init_worker() |
init_worker | 每個 Nginx worker 進(jìn)程啟動時執(zhí)行 |
:preread() |
preread | 每個連接執(zhí)行一次 |
:log() |
log | 每個連接中斷執(zhí)行一次 |
除了 :init_worker()
方法,每個方法都會攜帶一個參數(shù)细睡,這個參數(shù)由Kong給出谷羞,即插件的配置,這個參數(shù)的類型是 Lua table溜徙,包含了用戶定義的值湃缎,格式根據(jù)用戶定義的插件 schema 格式
handler.lua 格式
handler.lua
文件需要返回一個 table,里面包含了用戶希望執(zhí)行的方法蠢壹,為了方便起見嗓违,這里有一個示例模塊,實現(xiàn)了模塊中所有的可用方法:
-- Extending the Base Plugin handler is optional, as there is no real
-- concept of interface in Lua, but the Base Plugin handler's methods
-- can be called from your child implementation and will print logs
-- in your `error.log` file (where all logs are printed).
local BasePlugin = require "kong.plugins.base_plugin"
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 10
-- Your plugin handler's constructor. If you are extending the
-- Base Plugin handler, it's only role is to instantiate itself
-- with a name. The name is your plugin name as it will be printed in the logs.
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:init_worker()
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.init_worker(self)
-- Implement any custom logic here
end
function CustomHandler:preread(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.preread(self)
-- Implement any custom logic here
end
function CustomHandler:certificate(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.certificate(self)
-- Implement any custom logic here
end
function CustomHandler:rewrite(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.rewrite(self)
-- Implement any custom logic here
end
function CustomHandler:access(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.access(self)
-- Implement any custom logic here
end
function CustomHandler:header_filter(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.header_filter(self)
-- Implement any custom logic here
end
function CustomHandler:body_filter(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.body_filter(self)
-- Implement any custom logic here
end
function CustomHandler:log(config)
-- Eventually, execute the parent implementation
-- (will log that your plugin is entering this context)
CustomHandler.super.log(self)
-- Implement any custom logic here
end
-- This module needs to return the created table, so that Kong
-- can execute those functions.
return CustomHandler
插件本身的邏輯可以寫在另一個模塊图贸,然后在處理程序模塊中調(diào)用:
local BasePlugin = require "kong.plugins.base_plugin"
-- The actual logic is implemented in those modules
local access = require "kong.plugins.my-custom-plugin.access"
local body_filter = require "kong.plugins.my-custom-plugin.body_filter"
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 10
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:access(config)
CustomHandler.super.access(self)
-- Execute any function from the module loaded in `access`,
-- for example, `execute()` and passing it the plugin's configuration.
access.execute(config)
end
function CustomHandler:body_filter(config)
CustomHandler.super.body_filter(self)
-- Execute any function from the module loaded in `body_filter`,
-- for example, `execute()` and passing it the plugin's configuration.
body_filter.execute(config)
end
return CustomHandler
插件開發(fā)工具包
在插件開發(fā)過程中蹂季,需要與請求/響應(yīng)對象或其他核心組件交互冕广,Kong 為此提供了一個插件開發(fā)工具包,插件可以使用里面的函數(shù)和變量來執(zhí)行各種網(wǎng)關(guān)操作偿洁,并且插件開發(fā)工具包是向前兼容的
如果用戶嘗試實現(xiàn)一些與 Kong 交互的邏輯時(例如檢索請求頭撒汉、生成響應(yīng)、記錄錯誤或調(diào)試信息)涕滋,可以參考插件開發(fā)工具包
插件執(zhí)行順序
某些插件可能依賴其他插件來執(zhí)行某些操作睬辐,例如,依賴消費者身份的插件必須在身份驗證插件之后運行宾肺,考慮到這一點溯饵,Kong在插件執(zhí)行期間定義了優(yōu)先級,以確保插件執(zhí)行順序锨用,用戶可以定義這個屬性值來配置插件優(yōu)先級:
CustomHandler.PRIORITY = 10
優(yōu)先級越高丰刊,插件執(zhí)行的越早(例如 :access()
、:log()
方法)增拥,預(yù)綁定插件的優(yōu)先級如下:
插件 | 優(yōu)先級 |
---|---|
pre-function | +inf |
zipkin | 100000 |
ip-restriction | 3000 |
bot-detection | 2500 |
cors | 2000 |
session | 1900 |
kubernetes-sidecar-injector | 1006 |
jwt | 1005 |
oauth2 | 1004 |
key-auth | 1003 |
ldap-auth | 1002 |
basic-auth | 1001 |
hmac-auth | 1000 |
request-size-limiting | 951 |
acl | 950 |
rate-limiting | 901 |
response-ratelimiting | 900 |
request-transformer | 801 |
response-transformer | 800 |
aws-lambda | 750 |
azure-functions | 749 |
prometheus | 13 |
http-log | 12 |
statsd | 11 |
datadog | 10 |
file-log | 9 |
udp-log | 8 |
tcp-log | 7 |
loggly | 6 |
syslog | 4 |
request-termination | 2 |
correlation-id | 1 |
post-function | -1000 |
插件配置
簡介
大多數(shù)情況下啄巧,插件的配置可以滿足用戶的需求,插件的配置存儲在數(shù)據(jù)庫中跪者,當(dāng)插件運行時棵帽,Kong在數(shù)據(jù)庫中檢索出它們,并將其傳遞給 handler.lua 方法
配置在 Kong 中由 Lua table 組成渣玲,我們稱之為 schema,用戶通過 Admin API 啟用插件時弟晚,以鍵值對的形式輸入?yún)?shù)忘衍,Kong提供了驗證用戶插件配置的方法,當(dāng)用戶向 Admin API 發(fā)送請求啟用或更新給定 Service卿城、Route 或 Consumer 上的插件時枚钓,Kong 會根據(jù)用戶定義的 schema 來驗證插件配置,舉例瑟押,用戶執(zhí)行如下請求:
curl -X POST http://kong:8001/services/<service-name-or-id>/plugins -d "name=my-custom-plugin" -d "config.foo=bar"
如果配置對象的所有屬性都驗證有效搀捷,API 會返回 201 Created
,插件將和配置一起存儲在數(shù)據(jù)庫中:
{
foo = "bar"
}
如果配置驗證不通過多望,API 會返回 400 Bad Request
和錯誤信息
模塊
kong.plugins.<plugin_name>.schema
schema.lua 格式
這個模塊返回一個 Lua table嫩舟,其中包含了用戶可以配置插件哪些屬性,可用的屬性包含:
屬性名 | 數(shù)據(jù)類型 | 描述 |
---|---|---|
name |
string | 插件名稱怀偷,比如 key-auth
|
fields |
table | 字段定義數(shù)組 |
entity_checks |
function | 校驗條件數(shù)組 |
所有插件都默認(rèn)繼承的屬性:
屬性名 | 數(shù)據(jù)類型 | 描述 |
---|---|---|
id |
string | 自動生成的插件 Id |
name |
string | 插件名稱家厌,比如 key-auth
|
created_at |
number | 插件配置時間 |
route |
table | 綁定的路由 |
service |
table | 綁定的服務(wù) |
consumer |
table | 綁定的消費者 |
run_on |
string | 插件運行在服務(wù)網(wǎng)格上的哪個節(jié)點 |
protocols |
table | 插件運行的協(xié)議 |
enabled |
boolean | 插件是否生效 |
tags |
table | 插件的標(biāo)簽 |
大多數(shù)情況下,用戶可以使用默認(rèn)值椎工,或者讓用戶在啟用插件時指定值饭于,以下是一份示例 schema.lua
文件:
local typedefs = require "kong.db.schema.typedefs"
return {
name = "<plugin-name>",
fields = {
{
-- this plugin will only be applied to Services or Routes
consumer = typedefs.no_consumer
},
{
-- this plugin will only be executed on the first Kong node
-- if a request comes from a service mesh (when acting as
-- a non-service mesh gateway, the nodes are always considered
-- to be "first".
run_on = typedefs.run_on_first
},
{
-- this plugin will only run within Nginx HTTP module
protocols = typedefs.protocols_http
},
{
config = {
type = "record",
fields = {
-- Describe your plugin's configuration's schema here.
},
},
},
},
entity_checks = {
-- Describe your plugin's entity validation rules
},
}
描述配置 schema
schema.lua
文件中的 config.fields
屬性描述了插件配置的 schema蜀踏,例如:
{
name = "<plugin-name>",
fields = {
config = {
type = "record",
fields = {
{
some_string = {
type = "string",
required = false,
},
},
{
some_boolean = {
type = "boolean",
default = false,
},
},
{
some_array = {
type = "array",
elements = {
type = "string",
one_of = {
"GET",
"POST",
"PUT",
"DELETE",
},
},
},
},
},
},
},
}
這里羅列了一些常用的屬性規(guī)則:
規(guī)則 | 描述 |
---|---|
type |
屬性的類型 |
required |
屬性是否是必須的 |
default |
屬性的默認(rèn)值 |
elements |
array 或 set 格式的元素類型 |
keys |
map 格式的 key 元素類型 |
values |
map 格式的 value 元素類型 |
fields |
record 格式的元素類型 |
另外還有一些:
規(guī)則 | 描述 |
---|---|
between |
校驗輸入是否在約定的范圍之內(nèi) |
eq |
校驗輸入是否等于約定值 |
ne |
校驗輸入是否不等于約定值 |
gt |
校驗輸入是否大于約定值 |
len_eq |
校驗輸入字符串長度是否等于約定值 |
len_min |
校驗輸入字符串長度是否大于約定值 |
len_max |
校驗輸入字符串長度是否小于約定值 |
match |
校驗輸入字符串是否匹配約定正則表達(dá)式 |
not_match |
校驗輸入字符串是否不匹配約定正則表達(dá)式 |
match_all |
校驗輸入字符串是否全部匹配約定正則表達(dá)式列表 |
match_none |
校驗輸入字符串是否全部不匹配約定正則表達(dá)式列表 |
match_any |
校驗輸入字符串是否匹配約定正則表達(dá)式列表中的一個 |
starts_with |
校驗輸入字符串是否以約定值開頭 |
one_of |
校驗輸入字符串是否是約定值列表中的一個 |
contains |
校驗輸入字符串列表是否包含約定值 |
is_regex |
校驗輸入字符串是否是合法的正則表達(dá)式 |
custom_validator |
校驗輸入是否是標(biāo)準(zhǔn)的 Lua 方法 |
例子
這是 key-auth 插件的 schema.lua
文件:
-- schema.lua
local typedefs = require "kong.db.schema.typedefs"
return {
name = "key-auth",
fields = {
{
consumer = typedefs.no_consumer
},
{
run_on = typedefs.run_on_first
},
{
protocols = typedefs.protocols_http
},
{
config = {
type = "record",
fields = {
{
key_names = {
type = "array",
required = true,
elements = typedefs.header_name,
default = {
"apikey",
},
},
},
{
hide_credentials = {
type = "boolean",
default = false,
},
},
{
anonymous = {
type = "string",
uuid = true,
legacy = true,
},
},
{
key_in_body = {
type = "boolean",
default = false,
},
},
{
run_on_preflight = {
type = "boolean",
default = true,
},
},
},
},
},
},
}
訪問數(shù)據(jù)庫
簡介
Kong 通過 Dao 層與數(shù)據(jù)層交互,本章將詳細(xì)介紹與數(shù)據(jù)層交互的的 API掰吕,Kong支持兩類數(shù)據(jù)庫:Cassandra 3.x.x 和 PostgreSQL 9.5+
kong.db
Kong 中所有的實體可以表現(xiàn)為:
- 一份描述與實體相關(guān)聯(lián)的表結(jié)構(gòu)果覆,表結(jié)構(gòu)中有對字段的約束,如外鍵殖熟,非控約束等局待,這個 schema 是在插件配置章節(jié)中描述的
- 一份 Dao 層實體,與現(xiàn)正使用的數(shù)據(jù)庫作映射吗讶,Dao 層使用 schema父虑,并暴露公共方法增刪改查實體
Kong 中的核心實體包括:服務(wù)、路由万矾、消費者和插件谤民,所有這些都可以作為數(shù)據(jù)訪問對象(DAOs),通過 kong.db 全局單例訪問:
-- Core DAOs
local services = kong.db.services
local routes = kong.db.routes
local consumers = kong.db.consumers
local plugins = kong.db.plugins
Kong 的核心實體和插件自定義的實體都可以通過 kong.db.* 獲取
Dao 層 Lua API
Dao 層負(fù)責(zé)在操作存儲在數(shù)據(jù)庫中的數(shù)據(jù)膜毁,所有底層支持的數(shù)據(jù)庫(目前是 Cassandra 和 Postgres)都遵循相同的接口昭卓,這樣 Dao 與所有這些數(shù)據(jù)庫都兼容,插入服務(wù)和插件非常簡單瘟滨,例如:
local inserted_service, err = kong.db.services:insert({
name = "mockbin",
url = "http://mockbin.org",
})
local inserted_plugin, err = kong.db.plugins:insert({
name = "key-auth",
service = inserted_service,
})
存儲自定義實體
簡介
雖然并非所有插件都需要它候醒,但有些插件中,用戶可能需要在數(shù)據(jù)庫中存儲多于其配置的數(shù)據(jù)杂瘸,在這種情況下倒淫,Kong 會在數(shù)據(jù)層提供抽象,允許用戶存儲自定義實體
如上一章節(jié)所述败玉,Kong 將與數(shù)據(jù)層交互的類稱為 DAO 層敌土,可以通過使用 DAO Factory 單例訪問,本章將描述如何為用戶自定義的實體提供抽象
模塊
kong.plugins.<plugin_name>.daos
kong.plugins.<plugin_name>.migrations.init
kong.plugins.<plugin_name>.migrations.000_base_<plugin_name>
kong.plugins.<plugin_name>.migrations.001_<from-version>_to_<to_version>
kong.plugins.<plugin_name>.migrations.002_<from-version>_to_<to_version>
創(chuàng)建遷移目錄
定義完模型之后运翼,用戶必須創(chuàng)建遷移模塊返干,當(dāng) Kong 啟動時會創(chuàng)建表結(jié)構(gòu),用來存儲實體記錄血淌,如果用戶的插件需要同時支持 Cassandra 和 Postgres矩欠,那需要寫兩個遷移模塊
如果用戶的插件還沒有這個模塊,可以添加一個 <plugin_name>/migrations
目錄悠夯,然后創(chuàng)建 init.lua
文件癌淮,這是引用插件所有遷移信息的地方,初始版本的 migrations/init.lua
文件指向單個遷移疗疟,這里该默,我們稱之為 000_base_my_plugin
-- `migrations/init.lua`
return {
"000_base_my_plugin",
}
這意味著 <plugin_name>/migrations/000_base_my_plugin.lua
文件中包含了一份初始遷移文件,用戶馬上可以看到具體的工作原理
在現(xiàn)有插件上添加遷移
有時需要在發(fā)布插件的新版本中引入更改策彤,添加新功能栓袖,數(shù)據(jù)庫里的數(shù)據(jù)也可能需要更改匣摘,當(dāng)發(fā)生這種情況時,用戶需要創(chuàng)建一個新的遷移文件裹刮,發(fā)布插件后音榜,用戶嚴(yán)禁修改原有的遷移文件,雖然沒有嚴(yán)格的規(guī)則來命名遷移文件捧弃,但有一個約定赠叼,即初始的前綴為000
,下一個為前綴為001
违霞,依次類推
繼我們之前的示例嘴办,現(xiàn)在用戶想要發(fā)布新版本插件,需要修改數(shù)據(jù)庫(例如买鸽,需要一個名為 foo 的表)涧郊,我們可以添加一個文件加入它,文件名為 <plugin_name>/migrations/001_100_to_110.lua
眼五,并且在初始遷移文件中引入它(其中100是插件的先前版本1.0.0妆艘,110是插件現(xiàn)在的版本1.1.0)
-- `<plugin_name>/migrations/init.lua`
return {
"000_base_my_plugin",
"001_100_to_110",
}
遷移文件語法
雖然 Kong 的核心遷移同時支持 Postgres 和 Cassandra,自定義插件可以選擇全部支持看幼,或者只支持其中一個批旺,遷移文件是一個 Lua 文件,它返回一個表诵姜,結(jié)構(gòu)如下:
-- `<plugin_name>/migrations/000_base_my_plugin.lua`
return {
postgresql = {
up = [[
CREATE TABLE IF NOT EXISTS "my_plugin_table" (
"id" UUID PRIMARY KEY,
"created_at" TIMESTAMP WITHOUT TIME ZONE,
"col1" TEXT
);
DO $$
BEGIN
CREATE INDEX IF NOT EXISTS "my_plugin_table_col1"
ON "my_plugin_table" ("col1");
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]],
},
cassandra = {
up = [[
CREATE TABLE IF NOT EXISTS my_plugin_table (
id uuid PRIMARY KEY,
created_at timestamp,
col1 text
);
CREATE INDEX IF NOT EXISTS ON my_plugin_table (col1);
]],
}
}
-- `<plugin_name>/migrations/001_100_to_110.lua`
return {
postgresql = {
up = [[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" ADD "cache_key" TEXT UNIQUE;
EXCEPTION WHEN DUPLICATE_COLUMN THEN
-- Do nothing, accept existing state
END;
$$;
]],
teardown = function(connector, helpers)
assert(connector:connect_migrations())
assert(connector:query([[
DO $$
BEGIN
ALTER TABLE IF EXISTS ONLY "my_plugin_table" DROP "col1";
EXCEPTION WHEN UNDEFINED_COLUMN THEN
-- Do nothing, accept existing state
END$$;
]])
end,
},
cassandra = {
up = [[
ALTER TABLE my_plugin_table ADD cache_key text;
CREATE INDEX IF NOT EXISTS ON my_plugin_table (cache_key);
]],
teardown = function(connector, helpers)
assert(connector:connect_migrations())
assert(connector:query("ALTER TABLE my_plugin_table DROP col1"))
end,
}
}
如果插件僅支持 Postgres 或 Cassandra 中的一個汽煮,策略中只需要寫一部分,每個策略包含兩段棚唆,up
段和 teardown
段:
-
up
:可選的 SQL/CQL 語句逗物,當(dāng)Kong執(zhí)行遷移時,執(zhí)行這些語句 -
teardown
:可選的 Lua 方法瑟俭,接收一個連接器參數(shù),此類連接器可以調(diào)用查詢方法執(zhí)行 SQL/CQL 查詢契邀,在Kong遷移完成后執(zhí)行
建議在 up
段中執(zhí)行非破壞性操作(例如創(chuàng)建新表摆寄、添加新紀(jì)錄);在 teardown
段中執(zhí)行破壞性操作(例如刪除數(shù)據(jù)坯门、更改行類型
添加新數(shù)據(jù))微饥,在編寫 SQL/CQL 語句時,推薦可以重復(fù)使用古戴,比如使用 DROP TABLE IF EXISTS
欠橘,而不是 DROP TABLE
;使用 CREATE INDEX IF NOT EXIST
现恼,而不是 CREATE INDEX
肃续,這樣當(dāng)某個原因?qū)е逻w移失敗時黍檩,只需修復(fù)問題,重新運行遷移即可
Postgres 支持 NOT NULL始锚、UNIQUE刽酱、FOREIGN KEY 之類的約束,Cassandra 本身并不支持瞧捌,但是如果在定義模型 schema 時加入此類約束棵里,Kong 就會支持這些功能,所以對于 Postgres 和 Cassandra姐呐,這兩類模式是相同的殿怜,可以完全將 Cassandra 當(dāng)做純 SQL 模式使用,請注意:如果在 schema 中使用了 unique
約束曙砂,Cassandra 會強(qiáng)制執(zhí)行头谜,Postgres 需要在遷移中設(shè)置此約束
定義 Schema
在自定義插件中使用自定義實體的第一步是定義一個或多個 schema,schema 格式是 Lua 表麦轰,其中描述實體的信息乔夯,包括實體的不同字段如何命名以及數(shù)據(jù)類型,與插件描述配置的字段類似款侵,與插件配置 schema 相比末荐,自定義實體 schema 需要額外的元數(shù)據(jù)(比如實體主鍵),schema 在該模塊中定義:
kong.plugins.<plugin_name>.daos
這意味著插件文件夾中需要有一個名為 <plugin_name>/daos.lua
的文件新锈,daos.lua
文件返回一個表甲脏,其中包含了一個或多個 schema,例如:
-- daos.lua
local typedefs = require "kong.db.schema.typedefs"
return {
-- this plugin only results in one custom DAO, named `keyauth_credentials`:
keyauth_credentials = {
name = "keyauth_credentials", -- the actual table in the database
endpoint_key = "key",
primary_key = { "id" },
cache_key = { "key" },
generate_admin_api = true,
fields = {
{
-- a value to be inserted by the DAO itself
-- (think of serial id and the uniqueness of such required here)
id = typedefs.uuid,
},
{
-- also interted by the DAO itself
created_at = typedefs.auto_timestamp_s,
},
{
-- a foreign key to a consumer's id
consumer = {
type = "foreign",
reference = "consumers",
default = ngx.null,
on_delete = "cascade",
},
},
{
-- a unique API key
key = {
type = "string",
required = false,
unique = true,
auto = true,
},
},
},
},
}
daos.lua
示例文件的屬性描述如下:
名稱 | 類型 | 描述 |
---|---|---|
name | string妹笆,必須的 | 用于確定 DAO 的名稱(kong.db.[name]) |
primary_key | table块请,必須的 | 實體主鍵,大多數(shù)情況下 Kong 使用 UUID 的 id 作為主鍵拳缠,也可以使用復(fù)合主鍵 |
endpoint_key | string墩新,可選的 | 在 Admin API 中作為備用標(biāo)志符的字段,在上面的示例中窟坐,endpoint_key 是 key 海渊,這意味著 id = 123 和 key = "foo" 的憑證可以通過 /keyauth_credentials/123 和 /keyauth_credentials/foo 這兩條路徑獲取 |
cache_key | table,可選的 | 生成 cache_key 的字段 |
generate_admin_api | boolean哲鸳,可選的 | 是否自動生成 Admin API臣疑,默認(rèn)情況下,會為所有 DAOS 生成 Admin API徙菠,包括自定義的 DAO讯沈,如果要為 DAO 創(chuàng)建完全自定義的 Admin API,或者想要完全禁用自動生成功能婿奔,將此選項設(shè)置為 false
|
admin_api_name | boolean缺狠,可選的 | 啟用 generate_admin_api 時问慎,使用 name 屬性自動生成 Admin API |
admin_api_nested_name | boolean,可選的 | 類似于 admin_api_name
|
fields | table | 定義了字段屬性的描述 |
許多字段屬性編碼驗證規(guī)則儒老,在使用 DAO 插入或更新實體時蝴乔,將檢查這些驗證,如果輸入不符合這些驗證驮樊,則會返回錯誤薇正,typedefs
變量(通過 kong.db.schema.typedefs
獲得)是一個包含大量實用類型定義和別名的表,包括 typedefs.uuid
(主鍵的常用類型)和 typedefs.auto_timestamp_s
(用于 created_at
字段)囚衔,它們在定義字段時被廣泛使用挖腰,下面是一些字段屬性的解釋:
屬性名 | 類型 | 描述 |
---|---|---|
type | string | 支持以下標(biāo)量類型:string 、integer 练湿、number 猴仑、boolean ;還支持以下復(fù)合類型:array 肥哎、record 辽俗、set ,除此之外篡诽,type 還可以取 foreign 崖飘,表示外鍵關(guān)系,type 是所有字段必需的屬性 |
default | any杈女,與 type 指定的類型保持一致 | 默認(rèn)值朱浴,始終通過 Lua 設(shè)置,而不是由底層數(shù)據(jù)庫設(shè)置达椰,因此建議不要在遷移中的字段上設(shè)置任何默認(rèn)值 |
required | boolean | 是否必需翰蠢,如何設(shè)置 true,當(dāng)輸入時缺少該字段會拋出錯誤 |
unique | boolean | 是否唯一啰劲,如果設(shè)置 ture梁沧,當(dāng)另一個實體存在時會拋出錯誤,在使用 PostgreSQL 時蝇裤,必需在遷移中將該字段聲明為 UNIQUE 趁尼;Cassandra 在插入數(shù)據(jù)前會檢查 Lua,因此不需要任何特殊處理 |
auto | boolean | 是否自動填充猖辫,當(dāng) type == "uuid" 時,該字段將填充 UUID砚殿;當(dāng) type == "string" 時啃憎,該字段將填充隨機(jī)字符串;如果字段名為 created_at 或 updated_at 似炎,該字段將在插入/更新時填充當(dāng)前時間 |
reference | string | 當(dāng) type 是 foreign 時是必須的 |
on_delete | string | 當(dāng) type 是 foreign 時辛萍,定義了外鍵刪除的邏輯悯姊,在 Cassandra 中,這是用純 Lua 代碼處理的贩毕,但在 PostgreSQL 中悯许,在遷移時要將引用聲明為 <font ON DELETE CASCADE/NULL/RESTRICT` |
需要了解表結(jié)構(gòu)的更多信息,可以參考:
- typedefs.lua 的源代碼辉阶,用于了解默認(rèn)值
- 核心表結(jié)構(gòu)
- 預(yù)綁定插件的 daos.lua 文件
自定義 DAO
schema 不直接和數(shù)據(jù)庫交互先壕,DAO層通過 kong.db
接口與數(shù)據(jù)庫交互
查詢實體
local entity, err, err_t = kong.db.<name>:select(primary_key)
在數(shù)據(jù)庫中查詢實體并返回,可能會有三種結(jié)果:
- 如果找到對應(yīng)實體谆甜,將作為普通 Lua table 返回垃僚;
- 發(fā)生錯誤,例如數(shù)據(jù)連接丟失规辱,第一個參數(shù)返回 nil谆棺,第二個參數(shù)返回描述錯誤的字符串,最后一個參數(shù)返回相同錯誤罕袋,數(shù)據(jù)格式是 table
- 沒有錯誤改淑,但找不到實體,直接返回 nil浴讯;
示例:
local entity, err = kong.db.keyauth_credentials:select({
id = "c77c50d2-5947-4904-9f37-fa36182a71a9"
})
if err then
kong.log.err("Error when inserting keyauth credential: " .. err)
return nil
end
if not entity then
kong.log.err("Could not find credential.")
return nil
end
遍歷實體
for entity, err on kong.db.<name>:each(entities_per_page) do
if err then
...
end
...
end
這個方法通過創(chuàng)建分頁請求有效地迭代數(shù)據(jù)庫中的所有實體朵夏,entities_per_page
參數(shù)(默認(rèn)100),控制每頁返回的實體數(shù)兰珍,每次迭代時都會返回一個新實體侍郭,當(dāng)有錯誤時,err
參數(shù)會填充錯誤掠河,迭代的推薦方法是首先檢查錯誤亮元,然后假設(shè)實體存在,例如:
for credential, err on kong.db.keyauth_credentials:each(1000) do
if err then
kong.log.err("Error when iterating over keyauth credentials: " .. err)
return nil
end
kong.log("id: " .. credential.id)
end
示例中迭代了1000個元素的憑證唠摹,并記錄它們的ID爆捞,發(fā)生錯誤時,打印錯誤日志
插入實體
local entity, err, err_t = kong.db.<name>:insert(<values>)
在數(shù)據(jù)庫中插入實體勾拉,返回值包括插入實體的副本或 nil煮甥、錯誤消息(字符串)和錯誤表(table 形式),插入成功后藕赞,返回的實體包含默認(rèn)和自動生成的填充值成肘,以下示例使用 keyauth_credentials
DAO 為給定的消費者插入憑證,將 key
設(shè)置為 secret
斧蜕,注意此處引用外鍵的語法:
local entity, err = kong.db.keyauth_credentials:insert({
consumer = { id = "c77c50d2-5947-4904-9f37-fa36182a71a9" },
key = "secret",
})
if not entity then
kong.log.err("Error when inserting keyauth credential: " .. err)
return nil
end
假設(shè)沒有發(fā)生錯誤双霍,返回的實體將包含自動填充的字段,如 id
和 created_at
更新實體
local entity, err, err_t = kong.db.<name>:update(primary_key, <values>)
更新實體的前提是提供可以找到它的主鍵和一組值,返回的內(nèi)容是更新后的實體洒闸,或者是 nil + 錯誤信息 + 錯誤表染坯,以下示例是在給定憑證 ID 的情況下修改現(xiàn)有憑證的 font color=red>key` 字段:
local entity, err = kong.db.keyauth_credentials:update({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" },
{ key = "updated_secret" },
})
if not entity then
kong.log.err("Error when updating keyauth credential: " .. err)
return nil
end
注意此處指定主鍵的語法與之前指定外鍵的語法相類似
插入或更新實體
local entity, err, err_t = kong.db.<name>:upsert(primary_key, <values>)
upsert
是 insert
和 update
的結(jié)合:
- 當(dāng)提供的
primary_key
可以標(biāo)識,與更新實體類似 - 當(dāng)提供的
primary_key
不可以標(biāo)識丘逸,與插入實體類似
local entity, err = kong.db.keyauth_credentials:upsert({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" },
{ consumer = { id = "a96145fb-d71e-4c88-8a5a-2c8b1947534c" } },
})
if not entity then
kong.log.err("Error when upserting keyauth credential: " .. err)
return nil
end
刪除實體
local ok, err, err_t = kong.db.<name>:delete(primary_key)
local ok, err = kong.db.keyauth_credentials:delete({
{ id = "2b6a2022-770a-49df-874d-11e2bf2634f5" }
})
if not ok then
kong.log.err("Error when deleting keyauth credential: " .. err)
return nil
end
緩存自定義實體
有時单鹿,每個請求/響應(yīng)都需要訪問自定義實體,每個都會觸發(fā)數(shù)據(jù)庫的查詢深纲,這樣效率非常低仲锄,因為查詢數(shù)據(jù)庫會增加延遲并降低請求/響應(yīng)速度,并且數(shù)據(jù)庫的負(fù)載增加會影響數(shù)據(jù)庫本身性能囤萤,從而影響其他 Kong 節(jié)點昼窗,當(dāng)每個請求/響應(yīng)都需要訪問自定義實體時,最好利用 Kong 提供的緩存API將其緩存在內(nèi)存中涛舍,下一章將重點描述如何緩存自定義實體澄惊,并在數(shù)據(jù)庫內(nèi)容變化時使它們失效
緩存自定義實體
簡介
有時,用戶的插件在每個請求/響應(yīng)都需要訪問自定義實體(前一章有所描述)富雅,通常第一次加載它們掸驱,之后將它們緩存在內(nèi)存中會顯著提供性能,同時可以防止數(shù)據(jù)庫因負(fù)載增加而受到壓力
想象一下 api-key 鑒權(quán)插件需要在每個請求上驗證 api-key没佑,從而每次都從數(shù)據(jù)庫中讀取自定義的憑證對象毕贼,然后根據(jù)情況阻斷請求或者檢索到消費者 ID 識別用戶,每個請求都是如此蛤奢,這會相當(dāng)?shù)托В?/p>
- 查詢數(shù)據(jù)庫會增加每個請求的延遲鬼癣,使請求處理速度變慢
- 數(shù)據(jù)庫負(fù)載會增加,速度會變慢或者崩潰啤贩,這樣會反過來影響 Kong 節(jié)點
為了避免每次都查詢數(shù)據(jù)庫待秃,我們可以在節(jié)點上以內(nèi)存形式緩存自定義實體,這樣頻繁的實體查詢不會每次都觸發(fā)數(shù)據(jù)庫查詢痹屹,而是發(fā)生在內(nèi)存中章郁,這比查詢數(shù)據(jù)庫更快更可靠(特別在重負(fù)載下)
模塊
kong.plugins.<plugin_name>.daos
緩存自定義實體
用戶可以使用插件開發(fā)工具包提供的 kong.cache
模塊將自定義實體緩存在內(nèi)存中:
local cache = kong.cache
緩存有兩層:
- L1: Lua 緩存 - Nginx worker 進(jìn)程中,可以存儲任何類型的 Lua 值
- L2: 共享緩存(SHM)- Nginx 節(jié)點中志衍,在 worker 進(jìn)程中共享暖庄,只能保存標(biāo)量值,更復(fù)雜的類型比如 table楼肪,需要序列化
從數(shù)據(jù)庫提取數(shù)據(jù)后培廓,數(shù)據(jù)會同時存儲在兩個緩存中,如果同一個 worker 進(jìn)程再次請求數(shù)據(jù)春叫,Kong會從 Lua 緩存中檢索之前反序列化的數(shù)據(jù)医舆;如果同一個節(jié)點的另一個 worker 進(jìn)程請求該數(shù)據(jù)俘侠,Kong 會從 SHM 中找到該數(shù)據(jù),并對其反序列化(存儲在當(dāng)前進(jìn)程的緩存中)蔬将,然后將其返回
該模塊公開以下方法:
方法名 | 描述 |
---|---|
value, err = cache:get(key, opts?, cb, ...) | 從緩存中檢索值,如果緩存沒有值(未命中)央星,則在保護(hù)模式下調(diào)用 cb 霞怀,cb 會且僅會返回一個緩存的值,這個方法也會拋出錯誤莉给,這些錯誤會被Kong捕獲毙石,并記錄為 ngx.ERR 級別,這個方法會緩存 nil颓遏,因此必須通過第二個參數(shù)檢查可能的錯誤 |
ttl, err, value = cache:probe(key) | 檢查是否有緩存值徐矩,如果有,返回 ttl叁幢;如果沒有滤灯,返回 nil,緩存值可以是 nil曼玩,第三個返回值是緩存的內(nèi)容 |
cache:invalidate_local(key) | 在節(jié)點中刪除一個緩存 |
cache:invalidate(key) | 在集群中刪除一個緩存 |
cache:purge() | 在節(jié)點中刪除所有緩存 |
回到鑒權(quán)插件鳞骤,當(dāng)使用特定的 api-key 查找憑證時,會這樣寫:
-- handler.lua
local BasePlugin = require "kong.plugins.base_plugin"
local kong = kong
local function load_credential(key)
local credential, err = kong.db.keyauth_credentials:select_by_key(key)
if not credential then
return nil, err
end
return credential
end
local CustomHandler = BasePlugin:extend()
CustomHandler.VERSION = "1.0.0"
CustomHandler.PRIORITY = 1010
function CustomHandler:new()
CustomHandler.super.new(self, "my-custom-plugin")
end
function CustomHandler:access(config)
CustomHandler.super.access(self)
-- retrieve the apikey from the request querystring
local key = kong.request.get_query_arg("apikey")
local credential_cache_key = kong.db.keyauth_credentials:cache_key(key)
-- We are using cache.get to first check if the apikey has been already
-- stored into the in-memory cache. If it's not, then we lookup the datastore
-- and return the credential object. Internally cache.get will save the value
-- in-memory, and then return the credential.
local credential, err = kong.cache:get(credential_cache_key, nil,
load_credential, credential_cache_key)
if err then
kong.log.err(err)
return kong.response.exit(500, {
message = "Unexpected error"
})
end
if not credential then
-- no credentials in cache nor datastore
return kong.response.exit(401, {
message = "Invalid authentication credentials"
})
end
-- set an upstream header if the credential exists and is valid
kong.service.request.set_header("X-API-Key", credential.apikey)
end
return CustomHandler
在上面的示例中黍判,我們使用插件開發(fā)工具包中的各種組件與請求豫尽、緩存模塊進(jìn)行交互,甚至在插件中自定義了響應(yīng)顷帖,現(xiàn)在美旧,有了上述機(jī)制,一旦消費者攜帶 API key 發(fā)送請求贬墩,緩存就被預(yù)熱了榴嗅,后續(xù)請求不會再觸發(fā)數(shù)據(jù)庫查詢,緩存在 Key-Auth 插件的多個地方使用
更新或刪除自定義實體
每次在數(shù)據(jù)庫中更新或刪除緩存過的自定義實體時(比如使用 Admin API)震糖,都會造成數(shù)據(jù)庫中的數(shù)據(jù)與Kong內(nèi)存中緩存的數(shù)據(jù)不一致录肯,為了避免這種情況,用戶需要在內(nèi)存中刪除緩存的實體吊说,并強(qiáng)制Kong再次從數(shù)據(jù)庫中查詢它论咏,我們稱這個過程為緩存失效
實體緩存失效
如果用戶希望通過 CRUD 操作使實體失效,而不是等它們到達(dá) TTL 時間颁井,需要執(zhí)行幾個步驟厅贪,對于大多數(shù)實體,這個過程會自動執(zhí)行雅宾,但有些需要手動訂閱某些 CRUD 事件使具有復(fù)雜關(guān)系的實體失效
自動失效
在用戶實體的 schema 中設(shè)置 cache_key
可以直接啟用緩存失效功能养涮,例如:
local typedefs = require "kong.db.schema.typedefs"
return {
-- this plugin only results in one custom DAO, named `keyauth_credentials`:
keyauth_credentials = {
name = "keyauth_credentials", -- the actual table in the database
endpoint_key = "key",
primary_key = { "id" },
cache_key = { "key" },
generate_admin_api = true,
fields = {
{
-- a value to be inserted by the DAO itself
-- (think of serial id and the uniqueness of such required here)
id = typedefs.uuid,
},
{
-- also interted by the DAO itself
created_at = typedefs.auto_timestamp_s,
},
{
-- a foreign key to a consumer's id
consumer = {
type = "foreign",
reference = "consumers",
default = ngx.null,
on_delete = "cascade",
},
},
{
-- a unique API key
key = {
type = "string",
required = false,
unique = true,
auto = true,
},
},
},
},
}
如果 cache_key
是這樣生成的,并在實體的 schema 中指定,那么緩存失效過程是自動的:每個有關(guān) key 的 CRUD 操作都會影響到 cache_key
贯吓,并會廣播到集群上的其他節(jié)點懈凹,以便在緩存中清除這個值,再下一個請求中從數(shù)據(jù)庫中重新獲取
當(dāng)父實體執(zhí)行 CRUD 操作悄谐,Kong 會對父實體和子實體同時執(zhí)行緩存失效機(jī)制
手動失效
在某些情況下介评,實體 schema 的 cache_key
屬性不夠靈活,必須手動使緩存失效爬舰,在這些情況们陆,用戶需要手動在Kong的失效通道注冊訂閱,并執(zhí)行自定義失效流程情屹,要監(jiān)聽 Kong 內(nèi)部的失效通道坪仇,需要在 init_worker
段中實現(xiàn)以下內(nèi)容:
function MyCustomHandler:init_worker()
-- listen to all CRUD operations made on Consumers
kong.worker_events.register(function(data)
end, "crud", "consumers")
-- or, listen to a specific CRUD operation only
kong.worker_events.register(function(data)
kong.log.inspect(data.operation) -- "update"
kong.log.inspect(data.old_entity) -- old entity table (only for "update")
kong.log.inspect(data.entity) -- new entity table
kong.log.inspect(data.schema) -- entity's schema
end, "crud", "consumers:update")
end
一旦上述監(jiān)聽器適用于所需的實體,用戶就可以根據(jù)需要對插件緩存的任何實體手動執(zhí)行失效垃你,例如:
kong.worker_events.register(function(data)
if data.operation == "delete" then
local cache_key = data.entity.id
kong.cache:invalidate("prefix:" .. cache_key)
end
end, "crud", "consumers")
擴(kuò)展 Admin API
簡介
用戶可以使用稱為 Admin API 的 REST 接口配置 Kong椅文,插件可以通過添加自己的端點,管理插件中的自定義實體蜡镶,典型的例子是增刪改查
Admin API 是一個 Lapis 應(yīng)用程序雾袱,Kong 提供了抽象,用戶可以輕松添加端點
模塊
kong.plugins.<plugin_name>.api
在 Admin API 上添加端點
如果用戶以這個格式定義模塊官还,Kong 會檢測并加載端點:
"kong.plugins.<plugin_name>.api"
這個模塊必須返回一個 table芹橡,結(jié)構(gòu)如下:
{
["<path>"] = {
schema = <schema>,
methods = {
before = function(self) ... end,
on_error = function(self) ... end,
GET = function(self) ... end,
PUT = function(self) ... end,
...
}
},
...
}
其中:
-
<path>
:表示一個路徑,路由中可以包含差值參數(shù) -
<schema>
:結(jié)構(gòu)定義望伦,核心或者自定義插件實體的 schema 可以通過kong.db.<entity>.schema
獲取林说,schema 用于判斷字段根據(jù)什么類型解析,默認(rèn)情況下屯伞,表單字段的類型都是 string -
methods
:包含了一系列方法腿箩,索引是字符串
-
before
鍵是可選的,可以定義一個方法劣摇,如果存在珠移,則在調(diào)用任何其他方法之前,都會執(zhí)行這個方法 - 可以使用 HTTP 名稱(如
GET
或PUT
)作為索引末融,當(dāng)匹配對應(yīng)的路徑和 HTTP 方法時叮贩,將執(zhí)行索引對應(yīng)的方法谨湘,如果在路徑上存在 before 方法,則首先執(zhí)行該方法,注意酷誓,before 方法可以使用kong.response.exit
提前退出泽示,這樣可以跳過原有的 HTTP 方法 -
on_error
鍵是可選的贪染,可以定義一個方法,如果存在涂乌,當(dāng)其他方法拋出錯誤時會執(zhí)行該方法;如果不存在英岭,Kong會使用默認(rèn)錯誤處理程序返回錯誤
例如:
local endpoints = require "kong.api.endpoints"
local credentials_schema = kong.db.keyauth_credentials.schema
local consumers_schema = kong.db.consumers.schema
return {
["/consumers/:consumers/key-auth"] = {
schema = credentials_schema,
methods = {
GET = endpoints.get_collection_endpoint(
credentials_schema, consumers_schema, "consumer"),
POST = endpoints.post_collection_endpoint(
credentials_schema, consumers_schema, "consumer"),
},
},
}
上面這端代碼將在 /consumers/:consumers/key-auth
路徑上創(chuàng)建兩個 Admin API 端點湾盒,用來獲取(GET)和創(chuàng)建(POST)綁定在消費者上的憑證诅妹,此示例中历涝,方法由 kong.api.endpoints
庫提供,如果想要查看更完整的示例漾唉,并在方法中使用自定義代碼,請查看 key-auth 插件中的 api.lua 文件
端點模塊中當(dāng)前包含了Kong中最常用的 CRUD 操作的默認(rèn)實現(xiàn)堰塌,此模塊為用戶提供了增刪改查的幫助程序赵刑,并執(zhí)行對應(yīng)的 DAO 層操作,使用響應(yīng)的 HTTP 狀態(tài)碼回應(yīng)场刑,它還提供了從路徑中檢索參數(shù)的功能般此,例如服務(wù)名稱或 ID,消費者用戶名或ID
如果端點提供的功能不夠牵现,可以使用常規(guī)的 Lua 方法:
-
endpoints
模塊提供的方法 - PDK 提供的所有方法
-
self
參數(shù)铐懊,Lapis 的請求對象 - 用戶可以引入需要的 Lua 模塊
local endpoints = require "kong.api.endpoints"
local credentials_schema = kong.db.keyauth_credentials.schema
local consumers_schema = kong.db.consumers.schema
return {
["/consumers/:consumers/key-auth/:keyauth_credentials"] = {
schema = credentials_schema,
methods = {
before = function(self, db, helpers)
local consumer, _, err_t = endpoints.select_entity(self, db, consumers_schema)
if err_t then
return endpoints.handle_error(err_t)
end
if not consumer then
return kong.response.exit(404, { message = "Not found" })
end
self.consumer = consumer
if self.req.method ~= "PUT" then
local cred, _, err_t = endpoints.select_entity(self, db, credentials_schema)
if err_t then
return endpoints.handle_error(err_t)
end
if not cred or cred.consumer.id ~= consumer.id then
return kong.response.exit(404, { message = "Not found" })
end
self.keyauth_credential = cred
self.params.keyauth_credentials = cred.id
end
end,
GET = endpoints.get_entity_endpoint(credentials_schema),
PUT = function(self, db, helpers)
self.args.post.consumer = { id = self.consumer.id }
return endpoints.put_entity_endpoint(credentials_schema)(self, db, helpers)
end,
},
},
}
在上面的示例中,/consumers/:consumers/key-auth/:keyauth_credentials
路徑定義了三個方法:
-
before
方法是一個自定義的 Lua 方法瞎疼,其中使用了endpoints
提供的方法(endpoints.handle_error)和 PDK 中的方法(kong.response.exit)科乎,它也填充了self.consumer
參數(shù),以供后續(xù)方法調(diào)用 -
GET
方法完全使用endpoints
贼急,這是合理的茅茂,before
方法已經(jīng)預(yù)先準(zhǔn)備了東西,比如self.consumer
-
PUT
方法在調(diào)用endpoints
提供的put_entity_endpoint
方法前太抓,先填充了self.args.post.consumer
參數(shù)
測試用例
簡介
如果用戶認(rèn)真對待自己的插件空闲,需要為它編寫測試用例,單元測試 Lua 腳本很簡答走敌,也有很多測試框架可以選擇碴倾,如果用戶還想編寫集成測試,Kong 也提供了這樣的功能
編寫集成測試用例
Kong 首選的測試框架是 busted掉丽,與 resty-cli 解釋器一起運行跌榔,如果用戶愿意,也可以使用其他的机打,在 Kong 的倉庫中矫户,用戶可以找到 busted 的可執(zhí)行文件 bin/busted
Kong 提供了一個幫助程序 spec.helpers,可以在測試套件中啟停 Lua 腳本残邀,這個幫助程序還能在運行測試之前在數(shù)據(jù)中插入或刪除數(shù)據(jù)皆辽,以及其他各種幫助
如果用戶想在自己的倉庫中編寫插件柑蛇,可以復(fù)制以下文件:
local helpers = require "spec.helpers"
for _, strategy in helpers.each_strategy() do
describe("my plugin", function()
local bp = helpers.get_db_utils(strategy)
setup(function()
local service = bp.services:insert {
name = "test-service",
host = "httpbin.org"
}
bp.routes:insert({
hosts = { "test.com" },
service = { id = service.id }
})
-- start Kong with your testing Kong configuration (defined in "spec.helpers")
assert(helpers.start_kong( { plugins = "bundled,my-plugin" }))
admin_client = helpers.admin_client()
end)
teardown(function()
if admin_client then
admin_client:close()
end
helpers.stop_kong()
end)
before_each(function()
proxy_client = helpers.proxy_client()
end)
after_each(function()
if proxy_client then
proxy_client:close()
end
end)
describe("thing", function()
it("should do thing", function()
-- send requests through Kong
local res = proxy_client:get("/get", {
headers = {
["Host"] = "test.com"
}
})
local body = assert.res_status(200, res)
-- body is a string containing the response
end)
end)
end)
end
注意:當(dāng)使用測試環(huán)境的 Kong 配置文件時,Kong代理監(jiān)聽9000和9443端口驱闷,Admin API 監(jiān)聽9001端口
安裝/卸載插件
簡介
Kong 的自定義插件由Lua源文件組成耻台,這些源文件需要安裝在 Kong 節(jié)點的文件系統(tǒng)中,本章將逐步說明空另,使 Kong 節(jié)點可以理解用戶的自定義插件
這些步驟將作用于 Kong 集群的每個節(jié)點盆耽,以確保每個節(jié)點上都有用戶的自定義插件
打包源碼
用戶可以使用常規(guī)打包策略(比如 tar),也可以使用 LuaRocks 包管理器來做執(zhí)行這項工作扼菠,我們推薦使用 LuaRocks摄杂,因為它已經(jīng)攜帶在官方發(fā)布的Kong安裝包中
當(dāng)使用 LuaRocks,用戶必須創(chuàng)建一個 rockspec
文件來指定包內(nèi)容循榆,有關(guān)示例可以參考Kong的插件模板析恢,更多信息可以參考 LuaRocks 的文檔,用戶可以使用以下命令打包文件:
# install it locally (based on the `.rockspec` in the current directory)
luarocks make
# pack the installed rock
luarocks pack <plugin-name> <version>
假設(shè)用戶插件的 rockspec 文件叫 kong-plugin-my-plugin-0.1.0-1.rockspec
秧饮,命令行為:
luarocks pack kong-plugin-my-plugin 0.1.0-1
LuaRocks 的 pack
指令創(chuàng)建了一個 .rock
文件(這是一個包含安裝 rock 所需內(nèi)容的 zip 文件)
如果用戶選擇不使用 LuaRocks映挂,可以使用 tar
指令將包含的 .lua
文件打包 .tar.gz
存檔中
存檔的內(nèi)容類似如下格式:
tree <plugin-name>
<plugin-name>
├── INSTALL.txt
├── README.md
├── kong
│ └── plugins
│ └── <plugin-name>
│ ├── handler.lua
│ └── schema.lua
└── <plugin-name>-<version>.rockspec
安裝插件
要使 Kong 節(jié)點能夠使用自定義插件,必須在主機(jī)的文件系統(tǒng)上安裝自定義插件的 Lua 源盗尸,有多種方法可以達(dá)成:通過 LuaRocks柑船,或手動,選擇其中一個泼各,然后直接跳轉(zhuǎn)到第3部分:
- 通過 LuaRocks 創(chuàng)建 rock
.rock 文件是一個自包含的軟件包鞍时,可以在本地安裝,也可以從遠(yuǎn)程服務(wù)器安裝历恐,如果用戶的系統(tǒng)中安裝了 LuaRocks寸癌,可以在 LuaRocks 樹中安裝 rock,安裝指令如下:
luarocks install <rock-filename>
- 如果用戶的系統(tǒng)中已經(jīng)安裝了 luarocks弱贼,用戶可以將當(dāng)前目錄修改為插件存檔的目錄蒸苇,其中 rockspec 文件是:
cd <plugin-name>
luarocks make
這將在用戶系統(tǒng)中的 LuaRocks 樹中安裝 kong/plugins/<plugin-name>
的源文件
- 一個更保險的安裝方式是避免污染 LuaRocks 樹,而是將 Kong 指向包含它們的目錄吮旅,這通過調(diào)整
lua_package_path
屬性完成溪烤,如果你熟悉它,這個屬性是 Lua VM 中LUA_PATH
變量的別名庇勃,這個屬性包含以分號分隔的目錄列表檬嘀,用于搜索 Lua 源,配置大致如下:
lua_package_path = /<path-to-plugin-location>/?.lua;;
其中:
/<path-to-plugin-location>
:包含提取存檔的目錄的路徑
?
:占位符责嚷,在 Kong 嘗試加載插件時鸳兽,會被 kong.plugins.<plugin-name>
替換,不要修改它
;;
:默認(rèn) Lua 路徑的占位符罕拂,不要修改它
例如:
something
這個插件的 handler 文件在文件系統(tǒng)中這個位置:
/usr/local/custom/kong/plugins/<something>/handler.lua
Kong的目錄是 /usr/local/custom
揍异,因此全陨,正確的路徑可以設(shè)置為:
lua_package_path = /usr/local/custom/?.lua;;
多個插件:如果用戶希望通過這個方法安裝多個插件,可以這樣設(shè)置變量:
lua_package_path = /path/to/plugin1/?.lua;/path/to/plugin2/?.lua;;
;
:多個目錄之間的分隔符
;;
:依舊表示默認(rèn) Lua 路徑的占位符
加載插件
用戶必須將自定義插件的名稱添加到Kong配置中的插件列表(每個節(jié)點都需要):
plugins = bundled,<plugin-name>
或者衷掷,用戶可以不添加綁定的插件:
plugins = <plugin-name>
如果用戶需要使用多個插件辱姨,可以用逗號分隔:
plugins = bundled,plugin1,plugin2
或
plugins = plugin1,plugin2
注意,用戶還可以通過環(huán)境變量 KONG_PLUGINS
來設(shè)置此屬性戚嗅,不要忘記更新Kong集群中每個節(jié)點的 plugins
指令雨涛,插件重啟會生效
kong restart
如果用戶不希望Kong停機(jī)并加載上插件,可以這樣:
kong prepare
kong reload
驗證加載插件
現(xiàn)在用戶可以正常啟動 Kong 了懦胞,為了確保插件被 Kong 加載替久,可以使用調(diào)試
日志級別啟動 Kong:
log_level = debug
或者
KONG_LOG_LEVEL=debug
然后,用戶可以看到加載的每個插件
[debug] Loading plugin <plugin-name>
刪除插件
刪除插件通常有3個步驟:
- 先從 Kong 的服務(wù)或路由配置中刪除插件躏尉,確保該插件不再作用域全局侣肄,也不作用任何服務(wù)、路由或消費者醇份,對于整個 Kong 集群,只需執(zhí)行一次整個操作吼具,不需要執(zhí)行 restart 或 reload 指令僚纷,這個步驟只是讓集群不再使用該插件,但它仍然可以再被啟用
- 從
plugins
指令刪除插件拗盒,確保在執(zhí)行此操作之前已完成步驟1怖竭,在此步驟之后,任何人都不能將插件重新應(yīng)用在服務(wù)陡蝇、路由痊臭、消費者或者全局中,此步驟需要執(zhí)行 restart 或 reload 指令才能生效 - 要徹底刪除插件登夫,要在每個 Kong 節(jié)點刪除與插件相關(guān)的文件广匙,在刪除文件之前,確保已完成步驟2恼策,包括重啟 Kong鸦致,如果用戶之前使用 LuaRocks 安裝插件,可以使用
luarocks remove <plugin-name>
指令來刪除
發(fā)布插件
首選的方法是使用 LuaRocks涣楷,Lua 模塊的包管理器分唾,我們稱這些模塊是 rock
,用戶不必將模塊存儲在 Kong 的倉庫中狮斗,如果用戶希望維持Kong的設(shè)置绽乔,則需要這樣做
通過在 rockspec 文件中定義模塊(及其依賴項),用戶可以通過 LuaRocks 在平臺上安裝模塊碳褒,用戶也可以使用 LuaRocks 上傳模塊給其他人使用
排除故障
由于以下幾個原因折砸,配置錯誤的自定義插件可能無法啟動:
-
plugin is in use but not enabled
:用戶在其他節(jié)點配置了自定義插件看疗,并且數(shù)據(jù)庫中已經(jīng)保存了插件的配置,但是當(dāng)前節(jié)點的plugins
指令中沒有找到該插件鞍爱,要解決此問題鹃觉,需要將每個節(jié)點都添加plugins
指令 -
plugin is enabled but not installed
:插件已添加在plugins
指令中,但Kong無法從文件系統(tǒng)中加載handler.lua
源文件睹逃,要解決此問題盗扇,確保正確設(shè)置 lua_package_path 指令來加載 Lua 源文件 -
no configuration schema found for plugin
:插件已安裝,但Kong無法從文件系統(tǒng)中加載schema.lua
源文件沉填,要解決此問題疗隶,確保schema.lua
與handler.lua
文件一起存在