diff options
| author | Zousar Shaker <[email protected]> | 2026-03-20 05:45:44 -0600 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-03-20 12:45:44 +0100 |
| commit | d44a692aaeb6f78e8cf7520236b21977333fa3c3 (patch) | |
| tree | b8ae4ae8b2e6b51ae6fee2123424e2abd7cebb74 /src | |
| parent | Zen disk benchmark utility (#868) (diff) | |
| download | zen-d44a692aaeb6f78e8cf7520236b21977333fa3c3.tar.xz zen-d44a692aaeb6f78e8cf7520236b21977333fa3c3.zip | |
Zs/consul token (#870)
- Feature: Added support for consul token passed via environment variable, and specified a default env var name of CONSUL_HTTP_TOKEN for it in hub mode
Diffstat (limited to 'src')
| -rw-r--r-- | src/zenserver-test/hub-tests.cpp | 56 | ||||
| -rw-r--r-- | src/zenserver/hub/zenhubserver.cpp | 32 | ||||
| -rw-r--r-- | src/zenserver/hub/zenhubserver.h | 1 | ||||
| -rw-r--r-- | src/zenutil/consul/consul.cpp | 66 | ||||
| -rw-r--r-- | src/zenutil/include/zenutil/consul.h | 6 |
5 files changed, 146 insertions, 15 deletions
diff --git a/src/zenserver-test/hub-tests.cpp b/src/zenserver-test/hub-tests.cpp index 958a0b050..29c3b76ba 100644 --- a/src/zenserver-test/hub-tests.cpp +++ b/src/zenserver-test/hub-tests.cpp @@ -21,6 +21,11 @@ # include <zenutil/consul.h> # include <zencore/thread.h> # include <zencore/timer.h> +# if ZEN_PLATFORM_WINDOWS +# include <zencore/windows.h> +# else +# include <cstdlib> +# endif namespace zen::tests::hub { @@ -290,6 +295,57 @@ TEST_CASE("hub.consul.hub.registration") ConsulProc.StopConsulAgent(); } +TEST_CASE("hub.consul.hub.registration.token") +{ + // Set an env var that the server will read its Consul token from. + // Children inherit parent environment, so the spawned server will see it. + constexpr const char* TokenEnvVarName = "ZEN_TEST_CONSUL_TOKEN"; + constexpr const char* TokenValue = "test-token-value"; +# if ZEN_PLATFORM_WINDOWS + char PrevBuf[1024] = {}; + DWORD PrevLen = GetEnvironmentVariableA(TokenEnvVarName, PrevBuf, sizeof(PrevBuf)); + REQUIRE(PrevLen < sizeof(PrevBuf)); + SetEnvironmentVariableA(TokenEnvVarName, TokenValue); + auto EnvCleanup = MakeGuard([PrevEnvValue = std::string(PrevBuf, PrevLen), HadPrevToken = PrevLen > 0] { + SetEnvironmentVariableA(TokenEnvVarName, HadPrevToken ? PrevEnvValue.c_str() : nullptr); + }); +# else + const char* PrevEnvPtr = getenv(TokenEnvVarName); + setenv(TokenEnvVarName, TokenValue, /*overwrite=*/1); + auto EnvCleanup = MakeGuard([PrevEnvValue = std::string(PrevEnvPtr ? PrevEnvPtr : ""), HadPrevToken = PrevEnvPtr != nullptr] { + if (HadPrevToken) + { + setenv(TokenEnvVarName, PrevEnvValue.c_str(), /*overwrite=*/1); + } + else + { + unsetenv(TokenEnvVarName); + } + }); +# endif + + consul::ConsulProcess ConsulProc; + ConsulProc.SpawnConsulAgent(); + + ZenServerInstance Instance(TestEnv, ZenServerInstance::ServerMode::kHubServer); + const uint16_t PortNumber = Instance.SpawnServerAndWaitUntilReady( + "--consul-endpoint=http://localhost:8500/ --instance-id=test-instance " + "--consul-token-env=ZEN_TEST_CONSUL_TOKEN"); + REQUIRE(PortNumber != 0); + + // Use a plain client — dev-mode Consul doesn't enforce ACLs, but the + // server has exercised the ConsulTokenEnv -> GetEnvVariable -> ConsulClient path. + consul::ConsulClient Client("http://localhost:8500/"); + + REQUIRE(WaitForConsulService(Client, "zen-hub-test-instance", true, 5000)); + + Instance.Shutdown(); + + CHECK(!Client.HasService("zen-hub-test-instance")); + + ConsulProc.StopConsulAgent(); +} + TEST_CASE("hub.consul.provision.registration") { consul::ConsulProcess ConsulProc; diff --git a/src/zenserver/hub/zenhubserver.cpp b/src/zenserver/hub/zenhubserver.cpp index 7bced2809..45a3211b2 100644 --- a/src/zenserver/hub/zenhubserver.cpp +++ b/src/zenserver/hub/zenhubserver.cpp @@ -23,6 +23,13 @@ ZEN_THIRD_PARTY_INCLUDES_END namespace zen { +const std::string& +GetDefaultConsulTokenEnvVariableName() +{ + static const std::string Name = "CONSUL_HTTP_TOKEN"; + return Name; +} + void ZenHubServerConfigurator::AddCliOptions(cxxopts::Options& Options) { @@ -58,6 +65,14 @@ ZenHubServerConfigurator::AddCliOptions(cxxopts::Options& Options) Options.add_option("hub", "", + "consul-token-env", + fmt::format("Name of environment variable that holds the consul access token (defaults to '{}')", + GetDefaultConsulTokenEnvVariableName()), + cxxopts::value<std::string>(m_ServerOptions.ConsulTokenEnv)->default_value(""), + "<envvariable>"); + + Options.add_option("hub", + "", "hub-base-port-number", "Base port number for provisioned instances", cxxopts::value<uint16_t>(m_ServerOptions.HubBasePortNumber)->default_value("21000"), @@ -342,9 +357,24 @@ ZenHubServer::InitializeConsulRegistration(const ZenHubServerConfig& ServerConfi ZEN_INFO("Initializing Consul registration with endpoint: {}", ServerConfig.ConsulEndpoint); + std::string ConsulAccessTokenEnvName = + ServerConfig.ConsulTokenEnv.empty() ? GetDefaultConsulTokenEnvVariableName() : ServerConfig.ConsulTokenEnv; + std::string ConsulAccessToken = GetEnvVariable(ConsulAccessTokenEnvName); + if (ConsulAccessToken.empty()) + { + if (!ServerConfig.ConsulTokenEnv.empty()) + { + ZEN_WARN("Consul token environment variable '{}' is not set or empty", ServerConfig.ConsulTokenEnv); + } + } + else + { + ZEN_INFO("Consul token read from environment variable '{}'", ConsulAccessTokenEnvName); + } + try { - m_ConsulClient = std::make_unique<consul::ConsulClient>(ServerConfig.ConsulEndpoint); + m_ConsulClient = std::make_unique<consul::ConsulClient>(ServerConfig.ConsulEndpoint, ConsulAccessToken); consul::ServiceRegistrationInfo Info; Info.ServiceId = fmt::format("zen-hub-{}", ServerConfig.InstanceId); diff --git a/src/zenserver/hub/zenhubserver.h b/src/zenserver/hub/zenhubserver.h index 15efeaec9..33d813122 100644 --- a/src/zenserver/hub/zenhubserver.h +++ b/src/zenserver/hub/zenhubserver.h @@ -24,6 +24,7 @@ struct ZenHubServerConfig : public ZenServerConfig std::string UpstreamNotificationEndpoint; std::string InstanceId; // For use in notifications std::string ConsulEndpoint; // If set, enables Consul service registration + std::string ConsulTokenEnv; // Environment variable name to read a Consul token from; defaults to CONSUL_HTTP_TOKEN if empty uint16_t HubBasePortNumber = 21000; int HubInstanceLimit = 1000; bool HubUseJobObject = true; diff --git a/src/zenutil/consul/consul.cpp b/src/zenutil/consul/consul.cpp index 272096ebb..d463c0938 100644 --- a/src/zenutil/consul/consul.cpp +++ b/src/zenutil/consul/consul.cpp @@ -20,7 +20,7 @@ namespace zen::consul { struct ConsulProcess::Impl { - Impl(std::string_view BaseUri) : m_HttpClient(BaseUri) {} + Impl(std::string_view BaseUri, std::string_view Token = "") : m_Token(Token), m_HttpClient(BaseUri) {} ~Impl() = default; void SpawnConsulAgent() @@ -44,10 +44,16 @@ struct ConsulProcess::Impl // Poll to check when the agent is ready + HttpClient::KeyValueMap AdditionalHeaders; + if (!m_Token.empty()) + { + AdditionalHeaders.Entries.emplace("X-Consul-Token", m_Token); + } + do { Sleep(100); - HttpClient::Response Resp = m_HttpClient.Get("v1/status/leader"); + HttpClient::Response Resp = m_HttpClient.Get("v1/status/leader", AdditionalHeaders); if (Resp) { ZEN_INFO("Consul agent started successfully (waited {})", NiceTimeSpanMs(Timer.GetElapsedTimeMs())); @@ -75,6 +81,7 @@ struct ConsulProcess::Impl private: ProcessHandle m_ProcessHandle; + std::string m_Token; HttpClient m_HttpClient; }; @@ -100,7 +107,7 @@ ConsulProcess::StopConsulAgent() ////////////////////////////////////////////////////////////////////////// -ConsulClient::ConsulClient(std::string_view BaseUri) : m_HttpClient(BaseUri) +ConsulClient::ConsulClient(std::string_view BaseUri, std::string_view Token) : m_Token(Token), m_HttpClient(BaseUri) { } @@ -111,9 +118,13 @@ ConsulClient::~ConsulClient() void ConsulClient::SetKeyValue(std::string_view Key, std::string_view Value) { + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + AdditionalHeaders.Entries.emplace(HttpClient::Accept(HttpContentType::kJSON)); + IoBuffer ValueBuffer = IoBufferBuilder::MakeFromMemory(MakeMemoryView(Value)); ValueBuffer.SetContentType(HttpContentType::kText); - HttpClient::Response Result = m_HttpClient.Put(fmt::format("v1/kv/{}", Key), ValueBuffer, HttpClient::Accept(HttpContentType::kJSON)); + HttpClient::Response Result = m_HttpClient.Put(fmt::format("v1/kv/{}", Key), ValueBuffer, AdditionalHeaders); if (!Result) { throw runtime_error("ConsulClient::SetKeyValue() failed to set key '{}' ({})", Key, Result.ErrorMessage("")); @@ -123,7 +134,10 @@ ConsulClient::SetKeyValue(std::string_view Key, std::string_view Value) std::string ConsulClient::GetKeyValue(std::string_view Key) { - HttpClient::Response Result = m_HttpClient.Get(fmt::format("v1/kv/{}?raw", Key)); + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + + HttpClient::Response Result = m_HttpClient.Get(fmt::format("v1/kv/{}?raw", Key), AdditionalHeaders); if (!Result) { throw runtime_error("ConsulClient::GetKeyValue() failed to get key '{}' ({})", Key, Result.ErrorMessage("")); @@ -134,7 +148,10 @@ ConsulClient::GetKeyValue(std::string_view Key) void ConsulClient::DeleteKey(std::string_view Key) { - HttpClient::Response Result = m_HttpClient.Delete(fmt::format("v1/kv/{}", Key)); + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + + HttpClient::Response Result = m_HttpClient.Delete(fmt::format("v1/kv/{}", Key), AdditionalHeaders); if (!Result) { throw runtime_error("ConsulClient::DeleteKey() failed to delete key '{}' ({})", Key, Result.ErrorMessage("")); @@ -146,6 +163,10 @@ ConsulClient::RegisterService(const ServiceRegistrationInfo& Info) { using namespace std::literals; + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + AdditionalHeaders.Entries.emplace(HttpClient::Accept(HttpContentType::kJSON)); + CbObjectWriter Writer; { Writer.AddString("ID"sv, Info.ServiceId); @@ -178,7 +199,7 @@ ConsulClient::RegisterService(const ServiceRegistrationInfo& Info) IoBuffer PayloadBuffer(IoBuffer::Wrap, SB.Data(), SB.Size()); PayloadBuffer.SetContentType(HttpContentType::kJSON); - HttpClient::Response Result = m_HttpClient.Put("v1/agent/service/register", PayloadBuffer, HttpClient::Accept(HttpContentType::kJSON)); + HttpClient::Response Result = m_HttpClient.Put("v1/agent/service/register", PayloadBuffer, AdditionalHeaders); if (!Result) { @@ -192,8 +213,11 @@ ConsulClient::RegisterService(const ServiceRegistrationInfo& Info) bool ConsulClient::DeregisterService(std::string_view ServiceId) { - HttpClient::Response Result = - m_HttpClient.Put(fmt::format("v1/agent/service/deregister/{}", ServiceId), HttpClient::Accept(HttpContentType::kJSON)); + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + AdditionalHeaders.Entries.emplace(HttpClient::Accept(HttpContentType::kJSON)); + + HttpClient::Response Result = m_HttpClient.Put(fmt::format("v1/agent/service/deregister/{}", ServiceId), AdditionalHeaders); if (!Result) { @@ -204,6 +228,15 @@ ConsulClient::DeregisterService(std::string_view ServiceId) return true; } +void +ConsulClient::ApplyCommonHeaders(HttpClient::KeyValueMap& InOutHeaderMap) +{ + if (!m_Token.empty()) + { + InOutHeaderMap.Entries.emplace("X-Consul-Token", m_Token); + } +} + bool ConsulClient::FindServiceInJson(std::string_view Json, std::string_view ServiceId) { @@ -235,7 +268,10 @@ ConsulClient::FindServiceInJson(std::string_view Json, std::string_view ServiceI bool ConsulClient::HasService(std::string_view ServiceId) { - HttpClient::Response Result = m_HttpClient.Get("v1/agent/services"); + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + + HttpClient::Response Result = m_HttpClient.Get("v1/agent/services", AdditionalHeaders); if (!Result) { return false; @@ -246,10 +282,13 @@ ConsulClient::HasService(std::string_view ServiceId) bool ConsulClient::WatchService(std::string_view ServiceId, uint64_t& InOutIndex, int WaitSeconds) { + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + // Note: m_HttpClient uses unlimited HTTP timeout (Timeout{0}); the WaitSeconds parameter // governs the server-side bound on the blocking query. Do not add a separate client timeout. HttpClient::KeyValueMap Parameters({{"index", std::to_string(InOutIndex)}, {"wait", fmt::format("{}s", WaitSeconds)}}); - HttpClient::Response Result = m_HttpClient.Get("v1/agent/services", {}, Parameters); + HttpClient::Response Result = m_HttpClient.Get("v1/agent/services", AdditionalHeaders, Parameters); if (!Result) { return false; @@ -271,7 +310,10 @@ ConsulClient::WatchService(std::string_view ServiceId, uint64_t& InOutIndex, int std::string ConsulClient::GetAgentServicesJson() { - HttpClient::Response Result = m_HttpClient.Get("v1/agent/services"); + HttpClient::KeyValueMap AdditionalHeaders; + ApplyCommonHeaders(AdditionalHeaders); + + HttpClient::Response Result = m_HttpClient.Get("v1/agent/services", AdditionalHeaders); if (!Result) { return "{}"; diff --git a/src/zenutil/include/zenutil/consul.h b/src/zenutil/include/zenutil/consul.h index b5bfa42d1..7bf2ce437 100644 --- a/src/zenutil/include/zenutil/consul.h +++ b/src/zenutil/include/zenutil/consul.h @@ -28,7 +28,7 @@ struct ServiceRegistrationInfo class ConsulClient { public: - ConsulClient(std::string_view BaseUri); + ConsulClient(std::string_view BaseUri, std::string_view Token = ""); ~ConsulClient(); ConsulClient(const ConsulClient&) = delete; @@ -53,8 +53,10 @@ public: private: static bool FindServiceInJson(std::string_view Json, std::string_view ServiceId); + void ApplyCommonHeaders(HttpClient::KeyValueMap& InOutHeaderMap); - HttpClient m_HttpClient; + std::string m_Token; + HttpClient m_HttpClient; }; class ConsulProcess |