From 8d358cbfdcd14b53139e68fc2e204c1ebd80f78c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 21:46:47 +0000 Subject: [PATCH] feat: load restapi routes from config aliases Co-authored-by: renecannao <3645227+renecannao@users.noreply.github.com> Agent-Logs-Url: https://github.com/sysown/proxysql/sessions/40d309f0-bd3d-4616-9cd6-fa7d8f1cb355 --- lib/ProxySQL_Config.cpp | 56 +++-- .../test_load_from_config_validation-t.cpp | 24 ++- ...est_load_restapi_from_config_startup-t.cpp | 195 ++++++++++++++++++ 3 files changed, 256 insertions(+), 19 deletions(-) create mode 100644 test/tap/tests/test_load_restapi_from_config_startup-t.cpp diff --git a/lib/ProxySQL_Config.cpp b/lib/ProxySQL_Config.cpp index 1a0aecdc6..5005d7a1c 100644 --- a/lib/ProxySQL_Config.cpp +++ b/lib/ProxySQL_Config.cpp @@ -443,17 +443,26 @@ int ProxySQL_Config::Write_Restapi_to_configfile(std::string& data) { int ProxySQL_Config::Read_Restapi_from_configfile() { const Setting& root = GloVars.confFile->cfg.getRoot(); - if (root.exists("restapi")==false) return 0; - const Setting &routes = root["restapi"]; + const char* routes_section = nullptr; + if (root.exists("restapi_routes")) { + routes_section = "restapi_routes"; + } else if (root.exists("restapi")) { + routes_section = "restapi"; + } else { + return 0; + } + const Setting &routes = root[routes_section]; int count = routes.getLength(); //fprintf(stderr, "Found %d users\n",count); int i; int rows=0; admindb->execute("PRAGMA foreign_keys = OFF"); - char *q=(char *)"INSERT OR REPLACE INTO restapi_routes VALUES (%d, %d, %d, '%s', '%s', '%s', '%s')"; + const char *q_with_id = "INSERT OR REPLACE INTO restapi_routes VALUES (%d, %d, %d, '%s', '%s', '%s', '%s')"; + const char *q_without_id = "INSERT OR REPLACE INTO restapi_routes (active, timeout_ms, method, uri, script, comment) VALUES (%d, %d, '%s', '%s', '%s', '%s')"; for (i=0; i< count; i++) { const Setting &route = routes[i]; - int id; + int id=0; + bool id_exists=false; int active=1; // variable for parsing timeout_ms int timeout_ms=0; @@ -464,10 +473,7 @@ int ProxySQL_Config::Read_Restapi_from_configfile() { std::string comment=""; // validate arguments - if (route.lookupValue("id", id)==false) { - proxy_error("Admin: detected a restapi route in config file without a mandatory id\n"); - continue; - } + id_exists = route.lookupValue("id", id); route.lookupValue("active", active); if (route.lookupValue("interval_ms", timeout_ms) == false) { route.lookupValue("timeout_ms", timeout_ms); @@ -486,9 +492,9 @@ int ProxySQL_Config::Read_Restapi_from_configfile() { } route.lookupValue("comment", comment); + const char *q = id_exists ? q_with_id : q_without_id; int query_len=0; query_len+=strlen(q) + - strlen(std::to_string(id).c_str()) + strlen(std::to_string(active).c_str()) + strlen(std::to_string(timeout_ms).c_str()) + strlen(method.c_str()) + @@ -496,15 +502,29 @@ int ProxySQL_Config::Read_Restapi_from_configfile() { strlen(script.c_str()) + strlen(comment.c_str()) + 40; + if (id_exists) { + query_len += strlen(std::to_string(id).c_str()); + } char *query=(char *)malloc(query_len); - sprintf(query, q, - id, active, - timeout_ms, - method.c_str(), - uri.c_str(), - script.c_str(), - comment.c_str() - ); + if (id_exists) { + sprintf(query, q, + id, active, + timeout_ms, + method.c_str(), + uri.c_str(), + script.c_str(), + comment.c_str() + ); + } else { + sprintf(query, q, + active, + timeout_ms, + method.c_str(), + uri.c_str(), + script.c_str(), + comment.c_str() + ); + } admindb->execute(query); free(query); rows++; @@ -2336,4 +2356,4 @@ bool ProxySQL_Config::validate_proxysql_servers(const Setting& config, std::stri } return true; -} \ No newline at end of file +} diff --git a/test/tap/tests/test_load_from_config_validation-t.cpp b/test/tap/tests/test_load_from_config_validation-t.cpp index ec0f12181..41f36cd55 100644 --- a/test/tap/tests/test_load_from_config_validation-t.cpp +++ b/test/tap/tests/test_load_from_config_validation-t.cpp @@ -246,6 +246,26 @@ void create_valid_config(const string& config_file_path) { port=6033 } ) + + restapi_routes= + ( + { + active=1 + timeout_ms=5000 + method="GET" + uri="healthz" + script="/tmp/proxysql-healthcheck.bash" + comment="health check" + }, + { + active=1 + timeout_ms=6000 + method="POST" + uri="sync" + script="/tmp/proxysql-sync.bash" + comment="sync route" + } + ) )"; fstream config_file; @@ -283,7 +303,8 @@ int main(int argc, char** argv) { {"LOAD PGSQL USERS FROM CONFIG", "SELECT * FROM pgsql_users", 2}, {"LOAD MYSQL SERVERS FROM CONFIG", "SELECT * FROM mysql_servers", 2}, {"LOAD PGSQL SERVERS FROM CONFIG", "SELECT * FROM pgsql_servers", 2}, - {"LOAD PROXYSQL SERVERS FROM CONFIG", "SELECT * FROM proxysql_servers", 2} + {"LOAD PROXYSQL SERVERS FROM CONFIG", "SELECT * FROM proxysql_servers", 2}, + {"LOAD RESTAPI FROM CONFIG", "SELECT * FROM restapi_routes", 2} }; int n = tc_valid.size() @@ -361,6 +382,7 @@ int main(int argc, char** argv) { MYSQL_QUERY_T(admin, "DELETE FROM mysql_servers"); MYSQL_QUERY_T(admin, "DELETE FROM pgsql_servers"); MYSQL_QUERY_T(admin, "DELETE FROM proxysql_servers"); + MYSQL_QUERY_T(admin, "DELETE FROM restapi_routes"); create_valid_config(config_file); for (auto it = tc_valid.begin(); it != tc_valid.end(); it++) { diff --git a/test/tap/tests/test_load_restapi_from_config_startup-t.cpp b/test/tap/tests/test_load_restapi_from_config_startup-t.cpp new file mode 100644 index 000000000..7f6e16153 --- /dev/null +++ b/test/tap/tests/test_load_restapi_from_config_startup-t.cpp @@ -0,0 +1,195 @@ +/** + * @file test_load_restapi_from_config_startup-t.cpp + * @brief Verifies restapi routes from config are loaded into runtime on startup. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mysql.h" + +#include "proxysql_utils.h" +#include "tap.h" +#include "command_line.h" +#include "utils.h" + +using std::string; +namespace fs = std::filesystem; + +static constexpr const char* SEC_PROXY_HOST = "127.0.0.1"; +static constexpr int SEC_PROXY_ADMIN_PORT = 26082; +static constexpr int SEC_PROXY_MYSQL_PORT = 36082; +static constexpr int SEC_PROXY_RESTAPI_PORT = 26083; +static constexpr int SEC_PROXY_WAIT_TIMEOUT_S = 25; + +static int prepare_secondary_proxy_runtime( + const CommandLine& cl, const fs::path& runtime_dir, const fs::path& cfg_file, const fs::path& script_file +) { + try { + fs::remove_all(runtime_dir); + fs::create_directories(runtime_dir); + + std::ofstream script_stream { script_file }; + if (!script_stream.is_open()) { + diag("Failed to open RESTAPI script for writing path='%s'", script_file.c_str()); + return EXIT_FAILURE; + } + script_stream << "#!/bin/sh\n"; + script_stream << "printf '{\"ok\":true}'\n"; + script_stream.close(); + fs::permissions( + script_file, + fs::perms::owner_read | fs::perms::owner_write | fs::perms::owner_exec | + fs::perms::group_read | fs::perms::group_exec | + fs::perms::others_read | fs::perms::others_exec + ); + + std::ofstream cfg_stream { cfg_file }; + if (!cfg_stream.is_open()) { + diag("Failed to open ProxySQL config for writing path='%s'", cfg_file.c_str()); + return EXIT_FAILURE; + } + + cfg_stream + << "datadir=\"" << runtime_dir.string() << "\"\n" + << "errorlog=\"" << (runtime_dir / "proxysql.log").string() << "\"\n\n" + << "admin_variables=\n" + << "{\n" + << "\tadmin_credentials=\"admin:admin;" << cl.admin_username << ":" << cl.admin_password << "\"\n" + << "\tmysql_ifaces=\"0.0.0.0:" << SEC_PROXY_ADMIN_PORT << "\"\n" + << "\trestapi_enabled=true\n" + << "\trestapi_port=" << SEC_PROXY_RESTAPI_PORT << "\n" + << "}\n\n" + << "mysql_variables=\n" + << "{\n" + << "\tinterfaces=\"0.0.0.0:" << SEC_PROXY_MYSQL_PORT << "\"\n" + << "}\n\n" + << "restapi_routes=\n" + << "(\n" + << "\t{\n" + << "\t\tactive=1\n" + << "\t\ttimeout_ms=5000\n" + << "\t\tmethod=\"GET\"\n" + << "\t\turi=\"healthz\"\n" + << "\t\tscript=\"" << script_file.string() << "\"\n" + << "\t\tcomment=\"health check\"\n" + << "\t}\n" + << ")\n"; + cfg_stream.close(); + + return EXIT_SUCCESS; + } catch (const std::exception& ex) { + diag("Failed to prepare secondary ProxySQL runtime error=\"%s\"", ex.what()); + return EXIT_FAILURE; + } +} + +int main(int argc, char** argv) { + CommandLine cl; + const char* WORKSPACE = getenv("WORKSPACE"); + + if (cl.getEnv() || WORKSPACE == nullptr) { + diag("Failed to get the required environmental variables."); + return EXIT_FAILURE; + } + + plan(5); + + const fs::path runtime_dir { fs::path { cl.workdir } / "test_load_restapi_from_config_startup" }; + const fs::path cfg_file { runtime_dir / "proxysql.cfg" }; + const fs::path script_file { runtime_dir / "probe.bash" }; + + if (prepare_secondary_proxy_runtime(cl, runtime_dir, cfg_file, script_file) != EXIT_SUCCESS) { + return EXIT_FAILURE; + } + + std::atomic launch_res { -1 }; + string launch_stdout {}; + string launch_stderr {}; + + std::thread launch_proxy = std::thread( + [&WORKSPACE, &runtime_dir, &cfg_file, &launch_res, &launch_stdout, &launch_stderr]() -> void { + to_opts_t wexecvp_opts {}; + wexecvp_opts.poll_to_us = 100 * 1000; + wexecvp_opts.waitpid_delay_us = 500 * 1000; + wexecvp_opts.timeout_us = 30000 * 1000; + wexecvp_opts.sigkill_to_us = 3000 * 1000; + + const string proxysql_path { string { WORKSPACE } + "/src/proxysql" }; + const string cfg_file_str { cfg_file.string() }; + const string runtime_dir_str { runtime_dir.string() }; + const std::vector proxy_args { + "-f", "-M", "--reload", "-c", cfg_file_str.c_str(), "-D", runtime_dir_str.c_str() + }; + + launch_res.store(wexecvp(proxysql_path, proxy_args, wexecvp_opts, launch_stdout, launch_stderr)); + } + ); + + conn_opts_t conn_opts { SEC_PROXY_HOST, cl.admin_username, cl.admin_password, SEC_PROXY_ADMIN_PORT, 0 }; + MYSQL* proxy_admin = wait_for_proxysql(conn_opts, SEC_PROXY_WAIT_TIMEOUT_S); + + ok(proxy_admin != nullptr, "Secondary ProxySQL started with config-file RESTAPI routes"); + + if (proxy_admin) { + auto [main_err, main_rows] = mysql_query_ext_rows( + proxy_admin, "SELECT active, timeout_ms, method, uri, script, comment FROM restapi_routes" + ); + ok(main_err == EXIT_SUCCESS && main_rows.size() == 1, "Startup config loaded one row into restapi_routes"); + + auto [runtime_err, runtime_rows] = mysql_query_ext_rows( + proxy_admin, "SELECT active, timeout_ms, method, uri, script, comment FROM runtime_restapi_routes" + ); + ok( + runtime_err == EXIT_SUCCESS && runtime_rows.size() == 1, + "Startup config loaded one row into runtime_restapi_routes" + ); + + bool runtime_row_matches = false; + if (runtime_err == EXIT_SUCCESS && runtime_rows.size() == 1) { + const auto& row = runtime_rows.front(); + runtime_row_matches = + row[0] == "1" && + row[1] == "5000" && + row[2] == "GET" && + row[3] == "healthz" && + row[4] == script_file.string() && + row[5] == "health check"; + } + ok(runtime_row_matches, "Runtime RESTAPI row matches the config-file route definition"); + + const int shutdown_rc = mysql_query(proxy_admin, "PROXYSQL SHUTDOWN SLOW"); + const string shutdown_err = shutdown_rc == 0 ? "" : mysql_error(proxy_admin); + mysql_close(proxy_admin); + proxy_admin = nullptr; + + if (shutdown_rc != 0) { + diag("Shutdown query failed: %s", shutdown_err.c_str()); + } + } else { + ok(false, "Startup config loaded one row into restapi_routes"); + ok(false, "Startup config loaded one row into runtime_restapi_routes"); + ok(false, "Runtime RESTAPI row matches the config-file route definition"); + } + + if (launch_proxy.joinable()) { + launch_proxy.join(); + } + + ok(launch_res.load() == EXIT_SUCCESS, "Secondary ProxySQL exited cleanly after verification"); + + if (tests_failed()) { + diag("Secondary ProxySQL stdout:\n%s", launch_stdout.c_str()); + diag("Secondary ProxySQL stderr:\n%s", launch_stderr.c_str()); + } + + fs::remove_all(runtime_dir); + + return exit_status(); +}