diff --git a/plugins/mysqlx/Makefile b/plugins/mysqlx/Makefile index 635b51480..90495bba3 100644 --- a/plugins/mysqlx/Makefile +++ b/plugins/mysqlx/Makefile @@ -9,7 +9,7 @@ PLUGIN_DIR := $(PROXYSQL_PATH)/plugins/mysqlx ODIR := $(PLUGIN_DIR)/obj PLUGIN_SO := $(PLUGIN_DIR)/ProxySQL_MySQLX_Plugin.so -IDIRS := -I$(PROXYSQL_IDIR) -I$(PLUGIN_DIR)/include +IDIRS := -I$(PROXYSQL_IDIR) -I$(PLUGIN_DIR)/include -I$(SQLITE3_IDIR) OPTZ ?= -O2 -ggdb CXXFLAGS := $(STDCPP) -fPIC $(OPTZ) $(WGCOV) $(WASAN) @@ -17,7 +17,8 @@ CXXFLAGS := $(STDCPP) -fPIC $(OPTZ) $(WGCOV) $(WASAN) .DEFAULT_GOAL := all SRCS := $(PLUGIN_DIR)/src/mysqlx_plugin.cpp \ - $(PLUGIN_DIR)/src/mysqlx_admin_schema.cpp + $(PLUGIN_DIR)/src/mysqlx_admin_schema.cpp \ + $(PLUGIN_DIR)/src/mysqlx_config_store.cpp HEADERS := $(wildcard $(PLUGIN_DIR)/include/*.h) \ $(PROXYSQL_PATH)/include/ProxySQL_Plugin.h OBJS := $(patsubst $(PLUGIN_DIR)/src/%.cpp,$(ODIR)/%.o,$(SRCS)) diff --git a/plugins/mysqlx/include/mysqlx_config_store.h b/plugins/mysqlx/include/mysqlx_config_store.h new file mode 100644 index 000000000..ae32769db --- /dev/null +++ b/plugins/mysqlx/include/mysqlx_config_store.h @@ -0,0 +1,74 @@ +#ifndef PROXYSQL_MYSQLX_CONFIG_STORE_H +#define PROXYSQL_MYSQLX_CONFIG_STORE_H + +#include +#include +#include +#include +#include + +class SQLite3DB; + +enum class MysqlxBackendAuthMode : uint8_t { + mapped = 0, + service_account = 1, + pass_through = 2 +}; + +MysqlxBackendAuthMode mysqlx_backend_auth_mode_from_string(const std::string& value); + +struct MysqlxResolvedIdentity { + std::string username {}; + int default_hostgroup { 0 }; + int max_connections { 0 }; + bool x_enabled { false }; + bool require_tls { false }; + std::string allowed_auth_methods {}; + std::string default_route {}; + std::string policy_profile {}; + MysqlxBackendAuthMode backend_auth_mode { MysqlxBackendAuthMode::mapped }; + std::string backend_username {}; + std::string backend_password {}; + std::string attributes {}; +}; + +struct MysqlxRoute { + std::string name {}; + std::string bind {}; + int destination_hostgroup { 0 }; + int fallback_hostgroup { -1 }; + std::string strategy { "first_available" }; + bool active { true }; + std::string attributes {}; +}; + +struct MysqlxBackendEndpoint { + std::string hostname {}; + int mysql_port { 0 }; + int mysqlx_port { 33060 }; + bool use_ssl { false }; + std::string attributes {}; +}; + +class MysqlxConfigStore { +public: + MysqlxConfigStore() = default; + MysqlxConfigStore(const MysqlxConfigStore&) = delete; + MysqlxConfigStore& operator=(const MysqlxConfigStore&) = delete; + ~MysqlxConfigStore() = default; + + bool load_from_runtime(SQLite3DB& db, std::string& err); + std::optional resolve_identity(const std::string& username) const; + MysqlxBackendEndpoint pick_endpoint(const std::string& route_name) const; + + uint64_t topology_generation() const; + void bump_topology_generation(); + +private: + std::unordered_map identities_ {}; + std::unordered_map routes_ {}; + std::unordered_map> hostgroup_endpoints_ {}; + uint64_t topology_generation_ { 0 }; +}; + +#endif /* PROXYSQL_MYSQLX_CONFIG_STORE_H */ diff --git a/plugins/mysqlx/include/mysqlx_plugin.h b/plugins/mysqlx/include/mysqlx_plugin.h index a4ac0db77..09eff3e3a 100644 --- a/plugins/mysqlx/include/mysqlx_plugin.h +++ b/plugins/mysqlx/include/mysqlx_plugin.h @@ -3,11 +3,10 @@ #include "ProxySQL_Plugin.h" #include "mysqlx_admin_schema.h" +#include "mysqlx_config_store.h" #include -class MysqlxConfigStore; - struct MysqlxPluginContext { ProxySQL_PluginServices* services { nullptr }; std::unique_ptr config_store {}; diff --git a/plugins/mysqlx/src/mysqlx_admin_schema.cpp b/plugins/mysqlx/src/mysqlx_admin_schema.cpp index 3b1246b6f..287e956eb 100644 --- a/plugins/mysqlx/src/mysqlx_admin_schema.cpp +++ b/plugins/mysqlx/src/mysqlx_admin_schema.cpp @@ -1,8 +1,18 @@ #include "mysqlx_admin_schema.h" +#include "sqlite3db.h" + +#include + namespace { const char kMysqlxUsersTable[] = "mysqlx_users"; +const char kRuntimeMysqlxUsersTable[] = "runtime_mysqlx_users"; +const char kMysqlxRoutesTable[] = "mysqlx_routes"; +const char kRuntimeMysqlxRoutesTable[] = "runtime_mysqlx_routes"; +const char kMysqlxBackendEndpointsTable[] = "mysqlx_backend_endpoints"; +const char kRuntimeMysqlxBackendEndpointsTable[] = "runtime_mysqlx_backend_endpoints"; + const char kMysqlxUsersTableDef[] = "CREATE TABLE mysqlx_users (" " username VARCHAR NOT NULL PRIMARY KEY," @@ -18,25 +28,194 @@ const char kMysqlxUsersTableDef[] = " comment VARCHAR NOT NULL DEFAULT ''" " )"; -} // namespace +const char kRuntimeMysqlxUsersTableDef[] = + "CREATE TABLE runtime_mysqlx_users (" + " username VARCHAR NOT NULL PRIMARY KEY," + " active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1," + " require_tls INT CHECK (require_tls IN (0,1)) NOT NULL DEFAULT 0," + " allowed_auth_methods VARCHAR NOT NULL DEFAULT ''," + " default_route VARCHAR," + " policy_profile VARCHAR," + " backend_auth_mode VARCHAR NOT NULL DEFAULT 'mapped'," + " backend_username VARCHAR," + " backend_password VARCHAR," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''" + " )"; -bool mysqlx_register_admin_schema(ProxySQL_PluginServices& services) { - if (services.register_table == nullptr) { +const char kMysqlxRoutesTableDef[] = + "CREATE TABLE mysqlx_routes (" + " name VARCHAR NOT NULL PRIMARY KEY," + " bind VARCHAR NOT NULL," + " destination_hostgroup INT NOT NULL," + " fallback_hostgroup INT," + " strategy VARCHAR NOT NULL DEFAULT 'first_available'," + " active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''" + " )"; + +const char kRuntimeMysqlxRoutesTableDef[] = + "CREATE TABLE runtime_mysqlx_routes (" + " name VARCHAR NOT NULL PRIMARY KEY," + " bind VARCHAR NOT NULL," + " destination_hostgroup INT NOT NULL," + " fallback_hostgroup INT," + " strategy VARCHAR NOT NULL DEFAULT 'first_available'," + " active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''" + " )"; + +const char kMysqlxBackendEndpointsTableDef[] = + "CREATE TABLE mysqlx_backend_endpoints (" + " hostname VARCHAR NOT NULL," + " mysql_port INT NOT NULL," + " mysqlx_port INT NOT NULL DEFAULT 33060," + " use_ssl INT CHECK (use_ssl IN (0,1)) NOT NULL DEFAULT 0," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''," + " PRIMARY KEY (hostname, mysql_port)" + " )"; + +const char kRuntimeMysqlxBackendEndpointsTableDef[] = + "CREATE TABLE runtime_mysqlx_backend_endpoints (" + " hostname VARCHAR NOT NULL," + " mysql_port INT NOT NULL," + " mysqlx_port INT NOT NULL DEFAULT 33060," + " use_ssl INT CHECK (use_ssl IN (0,1)) NOT NULL DEFAULT 0," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''," + " PRIMARY KEY (hostname, mysql_port)" + " )"; + +ProxySQL_PluginCommandResult command_failure(const char* message) { + return {1, 0, message != nullptr ? message : "mysqlx admin command failed"}; +} + +bool copy_table(SQLite3DB& db, const char* source_table, const char* runtime_table) { + if (!db.execute("BEGIN")) { + return false; + } + + std::string delete_sql = "DELETE FROM "; + delete_sql += runtime_table; + if (!db.execute(delete_sql.c_str())) { + db.execute("ROLLBACK"); + return false; + } + + std::string insert_sql = "INSERT INTO "; + insert_sql += runtime_table; + insert_sql += " SELECT * FROM "; + insert_sql += source_table; + if (!db.execute(insert_sql.c_str())) { + db.execute("ROLLBACK"); + return false; + } + + if (!db.execute("COMMIT")) { + db.execute("ROLLBACK"); return false; } + return true; +} + +ProxySQL_PluginCommandResult load_users_to_runtime(const ProxySQL_PluginCommandContext& ctx, const char*) { + if (ctx.admindb == nullptr) { + return command_failure("mysqlx users load requires admin db"); + } + if (!copy_table(*ctx.admindb, kMysqlxUsersTable, kRuntimeMysqlxUsersTable)) { + return command_failure("failed to copy mysqlx users to runtime"); + } + + ProxySQL_PluginCommandResult result {0, 0, ""}; + result.rows_affected = ctx.admindb->return_one_int("SELECT COUNT(*) FROM runtime_mysqlx_users"); + result.message = "mysqlx users loaded to runtime"; + return result; +} + +ProxySQL_PluginCommandResult load_routes_to_runtime(const ProxySQL_PluginCommandContext& ctx, const char*) { + if (ctx.admindb == nullptr) { + return command_failure("mysqlx routes load requires admin db"); + } + if (!copy_table(*ctx.admindb, kMysqlxRoutesTable, kRuntimeMysqlxRoutesTable)) { + return command_failure("failed to copy mysqlx routes to runtime"); + } + + ProxySQL_PluginCommandResult result {0, 0, ""}; + result.rows_affected = ctx.admindb->return_one_int("SELECT COUNT(*) FROM runtime_mysqlx_routes"); + result.message = "mysqlx routes loaded to runtime"; + return result; +} + +ProxySQL_PluginCommandResult load_backend_endpoints_to_runtime(const ProxySQL_PluginCommandContext& ctx, const char*) { + if (ctx.admindb == nullptr) { + return command_failure("mysqlx backend endpoints load requires admin db"); + } + if (!copy_table(*ctx.admindb, kMysqlxBackendEndpointsTable, kRuntimeMysqlxBackendEndpointsTable)) { + return command_failure("failed to copy mysqlx backend endpoints to runtime"); + } + + ProxySQL_PluginCommandResult result {0, 0, ""}; + result.rows_affected = ctx.admindb->return_one_int("SELECT COUNT(*) FROM runtime_mysqlx_backend_endpoints"); + result.message = "mysqlx backend endpoints loaded to runtime"; + return result; +} + +void register_table_pair( + ProxySQL_PluginServices& services, + const char* table_name, + const char* table_def +) { ProxySQL_PluginTableDef admin_def { ProxySQL_PluginDBKind::admin_db, - kMysqlxUsersTable, - kMysqlxUsersTableDef + table_name, + table_def }; ProxySQL_PluginTableDef config_def { ProxySQL_PluginDBKind::config_db, - kMysqlxUsersTable, - kMysqlxUsersTableDef + table_name, + table_def }; services.register_table(admin_def); services.register_table(config_def); +} + +void register_runtime_table( + ProxySQL_PluginServices& services, + const char* table_name, + const char* table_def +) { + ProxySQL_PluginTableDef runtime_def { + ProxySQL_PluginDBKind::admin_db, + table_name, + table_def + }; + + services.register_table(runtime_def); +} + +} // namespace + +bool mysqlx_register_admin_schema(ProxySQL_PluginServices& services) { + if (services.register_table == nullptr || services.register_command == nullptr) { + return false; + } + + register_table_pair(services, kMysqlxUsersTable, kMysqlxUsersTableDef); + register_runtime_table(services, kRuntimeMysqlxUsersTable, kRuntimeMysqlxUsersTableDef); + + register_table_pair(services, kMysqlxRoutesTable, kMysqlxRoutesTableDef); + register_runtime_table(services, kRuntimeMysqlxRoutesTable, kRuntimeMysqlxRoutesTableDef); + + register_table_pair(services, kMysqlxBackendEndpointsTable, kMysqlxBackendEndpointsTableDef); + register_runtime_table(services, kRuntimeMysqlxBackendEndpointsTable, kRuntimeMysqlxBackendEndpointsTableDef); + + services.register_command("PLUGIN MYSQLX LOAD USERS TO RUNTIME", &load_users_to_runtime); + services.register_command("PLUGIN MYSQLX LOAD ROUTES TO RUNTIME", &load_routes_to_runtime); + services.register_command("PLUGIN MYSQLX LOAD BACKEND ENDPOINTS TO RUNTIME", &load_backend_endpoints_to_runtime); return true; } diff --git a/plugins/mysqlx/src/mysqlx_config_store.cpp b/plugins/mysqlx/src/mysqlx_config_store.cpp new file mode 100644 index 000000000..e783afac1 --- /dev/null +++ b/plugins/mysqlx/src/mysqlx_config_store.cpp @@ -0,0 +1,276 @@ +#include "mysqlx_config_store.h" + +#include "sqlite3db.h" + +#include +#include +#include +#include + +namespace { + +struct MysqlxEndpointOverride { + int mysqlx_port { 33060 }; + bool use_ssl { false }; + std::string attributes {}; +}; + +std::string nullable_string(const char* value) { + return value != nullptr ? value : ""; +} + +int nullable_int(const char* value, int default_value = 0) { + return value != nullptr ? std::atoi(value) : default_value; +} + +bool nullable_bool(const char* value, bool default_value = false) { + return value != nullptr ? std::atoi(value) != 0 : default_value; +} + +std::string endpoint_key(const std::string& hostname, int mysql_port) { + return hostname + ":" + std::to_string(mysql_port); +} + +bool fetch_result(SQLite3DB& db, const char* sql, std::unique_ptr& result, std::string& err) { + char* error = nullptr; + result.reset(db.execute_statement(sql, &error)); + if (error != nullptr) { + err = error; + free(error); + return false; + } + if (!result) { + err = "sqlite query returned no result"; + return false; + } + return true; +} + +void load_canonical_users( + SQLite3_result& rows, + std::unordered_map& identities +) { + for (auto* row : rows.rows) { + if (row == nullptr) { + continue; + } + + MysqlxResolvedIdentity identity {}; + identity.username = nullable_string(row->fields[0]); + identity.default_hostgroup = nullable_int(row->fields[1]); + identity.max_connections = nullable_int(row->fields[2]); + identities[identity.username] = std::move(identity); + } +} + +void merge_mysqlx_users( + SQLite3_result& rows, + std::unordered_map& identities +) { + for (auto* row : rows.rows) { + if (row == nullptr || row->fields[0] == nullptr) { + continue; + } + + const std::string username = row->fields[0]; + auto it = identities.find(username); + if (it == identities.end()) { + continue; + } + + MysqlxResolvedIdentity& identity = it->second; + identity.x_enabled = nullable_bool(row->fields[1]); + identity.require_tls = nullable_bool(row->fields[2]); + identity.allowed_auth_methods = nullable_string(row->fields[3]); + identity.default_route = nullable_string(row->fields[4]); + identity.policy_profile = nullable_string(row->fields[5]); + identity.backend_auth_mode = mysqlx_backend_auth_mode_from_string(nullable_string(row->fields[6])); + identity.backend_username = nullable_string(row->fields[7]); + identity.backend_password = nullable_string(row->fields[8]); + identity.attributes = nullable_string(row->fields[9]); + } +} + +void load_routes( + SQLite3_result& rows, + std::unordered_map& routes +) { + for (auto* row : rows.rows) { + if (row == nullptr || row->fields[0] == nullptr) { + continue; + } + + MysqlxRoute route {}; + route.name = nullable_string(row->fields[0]); + route.bind = nullable_string(row->fields[1]); + route.destination_hostgroup = nullable_int(row->fields[2]); + route.fallback_hostgroup = nullable_int(row->fields[3], -1); + route.strategy = nullable_string(row->fields[4]); + route.active = nullable_bool(row->fields[5], true); + route.attributes = nullable_string(row->fields[6]); + routes[route.name] = std::move(route); + } +} + +void load_endpoint_overrides( + SQLite3_result& rows, + std::unordered_map& overrides +) { + for (auto* row : rows.rows) { + if (row == nullptr || row->fields[0] == nullptr) { + continue; + } + + MysqlxEndpointOverride override {}; + const std::string hostname = nullable_string(row->fields[0]); + const int mysql_port = nullable_int(row->fields[1]); + override.mysqlx_port = nullable_int(row->fields[2], 33060); + override.use_ssl = nullable_bool(row->fields[3]); + override.attributes = nullable_string(row->fields[4]); + overrides[endpoint_key(hostname, mysql_port)] = std::move(override); + } +} + +void load_backend_servers( + SQLite3_result& rows, + const std::unordered_map& overrides, + std::unordered_map>& hostgroup_endpoints +) { + for (auto* row : rows.rows) { + if (row == nullptr || row->fields[1] == nullptr) { + continue; + } + + MysqlxBackendEndpoint endpoint {}; + const int hostgroup_id = nullable_int(row->fields[0]); + endpoint.hostname = nullable_string(row->fields[1]); + endpoint.mysql_port = nullable_int(row->fields[2]); + endpoint.use_ssl = nullable_bool(row->fields[3]); + + const auto it = overrides.find(endpoint_key(endpoint.hostname, endpoint.mysql_port)); + if (it != overrides.end()) { + endpoint.mysqlx_port = it->second.mysqlx_port; + endpoint.use_ssl = it->second.use_ssl; + endpoint.attributes = it->second.attributes; + } + + hostgroup_endpoints[hostgroup_id].push_back(std::move(endpoint)); + } +} + +} // namespace + +MysqlxBackendAuthMode mysqlx_backend_auth_mode_from_string(const std::string& value) { + if (strcasecmp(value.c_str(), "pass_through") == 0) { + return MysqlxBackendAuthMode::pass_through; + } + if (strcasecmp(value.c_str(), "service_account") == 0) { + return MysqlxBackendAuthMode::service_account; + } + return MysqlxBackendAuthMode::mapped; +} + +bool MysqlxConfigStore::load_from_runtime(SQLite3DB& db, std::string& err) { + err.clear(); + + std::unordered_map new_identities {}; + std::unordered_map new_routes {}; + std::unordered_map> new_hostgroup_endpoints {}; + std::unordered_map endpoint_overrides {}; + std::unique_ptr result {}; + + if (!fetch_result( + db, + "SELECT username, default_hostgroup, max_connections " + "FROM runtime_mysql_users WHERE active=1 AND frontend=1", + result, + err)) { + return false; + } + load_canonical_users(*result, new_identities); + + if (!fetch_result( + db, + "SELECT username, active, require_tls, allowed_auth_methods, default_route, policy_profile, " + "backend_auth_mode, backend_username, backend_password, attributes " + "FROM runtime_mysqlx_users", + result, + err)) { + return false; + } + merge_mysqlx_users(*result, new_identities); + + if (!fetch_result( + db, + "SELECT name, bind, destination_hostgroup, fallback_hostgroup, strategy, active, attributes " + "FROM runtime_mysqlx_routes WHERE active=1", + result, + err)) { + return false; + } + load_routes(*result, new_routes); + + if (!fetch_result( + db, + "SELECT hostname, mysql_port, mysqlx_port, use_ssl, attributes " + "FROM runtime_mysqlx_backend_endpoints", + result, + err)) { + return false; + } + load_endpoint_overrides(*result, endpoint_overrides); + + if (!fetch_result( + db, + "SELECT hostgroup_id, hostname, port, use_ssl " + "FROM runtime_mysql_servers WHERE UPPER(status)='ONLINE' " + "ORDER BY hostgroup_id, weight DESC, hostname, port", + result, + err)) { + return false; + } + load_backend_servers(*result, endpoint_overrides, new_hostgroup_endpoints); + + identities_.swap(new_identities); + routes_.swap(new_routes); + hostgroup_endpoints_.swap(new_hostgroup_endpoints); + return true; +} + +std::optional MysqlxConfigStore::resolve_identity(const std::string& username) const { + const auto it = identities_.find(username); + if (it == identities_.end()) { + return std::nullopt; + } + return it->second; +} + +MysqlxBackendEndpoint MysqlxConfigStore::pick_endpoint(const std::string& route_name) const { + const auto route_it = routes_.find(route_name); + if (route_it == routes_.end()) { + return {}; + } + + const MysqlxRoute& route = route_it->second; + const auto primary_it = hostgroup_endpoints_.find(route.destination_hostgroup); + if (primary_it != hostgroup_endpoints_.end() && !primary_it->second.empty()) { + return primary_it->second.front(); + } + + if (route.fallback_hostgroup >= 0) { + const auto fallback_it = hostgroup_endpoints_.find(route.fallback_hostgroup); + if (fallback_it != hostgroup_endpoints_.end() && !fallback_it->second.empty()) { + return fallback_it->second.front(); + } + } + + return {}; +} + +uint64_t MysqlxConfigStore::topology_generation() const { + return topology_generation_; +} + +void MysqlxConfigStore::bump_topology_generation() { + ++topology_generation_; +} diff --git a/plugins/mysqlx/src/mysqlx_plugin.cpp b/plugins/mysqlx/src/mysqlx_plugin.cpp index cdd055f86..0489bf5c8 100644 --- a/plugins/mysqlx/src/mysqlx_plugin.cpp +++ b/plugins/mysqlx/src/mysqlx_plugin.cpp @@ -1,23 +1,16 @@ #include "mysqlx_plugin.h" -class MysqlxConfigStore { -public: - MysqlxConfigStore() = default; - MysqlxConfigStore(const MysqlxConfigStore&) = delete; - MysqlxConfigStore& operator=(const MysqlxConfigStore&) = delete; - ~MysqlxConfigStore() = default; -}; - namespace { bool mysqlx_init(ProxySQL_PluginServices* services) { + if (services == nullptr) { + return false; + } + MysqlxPluginContext& ctx = mysqlx_context(); ctx.services = services; ctx.config_store = std::make_unique(); ctx.started = false; - if (services == nullptr) { - return false; - } return mysqlx_register_admin_schema(*services); } diff --git a/test/tap/tests/test_mysqlx_admin_tables-t.cpp b/test/tap/tests/test_mysqlx_admin_tables-t.cpp index 4a3715140..f1be80867 100644 --- a/test/tap/tests/test_mysqlx_admin_tables-t.cpp +++ b/test/tap/tests/test_mysqlx_admin_tables-t.cpp @@ -78,7 +78,7 @@ SQLite3DB* proxysql_plugin_get_statsdb() { } int main() { - plan(18); + plan(20); ok(test_init_minimal() == 0, "minimal test globals initialize"); diff --git a/test/tap/tests/unit/Makefile b/test/tap/tests/unit/Makefile index fb4ca685c..a1fd0f090 100644 --- a/test/tap/tests/unit/Makefile +++ b/test/tap/tests/unit/Makefile @@ -362,8 +362,8 @@ test_mysqlx_plugin_load-t: ../test_mysqlx_plugin_load-t.cpp $(ODIR)/tap.o $(ODIR $(IDIRS) $(LDIRS) $(OPT) $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) \ $(MYLIBS) -ldl $(ALLOW_MULTI_DEF) -o $@ -mysqlx_config_store_unit-t: mysqlx_config_store_unit-t.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) - $(CXX) $< $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ +mysqlx_config_store_unit-t: mysqlx_config_store_unit-t.cpp $(PROXYSQL_PATH)/plugins/mysqlx/src/mysqlx_config_store.cpp $(TEST_HELPERS_OBJ) $(LIBPROXYSQLAR) + $(CXX) $< $(PROXYSQL_PATH)/plugins/mysqlx/src/mysqlx_config_store.cpp $(TEST_HELPERS_OBJ) $(IDIRS) $(LDIRS) $(OPT) \ -I$(PROXYSQL_PATH)/plugins/mysqlx/include \ $(LIBPROXYSQLAR_FULL) $(STATIC_LIBS) $(MYLIBS) \ $(ALLOW_MULTI_DEF) -o $@ diff --git a/test/tap/tests/unit/mysqlx_config_store_unit-t.cpp b/test/tap/tests/unit/mysqlx_config_store_unit-t.cpp index d66ea2c2f..5d238f8b8 100644 --- a/test/tap/tests/unit/mysqlx_config_store_unit-t.cpp +++ b/test/tap/tests/unit/mysqlx_config_store_unit-t.cpp @@ -1,8 +1,61 @@ #include "mysqlx_config_store.h" +#include "ProxySQL_Admin_Tables_Definitions.h" +#include "sqlite3db.h" #include "tap.h" +#include +#include + +namespace { + +const char kRuntimeMysqlxUsersDdl[] = + "CREATE TABLE runtime_mysqlx_users (" + " username VARCHAR NOT NULL PRIMARY KEY," + " active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1," + " require_tls INT CHECK (require_tls IN (0,1)) NOT NULL DEFAULT 0," + " allowed_auth_methods VARCHAR NOT NULL DEFAULT ''," + " default_route VARCHAR," + " policy_profile VARCHAR," + " backend_auth_mode VARCHAR NOT NULL DEFAULT 'mapped'," + " backend_username VARCHAR," + " backend_password VARCHAR," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''" + " )"; + +const char kRuntimeMysqlxRoutesDdl[] = + "CREATE TABLE runtime_mysqlx_routes (" + " name VARCHAR NOT NULL PRIMARY KEY," + " bind VARCHAR NOT NULL," + " destination_hostgroup INT NOT NULL," + " fallback_hostgroup INT," + " strategy VARCHAR NOT NULL DEFAULT 'first_available'," + " active INT CHECK (active IN (0,1)) NOT NULL DEFAULT 1," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''" + " )"; + +const char kRuntimeMysqlxEndpointsDdl[] = + "CREATE TABLE runtime_mysqlx_backend_endpoints (" + " hostname VARCHAR NOT NULL," + " mysql_port INT NOT NULL," + " mysqlx_port INT NOT NULL DEFAULT 33060," + " use_ssl INT CHECK (use_ssl IN (0,1)) NOT NULL DEFAULT 0," + " attributes VARCHAR CHECK (JSON_VALID(attributes) OR attributes = '') NOT NULL DEFAULT ''," + " comment VARCHAR NOT NULL DEFAULT ''," + " PRIMARY KEY (hostname, mysql_port)" + " )"; + +std::unique_ptr create_test_db() { + auto db = std::make_unique(); + db->open((char*)":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX); + return db; +} + +} // namespace + int main() { - plan(5); + plan(16); MysqlxResolvedIdentity identity {}; identity.username = "canonical_user"; @@ -21,5 +74,63 @@ int main() { MysqlxBackendAuthMode::pass_through, "backend auth mode parser accepts pass_through"); + auto db = create_test_db(); + ok(db->execute(ADMIN_SQLITE_RUNTIME_MYSQL_USERS), + "runtime mysql users table is created"); + ok(db->execute(ADMIN_SQLITE_TABLE_RUNTIME_MYSQL_SERVERS), + "runtime mysql servers table is created"); + ok(db->execute(kRuntimeMysqlxUsersDdl) && + db->execute(kRuntimeMysqlxRoutesDdl) && + db->execute(kRuntimeMysqlxEndpointsDdl), + "runtime mysqlx tables are created"); + + ok(db->execute("INSERT INTO runtime_mysql_users (username, password, active, use_ssl, default_hostgroup, " + "default_schema, schema_locked, transaction_persistent, fast_forward, backend, frontend, " + "max_connections, attributes, comment) VALUES " + "('alice', 'pw', 1, 0, 10, NULL, 0, 1, 0, 0, 1, 25, '', 'canonical')"), + "canonical frontend mysql user is inserted"); + ok(db->execute("INSERT INTO runtime_mysqlx_users (username, active, require_tls, allowed_auth_methods, " + "default_route, policy_profile, backend_auth_mode, backend_username, backend_password, attributes, comment) VALUES " + "('alice', 1, 1, 'PLAIN', 'rw', 'policy-a', 'service_account', 'svc_user', 'svc_pass', '', 'override')"), + "mysqlx override row is inserted"); + ok(db->execute("INSERT INTO runtime_mysqlx_routes (name, bind, destination_hostgroup, fallback_hostgroup, strategy, active, attributes, comment) VALUES " + "('rw', '127.0.0.1:6603', 42, 43, 'first_available', 1, '', 'route')") && + db->execute("INSERT INTO runtime_mysql_servers (hostgroup_id, hostname, port, gtid_port, status, weight, compression, max_connections, max_replication_lag, use_ssl, max_latency_ms, comment) VALUES " + "(42, 'db1.internal', 3306, 0, 'ONLINE', 100, 0, 1000, 0, 0, 0, 'server')") && + db->execute("INSERT INTO runtime_mysqlx_backend_endpoints (hostname, mysql_port, mysqlx_port, use_ssl, attributes, comment) VALUES " + "('db1.internal', 3306, 33100, 1, '', 'endpoint')"), + "route, backend server, and mysqlx endpoint are inserted"); + + MysqlxConfigStore store {}; + std::string err {}; + ok(store.load_from_runtime(*db, err) && err.empty(), + "config store loads runtime mysql and mysqlx state"); + + const auto resolved = store.resolve_identity("alice"); + ok(resolved.has_value() && + resolved->default_hostgroup == 10 && + resolved->max_connections == 25 && + resolved->x_enabled && + resolved->require_tls && + resolved->default_route == "rw" && + resolved->policy_profile == "policy-a" && + resolved->backend_auth_mode == MysqlxBackendAuthMode::service_account && + resolved->backend_username == "svc_user" && + resolved->backend_password == "svc_pass", + "config store merges canonical mysql user and mysqlx override state"); + + const MysqlxBackendEndpoint endpoint = store.pick_endpoint("rw"); + ok(endpoint.hostname == "db1.internal" && + endpoint.mysql_port == 3306 && + endpoint.mysqlx_port == 33100 && + endpoint.use_ssl, + "config store picks backend endpoint from route hostgroup and mysqlx overrides"); + + ok(store.topology_generation() == 0, + "topology generation starts at zero"); + store.bump_topology_generation(); + ok(store.topology_generation() == 1, + "topology generation increments on demand"); + return exit_status(); }