diff --git a/CHANGELOG.md b/CHANGELOG.md index 7372f17..aff86ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Introduce latency observation for http endpoint (#17). +- Support `roles.httpd` integration (#15). ### Fixed diff --git a/README.md b/README.md index e0bfd2b..8529cb3 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ tt build Be careful, it is better to use a latest release version. 2. Enable and [configure](https://www.tarantool.io/en/doc/latest/concepts/configuration/) -the `roles.metrics-export` role for a Tarantool 3 instance. +the `roles.metrics-export` role for a Tarantool 3 instance. Use [httpd role](https://github.com/tarantool/http?tab=readme-ov-file#roles) +or `listen` field in to configure server instances. See below to get more detailed inforamtion about it. ```yaml groups: @@ -37,10 +38,21 @@ groups: replicaset-001: instances: master: - roles: [roles.metrics-export] + roles: [roles.httpd, roles.metrics-export] roles_cfg: + roles.httpd: + default: + - listen: '127.0.0.1:8083' + additional: + - listen: 8084 roles.metrics-export: http: + - endpoints: + - path: /metrics/json + format: json + - server: 'additional' + - path: /metrics/prometheus + format: prometheus - listen: 8081 endpoints: - path: /metrics/json @@ -55,9 +67,13 @@ groups: format: json ``` -In the example above, we configure two HTTP servers on `0.0.0.0:8081` and -`my_host:8082`. The servers will be running on the `master` Tarantool -instance. +In the example above, we configure four HTTP servers. There are serveral server fields: + +* first with `server` field which refers to an `additional` server in the `httpd` role; +* the next one, with no info about server, is configured with `default` name in `httpd` config; +* and the last two `listen` fields (`0.0.0.0:8081` and `my_host:8082`) that are listed directly. + +The servers will be running on the `master` Tarantool instance. Each server has two endpoints: @@ -103,10 +119,46 @@ set `metrics.enabled` to `true`: For now only `json` and `prometheus` formats are supported. +### Integration with httpd role + +Use [httpd role](https://github.com/tarantool/http?tab=readme-ov-file#roles) as well. +To enable it, you need to fill `server` field with name that was configured in `roles.httpd` block +instead of `listen` like it was earlier. To configure `httpd` role you need to write block in roles_cfg +section: + +```yaml +roles_cfg: + roles.httpd: + default: + - listen: 8081 + additional: + - listen: '127.0.0.1:8082' +``` + +After it you can use `server` name in `roles.metrics-export` block. If `server` and `listen` names +wasn't provided, the `default` server from `httpd` role configuration will be used: + +```yaml +roles.metrics-export: + http: + - server: 'additional' + endpoints: + ... + - endpoints: + ... +``` + +So now it is possible to mix `server` and `listen` parameteres. + Let's put it all together now: ```yaml roles_cfg: + roles.httpd: + default: + - listen: 8081 + additional: + - listen: '127.0.0.1:8082' roles.metrics-export: http: - listen: 8081 @@ -121,6 +173,13 @@ roles_cfg: format: json metrics: enabled: true + - server: 'additional' + endpoints: + - path: /metrics + format: json + - endpoints: + - path: /metrics + format: prometheus ``` With this configuration, metrics can be obtained on this machine with the diff --git a/roles/metrics-export.lua b/roles/metrics-export.lua index addef36..0833519 100644 --- a/roles/metrics-export.lua +++ b/roles/metrics-export.lua @@ -1,5 +1,6 @@ local urilib = require("uri") local http_server = require('http.server') +local httpd_role = require('roles.httpd') local M = {} @@ -23,19 +24,6 @@ local function is_array(tbl) return true end -local function delete_route(httpd, name) - local route = assert(httpd.iroutes[name]) - httpd.iroutes[name] = nil - table.remove(httpd.routes, route) - - -- Update httpd.iroutes numeration. - for n, r in ipairs(httpd.routes) do - if r.name then - httpd.iroutes[r.name] = n - end - end -end - -- Removes extra '/' from start and end of the path to avoid paths duplication. local function remove_side_slashes(path) if path:startswith('/') then @@ -48,9 +36,6 @@ local function remove_side_slashes(path) end 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 @@ -170,14 +155,39 @@ local function validate_http_endpoint(endpoint) end end +-- check_server_httpd_role validates that httpd configuration and provided name exists. +local function check_server_httpd_role(server) + local httpd_roles_cfg = (require("config"):get("roles_cfg") or {})['roles.httpd'] + if httpd_roles_cfg == nil then + error("there is no configuration for httpd role", 4) + end + if httpd_roles_cfg[server] == nil then + error(("server with name %s not found in httpd role config"):format(server), 4) + end +end + local function validate_http_node(node) if type(node) ~= "table" then error("http configuration node must be a table, got " .. type(node), 3) end - local _, _, err = parse_listen(node.listen) - if err ~= nil then - error("failed to parse http 'listen' param: " .. err, 3) + if node.server ~= nil then + if type(node.server) ~= 'string' then + error("server configuration sould be a string, got " .. type(node.server), 3) + end + + if node.listen ~= nil then + error("it is not possible to provide 'server' and 'listen' blocks simultaneously", 3) + end + + check_server_httpd_role(node.server) + elseif node.listen ~= nil then + local _, _, err = parse_listen(node.listen) + if err ~= nil then + error("failed to parse http 'listen' param: " .. err, 3) + end + else + check_server_httpd_role(httpd_role.DEFAULT_SERVER_NAME) end node.endpoints = node.endpoints or {} @@ -221,16 +231,29 @@ local function validate_http(conf) end for i, nodei in ipairs(conf) do - local hosti, porti, erri = parse_listen(nodei.listen) - assert(erri == nil) -- We should already successfully parse the URI. + local listen_address, server_name = nodei.listen, nodei.server + local hosti, porti = nil, nil + if listen_address ~= nil then + local erri + hosti, porti, erri = parse_listen(listen_address) + assert(erri == nil) -- We should already successfully parse the URI. + end for j, nodej in ipairs(conf) do if i ~= j then - local hostj, portj, errj = parse_listen(nodej.listen) + listen_address = nodej.listen + if listen_address == nil then + if server_name ~= nil and server_name == nodej.server then + error("server names must have different targets in httpd", 2) + end + goto continue + end + local hostj, portj, errj = parse_listen(listen_address) assert(errj == nil) -- The same. if hosti == hostj and porti == portj then error("http configuration nodes must have different listen targets", 2) end end + ::continue:: end end end @@ -258,24 +281,52 @@ local function apply_http(conf) local enabled = {} for _, node in ipairs(conf) do if #(node.endpoints or {}) > 0 then - local host, port, err = parse_listen(node.listen) - if err ~= nil then - error("failed to parse URI: " .. err, 2) + local host, port, target + if node.server ~= nil then + target = { + value = node.server, + is_httpd_role = true, + } + elseif node.listen ~= nil then + local err + host, port, err = parse_listen(node.listen) + if err ~= nil then + error("failed to parse URI: " .. err, 2) + end + target = { + value = node.listen, + is_httpd_role = false, + } + else + target = { + value = httpd_role.DEFAULT_SERVER_NAME, + is_httpd_role = true, + } end - local listen = node.listen http_servers = http_servers or {} - enabled[listen] = true + -- Since the 'listen' and 'server' names of other servers in the config may be + -- the same, we create a unique string concatenating the key name and information + -- about whether it is an httpd key or not. + enabled[tostring(target.value) .. tostring(target.is_httpd_role)] = true + + if http_servers[tostring(target.value) .. tostring(target.is_httpd_role)] == nil then + local httpd + if node.listen ~= nil then + httpd = http_server.new(host, port) + httpd:start() + else + httpd = httpd_role.get_server(target.value) + end - if http_servers[listen] == nil then - local httpd = http_server.new(host, port) - httpd:start() - http_servers[listen] = { + http_servers[tostring(target.value) .. tostring(target.is_httpd_role)] = { httpd = httpd, routes = {}, + is_httpd_role = target.is_httpd_role, } end - local server = http_servers[listen] + + local server = http_servers[tostring(target.value) .. tostring(target.is_httpd_role)] local httpd = server.httpd local old_routes = server.routes @@ -291,7 +342,7 @@ local function apply_http(conf) -- Remove old routes. for path, e in pairs(old_routes) do if new_routes[path] == nil or not routes_equal(e, new_routes[path]) then - delete_route(httpd, path) + httpd:delete(path) old_routes[path] = nil end end @@ -315,17 +366,29 @@ local function apply_http(conf) end end - for listen, server in pairs(http_servers) do - if not enabled[listen] then - server.httpd:stop() - http_servers[listen] = nil + for target, server in pairs(http_servers) do + if not enabled[target] then + if server.is_httpd_role then + for path, _ in pairs(server.routes) do + server.httpd:delete(path) + end + else + server.httpd:stop() + end + http_servers[target] = nil end end end local function stop_http() for _, server in pairs(http_servers or {}) do - server.httpd:stop() + if server.is_httpd_role then + for path, _ in pairs(server.routes) do + server.httpd:delete(path) + end + else + server.httpd:stop() + end end http_servers = nil end diff --git a/test/entrypoint/config.yaml b/test/entrypoint/config.yaml index 49bf315..bcc273d 100644 --- a/test/entrypoint/config.yaml +++ b/test/entrypoint/config.yaml @@ -9,8 +9,13 @@ groups: replicaset-001: instances: master: - roles: [roles.metrics-export] + roles: [roles.httpd, roles.metrics-export] roles_cfg: + roles.httpd: + default: + listen: 8085 + additional: + listen: '127.0.0.1:8086' roles.metrics-export: http: - listen: 8081 @@ -29,6 +34,19 @@ groups: format: prometheus metrics: enabled: true + - endpoints: + - path: /metrics/prometheus + format: prometheus + - path: /metrics/json + format: json + - server: 'additional' + endpoints: + - path: /metrics/prometheus + format: prometheus + - path: /metrics/json + format: json + metrics: + enabled: true iproto: listen: - uri: '127.0.0.1:3313' diff --git a/test/helpers/mocks.lua b/test/helpers/mocks.lua new file mode 100644 index 0000000..22ee3f5 --- /dev/null +++ b/test/helpers/mocks.lua @@ -0,0 +1,52 @@ +-- M is a mocks module that uses for changing methods behaviour. +-- It could be useful in unit tests when we neeed to determine +-- behaviour of methods that we don't need to test in it. +local M = {} + +local validate = function(mocks) + if type(mocks) ~= "table" then + error("mocks should have a table type, got " .. type(mocks)) + end + + for _, mock in ipairs(mocks) do + if type(mock.module) ~= "string" then + error("module name should have a string type, got " .. type(mock.module)) + end + local ok, _ = pcall(require, mock.module) + if not ok then + error("cannot require module " .. mock.module) + end + + if type(mock.method) ~= "string" then + error("method name should have a string type, got " .. type(mock.method)) + end + if require(mock.module)[mock.method] == nil then + error("there is no method called " .. mock.method .. " in " .. mock.module) + end + + if type(mock.implementation) ~= "function" then + error("implementation type should be a function, got " .. mock.implementation) + end + end +end + +-- M.apply validates mocks, initializes it and if everything +-- is fine replaces methods from initialized list. +M.apply = function(mocks) + validate(mocks) + M.mocks = mocks + + for _, mock in ipairs(M.mocks) do + mock.original_implementation = require(mock.module)[mock.method] + require(mock.module)[mock.method] = mock.implementation + end +end + +-- M.delete returns original implementation from mocked method. +M.clear = function() + for _, mock in ipairs(M.mocks) do + require(mock.module)[mock.method] = mock.original_implementation + end +end + +return M diff --git a/test/integration/role_test.lua b/test/integration/role_test.lua index 6361771..a6d6222 100644 --- a/test/integration/role_test.lua +++ b/test/integration/role_test.lua @@ -105,6 +105,16 @@ g.test_endpoints = function() assert_json("http://127.0.0.1:8082/metrics/json") assert_json("http://127.0.0.1:8082/metrics/json/") + assert_prometheus("http://127.0.0.1:8085/metrics/prometheus") + assert_prometheus("http://127.0.0.1:8085/metrics/prometheus/") + assert_json("http://127.0.0.1:8085/metrics/json") + assert_json("http://127.0.0.1:8085/metrics/json/") + + assert_prometheus("http://127.0.0.1:8086/metrics/prometheus") + assert_prometheus("http://127.0.0.1:8086/metrics/prometheus/") + assert_json("http://127.0.0.1:8086/metrics/json") + assert_json("http://127.0.0.1:8086/metrics/json/") + assert_not_observed("http://127.0.0.1:8081", "/metrics/prometheus") assert_not_observed("http://127.0.0.1:8082", "/metrics/prometheus") assert_observed("http://127.0.0.1:8082", "/metrics/observed/prometheus") diff --git a/test/unit/http_test.lua b/test/unit/http_test.lua index 7adc1b4..7574e7d 100644 --- a/test/unit/http_test.lua +++ b/test/unit/http_test.lua @@ -1,20 +1,45 @@ local json = require('json') local http_client = require('http.client') local metrics = require('metrics') +local mocks = require('test.helpers.mocks') local t = require('luatest') local g = t.group() +local httpd_config = { + default = { + listen = 8085, + }, + additional = { + listen = '127.0.0.1:8086', + }, + ["127.0.0.1:8081"] = { + listen = 8087, + }, +} + +local config_get_return_httpd_config = function(_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} +end + g.before_all(function(cg) cg.role = require('roles.metrics-export') + cg.httpd_role = require('roles.httpd') end) g.before_each(function(cg) + cg.httpd_role.apply(httpd_config) cg.counter = metrics.counter('some_counter') cg.counter:inc(1, {label = 'ANY'}) end) g.after_each(function(cg) + cg.httpd_role.stop() cg.role.stop() metrics.registry:unregister(cg.counter) end) @@ -56,220 +81,942 @@ some_counter{label="ANY"} 1 t.assert_equals(data, expected_prometheus) end -g.test_json_endpoint = function(cg) - cg.role.apply({ - http = { - { - listen = 8081, - endpoints = { - { - path = "/json_metrics", - format = "json", +local test_json_endpoint_cases = { + ['listen'] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/json_metrics", + format = "json", + }, }, }, }, }, - }) - assert_json("http://127.0.0.1:8081/json_metrics") + expected_url = "http://127.0.0.1:8081/json_metrics", + }, + ['httpd'] = { + cfg = { + http = { + { + server = "additional", + endpoints = { + { + path = "/json_metrics", + format = "json", + }, + }, + }, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_url = "http://127.0.0.1:8086/json_metrics" + }, +} + +for name, case in pairs(test_json_endpoint_cases) do + g['test_json_endpoint_' .. name] = function(cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + cg.role.apply(case.cfg) + assert_json(case.expected_url) + + if case.mocks ~= nil then + mocks.clear() + end + end end -g.test_prometheus_endpoint = function(cg) - cg.role.apply({ - http = { - { - listen = 8081, - endpoints = { - { - path = "/prometheus_metrics", - format = "prometheus", +local test_prometheus_endpoint_cases = { + ["listen"] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/prometheus_metrics", + format = "prometheus", + }, }, }, }, }, - }) - assert_prometheus("http://127.0.0.1:8081/prometheus_metrics") + expected_url = "http://127.0.0.1:8081/prometheus_metrics", + }, + ["httpd"] = { + cfg = { + http = { + { + server = "additional", + endpoints = { + { + path = "/prometheus_metrics", + format = "prometheus", + }, + }, + }, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_url = "http://127.0.0.1:8086/prometheus_metrics", + }, +} + +for name, case in pairs(test_prometheus_endpoint_cases) do + g['test_prometheus_endpoint_' .. name] = function(cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + cg.role.apply(case.cfg) + assert_prometheus(case.expected_url) + + if case.mocks ~= nil then + mocks.clear() + end + end end -g.test_mixed = function(cg) - cg.role.apply({ - http = { +local test_mixed_cases = { + ["listen"] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + listen = 8082, + endpoints = { + { + path = "/metrics3", + format = "json", + }, + { + path = "/metrics4", + format = "prometheus", + }, + }, + }, + }, + }, + expected_json = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8082/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8082/metrics4", + }, + expected_none = { + "http://127.0.0.1:8082/metrics1", + "http://127.0.0.1:8082/metrics2", + "http://127.0.0.1:8081/metrics3", + "http://127.0.0.1:8081/metrics4", + }, + }, + ["httpd"] = { + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + server = "additional", + endpoints = { + { + path = "/metrics3", + format = "json", + }, + { + path = "/metrics4", + format = "prometheus", + }, + }, + }, + }, + }, + mocks = { { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "json", + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_json = { + "http://127.0.0.1:8085/metrics1", + "http://127.0.0.1:8086/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8085/metrics2", + "http://127.0.0.1:8086/metrics4", + }, + expected_none = { + "http://127.0.0.1:8086/metrics1", + "http://127.0.0.1:8086/metrics2", + "http://127.0.0.1:8085/metrics3", + "http://127.0.0.1:8085/metrics4", + }, + }, + ["listen_httpd"] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, }, - { - path = "/metrics2", - format = "prometheus", + }, + { + server = "additional", + endpoints = { + { + path = "/metrics3", + format = "json", + }, + { + path = "/metrics4", + format = "prometheus", + }, }, }, }, + }, + mocks = { { - listen = 8082, - endpoints = { - { - path = "/metrics3", - format = "json", + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_json = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8086/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8086/metrics4", + }, + expected_none = { + "http://127.0.0.1:8086/metrics1", + "http://127.0.0.1:8086/metrics2", + "http://127.0.0.1:8081/metrics3", + "http://127.0.0.1:8081/metrics4", + }, + }, + ["listen_httpd_collision"] = { + cfg = { + http = { + { + listen = "127.0.0.1:8081", + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, }, - { - path = "/metrics4", - format = "prometheus", + }, + { + server = "127.0.0.1:8081", + endpoints = { + { + path = "/metrics3", + format = "json", + }, + { + path = "/metrics4", + format = "prometheus", + }, }, }, }, }, - }) - assert_json("http://127.0.0.1:8081/metrics1") - assert_prometheus("http://127.0.0.1:8081/metrics2") - assert_none("http://127.0.0.1:8081/metrics3") - assert_none("http://127.0.0.1:8081/metrics4") - - assert_none("http://127.0.0.1:8082/metrics1") - assert_none("http://127.0.0.1:8082/metrics2") - assert_json("http://127.0.0.1:8082/metrics3") - assert_prometheus("http://127.0.0.1:8082/metrics4") + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_json = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8087/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8087/metrics4", + }, + expected_none = { + "http://127.0.0.1:8087/metrics1", + "http://127.0.0.1:8087/metrics2", + "http://127.0.0.1:8081/metrics3", + "http://127.0.0.1:8081/metrics4", + }, + }, +} + +for name, case in pairs(test_mixed_cases) do + g['test_mixed_' .. name] = function(cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + cg.role.apply(case.cfg) + + for _, url in pairs(case.expected_json) do + assert_json(url) + end + for _, url in pairs(case.expected_prometheus) do + assert_prometheus(url) + end + for _, url in pairs(case.expected_none) do + assert_none(url) + end + + if case.mocks ~= nil then + mocks.clear() + end + end end -g.test_reapply_delete = function(cg) - cg.role.apply({ - http = { +local test_reapply_delete_cases = { + ["listen"] = { + apply_cases = { { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "json", + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + listen = 8082, + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, }, - { - path = "/metrics2", - format = "prometheus", + }, + expected_json_urls = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8082/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics2" + }, + expected_none_urls = {}, + }, + { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics1", + }, + expected_none_urls = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8082/metrics/1", + }, + }, + }, + }, + ["httpd"] = { + apply_cases = { + { + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + server = "additional", + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, + }, + }, + expected_json_urls = { + "http://127.0.0.1:8085/metrics1", + "http://127.0.0.1:8086/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8085/metrics2" + }, + expected_none_urls = {}, }, { - listen = 8082, - endpoints = { - { - path = "/metrics/1", - format = "json", + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8085/metrics1", + }, + expected_none_urls = { + "http://127.0.0.1:8085/metrics2", + "http://127.0.0.1:8086/metrics/1", + }, }, }, - }) - assert_json("http://127.0.0.1:8081/metrics1") - assert_prometheus("http://127.0.0.1:8081/metrics2") - assert_json("http://127.0.0.1:8082/metrics/1") - - cg.role.apply({ - http = { + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, + ["listen_httpd"] = { + apply_cases = { { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "prometheus", + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + server = "additional", + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, }, }, + expected_json_urls = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8086/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics2" + }, + expected_none_urls = {}, + }, + { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, + }, + }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics1", + }, + expected_none_urls = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8086/metrics/1", + }, }, }, - }) - assert_prometheus("http://127.0.0.1:8081/metrics1") - assert_none("http://127.0.0.1:8081/metrics2") - assert_none("http://127.0.0.1:8082/metrics/1") -end - -g.test_reapply_add = function(cg) - cg.role.apply({ - http = { + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, + ["listen_httpd_collision"] = { + apply_cases = { + { + cfg = { + http = { + { + listen = "127.0.0.1:8081", + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + server = "127.0.0.1:8081", + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, + }, + }, + expected_json_urls = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8087/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics2" + }, + expected_none_urls = {}, + }, { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "prometheus", + cfg = { + http = { + { + listen = "127.0.0.1:8081", + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics1", + }, + expected_none_urls = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8087/metrics/1", + }, }, }, - }) - assert_prometheus("http://127.0.0.1:8081/metrics1") - assert_none("http://127.0.0.1:8081/metrics2") - assert_none("http://127.0.0.1:8082/metrics/1") + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, +} + +for name, case in pairs(test_reapply_delete_cases) do + g["test_reapply_delete_" .. name] = function (cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + for _, apply_iter in ipairs(case.apply_cases) do + cg.role.apply(apply_iter.cfg) + for _, url in ipairs(apply_iter.expected_json_urls) do + assert_json(url) + end + for _, url in ipairs(apply_iter.expected_prometheus_urls) do + assert_prometheus(url) + end + for _, url in ipairs(apply_iter.expected_none_urls) do + assert_none(url) + end + end + + if case.mocks~= nil then + mocks.clear() + end + end +end - cg.role.apply({ - http = { +local test_reapply_add_cases = { + ["listen"] = { + apply_cases = { { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "json", + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, - { - path = "/metrics2", - format = "prometheus", + }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics1" + }, + expected_none_urls = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8082/metrics/1", + }, + }, + { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + listen = 8082, + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, }, }, + expected_json_urls = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8082/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8081/metrics2", + }, + expected_none_urls = {}, }, + }, + }, + ["httpd"] = { + apply_cases = { { - listen = 8082, - endpoints = { - { - path = "/metrics/1", - format = "json", + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8085/metrics1" + }, + expected_none_urls = { + "http://127.0.0.1:8085/metrics2", + "http://127.0.0.1:8086/metrics/1", + }, + }, + { + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, + }, + }, + { + server = "additional", + endpoints = { + { + path = "/metrics/1", + format = "json", + }, + }, + }, + }, + }, + expected_json_urls = { + "http://127.0.0.1:8085/metrics1", + "http://127.0.0.1:8086/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8085/metrics2", + }, + expected_none_urls = {}, }, }, - }) - assert_json("http://127.0.0.1:8081/metrics1") - assert_prometheus("http://127.0.0.1:8081/metrics2") - assert_json("http://127.0.0.1:8082/metrics/1") + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, +} + +for name, case in pairs(test_reapply_add_cases) do + g["test_reapply_add_" .. name] = function (cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + for _, apply_iter in ipairs(case.apply_cases) do + cg.role.apply(apply_iter.cfg) + for _, url in ipairs(apply_iter.expected_json_urls) do + assert_json(url) + end + for _, url in ipairs(apply_iter.expected_prometheus_urls) do + assert_prometheus(url) + end + for _, url in ipairs(apply_iter.expected_none_urls) do + assert_none(url) + end + end + + if case.mocks~= nil then + mocks.clear() + end + end end -g.test_stop = function(cg) - cg.role.apply({ - http = { - { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "json", +local test_stop_cases = { + ['listen'] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, }, }, }, }, - }) - assert_json("http://127.0.0.1:8081/metrics1") + expected_json_url = "http://127.0.0.1:8081/metrics1", + expected_none_url = "http://127.0.0.1:8082/metrics/1", + }, + ['httpd'] = { + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "json", + }, + }, + }, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + expected_json_url = "http://127.0.0.1:8085/metrics1", + expected_none_url = "http://127.0.0.1:8086/metrics/1", + }, +} - cg.role.stop() - assert_none("http://127.0.0.1:8082/metrics/1") +for name, case in pairs(test_stop_cases) do + g['test_stop_' .. name] = function(cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + cg.role.apply(case.cfg) + assert_json(case.expected_json_url) + + cg.role.stop() + assert_none(case.expected_none_url) + + if case.mocks ~= nil then + mocks.clear() + end + end end -g.test_endpoint_and_slashes = function(cg) - cg.role.apply({ - http = { - { - listen = 8081, - endpoints = { - { - path = "/endpoint", - format = "json", +local test_endpoint_and_slashes_cases = { + ['listen'] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/endpoint", + format = "json", + }, + { + path = "/endpoint/2/", + format = "json", + }, }, - { - path = "/endpoint/2/", - format = "json", + }, + }, + }, + expected_json_urls = { + "http://127.0.0.1:8081/endpoint", + "http://127.0.0.1:8081/endpoint/", + "http://127.0.0.1:8081/endpoint/2", + "http://127.0.0.1:8081/endpoint/2/", + }, + }, + ['httpd'] = { + cfg = { + http = { + { + endpoints = { + { + path = "/endpoint", + format = "json", + }, + { + path = "/endpoint/2/", + format = "json", + }, }, }, }, }, - }) - assert_json("http://127.0.0.1:8081/endpoint") - assert_json("http://127.0.0.1:8081/endpoint/") - assert_json("http://127.0.0.1:8081/endpoint/2") - assert_json("http://127.0.0.1:8081/endpoint/2/") + expected_json_urls = { + "http://127.0.0.1:8085/endpoint", + "http://127.0.0.1:8085/endpoint/", + "http://127.0.0.1:8085/endpoint/2", + "http://127.0.0.1:8085/endpoint/2/", + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, +} + +for name, case in pairs(test_endpoint_and_slashes_cases) do + g['test_endpoint_and_slashes_test_' .. name] = function(cg) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + + cg.role.apply(case.cfg) + for _, url in pairs(case.expected_json_urls) do + assert_json(url) + end + + if case.mocks ~= nil then + mocks.clear() + end + end end diff --git a/test/unit/validate_test.lua b/test/unit/validate_test.lua index df87bd8..6a73e0d 100644 --- a/test/unit/validate_test.lua +++ b/test/unit/validate_test.lua @@ -1,7 +1,30 @@ local t = require('luatest') - local g = t.group() +local mocks = require('test.helpers.mocks') + +local httpd_config = { + default = { + listen = 8081, + }, + additional = { + listen = '127.0.0.1:8082', + }, +} + +local config_get_return_httpd_config = function(_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} +end + +local config_get_return_empty_httpd_config = function(_, _) + return {} +end + g.before_all(function(gc) gc.role = require('roles.metrics-export') end) @@ -58,17 +81,6 @@ local error_cases = { }, err = "http configuration node must be a table, got number", }, - ["http_node_listen_not_exist"] = { - cfg = { - http = { - { - listen = nil, - endpoints = {}, - }, - }, - }, - err = "failed to parse http 'listen' param: must exist", - }, ["http_node_listen_not_string_and_not_number"] = { cfg = { http = { @@ -432,21 +444,97 @@ local error_cases = { }, err = "http endpoint metrics 'enabled' must be a boolean, got string", }, + ["integration_with_httpd_role_server_is_not_string"] = { + cfg = { http = { { server = 1 } } }, + err = "server configuration sould be a string, got number", + }, + ["integration_with_httpd_role_server_and_listen_simultaneously"] = { + cfg = { + http = { + { + server = "additional", + listen = 8001, + } + } + }, + err = "it is not possible to provide 'server' and 'listen' blocks simultaneously", + }, + ["integration_with_httpd_role_servers_not_unique"] = { + cfg = { + http = { + { + server = "additional", + }, + { + listen = 8001, + }, + { + server = "additional", + }, + } + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + err = "server names must have different targets in httpd", + }, + ["integration_with_httpd_role_cfg_missing"] = { + cfg = { http = { { server = "additional" } } }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_empty_httpd_config, + }, + }, + err = "there is no configuration for httpd role", + }, + ["integration_with_httpd_role_server_not_found"] = { + cfg = { http = { { server = "not_existing_server" } } }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + err = "server with name not_existing_server not found in httpd role config", + }, } for name, case in pairs(error_cases) do g["test_validate_error_" .. name] = function(gc) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + t.assert_error_msg_contains(case.err, function() gc.role.validate(case.cfg) end) + + if case.mocks ~= nil then + mocks.clear() + end end end for name, case in pairs(error_cases) do g["test_apply_validate_error_" .. name] = function(gc) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + t.assert_error_msg_contains(case.err, function() gc.role.apply(case.cfg) end) + + if case.mocks ~= nil then + mocks.clear() + end end end @@ -616,10 +704,68 @@ local ok_cases = { }, }, }, + ["integration_with_httpd_role_additional"] = { + cfg = { http = { { server = "additional" } } }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, + ["integration_with_httpd_role_default"] = { + cfg = { http = { { } } }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, + ["integration_with_httpd_role_different_servers"] = { + cfg = { + http = { + { server = "additional" }, + {}, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, + ["integration_with_httpd_role_different_variations"] = { + cfg = { + http = { + { server = "additional" }, + { listen = 8083 }, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = config_get_return_httpd_config, + }, + }, + }, } for name, case in pairs(ok_cases) do g["test_validate_ok_" .. name] = function(gc) + if case.mocks ~= nil then + mocks.apply(case.mocks) + end + t.assert_equals(gc.role.validate(case.cfg), nil) + + if case.mocks ~= nil then + mocks.clear() + end end end