Kong插件开发向导

作者: fossilman | 来源:发表于2019-10-03 20:49 被阅读0次

    简介

    在进一步讨论之前,这里先有必要简要阐述一下 Kong 是如何构建的,特别是它如何与Nginx集成,以及它与Lua脚本之间的关系
    使用 lua-nginx-module 模块可以在 Nginx 中启用 Lua 脚本功能,Kong 与 OpenResty 一起发布,OpenResty 中已经包含了 lua-nginx-module 模块,OpenResty 不是 Nginx 的分支,而是一组扩展 Nginx 功能的模块
    因此,Kong 是一个 Lua 应用程序,旨在加载和执行 Lua 模块(我们通常称之为“插件”),并且Kong还为此提供了整套开发环境,包括 SDK,数据库抽象、数据迁移等等
    插件由 Lua 模块组成,用户可以使用插件开发包(又称PDK),通过调用请求响应或者流交互实现各种功能 ,PDK 是一组 Lua 方法,插件可以使用它来促进 Kong 核心模块(或其他组件)与插件本身的交互
    这篇向导将详细描述插件的结构,它们的扩展点,以及如何发布和安装它们,有关PDK的详情,请查阅插件开发工具包向导

    文件结构

    简介

    插件其实是一组 Lua 模块,本章中描述的每个文件都可以视为一个单独的模块,如果它们的命名遵循某个约定,Kong 就会检测并加载插件模块:

    kong.plugins.<plugin_name>.<module_name>
    

    用户定义的插件模块需要通过 package.path 变量访问到,用户可以更改 lua_package_path 配置调整这个值,然而,安装插件的首选方法是通过 LuaRocks,它与 Kong 天然集成,有关 LuaRocks 安装插件的详情,请参考后面的章节
    为了让 Kong 意识到哪些插件需要安装,用户必须将它们添加到配置文件中的 plugins 属性中,格式是以逗号分隔的列表,例如:

    plugins = bundled,my-custom-plugin  # your plugin name here
    

    或者,用户不想加载任何预捆绑的插件:

    plugins = my-custom-plugin  # your plugin name here
    

    现在,Kong会试图从下列命名空间中加载Lua模块

    kong.plugins.my-custom-plugin.<module_name>
    

    其中一些模块是必需的(例如 handler.lua),有些是可选的,以允许插件实现一些额外的功能(例如 api.lua 可以扩展 Admin API 端点)
    现在我们将详细描述用户可以实现的模块以及它们的用途

    基础插件模块

    最基础的插件,必须包含两个模块:

    simple-plugin 
    ├── handler.lua 
    └── schema.lua
    
    • handler.lua:插件的核心,它是一个需要实现的接口,其中每个方法会在请求/连接的生命周期中运行
    • schema.lua:插件可能需要保留一些用户输入的配置,此模块定义一些规则保存配置的模式,以便用户只能输入有效的配置项

    高级插件模块

    有些插件与 Kong 之间有更深入地集成,比如在数据库中存数据,在 Admin API 中公开端点等等,每个插件都可以通过向插件添加新模块来完成,插件的结构大致如下:

    complete-plugin
    ├── api.lua
    ├── daos.lua
    ├── handler.lua
    ├── migrations
    │   ├── init.lua
    │   └── 000_base_complete_plugin.lua
    └── schema.lua
    

    以下是完整的模块列表,以及其简要说明:

    模块名 是否必须 描述
    api.lua 定义 Admin API 中也用的端点列表,与插件自定义的实体进行交互
    daos.lua 定义数据库访问对象列表
    handler.lua 一个需要实现的接口,其中每个方法会在请求/连接的生命周期中运行
    migrations/*.lua 数据源迁移,只有当用户的插件有自定义实体时才需要
    schema.lua 保存插件的配置项,以便用户只能输入有效的配置值

    Key-Auth 插件实现了整套完整的插件接口,可以查看源码了解细节

    实现自定义逻辑

    简介

    Kong 的插件允许用户在整个生命周期的几个切点加入自定义逻辑,为此,必须实现 base_plugin.lua 接口中的一些方法,这些方法在 kong.plugins.<plugin_name>.handler 模块中实现

    模块

    kong.plugins.<plugin_name>.handler
    

    可用的上下文

    插件接口允许用户覆盖 handler.lua 文件中的以下任何方法,在Kong的执行生命周期的各个切点实现自定义逻辑:

    • HTTP Module:为 HTTP / HTTPS 请求编写的插件
    方法名 段信息 描述
    :init_worker() init_worker 每个 Nginx worker 进程启动时执行
    :certificate() ssl_certificate 在 SSL 握手提供证书时执行
    :rewrite() rewrite 从客户端接收到请求,进入 rewrite 段执行,注意,在这个阶段没有识别服务,也没有消费者介入,只有配置成全局插件才会执行此处理程序
    :access() access 从客户端接收到请求到被代理到 upstream service 之前执行
    :header_filter() header_filter 从 upstream service 接收到所有响应头时执行
    :body_filter() body_filter 针对从 upstream service 接收到的响应体块执行,由于响应以流的形式返回给客户端,超过缓冲区大小的按块进行传输,因此,如果响应体很大,会多次调用这个方法
    :log() log 最后一个响应字节发送到客户端时执行
    • Stream Module:为 TCP 流连接编写的插件
    方法名 段信息 描述
    :init_worker() init_worker 每个 Nginx worker 进程启动时执行
    :preread() preread 每个连接执行一次
    :log() log 每个连接中断执行一次

    除了 :init_worker() 方法,每个方法都会携带一个参数,这个参数由Kong给出,即插件的配置,这个参数的类型是 Lua table,包含了用户定义的值,格式根据用户定义的插件 schema 格式

    handler.lua 格式

    handler.lua 文件需要返回一个 table,里面包含了用户希望执行的方法,为了方便起见,这里有一个示例模块,实现了模块中所有的可用方法:

    -- 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
    

    插件本身的逻辑可以写在另一个模块,然后在处理程序模块中调用:

    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
    

    插件开发工具包

    在插件开发过程中,需要与请求/响应对象或其他核心组件交互,Kong 为此提供了一个插件开发工具包,插件可以使用里面的函数和变量来执行各种网关操作,并且插件开发工具包是向前兼容的
    如果用户尝试实现一些与 Kong 交互的逻辑时(例如检索请求头、生成响应、记录错误或调试信息),可以参考插件开发工具包

    插件执行顺序

    某些插件可能依赖其他插件来执行某些操作,例如,依赖消费者身份的插件必须在身份验证插件之后运行,考虑到这一点,Kong在插件执行期间定义了优先级,以确保插件执行顺序,用户可以定义这个属性值来配置插件优先级:

    CustomHandler.PRIORITY = 10
    

    优先级越高,插件执行的越早(例如 :access():log() 方法),预绑定插件的优先级如下:

    插件 优先级
    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

    插件配置

    简介

    大多数情况下,插件的配置可以满足用户的需求,插件的配置存储在数据库中,当插件运行时,Kong在数据库中检索出它们,并将其传递给 handler.lua 方法
    配置在 Kong 中由 Lua table 组成,我们称之为 schema,用户通过 Admin API 启用插件时,以键值对的形式输入参数,Kong提供了验证用户插件配置的方法,当用户向 Admin API 发送请求启用或更新给定 Service、Route 或 Consumer 上的插件时,Kong 会根据用户定义的 schema 来验证插件配置,举例,用户执行如下请求:

    curl -X POST http://kong:8001/services/<service-name-or-id>/plugins -d "name=my-custom-plugin" -d "config.foo=bar"
    

    如果配置对象的所有属性都验证有效,API 会返回 201 Created,插件将和配置一起存储在数据库中:

    {
      foo = "bar"
    }
    

    如果配置验证不通过,API 会返回 400 Bad Request 和错误信息

    模块

    kong.plugins.<plugin_name>.schema
    

    schema.lua 格式

    这个模块返回一个 Lua table,其中包含了用户可以配置插件哪些属性,可用的属性包含:

    属性名 数据类型 描述
    name string 插件名称,比如 key-auth
    fields table 字段定义数组
    entity_checks function 校验条件数组

    所有插件都默认继承的属性:

    属性名 数据类型 描述
    id string 自动生成的插件 Id
    name string 插件名称,比如 key-auth
    created_at number 插件配置时间
    route table 绑定的路由
    service table 绑定的服务
    consumer table 绑定的消费者
    run_on string 插件运行在服务网格上的哪个节点
    protocols table 插件运行的协议
    enabled boolean 插件是否生效
    tags table 插件的标签

    大多数情况下,用户可以使用默认值,或者让用户在启用插件时指定值,以下是一份示例 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",
                  },
                },
              },
            },
          },
        },
      },
    }
    

    这里罗列了一些常用的属性规则:

    规则 描述
    type 属性的类型
    required 属性是否是必须的
    default 属性的默认值
    elements array 或 set 格式的元素类型
    keys map 格式的 key 元素类型
    values map 格式的 value 元素类型
    fields record 格式的元素类型

    另外还有一些:

    规则 描述
    between 校验输入是否在约定的范围之内
    eq 校验输入是否等于约定值
    ne 校验输入是否不等于约定值
    gt 校验输入是否大于约定值
    len_eq 校验输入字符串长度是否等于约定值
    len_min 校验输入字符串长度是否大于约定值
    len_max 校验输入字符串长度是否小于约定值
    match 校验输入字符串是否匹配约定正则表达式
    not_match 校验输入字符串是否不匹配约定正则表达式
    match_all 校验输入字符串是否全部匹配约定正则表达式列表
    match_none 校验输入字符串是否全部不匹配约定正则表达式列表
    match_any 校验输入字符串是否匹配约定正则表达式列表中的一个
    starts_with 校验输入字符串是否以约定值开头
    one_of 校验输入字符串是否是约定值列表中的一个
    contains 校验输入字符串列表是否包含约定值
    is_regex 校验输入字符串是否是合法的正则表达式
    custom_validator 校验输入是否是标准的 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,
                },
              },
            },
          },
        },
      },
    }
    

    访问数据库

    简介

    Kong 通过 Dao 层与数据层交互,本章将详细介绍与数据层交互的的 API,Kong支持两类数据库:Cassandra 3.x.x 和 PostgreSQL 9.5+

    kong.db

    Kong 中所有的实体可以表现为:

    • 一份描述与实体相关联的表结构,表结构中有对字段的约束,如外键,非控约束等,这个 schema 是在插件配置章节中描述的
    • 一份 Dao 层实体,与现正使用的数据库作映射,Dao 层使用 schema,并暴露公共方法增删改查实体

    Kong 中的核心实体包括:服务、路由、消费者和插件,所有这些都可以作为数据访问对象(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 层负责在操作存储在数据库中的数据,所有底层支持的数据库(目前是 Cassandra 和 Postgres)都遵循相同的接口,这样 Dao 与所有这些数据库都兼容,插入服务和插件非常简单,例如:

    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,
    })
    

    存储自定义实体

    简介

    虽然并非所有插件都需要它,但有些插件中,用户可能需要在数据库中存储多于其配置的数据,在这种情况下,Kong 会在数据层提供抽象,允许用户存储自定义实体
    如上一章节所述,Kong 将与数据层交互的类称为 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>
    

    创建迁移目录

    定义完模型之后,用户必须创建迁移模块,当 Kong 启动时会创建表结构,用来存储实体记录,如果用户的插件需要同时支持 Cassandra 和 Postgres,那需要写两个迁移模块
    如果用户的插件还没有这个模块,可以添加一个 <plugin_name>/migrations 目录,然后创建 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 文件中包含了一份初始迁移文件,用户马上可以看到具体的工作原理

    在现有插件上添加迁移

    有时需要在发布插件的新版本中引入更改,添加新功能,数据库里的数据也可能需要更改,当发生这种情况时,用户需要创建一个新的迁移文件,发布插件后,用户严禁修改原有的迁移文件,虽然没有严格的规则来命名迁移文件,但有一个约定,即初始的前缀为000,下一个为前缀为001,依次类推
    继我们之前的示例,现在用户想要发布新版本插件,需要修改数据库(例如,需要一个名为 foo 的表),我们可以添加一个文件加入它,文件名为 <plugin_name>/migrations/001_100_to_110.lua,并且在初始迁移文件中引入它(其中100是插件的先前版本1.0.0,110是插件现在的版本1.1.0)

    -- `<plugin_name>/migrations/init.lua`
    return {
      "000_base_my_plugin",
      "001_100_to_110",
    }
    

    迁移文件语法

    虽然 Kong 的核心迁移同时支持 Postgres 和 Cassandra,自定义插件可以选择全部支持,或者只支持其中一个,迁移文件是一个 Lua 文件,它返回一个表,结构如下:

    -- `<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 语句,当Kong执行迁移时,执行这些语句
    • teardown:可选的 Lua 方法,接收一个连接器参数,此类连接器可以调用查询方法执行 SQL/CQL 查询,在Kong迁移完成后执行

    建议在 up 段中执行非破坏性操作(例如创建新表、添加新纪录);在 teardown 段中执行破坏性操作(例如删除数据、更改行类型
    添加新数据),在编写 SQL/CQL 语句时,推荐可以重复使用,比如使用 DROP TABLE IF EXISTS,而不是 DROP TABLE;使用 CREATE INDEX IF NOT EXIST,而不是 CREATE INDEX,这样当某个原因导致迁移失败时,只需修复问题,重新运行迁移即可
    Postgres 支持 NOT NULL、UNIQUE、FOREIGN KEY 之类的约束,Cassandra 本身并不支持,但是如果在定义模型 schema 时加入此类约束,Kong 就会支持这些功能,所以对于 Postgres 和 Cassandra,这两类模式是相同的,可以完全将 Cassandra 当做纯 SQL 模式使用,请注意:如果在 schema 中使用了 unique 约束,Cassandra 会强制执行,Postgres 需要在迁移中设置此约束

    定义 Schema

    在自定义插件中使用自定义实体的第一步是定义一个或多个 schema,schema 格式是 Lua 表,其中描述实体的信息,包括实体的不同字段如何命名以及数据类型,与插件描述配置的字段类似,与插件配置 schema 相比,自定义实体 schema 需要额外的元数据(比如实体主键),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,必须的 实体主键,大多数情况下 Kong 使用 UUID 的 id 作为主键,也可以使用复合主键
    endpoint_key string,可选的 在 Admin API 中作为备用标志符的字段,在上面的示例中,endpoint_key 是 key,这意味着 id = 123key = "foo" 的凭证可以通过 /keyauth_credentials/123/keyauth_credentials/foo 这两条路径获取
    cache_key table,可选的 生成 cache_key 的字段
    generate_admin_api boolean,可选的 是否自动生成 Admin API,默认情况下,会为所有 DAOS 生成 Admin API,包括自定义的 DAO,如果要为 DAO 创建完全自定义的 Admin API,或者想要完全禁用自动生成功能,将此选项设置为 false
    admin_api_name boolean,可选的 启用 generate_admin_api 时,使用 name 属性自动生成 Admin API
    admin_api_nested_name boolean,可选的 类似于 admin_api_name
    fields table 定义了字段属性的描述

    许多字段属性编码验证规则,在使用 DAO 插入或更新实体时,将检查这些验证,如果输入不符合这些验证,则会返回错误,typedefs 变量(通过 kong.db.schema.typedefs 获得)是一个包含大量实用类型定义和别名的表,包括 typedefs.uuid(主键的常用类型)和 typedefs.auto_timestamp_s (用于 created_at 字段),它们在定义字段时被广泛使用,下面是一些字段属性的解释:

    属性名 类型 描述
    type string 支持以下标量类型:stringintegernumberboolean;还支持以下复合类型:arrayrecordset,除此之外,type 还可以取 foreign,表示外键关系,type 是所有字段必需的属性
    default any,与 type 指定的类型保持一致 默认值,始终通过 Lua 设置,而不是由底层数据库设置,因此建议不要在迁移中的字段上设置任何默认值
    required boolean 是否必需,如何设置 true,当输入时缺少该字段会抛出错误
    unique boolean 是否唯一,如果设置 ture,当另一个实体存在时会抛出错误,在使用 PostgreSQL 时,必需在迁移中将该字段声明为 UNIQUE;Cassandra 在插入数据前会检查 Lua,因此不需要任何特殊处理
    auto boolean 是否自动填充,当 type == "uuid" 时,该字段将填充 UUID;当 type == "string" 时,该字段将填充随机字符串;如果字段名为 created_atupdated_at,该字段将在插入/更新时填充当前时间
    reference string 当 type 是 foreign 时是必须的
    on_delete string 当 type 是 foreign 时,定义了外键删除的逻辑,在 Cassandra 中,这是用纯 Lua 代码处理的,但在 PostgreSQL 中,在迁移时要将引用声明为 <font ON DELETE CASCADE/NULL/RESTRICT`

    需要了解表结构的更多信息,可以参考:

    • typedefs.lua 的源代码,用于了解默认值
    • 核心表结构
    • 预绑定插件的 daos.lua 文件

    自定义 DAO

    schema 不直接和数据库交互,DAO层通过 kong.db 接口与数据库交互

    查询实体

    local  entity,  err,  err_t  =  kong.db.<name>:select(primary_key)
    

    在数据库中查询实体并返回,可能会有三种结果:

    • 如果找到对应实体,将作为普通 Lua table 返回;
    • 发生错误,例如数据连接丢失,第一个参数返回 nil,第二个参数返回描述错误的字符串,最后一个参数返回相同错误,数据格式是 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
    

    这个方法通过创建分页请求有效地迭代数据库中的所有实体,entities_per_page 参数(默认100),控制每页返回的实体数,每次迭代时都会返回一个新实体,当有错误时,err 参数会填充错误,迭代的推荐方法是首先检查错误,然后假设实体存在,例如:

    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,发生错误时,打印错误日志

    插入实体

    local  entity,  err,  err_t  =  kong.db.<name>:insert(<values>)
    

    在数据库中插入实体,返回值包括插入实体的副本或 nil、错误消息(字符串)和错误表(table 形式),插入成功后,返回的实体包含默认和自动生成的填充值,以下示例使用 keyauth_credentials DAO 为给定的消费者插入凭证,将 key 设置为 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
    

    假设没有发生错误,返回的实体将包含自动填充的字段,如 idcreated_at

    更新实体

    local  entity,  err,  err_t  =  kong.db.<name>:update(primary_key,  <values>)
    

    更新实体的前提是提供可以找到它的主键和一组值,返回的内容是更新后的实体,或者是 nil + 错误信息 + 错误表,以下示例是在给定凭证 ID 的情况下修改现有凭证的 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 的结合:

    • 当提供的 primary_key 可以标识,与更新实体类似
    • 当提供的 primary_key 不可以标识,与插入实体类似
    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
    

    缓存自定义实体

    有时,每个请求/响应都需要访问自定义实体,每个都会触发数据库的查询,这样效率非常低,因为查询数据库会增加延迟并降低请求/响应速度,并且数据库的负载增加会影响数据库本身性能,从而影响其他 Kong 节点,当每个请求/响应都需要访问自定义实体时,最好利用 Kong 提供的缓存API将其缓存在内存中,下一章将重点描述如何缓存自定义实体,并在数据库内容变化时使它们失效

    缓存自定义实体

    简介

    有时,用户的插件在每个请求/响应都需要访问自定义实体(前一章有所描述),通常第一次加载它们,之后将它们缓存在内存中会显著提供性能,同时可以防止数据库因负载增加而受到压力
    想象一下 api-key 鉴权插件需要在每个请求上验证 api-key,从而每次都从数据库中读取自定义的凭证对象,然后根据情况阻断请求或者检索到消费者 ID 识别用户,每个请求都是如此,这会相当低效:

    • 查询数据库会增加每个请求的延迟,使请求处理速度变慢
    • 数据库负载会增加,速度会变慢或者崩溃,这样会反过来影响 Kong 节点

    为了避免每次都查询数据库,我们可以在节点上以内存形式缓存自定义实体,这样频繁的实体查询不会每次都触发数据库查询,而是发生在内存中,这比查询数据库更快更可靠(特别在重负载下)

    模块

    kong.plugins.<plugin_name>.daos
    

    缓存自定义实体

    用户可以使用插件开发工具包提供的 kong.cache 模块将自定义实体缓存在内存中:

    local cache = kong.cache
    

    缓存有两层:

    1. L1: Lua 缓存 - Nginx worker 进程中,可以存储任何类型的 Lua 值
    2. L2: 共享缓存(SHM)- Nginx 节点中,在 worker 进程中共享,只能保存标量值,更复杂的类型比如 table,需要序列化

    从数据库提取数据后,数据会同时存储在两个缓存中,如果同一个 worker 进程再次请求数据,Kong会从 Lua 缓存中检索之前反序列化的数据;如果同一个节点的另一个 worker 进程请求该数据,Kong 会从 SHM 中找到该数据,并对其反序列化(存储在当前进程的缓存中),然后将其返回
    该模块公开以下方法:

    方法名 描述
    value, err = cache:get(key, opts?, cb, ...) 从缓存中检索值,如果缓存没有值(未命中),则在保护模式下调用 cbcb 会且仅会返回一个缓存的值,这个方法也会抛出错误,这些错误会被Kong捕获,并记录为 ngx.ERR 级别,这个方法会缓存 nil,因此必须通过第二个参数检查可能的错误
    ttl, err, value = cache:probe(key) 检查是否有缓存值,如果有,返回 ttl;如果没有,返回 nil,缓存值可以是 nil,第三个返回值是缓存的内容
    cache:invalidate_local(key) 在节点中删除一个缓存
    cache:invalidate(key) 在集群中删除一个缓存
    cache:purge() 在节点中删除所有缓存

    回到鉴权插件,当使用特定的 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
    

    在上面的示例中,我们使用插件开发工具包中的各种组件与请求、缓存模块进行交互,甚至在插件中自定义了响应,现在,有了上述机制,一旦消费者携带 API key 发送请求,缓存就被预热了,后续请求不会再触发数据库查询,缓存在 Key-Auth 插件的多个地方使用

    更新或删除自定义实体

    每次在数据库中更新或删除缓存过的自定义实体时(比如使用 Admin API),都会造成数据库中的数据与Kong内存中缓存的数据不一致,为了避免这种情况,用户需要在内存中删除缓存的实体,并强制Kong再次从数据库中查询它,我们称这个过程为缓存失效

    实体缓存失效

    如果用户希望通过 CRUD 操作使实体失效,而不是等它们到达 TTL 时间,需要执行几个步骤,对于大多数实体,这个过程会自动执行,但有些需要手动订阅某些 CRUD 事件使具有复杂关系的实体失效

    自动失效

    在用户实体的 schema 中设置 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 中指定,那么缓存失效过程是自动的:每个有关 key 的 CRUD 操作都会影响到 cache_key,并会广播到集群上的其他节点,以便在缓存中清除这个值,再下一个请求中从数据库中重新获取
    当父实体执行 CRUD 操作,Kong 会对父实体和子实体同时执行缓存失效机制

    手动失效

    在某些情况下,实体 schema 的 cache_key 属性不够灵活,必须手动使缓存失效,在这些情况,用户需要手动在Kong的失效通道注册订阅,并执行自定义失效流程,要监听 Kong 内部的失效通道,需要在 init_worker 段中实现以下内容:

    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
    

    一旦上述监听器适用于所需的实体,用户就可以根据需要对插件缓存的任何实体手动执行失效,例如:

    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")
    

    扩展 Admin API

    简介

    用户可以使用称为 Admin API 的 REST 接口配置 Kong,插件可以通过添加自己的端点,管理插件中的自定义实体,典型的例子是增删改查
    Admin API 是一个 Lapis 应用程序,Kong 提供了抽象,用户可以轻松添加端点

    模块

    kong.plugins.<plugin_name>.api
    

    在 Admin API 上添加端点

    如果用户以这个格式定义模块,Kong 会检测并加载端点:

    "kong.plugins.<plugin_name>.api"
    

    这个模块必须返回一个 table,结构如下:

    {
      ["<path>"] = {
         schema = <schema>,
         methods = {
           before = function(self) ... end,
           on_error = function(self) ... end,
           GET = function(self) ... end,
           PUT = function(self) ... end,
           ...
         }
      },
      ...
    }
    

    其中:

    • <path>:表示一个路径,路由中可以包含差值参数
    • <schema>:结构定义,核心或者自定义插件实体的 schema 可以通过 kong.db.<entity>.schema 获取,schema 用于判断字段根据什么类型解析,默认情况下,表单字段的类型都是 string
    • methods:包含了一系列方法,索引是字符串
    1. before 键是可选的,可以定义一个方法,如果存在,则在调用任何其他方法之前,都会执行这个方法
    2. 可以使用 HTTP 名称(如 GETPUT)作为索引,当匹配对应的路径和 HTTP 方法时,将执行索引对应的方法,如果在路径上存在 before 方法,则首先执行该方法,注意,before 方法可以使用 kong.response.exit 提前退出,这样可以跳过原有的 HTTP 方法
    3. on_error 键是可选的,可以定义一个方法,如果存在,当其他方法抛出错误时会执行该方法;如果不存在,Kong会使用默认错误处理程序返回错误
      例如:
    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 路径上创建两个 Admin API 端点,用来获取(GET)和创建(POST)绑定在消费者上的凭证,此示例中,方法由 kong.api.endpoints 库提供,如果想要查看更完整的示例,并在方法中使用自定义代码,请查看 key-auth 插件中的 api.lua 文件
    端点模块中当前包含了Kong中最常用的 CRUD 操作的默认实现,此模块为用户提供了增删改查的帮助程序,并执行对应的 DAO 层操作,使用响应的 HTTP 状态码回应,它还提供了从路径中检索参数的功能,例如服务名称或 ID,消费者用户名或ID
    如果端点提供的功能不够,可以使用常规的 Lua 方法:

    • endpoints 模块提供的方法
    • PDK 提供的所有方法
    • self 参数,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 参数,以供后续方法调用
    • GET 方法完全使用 endpoints,这是合理的,before 方法已经预先准备了东西,比如 self.consumer
    • PUT 方法在调用 endpoints 提供的 put_entity_endpoint 方法前,先填充了 self.args.post.consumer 参数

    测试用例

    简介

    如果用户认真对待自己的插件,需要为它编写测试用例,单元测试 Lua 脚本很简答,也有很多测试框架可以选择,如果用户还想编写集成测试,Kong 也提供了这样的功能

    编写集成测试用例

    Kong 首选的测试框架是 busted,与 resty-cli 解释器一起运行,如果用户愿意,也可以使用其他的,在 Kong 的仓库中,用户可以找到 busted 的可执行文件 bin/busted
    Kong 提供了一个帮助程序 spec.helpers,可以在测试套件中启停 Lua 脚本,这个帮助程序还能在运行测试之前在数据中插入或删除数据,以及其他各种帮助
    如果用户想在自己的仓库中编写插件,可以复制以下文件:

    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
    

    注意:当使用测试环境的 Kong 配置文件时,Kong代理监听9000和9443端口,Admin API 监听9001端口

    安装/卸载插件

    简介

    Kong 的自定义插件由Lua源文件组成,这些源文件需要安装在 Kong 节点的文件系统中,本章将逐步说明,使 Kong 节点可以理解用户的自定义插件
    这些步骤将作用于 Kong 集群的每个节点,以确保每个节点上都有用户的自定义插件

    打包源码

    用户可以使用常规打包策略(比如 tar),也可以使用 LuaRocks 包管理器来做执行这项工作,我们推荐使用 LuaRocks,因为它已经携带在官方发布的Kong安装包中
    当使用 LuaRocks,用户必须创建一个 rockspec 文件来指定包内容,有关示例可以参考Kong的插件模板,更多信息可以参考 LuaRocks 的文档,用户可以使用以下命令打包文件:

    # install it locally (based on the `.rockspec` in the current directory)
    luarocks make
    
    # pack the installed rock
    luarocks pack <plugin-name> <version>
    

    假设用户插件的 rockspec 文件叫 kong-plugin-my-plugin-0.1.0-1.rockspec,命令行为:

    luarocks pack kong-plugin-my-plugin 0.1.0-1
    

    LuaRocks 的 pack 指令创建了一个 .rock 文件(这是一个包含安装 rock 所需内容的 zip 文件)
    如果用户选择不使用 LuaRocks,可以使用 tar 指令将包含的 .lua 文件打包 .tar.gz 存档中
    存档的内容类似如下格式:

    tree <plugin-name>
    <plugin-name>
    ├── INSTALL.txt
    ├── README.md
    ├── kong
    │   └── plugins
    │       └── <plugin-name>
    │           ├── handler.lua
    │           └── schema.lua
    └── <plugin-name>-<version>.rockspec
    

    安装插件

    要使 Kong 节点能够使用自定义插件,必须在主机的文件系统上安装自定义插件的 Lua 源,有多种方法可以达成:通过 LuaRocks,或手动,选择其中一个,然后直接跳转到第3部分:

    1. 通过 LuaRocks 创建 rock
      .rock 文件是一个自包含的软件包,可以在本地安装,也可以从远程服务器安装,如果用户的系统中安装了 LuaRocks,可以在 LuaRocks 树中安装 rock,安装指令如下:
    luarocks install <rock-filename>
    
    1. 如果用户的系统中已经安装了 luarocks,用户可以将当前目录修改为插件存档的目录,其中 rockspec 文件是:
    cd <plugin-name>
    luarocks make
    

    这将在用户系统中的 LuaRocks 树中安装 kong/plugins/<plugin-name> 的源文件

    1. 一个更保险的安装方式是避免污染 LuaRocks 树,而是将 Kong 指向包含它们的目录,这通过调整 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> 替换,不要修改它
    ;;:默认 Lua 路径的占位符,不要修改它
    例如:
    something 这个插件的 handler 文件在文件系统中这个位置:

    /usr/local/custom/kong/plugins/<something>/handler.lua
    

    Kong的目录是 /usr/local/custom,因此,正确的路径可以设置为:

    lua_package_path = /usr/local/custom/?.lua;;
    

    多个插件:如果用户希望通过这个方法安装多个插件,可以这样设置变量:

    lua_package_path = /path/to/plugin1/?.lua;/path/to/plugin2/?.lua;;
    

    ;:多个目录之间的分隔符
    ;;:依旧表示默认 Lua 路径的占位符

    加载插件

    用户必须将自定义插件的名称添加到Kong配置中的插件列表(每个节点都需要):

    plugins = bundled,<plugin-name>
    

    或者,用户可以不添加绑定的插件:

    plugins = <plugin-name>
    

    如果用户需要使用多个插件,可以用逗号分隔:

    plugins = bundled,plugin1,plugin2
    

    plugins = plugin1,plugin2
    

    注意,用户还可以通过环境变量 KONG_PLUGINS 来设置此属性,不要忘记更新Kong集群中每个节点的 plugins 指令,插件重启会生效

    kong restart
    

    如果用户不希望Kong停机并加载上插件,可以这样:

    kong prepare 
    kong reload
    

    验证加载插件

    现在用户可以正常启动 Kong 了,为了确保插件被 Kong 加载,可以使用调试日志级别启动 Kong:

    log_level = debug
    

    或者

    KONG_LOG_LEVEL=debug
    

    然后,用户可以看到加载的每个插件

    [debug] Loading plugin <plugin-name>
    

    删除插件

    删除插件通常有3个步骤:

    1. 先从 Kong 的服务或路由配置中删除插件,确保该插件不再作用域全局,也不作用任何服务、路由或消费者,对于整个 Kong 集群,只需执行一次整个操作,不需要执行 restart 或 reload 指令,这个步骤只是让集群不再使用该插件,但它仍然可以再被启用
    2. plugins 指令删除插件,确保在执行此操作之前已完成步骤1,在此步骤之后,任何人都不能将插件重新应用在服务、路由、消费者或者全局中,此步骤需要执行 restart 或 reload 指令才能生效
    3. 要彻底删除插件,要在每个 Kong 节点删除与插件相关的文件,在删除文件之前,确保已完成步骤2,包括重启 Kong,如果用户之前使用 LuaRocks 安装插件,可以使用 luarocks remove <plugin-name> 指令来删除

    发布插件

    首选的方法是使用 LuaRocks,Lua 模块的包管理器,我们称这些模块是 rock,用户不必将模块存储在 Kong 的仓库中,如果用户希望维持Kong的设置,则需要这样做
    通过在 rockspec 文件中定义模块(及其依赖项),用户可以通过 LuaRocks 在平台上安装模块,用户也可以使用 LuaRocks 上传模块给其他人使用

    排除故障

    由于以下几个原因,配置错误的自定义插件可能无法启动:

    • plugin is in use but not enabled:用户在其他节点配置了自定义插件,并且数据库中已经保存了插件的配置,但是当前节点的 plugins 指令中没有找到该插件,要解决此问题,需要将每个节点都添加 plugins 指令
    • plugin is enabled but not installed:插件已添加在 plugins 指令中,但Kong无法从文件系统中加载 handler.lua 源文件,要解决此问题,确保正确设置 lua_package_path 指令来加载 Lua 源文件
    • no configuration schema found for plugin:插件已安装,但Kong无法从文件系统中加载 schema.lua 源文件,要解决此问题,确保 schema.luahandler.lua 文件一起存在

    相关文章

      网友评论

        本文标题:Kong插件开发向导

        本文链接:https://www.haomeiwen.com/subject/gouhyctx.html