Skip to content

Commit 9d3948e

Browse files
committed
feat core: allow reading files from YamlConfig in ComponentsManager
Fixes #548 Tests: проотестировано CI 259bb37f1a639d7f8d6d3ca04e107aff05582e82
1 parent 34580dc commit 9d3948e

File tree

5 files changed

+152
-72
lines changed

5 files changed

+152
-72
lines changed

core/src/components/manager_config.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ ManagerConfig ParseFromAny(
6565
config_vars = builder.ExtractValue();
6666
}
6767

68-
auto config =
69-
yaml_config::YamlConfig(config_yaml, std::move(config_vars),
70-
yaml_config::YamlConfig::Mode::kEnvAllowed);
68+
auto config = yaml_config::YamlConfig(
69+
config_yaml, std::move(config_vars),
70+
yaml_config::YamlConfig::Mode::kEnvAndFileAllowed);
7171
auto result = config[kManagerConfigField].As<ManagerConfig>();
7272
result.enabled_experiments =
7373
config[kUserverExperimentsField].As<utils::impl::UserverExperimentSet>(

universal/include/userver/yaml_config/yaml_config.hpp

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,29 +34,37 @@ using ParseException = formats::yaml::ParseException;
3434
/// @snippet universal/src/yaml_config/yaml_config_test.cpp sample vars
3535
/// Then the result of `yaml["some_element"]["some"].As<int>()` is `42`.
3636
///
37-
/// If YAML key ends on '#env' and the mode is YamlConfig::Mode::kEnvAllowed,
37+
/// If YAML key ends on '#env' and the mode is YamlConfig::Mode::kEnvAllowed
38+
/// or YamlConfig::Mode::kEnvAndFileAllowed,
3839
/// then the value of the key is searched in
3940
/// environment variables of the process and returned as a value. For example:
4041
/// @snippet universal/src/yaml_config/yaml_config_test.cpp sample env
4142
///
43+
/// If YAML key ends on '#file' and the mode is
44+
/// YamlConfig::Mode::kEnvAndFileAllowed, then the value of the key is the
45+
/// content of specified YAML parsed file. For example:
46+
/// @snippet universal/src/yaml_config/yaml_config_test.cpp sample read_file
47+
///
4248
/// If YAML key ends on '#fallback', then the value of the key is used as a
43-
/// fallback for environment and `$` variables. For example for the following
44-
/// YAML with YamlConfig::Mode::kEnvAllowed:
49+
/// fallback for environment, file and `$` variables. For example for the
50+
/// following YAML with YamlConfig::Mode::kEnvAndFileAllowed:
4551
/// @snippet universal/src/yaml_config/yaml_config_test.cpp sample multiple
4652
/// The result of `yaml["some_element"]["some"].As<int>()` is the value of
4753
/// `variable` from `config_vars` if it exists; otherwise the value is the
4854
/// contents of the environment variable `SOME_ENV_VARIABLE` if it exists;
49-
/// otherwise the value if `100500`, from the fallback.
55+
/// otherwise the value is the content of the file with name `file.yaml`;
56+
/// otherwise the value is `100500`, from the fallback.
5057
///
5158
/// Another example:
5259
/// @snippet universal/src/yaml_config/yaml_config_test.cpp sample env fallback
5360
/// With YamlConfig::Mode::kEnvAllowed the result of
5461
/// `yaml["some_element"]["value"].As<int>()` is the value of `ENV_NAME`
5562
/// environment variable if it exists; otherwise it is `5`.
5663
///
57-
/// @warning YamlConfig::Mode::kEnvAllowed should be used only on configs that
64+
/// @warning YamlConfig::Mode::kEnvAllowed or
65+
/// YamlConfig::Mode::kEnvAndFileAllowed should be used only on configs that
5866
/// come from trusted environments. Otherwise, an attacker could create a
59-
/// config with `#env` and read any of your environment variables, including
67+
/// config and read any of your environment variables of files, including
6068
/// variables that contain passwords and other sensitive data.
6169
class YamlConfig {
6270
public:
@@ -68,8 +76,10 @@ class YamlConfig {
6876
struct DefaultConstructed {};
6977

7078
enum class Mode {
71-
kSecure, /// < secure mode, without reading environment variables
72-
kEnvAllowed, /// < allows reading of environment variables
79+
kSecure, /// < secure mode, without reading environment variables or files
80+
kEnvAllowed, /// < allows reading of environment variables
81+
kEnvAndFileAllowed, /// < allows reading of environment variables and
82+
/// files
7383
};
7484

7585
using const_iterator = Iterator<IterTraits>;

universal/src/yaml_config/impl/validate_static_config.cpp

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,14 @@ namespace yaml_config::impl {
1616

1717
namespace {
1818

19-
constexpr std::string_view kFallbackSuffix = "#fallback";
20-
constexpr std::string_view kEnvSuffix = "#env";
21-
19+
// TODO: remove in TAXICOMMON-8973
2220
std::string RemoveFallbackAndEnvSuffix(std::string_view option) {
23-
if (utils::text::EndsWith(option, kFallbackSuffix)) {
24-
return std::string(
25-
option.substr(0, option.length() - kFallbackSuffix.length()));
26-
}
27-
if (utils::text::EndsWith(option, kEnvSuffix)) {
28-
return std::string(option.substr(0, option.length() - kEnvSuffix.length()));
21+
for (const std::string_view suffix : {"#env", "#file", "#fallback"}) {
22+
if (utils::text::EndsWith(option, suffix)) {
23+
return std::string{option.substr(0, option.size() - suffix.size())};
24+
}
2925
}
30-
return std::string(option);
26+
return std::string{option};
3127
}
3228

3329
bool IsTypeValid(FieldType type, const formats::yaml::Value& value) {

universal/src/yaml_config/yaml_config.cpp

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#include <userver/yaml_config/yaml_config.hpp>
22

33
#include <fmt/format.h>
4-
#include <boost/algorithm/string/predicate.hpp>
4+
#include <boost/filesystem/operations.hpp>
55

66
#include <userver/formats/common/conversion_stack.hpp>
77
#include <userver/formats/json/value.hpp>
88
#include <userver/formats/json/value_builder.hpp>
99
#include <userver/formats/yaml/serialize.hpp>
1010
#include <userver/logging/log.hpp>
1111
#include <userver/utils/string_to_duration.hpp>
12+
#include <userver/utils/text_light.hpp>
1213

1314
USERVER_NAMESPACE_BEGIN
1415

@@ -26,52 +27,72 @@ std::string GetSubstitutionVarName(const formats::yaml::Value& value) {
2627
return value.As<std::string>().substr(1);
2728
}
2829

29-
std::string GetFallbackName(std::string_view str) {
30-
return std::string{str} + "#fallback";
31-
}
32-
3330
std::string GetEnvName(std::string_view str) {
3431
return std::string{str} + "#env";
3532
}
3633

34+
std::string GetFileName(std::string_view str) {
35+
return std::string{str} + "#file";
36+
}
37+
38+
std::string GetFallbackName(std::string_view str) {
39+
return std::string{str} + "#fallback";
40+
}
41+
3742
template <typename Field>
3843
YamlConfig MakeMissingConfig(const YamlConfig& config, Field field) {
3944
const auto path = formats::common::MakeChildPath(config.GetPath(), field);
4045
return {formats::yaml::Value()[path], {}};
4146
}
4247

4348
void AssertEnvMode(YamlConfig::Mode mode) {
44-
if (mode != YamlConfig::Mode::kEnvAllowed) {
49+
if (mode == YamlConfig::Mode::kSecure) {
4550
throw std::runtime_error(
46-
"YamlConfig was not constructed with Mode::kEnvAllowed but an attempt "
51+
"YamlConfig was not constructed with Mode::kEnvAllowed or "
52+
"Mode::kEnvAndFileAllowed but an attempt "
4753
"to read an environment variable was made");
4854
}
4955
}
5056

51-
std::optional<formats::yaml::Value> GetFromEnvByKey(
52-
std::string_view key, const formats::yaml::Value& yaml,
53-
YamlConfig::Mode mode) {
54-
const auto env_name = yaml[GetEnvName(key)];
55-
if (!env_name.IsMissing()) {
56-
AssertEnvMode(mode);
57-
58-
// NOLINTNEXTLINE(concurrency-mt-unsafe)
59-
const auto* env_value = std::getenv(env_name.As<std::string>().c_str());
60-
if (env_value) {
61-
LOG_INFO() << "using env value for '" << key << '\'';
62-
return formats::yaml::FromString(env_value);
63-
}
57+
void AssertFileMode(YamlConfig::Mode mode) {
58+
if (mode != YamlConfig::Mode::kEnvAndFileAllowed) {
59+
throw std::runtime_error(
60+
"YamlConfig was not constructed with Mode::kEnvAndFileAllowed "
61+
"but an attempt to read a file was made");
62+
}
63+
}
6464

65-
const auto fallback_name = GetFallbackName(key);
66-
if (yaml.HasMember(fallback_name)) {
67-
LOG_INFO() << "using fallback value for '" << key << '\'';
68-
return yaml[fallback_name];
69-
}
65+
std::optional<formats::yaml::Value> GetFromEnvImpl(
66+
const formats::yaml::Value& env_name, YamlConfig::Mode mode) {
67+
if (env_name.IsMissing()) {
68+
return {};
69+
}
70+
71+
AssertEnvMode(mode);
72+
73+
// NOLINTNEXTLINE(concurrency-mt-unsafe)
74+
const auto* env_value = std::getenv(env_name.As<std::string>().c_str());
75+
if (env_value) {
76+
return formats::yaml::FromString(env_value);
7077
}
7178

7279
return {};
7380
}
7481

82+
std::optional<formats::yaml::Value> GetFromFileImpl(
83+
const formats::yaml::Value& file_name, YamlConfig::Mode mode) {
84+
if (file_name.IsMissing()) {
85+
return {};
86+
}
87+
88+
AssertFileMode(mode);
89+
const auto str_filename = file_name.As<std::string>();
90+
if (!boost::filesystem::exists(str_filename)) {
91+
return {};
92+
}
93+
return formats::yaml::blocking::FromFile(str_filename);
94+
}
95+
7596
} // namespace
7697

7798
YamlConfig::YamlConfig(formats::yaml::Value yaml,
@@ -83,54 +104,54 @@ YamlConfig::YamlConfig(formats::yaml::Value yaml,
83104
const formats::yaml::Value& YamlConfig::Yaml() const { return yaml_; }
84105

85106
YamlConfig YamlConfig::operator[](std::string_view key) const {
86-
if (boost::algorithm::ends_with(key, "#env")) {
87-
auto env_value = GetFromEnvByKey(key, yaml_, mode_);
88-
if (env_value) {
89-
// Strip substitutions off to disallow nested substitutions
90-
return YamlConfig{std::move(*env_value), {}, Mode::kSecure};
91-
}
92-
93-
// Avoid parsing #env as a string
107+
// TODO: fix the iterators and assert this case in TAXICOMMON-8973
108+
if (utils::text::EndsWith(key, "#env") ||
109+
utils::text::EndsWith(key, "#file") ||
110+
utils::text::EndsWith(key, "#fallback")) {
94111
return MakeMissingConfig(*this, key);
95112
}
96113

97114
auto value = yaml_[key];
98115

99-
if (IsSubstitution(value)) {
116+
const bool is_substitution = IsSubstitution(value);
117+
if (is_substitution) {
100118
const auto var_name = GetSubstitutionVarName(value);
101119

102120
auto var_data = config_vars_[var_name];
103121
if (!var_data.IsMissing()) {
104122
// Strip substitutions off to disallow nested substitutions
105123
return YamlConfig{std::move(var_data), {}, Mode::kSecure};
106124
}
125+
}
107126

108-
auto env_value = GetFromEnvByKey(key, yaml_, mode_);
109-
if (env_value) {
110-
// Strip substitutions off to disallow nested substitutions
111-
return YamlConfig{std::move(*env_value), {}, Mode::kSecure};
112-
}
127+
if (!value.IsMissing() && !is_substitution) {
128+
return YamlConfig{std::move(value), config_vars_, mode_};
129+
}
113130

131+
const auto env_name = yaml_[GetEnvName(key)];
132+
auto env_value = GetFromEnvImpl(env_name, mode_);
133+
if (env_value) {
134+
// Strip substitutions off to disallow nested substitutions
135+
return YamlConfig{std::move(*env_value), {}, Mode::kSecure};
136+
}
137+
138+
const auto file_name = yaml_[GetFileName(key)];
139+
auto file_value = GetFromFileImpl(file_name, mode_);
140+
if (file_value) {
141+
// Strip substitutions off to disallow nested substitutions
142+
return YamlConfig{std::move(*file_value), {}, Mode::kSecure};
143+
}
144+
145+
if (is_substitution || !env_name.IsMissing() || !file_name.IsMissing()) {
114146
const auto fallback_name = GetFallbackName(key);
115147
if (yaml_.HasMember(fallback_name)) {
116148
LOG_INFO() << "using fallback value for '" << key << '\'';
117149
// Strip substitutions off to disallow nested substitutions
118150
return YamlConfig{yaml_[fallback_name], {}, Mode::kSecure};
119151
}
120-
121-
// Avoid parsing $substitution as a string
122-
return MakeMissingConfig(*this, key);
123-
}
124-
125-
if (value.IsMissing()) {
126-
auto env_value = GetFromEnvByKey(key, yaml_, mode_);
127-
if (env_value) {
128-
// Strip substitutions off to disallow nested substitutions
129-
return YamlConfig{std::move(*env_value), {}, Mode::kSecure};
130-
}
131152
}
132153

133-
return YamlConfig{std::move(value), config_vars_, mode_};
154+
return MakeMissingConfig(*this, key);
134155
}
135156

136157
YamlConfig YamlConfig::operator[](size_t index) const {

universal/src/yaml_config/yaml_config_test.cpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include <formats/common/value_test.hpp>
66
#include <userver/formats/yaml/serialize.hpp>
77
#include <userver/formats/yaml/value_builder.hpp>
8+
#include <userver/fs/blocking/temp_directory.hpp>
9+
#include <userver/fs/blocking/write.hpp>
810
#include <userver/utest/assert_macros.hpp>
911

1012
USERVER_NAMESPACE_BEGIN
@@ -75,6 +77,7 @@ TEST(YamlConfig, SampleMultiple) {
7577
# yaml
7678
some_element:
7779
some: $variable
80+
some#file: /some/path/to/the/file.yaml
7881
some#env: SOME_ENV_VARIABLE
7982
some#fallback: 100500
8083
# /// [sample multiple]
@@ -91,6 +94,10 @@ TEST(YamlConfig, SampleMultiple) {
9194
yaml_config::YamlConfig::Mode::kEnvAllowed);
9295
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 42);
9396

97+
yaml = yaml_config::YamlConfig(
98+
node, vars, yaml_config::YamlConfig::Mode::kEnvAndFileAllowed);
99+
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 42);
100+
94101
yaml = yaml_config::YamlConfig(node, {},
95102
yaml_config::YamlConfig::Mode::kEnvAllowed);
96103
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 100);
@@ -103,6 +110,10 @@ TEST(YamlConfig, SampleMultiple) {
103110

104111
yaml = yaml_config::YamlConfig(node, {},
105112
yaml_config::YamlConfig::Mode::kEnvAllowed);
113+
UEXPECT_THROW(yaml["some_element"]["some"].As<int>(), std::exception);
114+
115+
yaml = yaml_config::YamlConfig(
116+
node, {}, yaml_config::YamlConfig::Mode::kEnvAndFileAllowed);
106117
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 100500);
107118
}
108119

@@ -121,6 +132,48 @@ TEST(YamlConfig, SampleEnvFallback) {
121132
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 5);
122133
}
123134

135+
TEST(YamlConfig, SampleFile) {
136+
const auto dir = fs::blocking::TempDirectory::Create();
137+
const auto path_to_file = dir.GetPath() + "/read_file_sample";
138+
139+
/// [sample read_file]
140+
fs::blocking::RewriteFileContents(path_to_file, R"(some_key: ['a', 'b'])");
141+
const auto yaml_content = fmt::format("some#file: {}", path_to_file);
142+
auto node = formats::yaml::FromString(yaml_content);
143+
144+
yaml_config::YamlConfig yaml(
145+
std::move(node), {}, yaml_config::YamlConfig::Mode::kEnvAndFileAllowed);
146+
EXPECT_EQ(yaml["some"]["some_key"][0].As<std::string>(), "a");
147+
/// [sample read_file]
148+
}
149+
150+
TEST(YamlConfig, FileFallback) {
151+
auto node = formats::yaml::FromString(R"(
152+
some_element:
153+
some#file: /some/path/to/the/file.yaml
154+
some#fallback: 5
155+
)");
156+
157+
yaml_config::YamlConfig yaml(
158+
std::move(node), {}, yaml_config::YamlConfig::Mode::kEnvAndFileAllowed);
159+
EXPECT_EQ(yaml["some_element"]["some"].As<int>(), 5);
160+
}
161+
162+
TEST(YamlConfig, FileFallbackUnallowed) {
163+
auto node = formats::yaml::FromString(R"(
164+
some_element:
165+
some#file: /some/path/to/the/file.yaml
166+
some#fallback: 5
167+
)");
168+
169+
yaml_config::YamlConfig yaml(node, {},
170+
yaml_config::YamlConfig::Mode::kEnvAllowed);
171+
UEXPECT_THROW(yaml["some_element"]["some"].As<int>(), std::exception);
172+
173+
yaml = yaml_config::YamlConfig{node, {}, {}};
174+
UEXPECT_THROW(yaml["some_element"]["some"].As<int>(), std::exception);
175+
}
176+
124177
TEST(YamlConfig, Basic) {
125178
auto node = formats::yaml::FromString(R"(
126179
string: hello

0 commit comments

Comments
 (0)