diff --git a/src/common/Configuration/Config.cpp b/src/common/Configuration/Config.cpp index d5750120d..d01d3f7b8 100644 --- a/src/common/Configuration/Config.cpp +++ b/src/common/Configuration/Config.cpp @@ -21,6 +21,7 @@ #include "StringFormat.h" #include "Tokenize.h" #include "Util.h" +#include #include #include #include @@ -215,6 +216,81 @@ namespace return false; } + + // 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(std::toupper(curr)); + result += '_'; + continue; + } + + currIsNumeric = isNumeric(curr); + nextIsNumeric = isNumeric(str[i + 1]); + + // handle "a1" to "a_1" + if (!currIsNumeric && nextIsNumeric) + { + result += static_cast(std::toupper(curr)); + result += '_'; + continue; + } + + // handle "1a" to "1_a" + if (currIsNumeric && !nextIsNumeric) + { + result += static_cast(std::toupper(curr)); + result += '_'; + continue; + } + } + + result += static_cast(std::toupper(curr)); + } + return result; + } + + Optional EnvVarForIniKey(std::string const& key) + { + std::string envKey = "AC_" + IniKeyToEnvVarKey(key); + char* val = std::getenv(envKey.c_str()); + if (!val) + return std::nullopt; + + return std::string(val); + } } bool ConfigMgr::LoadInitial(std::string const& file, bool isReload /*= false*/) @@ -243,25 +319,72 @@ bool ConfigMgr::Reload() return false; } - return LoadModulesConfigs(true, false); + if (!LoadModulesConfigs(true, false)) + { + return false; + } + + OverrideWithEnvVariablesIfAny(); + + return true; +} + +std::vector ConfigMgr::OverrideWithEnvVariablesIfAny() +{ + std::lock_guard lock(_configLock); + + std::vector overriddenKeys; + + for (auto& itr : _configOptions) + { + if (itr.first.empty()) + continue; + + Optional envVar = EnvVarForIniKey(itr.first); + if (!envVar) + continue; + + itr.second = *envVar; + + overriddenKeys.push_back(itr.first); + } + + return overriddenKeys; } template T ConfigMgr::GetValueDefault(std::string const& name, T const& def, bool showLogs /*= true*/) const { + std::string strValue; auto const& itr = _configOptions.find(name); if (itr == _configOptions.end()) { - if (showLogs) + Optional envVar = EnvVarForIniKey(name); + if (!envVar) { - LOG_ERROR("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file.", - name, _filename, name, Acore::ToString(def)); + if (showLogs) + { + LOG_ERROR("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file.", + name, _filename, name, Acore::ToString(def)); + } + + return def; } - return def; + if (showLogs) + { + LOG_WARN("server.loading", "Missing property {} in config file {}, recovered with environment '{}' value.", + name.c_str(), _filename.c_str(), envVar->c_str()); + } + + strValue = *envVar; + } + else + { + strValue = itr->second; } - auto value = Acore::StringTo(itr->second); + auto value = Acore::StringTo(strValue); if (!value) { if (showLogs) @@ -282,6 +405,18 @@ std::string ConfigMgr::GetValueDefault(std::string const& name, std auto const& itr = _configOptions.find(name); if (itr == _configOptions.end()) { + Optional envVar = EnvVarForIniKey(name); + if (envVar) + { + if (showLogs) + { + LOG_WARN("server.loading", "Missing property {} in config file {}, recovered with environment '{}' value.", + name.c_str(), _filename.c_str(), envVar->c_str()); + } + + return *envVar; + } + if (showLogs) { LOG_ERROR("server.loading", "> Config: Missing property {} in config file {}, add \"{} = {}\" to this file.", diff --git a/src/common/Configuration/Config.h b/src/common/Configuration/Config.h index ddac0efb6..db7410e05 100644 --- a/src/common/Configuration/Config.h +++ b/src/common/Configuration/Config.h @@ -39,6 +39,9 @@ public: bool Reload(); + /// Overrides configuration with environment variables and returns overridden keys + std::vector OverrideWithEnvVariablesIfAny(); + std::string const GetFilename(); std::string const GetConfigPath(); [[nodiscard]] std::vector const& GetArguments() const; diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index 4c110db7a..bc40f2f10 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -85,6 +85,8 @@ int main(int argc, char** argv) if (!sConfigMgr->LoadAppConfigs()) return 1; + std::vector overriddenKeys = sConfigMgr->OverrideWithEnvVariablesIfAny(); + // Init logging sLog->RegisterAppender(); sLog->Initialize(nullptr); @@ -101,6 +103,9 @@ int main(int argc, char** argv) LOG_INFO("server.authserver", "> Using Boost version: {}.{}.{}", BOOST_VERSION / 100000, BOOST_VERSION / 100 % 1000, BOOST_VERSION % 100); }); + for (std::string const& key : overriddenKeys) + LOG_INFO("server.authserver", "Configuration field {} was overridden with environment variable.", key); + OpenSSLCrypto::threadsSetup(); std::shared_ptr opensslHandle(nullptr, [](void*) { OpenSSLCrypto::threadsCleanup(); }); diff --git a/src/server/apps/worldserver/Main.cpp b/src/server/apps/worldserver/Main.cpp index 01b9f56ab..7ddcf9ceb 100644 --- a/src/server/apps/worldserver/Main.cpp +++ b/src/server/apps/worldserver/Main.cpp @@ -184,6 +184,8 @@ int main(int argc, char** argv) if (!sConfigMgr->LoadAppConfigs()) return 1; + std::vector overriddenKeys = sConfigMgr->OverrideWithEnvVariablesIfAny(); + std::shared_ptr ioContext = std::make_shared(); // Init all logs @@ -203,6 +205,9 @@ int main(int argc, char** argv) LOG_INFO("server.worldserver", "> Using Boost version: {}.{}.{}", BOOST_VERSION / 100000, BOOST_VERSION / 100 % 1000, BOOST_VERSION % 100); }); + for (std::string const& key : overriddenKeys) + LOG_INFO("server.worldserver", "Configuration field {} was overridden with environment variable.", key); + OpenSSLCrypto::threadsSetup(); std::shared_ptr opensslHandle(nullptr, [](void*) { OpenSSLCrypto::threadsCleanup(); }); diff --git a/src/test/common/Configuration/Config.cpp b/src/test/common/Configuration/Config.cpp new file mode 100644 index 000000000..81d784d39 --- /dev/null +++ b/src/test/common/Configuration/Config.cpp @@ -0,0 +1,124 @@ +/* + * This file is part of the AzerothCore 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 Affero General Public License as published by the + * Free Software Foundation; either version 3 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 Affero 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 . + */ + +#include "Config.h" +#include "gtest/gtest.h" + +#include +#include +#include + +std::string CreateConfigWithMap(std::map 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(); +} + +class ConfigEnvTest : public testing::Test { +protected: + void SetUp() override { + std::map config; + config["Int.Nested"] = "4242"; + config["lower"] = "simpleString"; + config["UPPER"] = "simpleString"; + config["SomeLong.NestedNameWithNumber.Like1"] = "1"; + config["GM.InGMList.Level"] = "50"; + + confFilePath = CreateConfigWithMap(config); + + sConfigMgr->Configure(confFilePath, std::vector()); + sConfigMgr->LoadAppConfigs(); + } + + void TearDown() override { + std::remove(confFilePath.c_str()); + } + + std::string confFilePath; +}; + +TEST_F(ConfigEnvTest, NestedInt) +{ + EXPECT_EQ(sConfigMgr->GetOption("Int.Nested", 10), 4242); + setenv("AC_INT_NESTED", "8080", 1); + EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false); + EXPECT_EQ(sConfigMgr->GetOption("Int.Nested", 10), 8080); +} + +TEST_F(ConfigEnvTest, SimpleLowerString) +{ + EXPECT_EQ(sConfigMgr->GetOption("lower", ""), "simpleString"); + setenv("AC_LOWER", "envstring", 1); + EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false); + EXPECT_EQ(sConfigMgr->GetOption("lower", ""), "envstring"); +} + +TEST_F(ConfigEnvTest, SimpleUpperString) +{ + EXPECT_EQ(sConfigMgr->GetOption("UPPER", ""), "simpleString"); + setenv("AC_UPPER", "envupperstring", 1); + EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false); + EXPECT_EQ(sConfigMgr->GetOption("UPPER", ""), "envupperstring"); +} + +TEST_F(ConfigEnvTest, LongNestedNameWithNumber) +{ + EXPECT_EQ(sConfigMgr->GetOption("SomeLong.NestedNameWithNumber.Like1", 0), 1); + setenv("AC_SOME_LONG_NESTED_NAME_WITH_NUMBER_LIKE_1", "42", 1); + EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false); + EXPECT_EQ(sConfigMgr->GetOption("SomeLong.NestedNameWithNumber.Like1", 0), 42); +} + +TEST_F(ConfigEnvTest, ValueWithSeveralUpperlLaters) +{ + EXPECT_EQ(sConfigMgr->GetOption("GM.InGMList.Level", 1), 50); + setenv("AC_GM_IN_GMLIST_LEVEL", "42", 1); + EXPECT_EQ(sConfigMgr->OverrideWithEnvVariablesIfAny().empty(), false); + EXPECT_EQ(sConfigMgr->GetOption("GM.InGMList.Level", 0), 42); +} + +TEST_F(ConfigEnvTest, StringThatNotExistInConfig) +{ + setenv("AC_UNIQUE_STRING", "somevalue", 1); + EXPECT_EQ(sConfigMgr->GetOption("Unique.String", ""), "somevalue"); +} + +TEST_F(ConfigEnvTest, IntThatNotExistInConfig) +{ + setenv("AC_UNIQUE_INT", "100", 1); + EXPECT_EQ(sConfigMgr->GetOption("Unique.Int", 1), 100); +} + +TEST_F(ConfigEnvTest, NotExistingString) +{ + EXPECT_EQ(sConfigMgr->GetOption("NotFound.String", "none"), "none"); +} + +TEST_F(ConfigEnvTest, NotExistingInt) +{ + EXPECT_EQ(sConfigMgr->GetOption("NotFound.Int", 1), 1); +}