daos.lua

Uses: Kong Gateway

The daos.lua file is used to define schemas for custom entities.

A schema is a Lua table which describes entities. There’s structural information such as the names and types of different fields, which is similar to the fields describing your plugin configuration. However, custom entity schemas require additional metadata. For example, you need to specify which field, or fields, constitute the entity’s primary key.

The daos.lua file should return a table containing one or more schemas. For example, the following configuration defines a keyauth_credentials:

local typedefs = require "kong.db.schema.typedefs"


return {
  {
    name                  = "keyauth_credentials", -- the name of table in the database
    endpoint_key          = "key",
    primary_key           = { "id" },
    cache_key             = { "key" },
    generate_admin_api    = true,
    admin_api_name        = "key-auths",
    admin_api_nested_name = "key-auth",
    fields = {
      {
        -- a value to be generated by the DAO itself
        id = typedefs.uuid,
      },
      {
        -- also inserted 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,
        },
      },
    },
  },
}

Here is a description of some top-level properties:

Name

Type

Required

Description

name string The name of database table, which is also used in the DAO name: kong.db.{name}.
primary_key table Field names forming the entity’s primary key. Schemas support composite keys, even if most Kong Gateway core entities currently use an UUID named id.
endpoint_key string The name of the field used as an alternative identifier on the Admin API. In the example above, key is the endpoint_key. This means that a credential with id = 123 and key = "foo" could be referenced as both /keyauth_credentials/123 and /keyauth_credentials/foo.
cache_key table Contains the name of the fields used for generating the cache_key, a string which must unequivocally identify the entity inside Kong Gateway’s cache. A unique field, like key in the example above, is usually good candidate. In other cases a combination of several fields is preferable.
generate_admin_api boolean Whether to auto-generate an Admin API for the entity or not. By default the Admin API is generated for all DAOs, including custom ones. If you want to create a fully customized Admin API for the DAO or want to disable auto-generation for the DAO altogether, set this option to false.
admin_api_name string When generate_admin_api is enabled, the Admin API auto-generator uses the name to derive the URLs for the auto-generated Admin API. Sometimes you may want to name the URLs differently from the name. In the example above, the keyauth_credentials endpoints will use the name key-auths.
admin_api_nested_name string Similar to admin_api_name, the admin_api_nested_name parameter specifies the name for a DAO that the Admin API auto-generator creates in nested contexts. You only need to use this parameter if you don’t want to use name or admin_api_name. For example, Kong Gateway for legacy reasons has urls such as consumers/john/key-auth where key-auth does not follow plural form of /key-auths. admin_api_nested_name enables you to specify a different name in those cases.
fields table Each field definition is a table with a single key, which is the field’s name. The table value is a sub-table containing the field’s attributes, some of which will be described in the table below.

Many field attributes encode validation rules. When attempting to insert or update entities using the DAO, these validations will be checked, and an error returned if the provided input doesn’t conform to them.

The typedefs variable (obtained by requiring kong.db.schema.typedefs) is a table containing a lot of useful type definitions and aliases, including typedefs.uuid, the most usual type for the primary key, and typedefs.auto_timestamp_s, for created_at fields. It is used extensively when defining fields.

Here’s a non-exhaustive list of available field attributes:

Name

Type

Description

type string The attribute type. The following values are supported:
  • string
  • integer
  • number
  • boolean
  • array
  • record
  • set

In addition to these values, the type attribute can also take the special "foreign" value, which denotes a foreign relationship.

Each field needs to be backed by a database field of a similar type, created via migrations.

type is the only required attribute for all field definitions.

default any (matching with the value to type) Specifies the value the field will have when attempting to insert it, if no value was provided. Default values are always set via Lua, never by the underlying database. Thus it’s not recommended to set any default values on fields in migrations.
required boolean When set to true on a field, an error will be thrown when attempting to insert an entity without a value for this field, unless the field in question has a default value.
unique boolean When set to true on a field, an error will be thrown when attempting to insert an entity in the database if another entity already has the same value for this field.

This attribute must be backed up by declaring the field as UNIQUE in migrations.

auto boolean When set to true on a field, a value will be autogenerated when attempting to insert an entity without a value for this field. The value will be generated as follows:
  • If type == "uuid", the field will take a random UUID as value.
  • If type == "string", the field will take a random string.
  • If the field name is created_at or updated_at, the field will take the current time when inserting or updating, respectively.
reference string Reference to another schema. This is required for fields of type foreign. The given string must be the name of an existing schema, to which the foreign key will point to. This means that if a schema B has a foreign key pointing to schema A, then A needs to be loaded before B.
on_delete string Optional and exclusive for fields of type foreign. It dictates what must happen with entities linked by a foreign key when the entity being referenced is deleted. It can have three possible values:
  • cascade: When the linked entity is deleted, all the dependent entities must also be deleted.
  • null: When the linked entity is deleted, all the dependent entities will have their foreign key field set to null.
  • restrict: Attempting to delete an entity with linked entities will result in an error.

To learn more about schemas, see:

  • The source code of typedefs.lua to get an idea of what’s provided there by default.
  • The core schemas to see examples of some other field attributes not documented here.
  • The daos.lua files for existing plugins, such as the key-auth one.

Custom entity DAO

The schemas are not used directly to interact with the database. Instead, a DAO is built for each valid schema. A DAO takes the name of the schema it wraps, and is accessible through the kong.db interface.

For the example schema above, the DAO generated would be available for plugins via kong.db.keyauth_credentials.

The following examples describe how to interact with the entity using the custom DAO in the handler.lua file.

Select an entity

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

This code attempts to find an entity in the database and return it. Three things can happen:

  • The entity is found. In this case, it’s returned as a regular Lua table.
  • An error occurred, for example the connection with the database was lost. In that case the first returned value will be nil, the second one will be a string describing the error, and the last one will be the same error in table form.
  • An error doesn’t occur but the entity isn’t found. Then the function will just return nil, with no error.

The following example looks for a specific credential and checks for errors and missing values:

local entity, err = kong.db.keyauth_credentials:select({
  id = "c77c50d2-5947-4904-9f37-fa36182a71a9"
})

if err then
  kong.log.err("Error when selecting keyauth credential: " .. err)
  return nil
end

if not entity then
  kong.log.err("Could not find credential.")
  return nil
end

Iterate over all the entities

for entity, err in kong.db.<name>:each(entities_per_page) do
  if err then
    ...
  end
  ...
end

This method efficiently iterates over all the entities in the database by making paginated requests. The entities_per_page parameter, which defaults to 100, controls how many entities per page are returned.

On each iteration, a new entity will be returned or, if there is any error, the err variable will be populated with an error. The recommended way to iterate is checking err first, and otherwise assume that entity is present.

The following example iterates over the credentials in pages of 1000 items, logging their IDs unless an error happens:

for credential, err in 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

Insert an entity

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

This code inserts an entity in the database, and returns a copy of the inserted entity, or nil, an error message, and a table describing the error in table form.

When the insert is successful, the returned entity contains the extra values produced by default and auto.

The following example uses the keyauth_credentials DAO to insert a credential for a given Consumer, setting its key to "secret". Notice the syntax for referencing foreign keys:

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

The returned entity, assuming no error happened will have auto-filled fields, like id and created_at.

Update an entity

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

This code updates an existing entity, if it can be found using the provided primary key and a set of values.

The returned entity will be the entity after the update takes place, or nil with an error message and an error table.

The following example modifies the key field of an existing credential given the credential’s ID:

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

Notice how the syntax for specifying a primary key is similar to the one used to specify a foreign key.

Upsert an entity

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

upsert is a mixture of insert and update:

  • When the provided primary_key identifies an existing entity, it works like update.
  • When the provided primary_key does not identify an existing entity, it works like insert

Given this code:

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

Two things can happen:

  • If a credential with ID 2b6a2022-770a-49df-874d-11e2bf2634f5 exists, then this code will attempt to set its Consumer to the provided one.
  • If the credential doesn’t exist, then this code attempts to create a new credential, with the given ID and Consumer.

Delete an entity

local ok, err, err_t = kong.db.<name>:delete(primary_key)

This code attempts to delete the entity identified by primary_key. It returns true if the entity doesn’t exist after calling this method, or nil with and error and error table if an error is detected.

Notice that calling delete will succeed if the entity didn’t exist before calling it. This is for performance reasons, we want to avoid doing a read-before-delete if we can avoid it. If you want to do this check, you must do it manually, by checking with select before invoking delete.

The following example deletes a credential:

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

Cache custom entities

Your plugin may need to frequently access custom entities on every request and/or response. Usually, loading them once and caching them in-memory dramatically improves the performance while making sure the data store is not stressed with an increased load.

Think of an API key authentication plugin that needs to validate the API key on every request, thus loading the custom credential object from the data store on every request. When the client provides an API key along with the request, normally you would query the data store to check if that key exists, and then either block the request or retrieve the Consumer ID to identify the user. This would happen on every request, and it would be very inefficient:

  • Querying the data store adds latency on every request, making the request processing slower.
  • The data store would also be affected by an increase of load, potentially crashing or slowing down, which in turn would affect every Kong Gateway node.

To avoid querying the data store every time, we can cache custom entities in-memory on the node, so that frequent entity lookups don’t trigger a data store query every time (only the first time), but happen in-memory, which is much faster and reliable that querying it from the data store.

Once you have defined your custom entities, you can cache them in-memory in your code by using the kong.cache module:

local cache = kong.cache

There are 2 levels of cache:

  1. L1: Lua memory cache - local to an Nginx worker process. This can hold any type of Lua value.
  2. L2: Shared memory cache (SHM) - local to an Nginx node, but shared between all the workers. This can only hold scalar values, and hence requires (de)serialization of a more complex types such as Lua tables.

When data is fetched from the database, it will be stored in both caches. If the same worker process requests the data again, it will retrieve the previously deserialized data from the Lua memory cache. If a different worker within the same Nginx node requests that data, it will find the data in the SHM, deserialize it, store it in its own Lua memory cache, and then return it.

This module exposes the following functions:

Function

Description

value, err = cache:get(key, opts?, cb, ...) Retrieves the value from the cache. If the cache does not have the value, it invokes cb in protected mode. cb must return one (and only one) value that will be cached. It can throw errors, as those will be caught and properly logged by Kong Gateway, at the ngx.ERR level. This function does cache negative results (nil). As such, you must rely on its second argument err when checking for errors.
ttl, err, value = cache:probe(key) Checks if a value is cached. If it is, returns its remaining TTL. If not, returns nil. The value being cached can also be a negative caching. The third return value is the value being cached itself.
cache:invalidate_local(key) Evicts a value from the node’s cache.
cache:invalidate(key) Evicts a value from the node’s cache and propagates the eviction events to all other nodes in the cluster.
cache:purge() Evicts all values from the node’s cache.

To look up a credential with a specific API key, we would write something like this in handler.lua:

local CustomHandler = {
  VERSION  = "1.0.0",
  PRIORITY = 10,
}

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


function CustomHandler:access(config)

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

In the example above, we use various components from the Plugin Development Kit to interact with the request, cache module, and produce a response from the plugin.

With this mechanism in place, once a Consumer has made a request with their API key, the cache will be considered warm and subsequent requests won’t result in a database query.

The cache is used in several places in the Key-Auth plugin handler. Give that file a look in order to see how an official plugin uses the cache.

Update or delete a custom entity

Every time a cached custom entity is updated or deleted in the data store (i.e. using the Admin API), it creates an inconsistency between the data in the data store, and the data cached in the Kong Gateway nodes’ memory. To avoid this inconsistency, we need to evict the cached entity from the in-memory store and force Kong Gateway to request it again from the data store. We refer to this process as cache invalidation.

Cache invalidation for your entities

If you want your cached entities to be invalidated upon a CRUD operation rather than having to wait for them to reach their TTL, you have to follow a few steps. This process can be automated for most entities, but manually subscribing to some CRUD events might be required to invalidate some entities with more complex relationships.

Automatic cache invalidation

Cache invalidation can be provided out of the box for your entities if you rely on the cache_key property of your entity’s schema. For example, in the following schema:

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,
    admin_api_name        = "key-auths",
    admin_api_nested_name = "key-auth",    
    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,
        },
      },
    },
  },
}

We can see that we declare the cache key of this API key entity to be its key attribute. We use key here because it has a unique constraints applied to it. The attributes added to cache_key should result in a unique combination, so that no two entities could have the same cache key.

Adding this value allows you to use the following function on the DAO of that entity:

cache_key = kong.db.{entity-name}:cache_key(arg1, arg2, arg3, ...)

The arguments must be the attributes specified in your schema’s cache_key property, in the order they were specified. This function then computes a string value cache_key that is ensured to be unique.

For example, if we were to generate the cache_key of an API key:

local cache_key = kong.db.keyauth_credentials:cache_key("abcd")

This would produce a cache_key for the API key "abcd" (retrieved from one of the query’s arguments) that we can then use to retrieve the key from the cache (or fetch from the database if the cache is a miss):

local key       = kong.request.get_query_arg("apikey")
local cache_key = kong.db.keyauth_credentials:cache_key(key)

local credential, err = kong.cache:get(cache_key, nil, load_entity_key, apikey)
if err then
  kong.log.err(err)
  return kong.response.exit(500, { message = "Unexpected error" })
end

if not credential then
  return kong.response.exit(401, { message = "Invalid authentication credentials" })
end

If the cache_key is generated like this and specified in an entity’s schema, cache invalidation will be an automatic process: every CRUD operation that affects this API key will be make Kong Gateway generate the affected cache_key, and broadcast it to all of the other nodes on the cluster so they can evict that particular value from their cache, and fetch the fresh value from the data store on the next request.

When a parent entity is receiving a CRUD operation (for example, the Consumer owning this API key, as per our schema’s consumer_id attribute), Kong Gateway performs the cache invalidation mechanism for both the parent and the child entity.

Be aware of the negative caching that Kong Gateway provides. In the above example, if there is no API key in the data store for a given key, the cache module will store the miss as if it was a hit. This means that a Create event (one that would create an API key with this given key) is also propagated by Kong Gateway so that all nodes that stored the miss can evict it, and properly fetch the newly created API key from the data store.

See the cluster documentation to ensure that you have properly configured your cluster for such invalidation events.

Manual cache invalidation

In some cases, the cache_key property of an entity’s schema is not flexible enough, and you must manually invalidate its cache. Reasons for this could be that the plugin is not defining a relationship with another entity via the traditional foreign = "parent_entity:parent_attribute" syntax, or because it’s not using the cache_key method from its DAO, or even because it is somehow abusing the caching mechanism.

In those cases, you can manually setup your own subscriber to the same invalidation channels Kong Gateway is listening to, and perform your own, custom invalidation work.

To listen on invalidation channels inside of Kong Gateway, implement the following in your plugin’s init_worker handler:

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

Once the above listeners are in place for the entities, you can perform manual invalidations of any entity that your plugin has cached. For instance:

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

In many cases it’s worth to check whether implementing configure with the plugin solves the issue/need without having to use events. For examples events might work differently depending on Kong Gateway node’s role (traditional, DB-less, or Data Plane).

Something wrong?

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!
OSZAR »