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
copilot/feature-load-restapi-routes-config
copilot-swe-agent[bot] 4 weeks ago
parent f1212015f2
commit 8d358cbfdc

@ -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;
}
}

@ -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++) {

@ -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 <atomic>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
#include <thread>
#include <vector>
#include <unistd.h>
#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<int> 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<const char*> 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();
}
Loading…
Cancel
Save