apache / apisix

The Cloud-Native API Gateway
https://apisix.apache.org/blog/
Apache License 2.0
14.58k stars 2.53k forks source link

feat: support to use `plugin_config_id` for `consumer` object #5800

Open membphis opened 2 years ago

membphis commented 2 years ago

Issue description

Now, the router object supports the plugin_config_id, here is the doc link: https://github.com/apache/apisix/blob/master/docs/en/latest/admin-api.md#route

And I think we need to support plugin_config_id for consumer object.

What do you think?

Environment

kaori-seasons commented 2 years ago

@membphis

Issue description https://www.processon.com/view/link/626a5b656376891e1c158aad At present, the configuration items of the nginx file of apisix and the processing flow of APISIX are shown in the following figure

apisix process

In the calling phase of the ``http_access_phase```` method, the relevant upstream id will be obtained to split the sent data. And in the init.lua method, the following code will appear

init.lua

function _M.http_access_phase()

    local ngx_ctx = ngx.ctx

    if not verify_tls_client(ngx_ctx.api_ctx) then
        return core.response.exit(400)
    end

    -- always fetch table from the table pool, we don't need a reused api_ctx
    local api_ctx = core.tablepool.fetch("api_ctx", 0, 32)
    ngx_ctx.api_ctx = api_ctx
     local up_id = route.value.upstream_id

    -- used for the traffic-split plugin
    if api_ctx.upstream_id then
        up_id = api_ctx.upstream_id
    end

    if up_id then
 >     local upstream = get_upstream_by_id(up_id)
        api_ctx.matched_upstream = upstream

end

local function get_upstream_by_id(up_id)
    local upstreams = core.config.fetch_created_obj("/upstreams")
    if upstreams then
        local upstream = upstreams:get(tostring(up_id))
        if not upstream then
            core.log.error("failed to find upstream by id: " .. up_id)
            if is_http then
                return core.response.exit(502)
            end

            return ngx_exit(1)
        end

        if upstream.has_domain then
            local err
            upstream, err = parse_domain_in_up(upstream)
            if err then
                core.log.error("failed to get resolved upstream: ", err)
                if is_http then
                    return core.response.exit(500)
                end

                return ngx_exit(1)
            end
        end

        core.log.info("parsed upstream: ", core.json.delay_encode(upstream, true))
        return upstream.dns_value or upstream.value
    end
end

To understand how router.lua is called based on upstream_id, I traced the following code. In the request process of Route's admin api:

local function filter(route)
    route.orig_modifiedIndex = route.modifiedIndex
    route.update_count = 0

    route.has_domain = false
    if not route.value then
        return
    end

    if route.value.host then
        route.value.host = str_lower(route.value.host)
    elseif route.value.hosts then
        for i, v in ipairs(route.value.hosts) do
            route.value.hosts[i] = str_lower(v)
        end
    end

    apisix_upstream.filter_upstream(route.value.upstream, route)

    core.log.info("filter route: ", core.json.delay_encode(route, true))

To sum up, I think that consumer.lua should do some attribute extraction before calling the apisix_upstream.filter_upstream method and then execute the filter. So based on this idea, I decided to follow up the related call link of route call /upstream

I would like to know suppose now that I want to configure a set of admin api as shown below, how should I do the relevant verification on the consumer side to complete the connectivity test of calling /upstream from the route /consumer?

location /apisix/admin {
            set $upstream_scheme             'http';
            set $upstream_host               $http_host;
            set $upstream_uri                '';

                allow 127.0.0.0/24;
                deny all;

            content_by_lua_block {
                apisix.http_admin()
            }
        }
tzssangglass commented 2 years ago

Hi @complone , from your trace path, I think you have missed the key point, APISIX merge the consumer's plugins and route's plugins is here: https://github.com/apache/apisix/blob/4afc8a7fbf82d4311504abd00f08156a806e5b40/apisix/plugin.lua#L491-L513

kaori-seasons commented 2 years ago

@tzssangglass

I understand that I should define the relevant plugin_config_id in shcema_def.lua, and then set the relevant plugin_config_id in consumer.lua#plugin_consumer

shcema_def.lua

_M.consumer = {
    type = "object",
    properties = {
        username = {
            type = "string", minLength = 1, maxLength = rule_name_def.maxLength,
            pattern = [[^[a-zA-Z0-9_]+$]]
        },
  >    id = id_schema,
        plugins = plugins_schema,
        labels = labels_def,
        create_time = timestamp_def,
        update_time = timestamp_def,
        desc = desc_def,
    },
    required = {"username"},
}

consumer.lua

local function plugin_consumer()
    local plugins = {}

    if consumers.values == nil then
        return plugins
    end

    for _, consumer in ipairs(consumers.values) do
        if type(consumer) ~= "table" then
            goto CONTINUE
        end

        for name, config in pairs(consumer.value.plugins or {}) do
            local plugin_obj = plugin.get(name)
            if plugin_obj and plugin_obj.type == "auth" then
                if not plugins[name] then
                    plugins[name] = {
                        nodes = {},
                        conf_version = consumers.conf_version
                    }
                end

                local new_consumer = core.table.clone(consumer.value)
                -- Note: the id here is the key of consumer data, which
                -- is 'username' field in admin
          >     new_consumer.id = consumer.value.id

                new_consumer.consumer_name = new_consumer.id
                new_consumer.auth_conf = config
                core.log.info("consumer:", core.json.delay_encode(new_consumer))
                core.table.insert(plugins[name].nodes, new_consumer)
            end
        end

        ::CONTINUE::
    end

    return plugins
end

I will start testing the relevant request processing link in the near future