This is an automated email from the ASF dual-hosted git repository. mgreber pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/kudu.git
The following commit(s) were added to refs/heads/master by this push: new 18a132a9f Add /api/v1/leader endpoint to REST API endpoints 18a132a9f is described below commit 18a132a9f377cb1ee49cf7fc84d0cda3ff781e43 Author: gabriellalotz <lotzgabrie...@gmail.com> AuthorDate: Mon Apr 14 12:48:01 2025 +0000 Add /api/v1/leader endpoint to REST API endpoints To support leader discovery without relying on HTTP redirects, which are incompatible with SPNEGO authentication, a new REST API endpoint /api/v1/leader has been introduced. This endpoint returns the current leader master's HTTP or HTTPS address in a compact JSON format, e.g.: { "leader": "http://hostname:port" } Change-Id: If74db14bee2d273529ebfb02e72c6675bb094bce Reviewed-on: http://gerrit.cloudera.org:8080/22779 Tested-by: Marton Greber <greber...@gmail.com> Reviewed-by: Marton Greber <greber...@gmail.com> Reviewed-by: Zoltan Chovan <zcho...@cloudera.com> --- src/kudu/master/rest_catalog-test.cc | 53 ++++++++++++++++++++++++ src/kudu/master/rest_catalog_path_handlers.cc | 58 ++++++++++++++++++++++++++- src/kudu/master/rest_catalog_path_handlers.h | 2 + 3 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/kudu/master/rest_catalog-test.cc b/src/kudu/master/rest_catalog-test.cc index 6e17ff9c7..de97544ac 100644 --- a/src/kudu/master/rest_catalog-test.cc +++ b/src/kudu/master/rest_catalog-test.cc @@ -16,6 +16,7 @@ // under the License. #include <memory> +#include <set> #include <string> #include <type_traits> #include <vector> @@ -25,6 +26,7 @@ #include "kudu/client/client.h" #include "kudu/client/schema.h" +#include "kudu/gutil/strings/join.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/master/mini_master.h" #include "kudu/master/rest_catalog_test_base.h" @@ -44,6 +46,7 @@ using kudu::client::KuduTable; using kudu::client::sp::shared_ptr; using kudu::cluster::InternalMiniCluster; using kudu::cluster::InternalMiniClusterOptions; +using std::set; using std::string; using std::unique_ptr; using std::vector; @@ -577,5 +580,55 @@ TEST_F(RestCatalogTest, TestPutTableEndpointChangeOwner) { ASSERT_EQ("new_owner", table->owner()); } +class MultiMasterTest : public RestCatalogTestBase { + public: + void SetUp() override { + KuduTest::SetUp(); + FLAGS_enable_rest_api = true; + + InternalMiniClusterOptions opts; + opts.num_masters = 3; + cluster_.reset(new InternalMiniCluster(env_, opts)); + ASSERT_OK(cluster_->Start()); + KuduClientBuilder client_builder; + ASSERT_OK(cluster_->CreateClient(&client_builder, &client_)); + } + + protected: + unique_ptr<InternalMiniCluster> cluster_; +}; + +TEST_F(MultiMasterTest, TestGetLeaderEndpoint) { + set<string> leader_addresses; + + static KuduRegex re("\"leader\":\"([^\"]+)\"", 1); + + for (int i = 0; i < cluster_->num_masters(); i++) { + EasyCurl c; + faststring buf; + ASSERT_OK(c.FetchURL(Substitute("http://$0/api/v1/leader", + cluster_->mini_master(i)->bound_http_addr().ToString()), + &buf)); + vector<string> matches; + ASSERT_TRUE(re.Match(buf.ToString(), &matches)); + ASSERT_FALSE(matches.empty()) << "No leader match in: " << buf.ToString(); + + leader_addresses.insert(matches[0]); + } + + // All masters should report the same leader + ASSERT_EQ(1, leader_addresses.size()) + << JoinStrings(leader_addresses, ", "); + + string leader_addr = *leader_addresses.begin(); + ASSERT_FALSE(leader_addr.empty()); + EasyCurl leader_curl; + faststring leader_buf; + leader_curl.set_verbose(true); + ASSERT_OK(leader_curl.FetchURL(Substitute("$0/api/v1/tables", leader_addr), + &leader_buf)); + ASSERT_STR_CONTAINS(leader_buf.ToString(), "{\"tables\":[]}"); +} + } // namespace master } // namespace kudu diff --git a/src/kudu/master/rest_catalog_path_handlers.cc b/src/kudu/master/rest_catalog_path_handlers.cc index ee5ecd626..8ae1f3e25 100644 --- a/src/kudu/master/rest_catalog_path_handlers.cc +++ b/src/kudu/master/rest_catalog_path_handlers.cc @@ -22,12 +22,15 @@ #include <string> #include <unordered_map> #include <utility> +#include <vector> #include <gflags/gflags.h> #include <google/protobuf/stubs/status.h> #include <google/protobuf/util/json_util.h> #include "kudu/common/common.pb.h" +#include "kudu/common/wire_protocol.pb.h" +#include "kudu/consensus/metadata.pb.h" #include "kudu/gutil/ref_counted.h" #include "kudu/gutil/strings/substitute.h" #include "kudu/master/catalog_manager.h" @@ -64,9 +67,11 @@ TAG_FLAG(rest_catalog_default_request_timeout_ms, runtime); using google::protobuf::util::JsonParseOptions; using google::protobuf::util::JsonStringToMessage; +using kudu::consensus::RaftPeerPB; using std::optional; using std::ostringstream; using std::string; +using std::vector; using strings::Substitute; namespace kudu { @@ -164,7 +169,50 @@ void RestCatalogPathHandlers::HandleApiTablesEndpoint(const Webserver::WebReques } } -void RestCatalogPathHandlers::HandleGetTables(ostringstream* output, +void RestCatalogPathHandlers::HandleLeaderEndpoint(const Webserver::WebRequest& req, + Webserver::PrerenderedWebResponse* resp) { + ostringstream* output = &resp->output; + JsonWriter jw(output, JsonWriter::COMPACT); + + if (req.request_method != "GET") { + RETURN_JSON_ERROR( + jw, "Method not allowed", resp->status_code, HttpStatusCode::MethodNotAllowed); + } + CatalogManager::ScopedLeaderSharedLock l(master_->catalog_manager()); + vector<ServerEntryPB> masters; + Status s = master_->ListMasters(&masters, + /*use_external_addr=*/false); + + if (!s.ok()) { + RETURN_JSON_ERROR( + jw, "unable to list masters", resp->status_code, HttpStatusCode::InternalServerError); + } + + for (const auto& master : masters) { + if (master.has_error() || master.role() != RaftPeerPB::LEADER) { + continue; + } + + const ServerRegistrationPB& reg = master.registration(); + + if (reg.http_addresses().empty()) { + RETURN_JSON_ERROR( + jw, "leader master has no http address", resp->status_code, HttpStatusCode::NotFound); + } + jw.StartObject(); + jw.String("leader"); + jw.String(Substitute("$0://$1:$2", + reg.https_enabled() ? "https" : "http", + reg.http_addresses(0).host(), + reg.http_addresses(0).port())); + jw.EndObject(); + resp->status_code = HttpStatusCode::Ok; + return; + } + RETURN_JSON_ERROR(jw, "No leader master found", resp->status_code, HttpStatusCode::NotFound); +} + +void RestCatalogPathHandlers::HandleGetTables(std::ostringstream* output, const Webserver::WebRequest& req, HttpStatusCode* status_code) { ListTablesRequestPB request; @@ -386,6 +434,14 @@ void RestCatalogPathHandlers::Register(Webserver* server) { }, StyleMode::JSON, false); + server->RegisterPrerenderedPathHandler( + "/api/v1/leader", + "", + [this](const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp) { + this->HandleLeaderEndpoint(req, resp); + }, + StyleMode::JSON, + false); } } // namespace master diff --git a/src/kudu/master/rest_catalog_path_handlers.h b/src/kudu/master/rest_catalog_path_handlers.h index a4f0d97ce..89c51065a 100644 --- a/src/kudu/master/rest_catalog_path_handlers.h +++ b/src/kudu/master/rest_catalog_path_handlers.h @@ -45,6 +45,8 @@ class RestCatalogPathHandlers final { Webserver::PrerenderedWebResponse* resp); void HandleApiTablesEndpoint(const Webserver::WebRequest& req, Webserver::PrerenderedWebResponse* resp); + void HandleLeaderEndpoint(const Webserver::WebRequest& req, + Webserver::PrerenderedWebResponse* resp); // Handles REST API endpoints based on the request method and path. void HandleGetTables(std::ostringstream* output,