Skip to content

Commit

Permalink
roles: introduce role for http servers
Browse files Browse the repository at this point in the history
This patch adds `roles.httpd`. This role allows to configure one or
more HTTP servers. Those servers could be reused by several other roles.

Servers could be accessed by their names (from the config).

`get_server(name)` method returns a server by its name. If `nil` is passed,
default server is returned. The server is default, if it has
`DEFAULT_SERVER_NAME` (`"default"`) as a name.

Closes #196
  • Loading branch information
DerekBum authored and oleg-jukovec committed Sep 12, 2024
1 parent 0b471d8 commit 977a618
Show file tree
Hide file tree
Showing 13 changed files with 596 additions and 15 deletions.
21 changes: 16 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,28 @@ jobs:
strategy:
fail-fast: false
matrix:
tarantool: ['1.10', '2.5', '2.6', '2.7', '2.8']
tarantool: ['1.10', '2.10', '2.11', '3.1', '3.2']
coveralls: [false]
include:
- tarantool: '2.10'
- tarantool: '2.11'
coveralls: true
runs-on: [ubuntu-20.04]
steps:
- uses: actions/checkout@master
- uses: tarantool/setup-tarantool@v1
- uses: tarantool/setup-tarantool@v3
with:
tarantool-version: ${{ matrix.tarantool }}

- name: Prepare the repo
run: curl -L https://tarantool.io/release/2/installer.sh | bash
env:
DEBIAN_FRONTEND: noninteractive

- name: Install tt cli
run: sudo apt install -y tt=2.4.0
env:
DEBIAN_FRONTEND: noninteractive

- name: Cache rocks
uses: actions/cache@v2
id: cache-rocks
Expand All @@ -40,8 +50,9 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Run tests and code coverage analysis
run: make -C build coverage
- name: Run tests without code coverage analysis
run: make -C build luatest
if: matrix.coveralls != true

- name: Send code coverage to coveralls.io
run: make -C build coveralls
Expand Down
2 changes: 2 additions & 0 deletions .luacheckrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ ignore = {
"143/string",
-- Accessing an undefined field of a global variable <table>.
"143/table",
-- Accessing an undefined field of a global variable <package>.
"143/package",
-- Unused argument <self>.
"212/self",
-- Redefining a local variable.
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]

### Fixed

- Fixed request crash with empty body and unexpected header Content-Type (#189)

### Added

- `roles.httpd` role to configure one or more HTTP servers (#196)

## [1.5.0] - 2023-03-29

### Added
Expand Down
13 changes: 10 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,25 @@ set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra")
string(RANDOM ALPHABET 0123456789 seed)

add_subdirectory(http)
add_subdirectory(roles)

add_custom_target(luacheck
COMMAND ${LUACHECK} ${PROJECT_SOURCE_DIR}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Run luacheck"
)

add_custom_target(luatest
add_custom_target(luatest-coverage
COMMAND ${LUATEST} -v --coverage --shuffle all:${seed}
BYPRODUCTS ${CODE_COVERAGE_STATS}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Run regression tests"
COMMENT "Run regression tests with coverage"
)

add_custom_target(luatest
COMMAND ${LUATEST} -v --shuffle all:${seed}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Run regression tests without coverage"
)

add_custom_target(coverage
Expand Down Expand Up @@ -65,4 +72,4 @@ add_custom_target(coveralls

set (LUA_PATH "LUA_PATH=${PROJECT_SOURCE_DIR}/?.lua\\;${PROJECT_SOURCE_DIR}/?/init.lua\\;\\;")
set (LUA_SOURCE_DIR "LUA_SOURCE_DIR=${PROJECT_SOURCE_DIR}")
set_target_properties(luatest PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_SOURCE_DIR}")
set_target_properties(luatest-coverage PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_SOURCE_DIR}")
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ http v2 revert and decisions regarding each reverted commit see
* [before\_dispatch(httpd, req)](#before_dispatchhttpd-req)
* [after\_dispatch(cx, resp)](#after_dispatchcx-resp)
* [Using a special socket](#using-a-special-socket)
* [Roles](#roles)
* [roles.httpd](#roleshttpd)
* [See also](#see-also)

## Prerequisites
Expand Down Expand Up @@ -502,6 +504,114 @@ server:start()
[socket_ref]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/socket/#socket-tcp-server
## Roles
Tarantool 3 roles could be accessed from this project.
### `roles.httpd`
It allows configuring one or more HTTP servers. Those servers could be reused
by several other roles.
Example of the configuration:
```yaml
roles_cfg:
roles.httpd:
default:
- listen: 8081
additional:
- listen: '127.0.0.1:8082'
```
Server address should be provided either as a URI or as a single port
(in this case, `0.0.0.0` address is used).
User can access every working HTTP server from the configuration by name,
using `require('roles.httpd').get_server(name)` method.
If the `name` argument is `nil`, the default server is returned
(its name should be equal to constant
`require('roles.httpd').DEFAULT_SERVER_NAME`, which is `"default"`).
Let's look at the example of using this role. Consider a new role
`roles/hello_world.lua`:
```lua
local M = { dependencies = { 'roles.httpd' } }
local server = {}
M.validate = function(conf)
if conf == nil or conf.httpd == nil then
error("httpd must be set")
end
local server = require('roles.httpd').get_server(conf.httpd)
if server == nil then
error("the httpd server " .. conf.httpd .. " not found")
end
end
M.apply = function(conf)
server = require('roles.httpd').get_server(conf.httpd)
server:route({
path = '/hello/world',
name = 'greeting',
}, function(tx)
return tx:render({text = 'Hello, world!'})
end)
end
M.stop = function()
server:delete('greeting')
end
return M
```
This role accepts a server by name from a config and creates a route to return
`Hello, world!` to every request by this route.
Then we need to write a simple config to start the Tarantool instance via
`tt`:
```yaml
app:
file: 'myapp.lua'
groups:
group001:
replicasets:
replicaset001:
roles: [roles.httpd, roles.hello_world]
roles_cfg:
roles.httpd:
default:
listen: 8081
additional:
listen: '127.0.0.1:8082'
roles.hello_world:
httpd: 'additional'
instances:
instance001:
iproto:
listen:
- uri: '127.0.0.1:3301'
```
Next step, we need to start this instance using `tt start`:
```bash
$ tt start
• Starting an instance [app:instance001]...
$ tt status
INSTANCE STATUS PID MODE
app:instance001 RUNNING 2499387 RW
```
And then, we can get the greeting by running a simple curl command from a
terminal:
```bash
$ curl http://127.0.0.1:8082/hello/world
Hello, world!
```
## See also
* [Tarantool project][Tarantool] on GitHub
Expand Down
15 changes: 8 additions & 7 deletions deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
set -e

# Test dependencies:
tarantoolctl rocks install luatest 0.5.7
tarantoolctl rocks install luacheck 0.25.0
tarantoolctl rocks install luacov 0.13.0
tarantoolctl rocks install luafilesystem 1.7.0-2
# Could be replaced with luatest >= 1.1.0 after a release.
tt rocks install luatest
tt rocks install luacheck 0.25.0
tt rocks install luacov 0.13.0
tt rocks install luafilesystem 1.7.0-2

# cluacov, luacov-coveralls and dependencies
tarantoolctl rocks install luacov-coveralls 0.2.3-1 --server=https://luarocks.org
tarantoolctl rocks install cluacov 0.1.2-1 --server=https://luarocks.org
tt rocks install luacov-coveralls 0.2.3-1 --server=https://luarocks.org
tt rocks install cluacov 0.1.2-1 --server=https://luarocks.org

tarantoolctl rocks make
tt rocks make
1 change: 1 addition & 0 deletions http-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ build = {
['http.version'] = 'http/version.lua',
['http.mime_types'] = 'http/mime_types.lua',
['http.codes'] = 'http/codes.lua',
['roles.httpd'] = 'roles/httpd.lua',
}
}

Expand Down
2 changes: 2 additions & 0 deletions roles/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Install.
install(FILES httpd.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/roles)
133 changes: 133 additions & 0 deletions roles/httpd.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
local checks = require('checks')
local urilib = require('uri')
local http_server = require('http.server')

local M = {
DEFAULT_SERVER_NAME = 'default',
}
local servers = {}

local function parse_listen(listen)
if listen == nil then
return nil, nil, "must exist"
end
if type(listen) ~= "string" and type(listen) ~= "number" then
return nil, nil, "must be a string or a number, got " .. type(listen)
end

local host
local port
if type(listen) == "string" then
local uri, err = urilib.parse(listen)
if err ~= nil then
return nil, nil, "failed to parse URI: " .. err
end

if uri.scheme ~= nil then
if uri.scheme == "unix" then
uri.unix = uri.path
else
return nil, nil, "URI scheme is not supported"
end
end

if uri.login ~= nil or uri.password then
return nil, nil, "URI login and password are not supported"
end

if uri.query ~= nil then
return nil, nil, "URI query component is not supported"
end

if uri.unix ~= nil then
host = "unix/"
port = uri.unix
else
if uri.service == nil then
return nil, nil, "URI must contain a port"
end

port = tonumber(uri.service)
if port == nil then
return nil, nil, "URI port must be a number"
end
if uri.host ~= nil then
host = uri.host
elseif uri.ipv4 ~= nil then
host = uri.ipv4
elseif uri.ipv6 ~= nil then
host = uri.ipv6
else
host = "0.0.0.0"
end
end
elseif type(listen) == "number" then
host = "0.0.0.0"
port = listen
end

if type(port) == "number" and (port < 1 or port > 65535) then
return nil, nil, "port must be in the range [1, 65535]"
end
return host, port, nil
end

local function apply_http(name, node)
local host, port, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse URI: " .. err)
end

if servers[name] == nil then
local httpd = http_server.new(host, port)
httpd:start()
servers[name] = {
httpd = httpd,
routes = {},
}
end
end

M.validate = function(conf)
if conf ~= nil and type(conf) ~= "table" then
error("configuration must be a table, got " .. type(conf))
end
conf = conf or {}

for name, node in pairs(conf) do
if type(name) ~= 'string' then
error("name of the server must be a string")
end

local _, _, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse http 'listen' param: " .. err)
end
end
end

M.apply = function(conf)
-- This should be called on the role's lifecycle, but it's better to give
-- a meaningful error if something goes wrong.
M.validate(conf)

for name, node in pairs(conf or {}) do
apply_http(name, node)
end
end

M.stop = function()
for _, server in pairs(servers) do
server.httpd:stop()
end
servers = {}
end

M.get_server = function(name)
checks('?string')

name = name or M.DEFAULT_SERVER_NAME
return (servers[name] or {}).httpd
end

return M
Loading

0 comments on commit 977a618

Please sign in to comment.