Kong插件開發(fā)向?qū)?/h1>

簡介

在進(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 = 123key = "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)量類型:stringinteger练湿、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_atupdated_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ā)生錯誤双霍,返回的實體將包含自動填充的字段,如 idcreated_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>)

upsertinsertupdate 的結(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

緩存有兩層:

  1. L1: Lua 緩存 - Nginx worker 進(jìn)程中,可以存儲任何類型的 Lua 值
  2. 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:包含了一系列方法腿箩,索引是字符串
  1. before 鍵是可選的,可以定義一個方法劣摇,如果存在珠移,則在調(diào)用任何其他方法之前,都會執(zhí)行這個方法
  2. 可以使用 HTTP 名稱(如 GETPUT)作為索引末融,當(dāng)匹配對應(yīng)的路徑和 HTTP 方法時叮贩,將執(zhí)行索引對應(yīng)的方法谨湘,如果在路徑上存在 before 方法,則首先執(zhí)行該方法,注意酷誓,before 方法可以使用 kong.response.exit 提前退出泽示,這樣可以跳過原有的 HTTP 方法
  3. 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部分:

  1. 通過 LuaRocks 創(chuàng)建 rock
    .rock 文件是一個自包含的軟件包鞍时,可以在本地安裝,也可以從遠(yuǎn)程服務(wù)器安裝历恐,如果用戶的系統(tǒng)中安裝了 LuaRocks寸癌,可以在 LuaRocks 樹中安裝 rock,安裝指令如下:
luarocks install <rock-filename>
  1. 如果用戶的系統(tǒng)中已經(jīng)安裝了 luarocks弱贼,用戶可以將當(dāng)前目錄修改為插件存檔的目錄蒸苇,其中 rockspec 文件是:
cd <plugin-name>
luarocks make

這將在用戶系統(tǒng)中的 LuaRocks 樹中安裝 kong/plugins/<plugin-name> 的源文件

  1. 一個更保險的安裝方式是避免污染 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個步驟:

  1. 先從 Kong 的服務(wù)或路由配置中刪除插件躏尉,確保該插件不再作用域全局侣肄,也不作用任何服務(wù)、路由或消費者醇份,對于整個 Kong 集群,只需執(zhí)行一次整個操作吼具,不需要執(zhí)行 restart 或 reload 指令僚纷,這個步驟只是讓集群不再使用該插件,但它仍然可以再被啟用
  2. plugins 指令刪除插件拗盒,確保在執(zhí)行此操作之前已完成步驟1怖竭,在此步驟之后,任何人都不能將插件重新應(yīng)用在服務(wù)陡蝇、路由痊臭、消費者或者全局中,此步驟需要執(zhí)行 restart 或 reload 指令才能生效
  3. 要徹底刪除插件登夫,要在每個 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.luahandler.lua 文件一起存在
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者

  • 序言:七十年代末,一起剝皮案震驚了整個濱河市翼闹,隨后出現(xiàn)的幾起案子斑鼻,更是在濱河造成了極大的恐慌,老刑警劉巖猎荠,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坚弱,死亡現(xiàn)場離奇詭異,居然都是意外死亡关摇,警方通過查閱死者的電腦和手機(jī)荒叶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來输虱,“玉大人些楣,你說我怎么就攤上這事∠芏茫” “怎么了愁茁?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長亭病。 經(jīng)常有香客問我鹅很,道長,這世上最難降的妖魔是什么罪帖? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任道宅,我火速辦了婚禮,結(jié)果婚禮上胸蛛,老公的妹妹穿的比我還像新娘污茵。我一直安慰自己,他們只是感情好葬项,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布泞当。 她就那樣靜靜地躺著,像睡著了一般民珍。 火紅的嫁衣襯著肌膚如雪襟士。 梳的紋絲不亂的頭發(fā)上盗飒,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機(jī)與錄音陋桂,去河邊找鬼逆趣。 笑死,一個胖子當(dāng)著我的面吹牛嗜历,可吹牛的內(nèi)容都是我干的宣渗。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼梨州,長吁一口氣:“原來是場噩夢啊……” “哼痕囱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起暴匠,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鞍恢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后每窖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帮掉,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年窒典,在試婚紗的時候發(fā)現(xiàn)自己被綠了旭寿。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡崇败,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肩祥,到底是詐尸還是另有隱情后室,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布混狠,位于F島的核電站岸霹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏将饺。R本人自食惡果不足惜贡避,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望予弧。 院中可真熱鬧刮吧,春花似錦、人聲如沸掖蛤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蚓庭。三九已至致讥,卻和暖如春仅仆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背垢袱。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工墓拜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人请契。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓咳榜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親姚糊。 傳聞我的和親對象是個殘疾皇子贿衍,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容