mirror of
https://github.com/TrinityCore/TrinityCore.git
synced 2026-01-16 07:30:42 +01:00
Core/Config: Implement config override with env vars (#26811)
* Core/Config: Implement config override with env vars Implement overriding of configuration from the .conf file with environment variables. Environment variables keys are autogenerated based on the keys defined in .conf file. Usage example: $ export TC_DATA_DIR=/usr $ TC_WORLD_SERVER_PORT=8080 ./worldserver * Core/Config Fix typo in logs Co-authored-by: Giacomo Pozzoni <giacomopoz@gmail.com> * Core/Config Fix code style in EnvVarForIniKey Co-authored-by: Shauren <shauren.trinity@gmail.com> * Update tests/common/Config.cpp * Apply suggestions from code review Co-authored-by: Peter Keresztes Schmidt <carbenium@outlook.com> * Apply suggestions from code review Co-authored-by: Peter Keresztes Schmidt <carbenium@outlook.com> Co-authored-by: Anton Popovichenko <anton.popovichenko@mendix.com> Co-authored-by: Giacomo Pozzoni <giacomopoz@gmail.com> Co-authored-by: Shauren <shauren.trinity@gmail.com> Co-authored-by: Peter Keresztes Schmidt <carbenium@outlook.com>
This commit is contained in:
committed by
GitHub
parent
0bdc55b07c
commit
1ddd9dc19c
@@ -21,6 +21,7 @@
|
||||
#include "Util.h"
|
||||
#include <boost/property_tree/ini_parser.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
@@ -57,6 +58,81 @@ namespace
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Converts ini keys to the environment variable key (upper snake case).
|
||||
// Example of conversions:
|
||||
// SomeConfig => SOME_CONFIG
|
||||
// myNestedConfig.opt1 => MY_NESTED_CONFIG_OPT_1
|
||||
// LogDB.Opt.ClearTime => LOG_DB_OPT_CLEAR_TIME
|
||||
std::string IniKeyToEnvVarKey(std::string const& key)
|
||||
{
|
||||
std::string result;
|
||||
|
||||
const char *str = key.c_str();
|
||||
size_t n = key.length();
|
||||
|
||||
char curr;
|
||||
bool isEnd;
|
||||
bool nextIsUpper;
|
||||
bool currIsNumeric;
|
||||
bool nextIsNumeric;
|
||||
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
{
|
||||
curr = str[i];
|
||||
if (curr == ' ' || curr == '.' || curr == '-')
|
||||
{
|
||||
result += '_';
|
||||
continue;
|
||||
}
|
||||
|
||||
isEnd = i == n - 1;
|
||||
if (!isEnd)
|
||||
{
|
||||
nextIsUpper = isupper(str[i + 1]);
|
||||
|
||||
// handle "aB" to "A_B"
|
||||
if (!isupper(curr) && nextIsUpper)
|
||||
{
|
||||
result += static_cast<char>(std::toupper(curr));
|
||||
result += '_';
|
||||
continue;
|
||||
}
|
||||
|
||||
currIsNumeric = isNumeric(curr);
|
||||
nextIsNumeric = isNumeric(str[i + 1]);
|
||||
|
||||
// handle "a1" to "a_1"
|
||||
if (!currIsNumeric && nextIsNumeric)
|
||||
{
|
||||
result += static_cast<char>(std::toupper(curr));
|
||||
result += '_';
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle "1a" to "1_a"
|
||||
if (currIsNumeric && !nextIsNumeric)
|
||||
{
|
||||
result += static_cast<char>(std::toupper(curr));
|
||||
result += '_';
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result += static_cast<char>(std::toupper(curr));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Optional<std::string> EnvVarForIniKey(std::string const& key)
|
||||
{
|
||||
std::string envKey = "TC_" + IniKeyToEnvVarKey(key);
|
||||
char* val = std::getenv(envKey.c_str());
|
||||
if (!val)
|
||||
return std::nullopt;
|
||||
|
||||
return std::string(val);
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigMgr::LoadInitial(std::string file, std::vector<std::string> args,
|
||||
@@ -92,6 +168,29 @@ bool ConfigMgr::LoadAdditionalFile(std::string file, bool keepOnReload, std::str
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> ConfigMgr::OverrideWithEnvVariablesIfAny()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(_configLock);
|
||||
|
||||
std::vector<std::string> overriddenKeys;
|
||||
|
||||
for (bpt::ptree::value_type& itr: _config)
|
||||
{
|
||||
if (!itr.second.empty() || itr.first.empty())
|
||||
continue;
|
||||
|
||||
Optional<std::string> envVar = EnvVarForIniKey(itr.first);
|
||||
if (!envVar)
|
||||
continue;
|
||||
|
||||
itr.second = bpt::ptree(*envVar);
|
||||
|
||||
overriddenKeys.push_back(itr.first);
|
||||
}
|
||||
|
||||
return overriddenKeys;
|
||||
}
|
||||
|
||||
ConfigMgr* ConfigMgr::instance()
|
||||
{
|
||||
static ConfigMgr instance;
|
||||
@@ -108,6 +207,8 @@ bool ConfigMgr::Reload(std::vector<std::string>& errors)
|
||||
if (!LoadAdditionalFile(additionalFile, false, error))
|
||||
errors.push_back(std::move(error));
|
||||
|
||||
OverrideWithEnvVariablesIfAny();
|
||||
|
||||
return errors.empty();
|
||||
}
|
||||
|
||||
@@ -120,7 +221,22 @@ T ConfigMgr::GetValueDefault(std::string const& name, T def, bool quiet) const
|
||||
}
|
||||
catch (bpt::ptree_bad_path const&)
|
||||
{
|
||||
if (!quiet)
|
||||
Optional<std::string> envVar = EnvVarForIniKey(name);
|
||||
if (envVar)
|
||||
{
|
||||
Optional<T> castedVar = Trinity::StringTo<T>(*envVar);
|
||||
if (!castedVar)
|
||||
{
|
||||
TC_LOG_ERROR("server.loading", "Bad value defined for name %s in environment variables, going to use default instead", name.c_str());
|
||||
return def;
|
||||
}
|
||||
|
||||
if (!quiet)
|
||||
TC_LOG_WARN("server.loading", "Missing name %s in config file %s, recovered with environment '%s' value.", name.c_str(), _filename.c_str(), envVar->c_str());
|
||||
|
||||
return *castedVar;
|
||||
}
|
||||
else if (!quiet)
|
||||
{
|
||||
TC_LOG_WARN("server.loading", "Missing name %s in config file %s, add \"%s = %s\" to this file",
|
||||
name.c_str(), _filename.c_str(), name.c_str(), std::to_string(def).c_str());
|
||||
@@ -144,7 +260,15 @@ std::string ConfigMgr::GetValueDefault<std::string>(std::string const& name, std
|
||||
}
|
||||
catch (bpt::ptree_bad_path const&)
|
||||
{
|
||||
if (!quiet)
|
||||
Optional<std::string> envVar = EnvVarForIniKey(name);
|
||||
if (envVar)
|
||||
{
|
||||
if (!quiet)
|
||||
TC_LOG_WARN("server.loading", "Missing name %s in config file %s, recovered with environment '%s' value.", name.c_str(), _filename.c_str(), envVar->c_str());
|
||||
|
||||
return *envVar;
|
||||
}
|
||||
else if (!quiet)
|
||||
{
|
||||
TC_LOG_WARN("server.loading", "Missing name %s in config file %s, add \"%s = %s\" to this file",
|
||||
name.c_str(), _filename.c_str(), name.c_str(), def.c_str());
|
||||
|
||||
@@ -34,6 +34,9 @@ public:
|
||||
bool LoadInitial(std::string file, std::vector<std::string> args, std::string& error);
|
||||
bool LoadAdditionalFile(std::string file, bool keepOnReload, std::string& error);
|
||||
|
||||
/// Overrides configuration with environment variables and returns overridden keys
|
||||
std::vector<std::string> OverrideWithEnvVariablesIfAny();
|
||||
|
||||
static ConfigMgr* instance();
|
||||
|
||||
bool Reload(std::vector<std::string>& errors);
|
||||
|
||||
@@ -108,6 +108,8 @@ int main(int argc, char** argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<std::string> overriddenKeys = sConfigMgr->OverrideWithEnvVariablesIfAny();
|
||||
|
||||
sLog->RegisterAppender<AppenderDB>();
|
||||
sLog->Initialize(nullptr);
|
||||
|
||||
@@ -124,6 +126,9 @@ int main(int argc, char** argv)
|
||||
}
|
||||
);
|
||||
|
||||
for (std::string const& key : overriddenKeys)
|
||||
TC_LOG_INFO("server.authserver", "Configuration field '%s' was overridden with environment variable.", key.c_str());
|
||||
|
||||
// authserver PID file creation
|
||||
std::string pidFile = sConfigMgr->GetStringDefault("PidFile", "");
|
||||
if (!pidFile.empty())
|
||||
|
||||
@@ -146,6 +146,8 @@ extern int main(int argc, char** argv)
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::vector<std::string> overriddenKeys = sConfigMgr->OverrideWithEnvVariablesIfAny();
|
||||
|
||||
std::shared_ptr<Trinity::Asio::IoContext> ioContext = std::make_shared<Trinity::Asio::IoContext>();
|
||||
|
||||
sLog->RegisterAppender<AppenderDB>();
|
||||
@@ -165,6 +167,9 @@ extern int main(int argc, char** argv)
|
||||
}
|
||||
);
|
||||
|
||||
for (std::string const& key : overriddenKeys)
|
||||
TC_LOG_INFO("server.worldserver", "Configuration field '%s' was overridden with environment variable.", key.c_str());
|
||||
|
||||
OpenSSLCrypto::threadsSetup();
|
||||
|
||||
std::shared_ptr<void> opensslHandle(nullptr, [](void*) { OpenSSLCrypto::threadsCleanup(); });
|
||||
|
||||
115
tests/common/Config.cpp
Normal file
115
tests/common/Config.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation; either version 2 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#define CATCH_CONFIG_ENABLE_CHRONO_STRINGMAKER
|
||||
#include "tc_catch2.h"
|
||||
|
||||
#include "Config.h"
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
std::string CreateConfigWithMap(std::map<std::string, std::string> const& map)
|
||||
{
|
||||
auto mTempFileRel = boost::filesystem::unique_path("deleteme.ini");
|
||||
auto mTempFileAbs = boost::filesystem::temp_directory_path() / mTempFileRel;
|
||||
std::ofstream iniStream;
|
||||
iniStream.open(mTempFileAbs.c_str());
|
||||
|
||||
iniStream << "[test]\n";
|
||||
for (auto const& itr : map)
|
||||
iniStream << itr.first << " = " << itr.second << "\n";
|
||||
|
||||
iniStream.close();
|
||||
|
||||
return mTempFileAbs.native();
|
||||
}
|
||||
|
||||
TEST_CASE("Envariable variables", "[Config]")
|
||||
{
|
||||
std::map<std::string, std::string> config;
|
||||
config["Int.Nested"] = "4242";
|
||||
config["lower"] = "simpleString";
|
||||
config["UPPER"] = "simpleString";
|
||||
config["SomeLong.NestedNameWithNumber.Like1"] = "1";
|
||||
|
||||
auto filePath = CreateConfigWithMap(config);
|
||||
|
||||
std::string err;
|
||||
REQUIRE(sConfigMgr->LoadInitial(filePath, std::vector<std::string>(), err));
|
||||
REQUIRE(err.empty());
|
||||
|
||||
SECTION("Nested int")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetIntDefault("Int.Nested", 10) == 4242);
|
||||
|
||||
setenv("TC_INT_NESTED", "8080", 1);
|
||||
REQUIRE(!sConfigMgr->OverrideWithEnvVariablesIfAny().empty());
|
||||
REQUIRE(sConfigMgr->GetIntDefault("Int.Nested", 10) == 8080);
|
||||
}
|
||||
|
||||
SECTION("Simple lower string")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetStringDefault("lower", "") == "simpleString");
|
||||
|
||||
setenv("TC_LOWER", "envstring", 1);
|
||||
REQUIRE(!sConfigMgr->OverrideWithEnvVariablesIfAny().empty());
|
||||
REQUIRE(sConfigMgr->GetStringDefault("lower", "") == "envstring");
|
||||
}
|
||||
|
||||
SECTION("Simple upper string")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetStringDefault("UPPER", "") == "simpleString");
|
||||
|
||||
setenv("TC_UPPER", "envupperstring", 1);
|
||||
REQUIRE(!sConfigMgr->OverrideWithEnvVariablesIfAny().empty());
|
||||
REQUIRE(sConfigMgr->GetStringDefault("UPPER", "") == "envupperstring");
|
||||
}
|
||||
|
||||
SECTION("Long nested name with number")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetFloatDefault("SomeLong.NestedNameWithNumber.Like1", 0) == 1);
|
||||
|
||||
setenv("TC_SOME_LONG_NESTED_NAME_WITH_NUMBER_LIKE_1", "42", 1);
|
||||
REQUIRE(!sConfigMgr->OverrideWithEnvVariablesIfAny().empty());
|
||||
REQUIRE(sConfigMgr->GetFloatDefault("SomeLong.NestedNameWithNumber.Like1", 0) == 42);
|
||||
}
|
||||
|
||||
SECTION("String that not exist in config")
|
||||
{
|
||||
setenv("TC_UNIQUE_STRING", "somevalue", 1);
|
||||
REQUIRE(sConfigMgr->GetStringDefault("Unique.String", "") == "somevalue");
|
||||
}
|
||||
|
||||
SECTION("Int that not exist in config")
|
||||
{
|
||||
setenv("TC_UNIQUE_INT", "100", 1);
|
||||
REQUIRE(sConfigMgr->GetIntDefault("Unique.Int", 1) == 100);
|
||||
}
|
||||
|
||||
SECTION("Not existing string")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetStringDefault("NotFound.String", "none") == "none");
|
||||
}
|
||||
|
||||
SECTION("Not existing int")
|
||||
{
|
||||
REQUIRE(sConfigMgr->GetIntDefault("NotFound.Int", 1) == 1);
|
||||
}
|
||||
|
||||
std::remove(filePath.c_str());
|
||||
}
|
||||
Reference in New Issue
Block a user