Skip to content

Commit 1c810bf

Browse files
committed
roles: introduce role for http servers
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` as a name. Closes #196
1 parent 0b471d8 commit 1c810bf

File tree

12 files changed

+562
-13
lines changed

12 files changed

+562
-13
lines changed

.github/workflows/test.yml

+15-4
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,28 @@ jobs:
1010
strategy:
1111
fail-fast: false
1212
matrix:
13-
tarantool: ['1.10', '2.5', '2.6', '2.7', '2.8']
13+
tarantool: ['1.10', '2.5', '2.6', '2.7', '2.8', '3.2']
1414
coveralls: [false]
1515
include:
1616
- tarantool: '2.10'
1717
coveralls: true
1818
runs-on: [ubuntu-20.04]
1919
steps:
2020
- uses: actions/checkout@master
21-
- uses: tarantool/setup-tarantool@v1
21+
- uses: tarantool/setup-tarantool@v3
2222
with:
2323
tarantool-version: ${{ matrix.tarantool }}
2424

25+
- name: Prepare the repo
26+
run: curl -L https://tarantool.io/release/2/installer.sh | bash
27+
env:
28+
DEBIAN_FRONTEND: noninteractive
29+
30+
- name: Install tt cli
31+
run: sudo apt install -y tt=2.4.0
32+
env:
33+
DEBIAN_FRONTEND: noninteractive
34+
2535
- name: Cache rocks
2636
uses: actions/cache@v2
2737
id: cache-rocks
@@ -40,8 +50,9 @@ jobs:
4050
env:
4151
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4252

43-
- name: Run tests and code coverage analysis
44-
run: make -C build coverage
53+
- name: Run tests without code coverage analysis
54+
run: make -C build luatest-no-coverage
55+
if: matrix.coveralls != true
4556

4657
- name: Send code coverage to coveralls.io
4758
run: make -C build coveralls

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## [Unreleased]
88

99
### Fixed
10+
1011
- Fixed request crash with empty body and unexpected header Content-Type (#189)
1112

13+
### Added
14+
15+
- `httpd` role to configure one or more HTTP servers (#196)
16+
1217
## [1.5.0] - 2023-03-29
1318

1419
### Added

CMakeLists.txt

+13-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra")
2525
string(RANDOM ALPHABET 0123456789 seed)
2626

2727
add_subdirectory(http)
28+
add_subdirectory(roles)
2829

2930
add_custom_target(luacheck
3031
COMMAND ${LUACHECK} ${PROJECT_SOURCE_DIR}
@@ -39,6 +40,12 @@ add_custom_target(luatest
3940
COMMENT "Run regression tests"
4041
)
4142

43+
add_custom_target(luatest-no-coverage
44+
COMMAND ${LUATEST} -v
45+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
46+
COMMENT "Run regression tests without coverage"
47+
)
48+
4249
add_custom_target(coverage
4350
COMMAND ${LUACOV} ${PROJECT_SOURCE_DIR} && grep -A999 '^Summary' ${CODE_COVERAGE_REPORT}
4451
DEPENDS ${CODE_COVERAGE_STATS}
@@ -63,6 +70,10 @@ add_custom_target(coveralls
6370
COMMENT "Send code coverage data to the coveralls.io service"
6471
)
6572

66-
set (LUA_PATH "LUA_PATH=${PROJECT_SOURCE_DIR}/?.lua\\;${PROJECT_SOURCE_DIR}/?/init.lua\\;\\;")
73+
set (LUA_PATH "LUA_PATH=${PROJECT_SOURCE_DIR}/?.lua\\;${PROJECT_SOURCE_DIR}/?/init.lua\\;\
74+
${PROJECT_SOURCE_DIR}/.rocks/share/tarantool/?.lua\\;\
75+
${PROJECT_SOURCE_DIR}/.rocks/share/tarantool/?/init.lua\\;\\;")
76+
set (LUA_CPATH "LUA_CPATH=${PROJECT_SOURCE_DIR}/.rocks/lib/tarantool/?.so\\;\
77+
${PROJECT_SOURCE_DIR}/.rocks/lib/tarantool/?/?.so\\;\\;")
6778
set (LUA_SOURCE_DIR "LUA_SOURCE_DIR=${PROJECT_SOURCE_DIR}")
68-
set_target_properties(luatest PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_SOURCE_DIR}")
79+
set_target_properties(luatest PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_CPATH};${LUA_SOURCE_DIR}")

README.md

+87
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ http v2 revert and decisions regarding each reverted commit see
4848
* [before\_dispatch(httpd, req)](#before_dispatchhttpd-req)
4949
* [after\_dispatch(cx, resp)](#after_dispatchcx-resp)
5050
* [Using a special socket](#using-a-special-socket)
51+
* [Roles](#roles)
52+
* [httpd](#httpd-role)
5153
* [See also](#see-also)
5254

5355
## Prerequisites
@@ -502,6 +504,91 @@ server:start()
502504

503505
[socket_ref]: https://www.tarantool.io/en/doc/latest/reference/reference_lua/socket/#socket-tcp-server
504506

507+
## Roles
508+
509+
New roles could be accessed from this project.
510+
511+
### `httpd` role
512+
513+
This role can be used only with Tarantool 3. It allows configuring one or more
514+
HTTP servers. Those servers could be reused by several other roles.
515+
516+
Example of the configuration:
517+
518+
```yaml
519+
roles_cfg:
520+
roles.httpd:
521+
default:
522+
- listen: 8081
523+
additional:
524+
- listen: '127.0.0.1:8082'
525+
```
526+
527+
Server address should be provided either as a URI or as a single port
528+
(in this case, `0.0.0.0` address is used).
529+
530+
User can access every working HTTP server from the configuration by name,
531+
using `get_server(name)` method.
532+
If the `name` argument is `nil`, the default server is returned
533+
(its name should be equal to constant `DEFAULT_SERVER_NAME`).
534+
535+
Let's look at the example of using this role. Consider a new role `hello_world`:
536+
```lua
537+
local http_client = require('http.client')
538+
539+
local M = {}
540+
local server = {}
541+
542+
M.apply = function(conf, http_server_role)
543+
server = http_server_role.get_server(conf.httpd).httpd
544+
545+
server:route({
546+
path = '/hello/world'
547+
}, function(tx)
548+
return tx:render({text = 'Hello, world!'})
549+
end)
550+
end
551+
552+
M.get = function()
553+
local server_uri = ('http://%s:%s'):format(server.host, server.port)
554+
return http_client.get(server_uri .. '/hello/world')
555+
end
556+
557+
return M
558+
```
559+
560+
This role accepts a server by name from a config and creates a route to return
561+
`Hello, world!` to every request by this route.
562+
563+
Config for `httpd` and `hello_world` might look like this:
564+
```yaml
565+
roles_cfg = {
566+
httpd = {
567+
default = {
568+
listen = 8081,
569+
},
570+
additional = {
571+
listen = '127.0.0.1:8082',
572+
}
573+
},
574+
hello_world = {
575+
httpd = 'additional',
576+
},
577+
}
578+
```
579+
580+
After that, we can get `Hello, world!` by just running this script on a
581+
Tarantool instance:
582+
```lua
583+
local httpd_role = require('roles.httpd')
584+
local hello_role = require('roles.hello_world')
585+
586+
httpd_role.apply(config.roles_cfg.httpd)
587+
hello_role.apply(config.roles_cfg.hello_world, httpd_role)
588+
589+
print(hello_role.get().body)
590+
```
591+
505592
## See also
506593

507594
* [Tarantool project][Tarantool] on GitHub

deps.sh

+20-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,26 @@
55
set -e
66

77
# Test dependencies:
8-
tarantoolctl rocks install luatest 0.5.7
9-
tarantoolctl rocks install luacheck 0.25.0
10-
tarantoolctl rocks install luacov 0.13.0
11-
tarantoolctl rocks install luafilesystem 1.7.0-2
8+
tt rocks install luatest
9+
tt rocks install luacheck 0.25.0
10+
tt rocks install luacov 0.13.0
1211

1312
# cluacov, luacov-coveralls and dependencies
14-
tarantoolctl rocks install luacov-coveralls 0.2.3-1 --server=https://luarocks.org
15-
tarantoolctl rocks install cluacov 0.1.2-1 --server=https://luarocks.org
13+
tt rocks install https://raw.githubusercontent.com/luarocks/cluacov/master/cluacov-dev-1.rockspec
14+
tt rocks install https://luarocks.org/manifests/dhkolf/dkjson-2.8-1.rockspec
15+
tt rocks install https://raw.githubusercontent.com/keplerproject/luafilesystem/master/luafilesystem-scm-1.rockspec
16+
tt rocks install https://raw.githubusercontent.com/moteus/lua-path/master/rockspecs/lua-path-scm-0.rockspec
1617

17-
tarantoolctl rocks make
18+
# Most of this code is the workaround for
19+
# https://github.com/moteus/luacov-coveralls/pull/30
20+
# Remove it, when the pull request will be merged.
21+
TMPDIR="$(mktemp -d)"
22+
LUACOV_COVERALLS_ROCKSPEC_URL="https://raw.githubusercontent.com/moteus/luacov-coveralls/master/rockspecs/luacov-coveralls-scm-0.rockspec"
23+
LUACOV_COVERALLS_ROCKSPEC_FILE="${TMPDIR}/luacov-coveralls-scm-0.rockspec"
24+
curl -fsSL "${LUACOV_COVERALLS_ROCKSPEC_URL}" > "${LUACOV_COVERALLS_ROCKSPEC_FILE}"
25+
sed -i -e 's@git://@git+https://@' "${LUACOV_COVERALLS_ROCKSPEC_FILE}"
26+
tt rocks install "${LUACOV_COVERALLS_ROCKSPEC_FILE}"
27+
rm "${LUACOV_COVERALLS_ROCKSPEC_FILE}"
28+
rmdir "${TMPDIR}"
29+
30+
tt rocks make

http-scm-1.rockspec

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ build = {
3131
['http.version'] = 'http/version.lua',
3232
['http.mime_types'] = 'http/mime_types.lua',
3333
['http.codes'] = 'http/codes.lua',
34+
['roles.httpd'] = 'roles/httpd.lua',
3435
}
3536
}
3637

roles/CMakeLists.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Install
2+
install(FILES httpd.lua DESTINATION ${TARANTOOL_INSTALL_LUADIR}/roles)

roles/httpd.lua

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
local checks = require('checks')
2+
local urilib = require("uri")
3+
local http_server = require('http.server')
4+
5+
local M = {
6+
DEFAULT_SERVER_NAME = 'default',
7+
}
8+
local servers = {}
9+
10+
local function parse_listen(listen)
11+
if listen == nil then
12+
return nil, nil, "must exist"
13+
end
14+
if type(listen) ~= "string" and type(listen) ~= "number" then
15+
return nil, nil, "must be a string or a number, got " .. type(listen)
16+
end
17+
18+
local host
19+
local port
20+
if type(listen) == "string" then
21+
local uri, err = urilib.parse(listen)
22+
if err ~= nil then
23+
return nil, nil, "failed to parse URI: " .. err
24+
end
25+
26+
if uri.scheme ~= nil then
27+
if uri.scheme == "unix" then
28+
uri.unix = uri.path
29+
else
30+
return nil, nil, "URI scheme is not supported"
31+
end
32+
end
33+
34+
if uri.login ~= nil or uri.password then
35+
return nil, nil, "URI login and password are not supported"
36+
end
37+
38+
if uri.query ~= nil then
39+
return nil, nil, "URI query component is not supported"
40+
end
41+
42+
if uri.unix ~= nil then
43+
host = "unix/"
44+
port = uri.unix
45+
else
46+
if uri.service == nil then
47+
return nil, nil, "URI must contain a port"
48+
end
49+
50+
port = tonumber(uri.service)
51+
if port == nil then
52+
return nil, nil, "URI port must be a number"
53+
end
54+
if uri.host ~= nil then
55+
host = uri.host
56+
elseif uri.ipv4 ~= nil then
57+
host = uri.ipv4
58+
elseif uri.ipv6 ~= nil then
59+
host = uri.ipv6
60+
else
61+
host = "0.0.0.0"
62+
end
63+
end
64+
elseif type(listen) == "number" then
65+
host = "0.0.0.0"
66+
port = listen
67+
end
68+
69+
if type(port) == "number" and (port < 1 or port > 65535) then
70+
return nil, nil, "port must be in the range [1, 65535]"
71+
end
72+
return host, port, nil
73+
end
74+
75+
local function apply_http(name, node)
76+
local host, port, err = parse_listen(node.listen)
77+
if err ~= nil then
78+
error("failed to parse URI: " .. err)
79+
end
80+
81+
if servers[name] == nil then
82+
local httpd = http_server.new(host, port)
83+
httpd:start()
84+
servers[name] = {
85+
httpd = httpd,
86+
routes = {},
87+
}
88+
end
89+
end
90+
91+
M.validate = function(conf)
92+
if conf ~= nil and type(conf) ~= "table" then
93+
error("configuration must be a table, got " .. type(conf))
94+
end
95+
conf = conf or {}
96+
97+
for name, node in pairs(conf) do
98+
if type(name) ~= 'string' then
99+
error("name of the server must be a string")
100+
end
101+
102+
local _, _, err = parse_listen(node.listen)
103+
if err ~= nil then
104+
error("failed to parse http 'listen' param: " .. err)
105+
end
106+
end
107+
end
108+
109+
M.apply = function(conf)
110+
-- This should be called on the role's lifecycle, but it's better to give
111+
-- a meaningful error if something goes wrong.
112+
M.validate(conf)
113+
114+
for name, node in pairs(conf or {}) do
115+
apply_http(name, node)
116+
end
117+
end
118+
119+
M.stop = function()
120+
for _, server in pairs(servers) do
121+
server.httpd:stop()
122+
end
123+
servers = {}
124+
end
125+
126+
M.get_server = function(name)
127+
checks('?string')
128+
129+
name = name or M.DEFAULT_SERVER_NAME
130+
return servers[name]
131+
end
132+
133+
return M

test/helpers.lua

+12
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ local socket = require('socket')
44

55
local helpers = table.copy(require('luatest').helpers)
66

7+
local luatest = require('luatest')
8+
local luatest_utils = require('luatest.utils')
9+
710
helpers.base_port = 12345
811
helpers.base_host = '127.0.0.1'
912
helpers.base_uri = ('http://%s:%s'):format(helpers.base_host, helpers.base_port)
@@ -101,4 +104,13 @@ helpers.teardown = function(httpd)
101104
end)
102105
end
103106

107+
helpers.is_tarantool3 = function()
108+
local tarantool_version = luatest_utils.get_tarantool_version()
109+
return luatest_utils.version_ge(tarantool_version, luatest_utils.version(3, 0, 0))
110+
end
111+
112+
helpers.skip_if_not_tarantool3 = function()
113+
luatest.skip_if(not helpers.is_tarantool3(), 'Only Tarantool 3 is supported')
114+
end
115+
104116
return helpers

0 commit comments

Comments
 (0)