Skip to content

Commit aa1b2c3

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` (`"default"`) as a name. Closes #196
1 parent 0b471d8 commit aa1b2c3

File tree

12 files changed

+576
-15
lines changed

12 files changed

+576
-15
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.10', '2.11', '3.1', '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
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+
- `roles.httpd` role to configure one or more HTTP servers (#196)
16+
1217
## [1.5.0] - 2023-03-29
1318

1419
### Added

CMakeLists.txt

+15-4
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,25 @@ 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}
3132
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
3233
COMMENT "Run luacheck"
3334
)
3435

35-
add_custom_target(luatest
36+
add_custom_target(luatest-coverage
3637
COMMAND ${LUATEST} -v --coverage --shuffle all:${seed}
3738
BYPRODUCTS ${CODE_COVERAGE_STATS}
3839
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
39-
COMMENT "Run regression tests"
40+
COMMENT "Run regression tests with coverage"
41+
)
42+
43+
add_custom_target(luatest
44+
COMMAND ${LUATEST} -v --shuffle all:${seed}
45+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
46+
COMMENT "Run regression tests without coverage"
4047
)
4148

4249
add_custom_target(coverage
@@ -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-coverage PROPERTIES ENVIRONMENT "${LUA_PATH};${LUA_CPATH};${LUA_SOURCE_DIR}")

README.md

+111
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+
* [roles.httpd](#roleshttpd)
5153
* [See also](#see-also)
5254

5355
## Prerequisites
@@ -502,6 +504,115 @@ 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+
### `roles.httpd`
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 `require('roles.httpd').get_server(name)` method.
532+
If the `name` argument is `nil`, the default server is returned
533+
(its name should be equal to constant
534+
`require('roles.httpd').DEFAULT_SERVER_NAME`, which is `"default"`).
535+
536+
Let's look at the example of using this role. Consider a new role
537+
`roles/hello_world.lua`:
538+
```lua
539+
local M = {}
540+
local server = {}
541+
542+
M.validate = function() end
543+
544+
M.apply = function(conf)
545+
server = require('roles.httpd').get_server(conf.httpd).httpd
546+
547+
server:route({
548+
path = '/hello/world',
549+
name = 'greeting',
550+
}, function(tx)
551+
return tx:render({text = 'Hello, world!'})
552+
end)
553+
end
554+
555+
M.stop = function()
556+
server:delete('greeting')
557+
end
558+
559+
return M
560+
```
561+
562+
This role accepts a server by name from a config and creates a route to return
563+
`Hello, world!` to every request by this route.
564+
565+
Then we need to write a simple config to start the Tarantool instance via
566+
`tt`:
567+
```yaml
568+
app:
569+
file: 'myapp.lua'
570+
571+
groups:
572+
group001:
573+
replicasets:
574+
replicaset001:
575+
roles: [roles.httpd, roles.hello_world]
576+
roles_cfg:
577+
roles.httpd:
578+
default:
579+
listen: 8081
580+
additional:
581+
listen: '127.0.0.1:8082'
582+
roles.hello_world:
583+
httpd: 'additional'
584+
instances:
585+
instance001:
586+
iproto:
587+
listen:
588+
- uri: '127.0.0.1:3301'
589+
```
590+
591+
In `myapp.lua` we need to write a script to initialize both roles:
592+
```lua
593+
local httpd_role = require('roles.httpd')
594+
local hello_role = require('roles.hello_world')
595+
596+
httpd_role.apply(config.roles_cfg.httpd)
597+
hello_role.apply(config.roles_cfg.hello_world)
598+
```
599+
600+
Next step, we need to start this instance using `tt start`:
601+
```bash
602+
$ tt start
603+
• Starting an instance [app:instance001]...
604+
$ tt status
605+
INSTANCE STATUS PID MODE
606+
app:instance001 RUNNING 2499387 RW
607+
```
608+
609+
And then, we can get the greeting by running a simple curl command from a
610+
terminal:
611+
```bash
612+
$ curl http://127.0.0.1:8082/hello/world
613+
Hello, world!
614+
```
615+
505616
## See also
506617

507618
* [Tarantool project][Tarantool] on GitHub

deps.sh

+8-7
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
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+
# Could be replaced with luatest >= 1.1.0 after a release.
9+
tt rocks install luatest
10+
tt rocks install luacheck 0.25.0
11+
tt rocks install luacov 0.13.0
12+
tt rocks install luafilesystem 1.7.0-2
1213

1314
# 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
15+
tt rocks install luacov-coveralls 0.2.3-1 --server=https://luarocks.org
16+
tt rocks install cluacov 0.1.2-1 --server=https://luarocks.org
1617

17-
tarantoolctl rocks make
18+
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

0 commit comments

Comments
 (0)