kong/autodoc/admin-api/openapi-gen.lua (338 lines of code) (raw):
setmetatable(_G, nil) -- silence OpenResty's global var warnings
local admin_api_data = require "autodoc.admin-api.data.admin-api"
local kong_meta = require "kong.meta"
local lfs = require "lfs"
local lyaml = require "lyaml"
local typedefs = require "kong.db.schema.typedefs"
local OPENAPI_VERSION = "3.1.0"
local KONG_CONTACT_NAME = "Kong"
local KONG_CONTACT_URL = "https://github.com/Kong/kong"
local LICENSE_NAME = "Apache 2.0"
local LICENSE_URL = "https://github.com/Kong/kong/blob/master/LICENSE"
local METHOD_NA_DBLESS = "This method is not available when using DB-less mode."
local METHOD_ONLY_DBLESS = "This method is only available when using DB-less mode."
local HTTP_METHODS = {
["GET"] = true,
["HEAD"] = true,
["POST"] = true,
["PUT"] = true,
["DELETE"] = true,
["CONNECT"] = true,
["OPTIONS"] = true,
["TRACE"] = true,
["PATCH"] = true,
}
local entities_path = "kong/db/schema/entities"
local routes_path = "kong/api/routes"
-- workaround to load module files
_KONG = require("kong.meta") -- luacheck: ignore
kong = require("kong.global").new() -- luacheck: ignore
kong.configuration = { -- luacheck: ignore
loaded_plugins = {},
loaded_vaults = {},
}
kong.db = require("kong.db").new({ -- luacheck: ignore
database = "postgres",
})
kong.configuration = { -- luacheck: ignore
loaded_plugins = {},
loaded_vaults = {},
}
local property_format = {
["auto_timestamp_ms"] = "float",
["auto_timestamp_s"] = "int32",
["host"] = "hostname",
["ip"] = "ip",
["port"] = "int32",
["uuid"] = "uuid",
}
local property_type = {
["array"] = "array",
["boolean"] = "boolean",
["foreign"] = nil,
["integer"] = "integer",
["map"] = "array",
["number"] = "number",
["record"] = "array",
["set"] = "array",
["string"] = "string",
["auto_timestamp_ms"] = "number",
["auto_timestamp_s"] = "integer",
["destinations"] = "array",
["header_name"] = "string",
["host"] = "string",
["host_with_optional_port"] = "string",
["hosts"] = "array",
["ip"] = "string",
["methods"] = "array",
["path"] = "string",
["paths"] = "array",
["port"] = "integer",
["protocols"] = "array",
["semantic_version"] = "string",
["sources"] = "array",
["tag"] = "string",
["tags"] = "array",
["utf8_name"] = "string",
["uuid"] = "string",
}
local property_enum = {
["protocols"] = {
"http",
"https",
"tcp",
"tls",
"udp",
"grpc",
"grpcs"
},
}
local property_minimum = {
["port"] = 0,
}
local property_maximum = {
["port"] = 65535,
}
local function sanitize_text(text)
if text == nil then
return text
end
if type(text) ~= "string" then
error("invalid type received: " .. type(text) ..
". sanitize_text() sanitizes text", 2)
end
-- remove all <div></div> from text
text = text:gsub("<div.->(.-)</div>","")
return text
end
local function get_openapi()
local openapi = OPENAPI_VERSION
return openapi
end
local function get_info()
local info = {
["title"] = "Kong Admin API",
["summary"] = "Kong RESTful Admin API for administration purposes.",
["description"] = sanitize_text(admin_api_data["intro"][1]["text"]),
["version"] = kong_meta._VERSION,
["contact"] = {
["name"] = KONG_CONTACT_NAME,
["url"] = KONG_CONTACT_URL,
--["email"] = "",
},
["license"] = {
["name"] = LICENSE_NAME,
["url"] = LICENSE_URL,
},
}
return info
end
local function get_servers()
local servers = {
{
["url"] = "http://localhost:8001",
["description"] = "8001 is the default port on which the Admin API listens.",
},
{
["url"] = "https://localhost:8444",
["description"] = "8444 is the default port for HTTPS traffic to the Admin API.",
},
}
return servers
end
local function get_package_from_path(path)
if type(path) ~= "string" then
error("path must be a string, but it is " .. type(path), 2)
end
local package = path:gsub("(.lua)","")
package = package:gsub("/",".")
return package
end
local function get_property_reference(reference)
local reference_path
if reference ~= nil and type(reference) == "string" then
reference_path = "#/components/schemas/" .. reference
end
return reference_path
end
local function get_full_type(properties)
local actual_type
if properties.type == nil or properties.type == "foreign" then
return nil
end
for type_name, type_content in pairs(typedefs) do
if properties == type_content then
actual_type = type_name
break
end
end
if actual_type == nil and properties.type then
actual_type = properties.type
end
return actual_type
end
local function get_field_details(field_properties)
local details = {}
local actual_type = get_full_type(field_properties)
if actual_type then
details.type = property_type[actual_type]
details.format = property_format[actual_type]
details.enum = property_enum[actual_type]
details.minimum = property_minimum[actual_type]
details.maximum = property_maximum[actual_type]
end
details["$ref"] = get_property_reference(field_properties.reference)
if field_properties.default == ngx.null then
details.nullable = true
details.default = lyaml.null
else
details.default = field_properties.default
end
return details
end
local function get_properties_from_entity_fields(fields)
local properties = {}
local required = {}
for _, field in ipairs(fields) do
for field_name, field_props in pairs(field) do
properties[field_name] = get_field_details(field_props)
if field_props.required then
table.insert(required, field_name)
end
end
end
return properties, required
end
local function get_schemas()
local schemas = {}
for file in lfs.dir(entities_path) do
if file ~= "." and file ~= ".." then
local entity_path = entities_path .. "/" .. file
local entity_package = get_package_from_path(entity_path)
local entity = require(entity_package)
if entity then
if entity.name then -- TODO: treat special case "routes_subschemas"
local entity_content = {}
entity_content.type = "object"
entity_content.properties, entity_content.required = get_properties_from_entity_fields(entity.fields)
schemas[entity.name] = entity_content
end
end
end
end
return schemas
end
local function get_components()
local components = {}
components.schemas = get_schemas()
return components
end
local function get_all_routes()
local routes = {}
for file in lfs.dir(routes_path) do
if file ~= "." and file ~= ".." then
local route_path = routes_path .. "/" .. file
local route_package = get_package_from_path(route_path)
local route = require(route_package)
table.insert(routes, route)
end
end
return routes
end
local function is_http_method(name)
return HTTP_METHODS[name] == true
end
local function translate_path(entry)
if entry:len() < 2 then
return entry
end
local translated = ""
for segment in string.gmatch(entry, "([^/]+)") do
if segment:byte(1) == string.byte(":") then
segment = "{" .. segment:sub(2, segment:len()) .. "}"
end
translated = translated .. "/" .. segment
end
return translated
end
local function fill_paths(paths)
local entities = admin_api_data.entities
local general_routes = admin_api_data.general
local path_content = {}
-- extract path details from entities
for name, entity in pairs(entities) do
for entry, content in pairs(entity) do
if type(entry) == "string" and entry:sub(1,1) == "/" then
path_content[entry] = content
end
end
end
-- extract path details from general entries
for x, content in pairs(general_routes) do
if type(content) == "table" then
for entry, entry_content in pairs(content) do
if type(entry) == "string" and entry:sub(1,1) == "/" then
path_content[entry] = entry_content
end
end
end
end
-- fill received paths
for path, methods in pairs(paths) do
for method, content in pairs(methods) do
if path == "/config" then
content.description = METHOD_ONLY_DBLESS
elseif method ~= "get" then
content.description = METHOD_NA_DBLESS
end
if path_content[path] and path_content[path][method:upper()] then
content.summary = path_content[path][method:upper()].title
end
end
-- translate :entity to {entity} in paths
local actual_path = translate_path(path)
if actual_path ~= path then
paths[actual_path] = paths[path]
paths[path] = nil
end
end
end
local function get_paths()
local paths = {}
local routes = get_all_routes()
for _, route in ipairs(routes) do
for entry, functions in pairs(route) do
if type(entry) == "string" and entry:sub(1,1) == "/" then
paths[entry] = {}
for function_name, _ in pairs(functions) do
if is_http_method(function_name) then
paths[entry][function_name:lower()] = {}
end
end
end
end
end
fill_paths(paths)
return paths
end
local function write_file(filename, content)
local pok, yaml, err = pcall(lyaml.dump, { content })
if not pok then
error("lyaml failed: " .. yaml, 2)
end
if not yaml then
print("creating yaml failed: " .. err, 2)
end
-- drop the multi-document "---\n" header and "\n..." trailer
local content = yaml:sub(5, -5)
if content then
local file, errmsg = io.open(filename, "w")
if errmsg then
error("could not open " .. filename .. " for writing: " .. errmsg, 2)
end
file:write(content)
file:close()
end
end
local function main(filepath)
local openapi_spec_content = {}
openapi_spec_content.openapi = get_openapi()
openapi_spec_content.info = get_info()
openapi_spec_content.servers = get_servers()
openapi_spec_content.components = get_components()
openapi_spec_content.paths = get_paths()
write_file(filepath, openapi_spec_content)
end
main(arg[1])