Skip to content

roles: introduce role for http servers #197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ 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)
- `httpd:delete(name)` method to delete named routes (#197)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have never seen anyone using name option for a route. Until now, I didn't know it even exists. As far as I understand, it actually works not only with named routes, but with unnamed ones as well by using path as a key?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we can't use just path. Routes are stored as opts (which is a table of many fields, some of them are filled inside of add_route function).

http/http/server.lua

Lines 1248 to 1259 in 2e3dbd2

if opts.name ~= nil then
if opts.name == 'current' then
error("Route can not have name 'current'")
end
if self.iroutes[ opts.name ] ~= nil then
errorf("Route with name '%s' is already exists", opts.name)
end
table.insert(self.routes, opts)
self.iroutes[ opts.name ] = #self.routes
else
table.insert(self.routes, opts)
end

And those opts stored in the map with the name as a key. If there are no name, then we can't get opts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. It uses name, BUT we could delete a route by path too. I don't see any problems.

Copy link
Contributor

@oleg-jukovec oleg-jukovec Sep 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only problem is to create a nice and non-confusing API for the call. Maybe:

httpd:delete(path, name)
httpd:delete(path)
httpd:delete(nil, name)

But it looks a little strange.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. It uses name, BUT we could delete a route by path too. I don't see any problems.

I'm not sure, but as far as I remember that httpd has that weird thing that allows to set multiple handlers on the same route since route is not a unique identificator (and only one of handlers will be invoked).


## [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}")
117 changes: 117 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 @@ -182,6 +184,13 @@ httpd:route({ path = '/objects', method = 'GET' }, handle3)
...
```

To delete a named route, use `delete()` method of the `httpd` object:

```lua
httpd:route({ path = '/path/to', name = 'route' }, 'controller#action')
httpd:delete('route')
```

The first argument for `route()` is a Lua table with one or more keys:

* `file` - a template file name (can be relative to.
Expand Down Expand Up @@ -502,6 +511,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
18 changes: 18 additions & 0 deletions http/server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,23 @@ local function add_route(self, opts, sub)
return self
end

local function delete_route(self, name)
local route = self.iroutes[name]
if route == nil then
return
end

self.iroutes[name] = nil
table.remove(self.routes, route)

-- Update iroutes numeration.
for n, r in ipairs(self.routes) do
if r.name then
self.iroutes[r.name] = n
end
end
end

local function url_for_httpd(httpd, name, args, query)

local idx = httpd.iroutes[ name ]
Expand Down Expand Up @@ -1357,6 +1374,7 @@ local exports = {

-- methods
route = add_route,
delete = delete_route,
match = match_route,
helper = set_helper,
hook = set_hook,
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)
Loading
Loading