diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml index 571275b..d291b6c 100644 --- a/.github/workflows/testing.yaml +++ b/.github/workflows/testing.yaml @@ -102,6 +102,10 @@ jobs: - run: make check + - run: | + lsof -i :8083 + lsof -i :8084 + - run: make test - name: Send code coverage to 'coveralls.io' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7372f17..e753413 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). +- Migration to HTTP role server (#15). ### Fixed diff --git a/README.md b/README.md index e0bfd2b..c6f196f 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,46 @@ set `metrics.enabled` to `true`: For now only `json` and `prometheus` formats are supported. +### Integration with httpd role + +It is also possible to use [httpd role](https://github.com/tarantool/http?tab=readme-ov-file#roles). +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 one 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 +157,15 @@ roles_cfg: format: json metrics: enabled: true + - server: 'additional' + endpoints: + - path: /metrics + format: json + - endpoints: + - path: /metrics + format: prometheus + metrics: + enabled: true ``` With this configuration, metrics can be obtained on this machine with the diff --git a/metrics-export-role-scm-1.rockspec b/metrics-export-role-scm-1.rockspec index 2d88402..79ad899 100644 --- a/metrics-export-role-scm-1.rockspec +++ b/metrics-export-role-scm-1.rockspec @@ -16,7 +16,7 @@ description = { dependencies = { "lua >= 5.1", "tarantool >= 3.0", - "http >= 1.5.0", + "http >= 1.6.0", } build = { diff --git a/roles/metrics-export.lua b/roles/metrics-export.lua index addef36..886b89d 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 = {} @@ -48,9 +49,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 +168,48 @@ 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")['roles.httpd'] + if httpd_roles_cfg == nil then + error("there is no configuration for httpd role") + end + if httpd_roles_cfg[server] == nil then + error(('server with name %s not found in httpd role config'):format(server)) + end +end + +-- get_server_from_httpd_role_cfg returns http server by provided server_name from httpd role. +-- If server_name not found returns server by DEFAULT_SERVER_NAME. +local function get_server_from_httpd_role_cfg(server_name) + local httpd_roles_cfg = require("config"):get("roles_cfg")['roles.httpd'] + return httpd_roles_cfg[server_name] or + httpd_roles_cfg[httpd_role.DEFAULT_SERVER_NAME] +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)) + end + + if node.listen ~= nil then + error("it is not possible to provide 'server' and 'listen' blocks simultaneously") + 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,11 +253,19 @@ local function validate_http(conf) end for i, nodei in ipairs(conf) do - local hosti, porti, erri = parse_listen(nodei.listen) + local listen_address = nodei.listen + if listen_address == nil then + listen_address = get_server_from_httpd_role_cfg(nodei.server).listen + end + local hosti, porti, erri = parse_listen(listen_address) assert(erri == nil) -- We should already successfully parse the URI. 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 + listen_address = get_server_from_httpd_role_cfg(nodej.server).listen + 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) @@ -258,25 +298,47 @@ 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 + local server = httpd_role.get_server(node.server) + host, port = server.host, server.port + target = node.server + elseif node.listen ~= nil then + target = node.listen + local err + host, port, err = parse_listen(node.listen) + if err ~= nil then + error("failed to parse URI: " .. err, 2) + end + else + local server = httpd_role.get_server() + host, port = server.host, server.port + target = httpd_role.DEFAULT_SERVER_NAME end - local listen = node.listen http_servers = http_servers or {} - enabled[listen] = true + enabled[target] = true + + if http_servers[target] == nil then + local httpd + local is_httpd_role = false + if node.listen ~= nil then + httpd = http_server.new(host, port) + httpd:start() + else + httpd = httpd_role.get_server(target) + is_httpd_role = true + end - if http_servers[listen] == nil then - local httpd = http_server.new(host, port) - httpd:start() - http_servers[listen] = { + http_servers[target] = { httpd = httpd, routes = {}, + is_httpd_role = is_httpd_role, } end - local server = http_servers[listen] - local httpd = server.httpd + + local server = http_servers[target] + local httpd = server.httpd or server local old_routes = server.routes local new_routes = {} @@ -317,7 +379,13 @@ local function apply_http(conf) for listen, server in pairs(http_servers) do if not enabled[listen] then - server.httpd:stop() + if not server.is_httpd_role then + server.httpd:stop() + else + for path, _ in pairs(server.routes) do + server.httpd:delete(path) + end + end http_servers[listen] = nil end end @@ -325,7 +393,9 @@ end local function stop_http() for _, server in pairs(http_servers or {}) do - server.httpd:stop() + if not server.is_httpd_role then + server.httpd:stop() + end end http_servers = nil end diff --git a/test/entrypoint/config.yaml b/test/entrypoint/config.yaml index 49bf315..14b08ea 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: 8083 + additional: + listen: '127.0.0.1:8084' 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..f439032 --- /dev/null +++ b/test/helpers/mocks.lua @@ -0,0 +1,55 @@ +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.init validates mocks and initializes it. +M.init = function (mocks) + validate(mocks) + + M.mocks = {} + for _, mock in ipairs(mocks) do + table.insert(M.mocks, mock) + end +end + +-- M.apply replaces methods from initialized list. +M.apply = function () + 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.delete = 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..42b32ec 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:8083/metrics/prometheus") + assert_prometheus("http://127.0.0.1:8083/metrics/prometheus/") + assert_json("http://127.0.0.1:8083/metrics/json") + assert_json("http://127.0.0.1:8083/metrics/json/") + + assert_prometheus("http://127.0.0.1:8084/metrics/prometheus") + assert_prometheus("http://127.0.0.1:8084/metrics/prometheus/") + assert_json("http://127.0.0.1:8084/metrics/json") + assert_json("http://127.0.0.1:8084/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..9706041 100644 --- a/test/unit/http_test.lua +++ b/test/unit/http_test.lua @@ -1,22 +1,35 @@ 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 = 8083, + }, + additional = { + listen = '127.0.0.1:8084', + }, +} + g.before_all(function(cg) cg.role = require('roles.metrics-export') + cg.httpd_role = require('roles.httpd') end) g.before_each(function(cg) cg.counter = metrics.counter('some_counter') cg.counter:inc(1, {label = 'ANY'}) + cg.httpd_role.apply(httpd_config) end) g.after_each(function(cg) cg.role.stop() metrics.registry:unregister(cg.counter) + cg.httpd_role.stop() end) local function assert_none(uri) @@ -56,220 +69,889 @@ 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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + expected_url = "http://127.0.0.1:8084/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.init(case.mocks) + mocks.apply() + end + + cg.role.apply(case.cfg) + assert_json(case.expected_url) + + if case.mocks ~= nil then + mocks.delete() + 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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + expected_url = "http://127.0.0.1:8084/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.init(case.mocks) + mocks.apply() + end + + cg.role.apply(case.cfg) + assert_prometheus(case.expected_url) + + if case.mocks ~= nil then + mocks.delete() + end + end end -g.test_mixed = function(cg) - cg.role.apply({ - http = { - { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "json", +local test_mixed_cases = { + ["listen"] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, }, - { - 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 = 8082, - endpoints = { - { - path = "/metrics3", - format = "json", + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + expected_json = { + "http://127.0.0.1:8083/metrics1", + "http://127.0.0.1:8084/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8083/metrics2", + "http://127.0.0.1:8084/metrics4", + }, + expected_none = { + "http://127.0.0.1:8084/metrics1", + "http://127.0.0.1:8084/metrics2", + "http://127.0.0.1:8083/metrics3", + "http://127.0.0.1:8083/metrics4", + }, + }, + ["listen_httpd"] = { + cfg = { + http = { + { + listen = 8081, + endpoints = { + { + path = "/metrics1", + format = "json", + }, + { + path = "/metrics2", + format = "prometheus", + }, }, - { - path = "/metrics4", - format = "prometheus", + }, + { + server = "additional", + 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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + expected_json = { + "http://127.0.0.1:8081/metrics1", + "http://127.0.0.1:8084/metrics3", + }, + expected_prometheus = { + "http://127.0.0.1:8081/metrics2", + "http://127.0.0.1:8084/metrics4", + }, + expected_none = { + "http://127.0.0.1:8084/metrics1", + "http://127.0.0.1:8084/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.init(case.mocks) + mocks.apply() + 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.delete() + 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:8083/metrics1", + "http://127.0.0.1:8084/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8083/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:8083/metrics1", + }, + expected_none_urls = { + "http://127.0.0.1:8083/metrics2", + "http://127.0.0.1:8084/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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, + ["listen_httpd"] = { + apply_cases = { + { + 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:8084/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 = 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:8084/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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, +} + +for name, case in pairs(test_reapply_delete_cases) do + g["test_reapply_delete_" .. name] = function (cg) + if case.mocks ~= nil then + mocks.init(case.mocks) + mocks.apply() + 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.delete() + end + end end -g.test_reapply_add = function(cg) - cg.role.apply({ - http = { +local test_reapply_add_cases = { + ["listen"] = { + apply_cases = { { - listen = 8081, - endpoints = { - { - path = "/metrics1", - format = "prometheus", + 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", + }, }, - }, - }) - 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") - - cg.role.apply({ - http = { { - 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 = {}, + }, + }, + }, + ["httpd"] = { + apply_cases = { + { + cfg = { + http = { + { + endpoints = { + { + path = "/metrics1", + format = "prometheus", + }, + }, + }, }, }, + expected_json_urls = {}, + expected_prometheus_urls = { + "http://127.0.0.1:8083/metrics1" + }, + expected_none_urls = { + "http://127.0.0.1:8083/metrics2", + "http://127.0.0.1:8084/metrics/1", + }, }, { - listen = 8082, - endpoints = { - { - path = "/metrics/1", - format = "json", + 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:8083/metrics1", + "http://127.0.0.1:8084/metrics/1", + }, + expected_prometheus_urls = { + "http://127.0.0.1:8083/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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, +} + +for name, case in pairs(test_reapply_add_cases) do + g["test_reapply_add_" .. name] = function (cg) + if case.mocks ~= nil then + mocks.init(case.mocks) + mocks.apply() + 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.delete() + 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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + expected_json_url = "http://127.0.0.1:8083/metrics1", + expected_none_url = "http://127.0.0.1:8084/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.init(case.mocks) + mocks.apply() + 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.delete() + 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:8083/endpoint", + "http://127.0.0.1:8083/endpoint/", + "http://127.0.0.1:8083/endpoint/2", + "http://127.0.0.1:8083/endpoint/2/", + }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, +} + +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.init(case.mocks) + mocks.apply() + 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.delete() + end + end end diff --git a/test/unit/validate_test.lua b/test/unit/validate_test.lua index df87bd8..3207ee3 100644 --- a/test/unit/validate_test.lua +++ b/test/unit/validate_test.lua @@ -1,7 +1,17 @@ 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', + }, +} + g.before_all(function(gc) gc.role = require('roles.metrics-export') end) @@ -58,17 +68,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 +431,131 @@ 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_cfg_missing"] = { + cfg = { http = { { server = "additional" } } }, + mocks = { + { + module = "config", + method = "get", + implementation = function () + return {} + end, + }, + }, + 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 = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + err = "server with name not_existing_server not found in httpd role config", + }, + ["integration_with_httpd_role_server_different_listen_targets"] = { + cfg = { + http = { + { server = "additional" }, + { server = "additional" }, + } + }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + err = "http configuration nodes must have different listen targets", + }, + ["integration_with_httpd_role_server_different_listen_targets_mixed_targets"] = { + cfg = { + http = { + { server = "additional" }, + { listen = '127.0.0.1:8082' }, + } + }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + err = "http configuration nodes must have different listen targets", + }, } for name, case in pairs(error_cases) do g["test_validate_error_" .. name] = function(gc) + if case.mocks ~= nil then + mocks.init(case.mocks) + mocks.apply() + end + t.assert_error_msg_contains(case.err, function() gc.role.validate(case.cfg) end) + + if case.mocks ~= nil then + mocks.delete() + 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.init(case.mocks) + mocks.apply() + end + t.assert_error_msg_contains(case.err, function() gc.role.apply(case.cfg) end) + + if case.mocks ~= nil then + mocks.delete() + end end end @@ -616,10 +725,97 @@ local ok_cases = { }, }, }, + ["integration_with_httpd_role_additional"] = { + cfg = { http = { { server = "additional" } } }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, + ["integration_with_httpd_role_default"] = { + cfg = { http = { { } } }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, + ["integration_with_httpd_role_different_servers"] = { + cfg = { + http = { + { server = "additional" }, + {}, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, + ["integration_with_httpd_role_different_variations"] = { + cfg = { + http = { + { server = "additional" }, + { listen = 8083 }, + }, + }, + mocks = { + { + module = "config", + method = "get", + implementation = function (_, param) + if param == "roles_cfg" then + return { + ['roles.httpd'] = httpd_config, + } + end + return {} + end, + }, + }, + }, } for name, case in pairs(ok_cases) do g["test_validate_ok_" .. name] = function(gc) + if case.mocks ~= nil then + mocks.init(case.mocks) + mocks.apply() + end + t.assert_equals(gc.role.validate(case.cfg), nil) + + if case.mocks ~= nil then + mocks.delete() + end end end