blob: 1bb7b9e45e9736cc3889713cb6623b5b1432410c [file]
/** @file
Decode the records.yaml configuration file.
@section license License
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include "P_RecCore.h"
#include "records/RecYAMLDecoder.h"
#include "records/RecYAMLDefs.h"
#include "tscore/Diags.h"
#include "tsutil/YamlCfg.h"
#include "records/RecordsConfig.h"
#include <string_view>
#include <swoc/Errata.h>
#include <swoc/BufferWriter.h>
static std::array<std::string_view, 5> Node_Type_to_Str{
{"Undefined", "Null", "Scalar", "Sequence", "Map"}
};
namespace
{
constexpr std::string_view CONFIG_RECORD_PREFIX{"proxy.config"};
const inline std::string RECORD_YAML_ROOT_STR{"records"};
} // namespace
namespace detail
{
std::pair<RecDataT, std::string> try_deduce_type(YAML::Node const &node);
// Helper class to make the code less verbose when lock is needed.
struct scoped_cond_lock {
scoped_cond_lock(bool lock = false) : _lock(lock)
{
if (_lock) {
ink_rwlock_wrlock(&g_records_rwlock);
}
}
~scoped_cond_lock()
{
if (_lock) {
ink_rwlock_unlock(&g_records_rwlock);
}
}
bool _lock{false};
};
/// @brief Iterate over a node and build up the field name from it.
///
/// This function walks down a YAML node till it find a scalar type while building the record name, so if a node is something like
/// this:
///
/// diags:
/// debug:
/// enabled: 0
///
/// this function will build up the record name "diags.debug.enabled" and then it prepend the "proxy.config" to each name, this
/// will be the record name already known by ATS. Every time a scalar node is completed then the handler function will be called.
///
/// @param field Parent node.
/// @param handler Scalar node function handler, called every time a scalar type is found.
/// @param errata Holds the errors detected.
template <typename T>
void
flatten_node(T &&field, RecYAMLNodeHandler handler, swoc::Errata &errata)
{
switch (field.value_node.Type()) {
case YAML::NodeType::Map: {
field.append_field_name();
for (auto &&it : field.value_node) {
flatten_node(T{it.first, it.second, field.get_record_name()}, handler, errata);
}
} break;
case YAML::NodeType::Sequence:
case YAML::NodeType::Scalar:
case YAML::NodeType::Null: {
field.append_field_name();
handler(field, errata);
} break;
default:; // done
}
}
} // namespace detail
void
SetRecordFromYAMLNode(CfgNode const &field, swoc::Errata &errata)
{
std::string record_name{field.get_record_name()};
RecT rec_type{RecT::RECT_CONFIG};
RecDataT data_type{RecDataT::RECD_NULL};
RecCheckT check_type{RecCheckT::RECC_NULL};
std::string check_expr;
// this function (GetRec..) should be generic and possibly getting the value either
// from where it gets it currently or a schema file.
if (const auto *found = GetRecordElementByName(record_name.c_str()); found) {
if (REC_TYPE_IS_STAT(found->type)) {
ink_release_assert(REC_TYPE_IS_STAT(found->type));
}
rec_type = found->type;
data_type = found->value_type;
check_type = found->check;
if (found->regex) {
check_expr = found->regex;
}
} else {
// Not registered in ATS, could be a plugin or an invalid(not registered) records.
// Externally registered records should have the type set in each field (!!int, !!float, etc), otherwise we will not be able to
// deduce the type and we could end up doing a bad type cast at the end. So we say if there is no type(tag) specified, then
// we ignore it.
auto [dtype, e] = detail::try_deduce_type(field.value_node);
if (!e.empty()) {
errata.note(ERRATA_WARN, "Ignoring field '{}' [{}] at {}. Not registered and {}", field.node.as<std::string>(),
field.get_record_name(), field.mark_as_view("line={}, col={}"), e);
// We can't continue without knowing the type.
return;
}
data_type = dtype; // field tags found.
}
// It could happen that a field was set to null. We only care for string type, we want
// this to be explicitly set so the librecords can deal with this. For non strings we
// will use the default value.
//
if (YAML::NodeType::Null == field.value_node.Type()) {
switch (data_type) {
case RecDataT::RECD_INT:
case RecDataT::RECD_FLOAT:
errata.note(ERRATA_DEBUG, "Field '{}' set to null. Default value will be used", field.node.as<std::string>());
return;
default:;
}
}
std::string field_value = field.value_node.as<std::string>(); // in case of a string, the library will give us the literal
// 'null' which is exactly what we want.
auto [value_str, override_source] = RecConfigOverrideFromEnvironment(record_name.c_str(), field_value.c_str());
RecSourceT source = (override_source == RecConfigOverrideSource::NONE) ? REC_SOURCE_EXPLICIT : REC_SOURCE_ENV;
if (override_source != RecConfigOverrideSource::NONE) {
errata.note(ERRATA_DEBUG, "'{}' overridden with '{}' by {}", record_name, value_str,
RecConfigOverrideSourceName(override_source));
}
if (!check_expr.empty() && RecordValidityCheck(value_str.c_str(), check_type, check_expr.c_str()) == false) {
errata.note(ERRATA_WARN, "{} - Validity Check error {}. Pattern '{}' failed against '{}'. Default value will be used",
record_name, field.mark_as_view("at line={}, col={}"), check_expr, value_str);
return;
}
RecData data;
memset(&data, 0, sizeof(RecData));
RecDataSetFromString(data_type, &data, value_str.c_str());
RecSetRecord(rec_type, record_name.c_str(), data_type, &data, nullptr, source, false);
RecDataZero(data_type, &data);
};
swoc::Errata
ParseRecordsFromYAML(YAML::Node root, RecYAMLNodeHandler handler, bool lock /*false by default*/)
{
[[maybe_unused]] detail::scoped_cond_lock cond_lock(lock);
swoc::Errata errata;
if (YAML::NodeType::Map != root.Type()) {
return swoc::Errata(ERRATA_ERROR, "Node is expected to be a map, got '{}' instead.", root.Type());
}
if (auto ts = root[RECORD_YAML_ROOT_STR]; ts.size()) {
for (auto &&n : ts) {
detail::flatten_node(CfgNode{n.first, n.second, CONFIG_RECORD_PREFIX}, handler, errata);
}
} else {
return swoc::Errata(ERRATA_ERROR, "'{}' root key not present or no fields to read. Default values will be used",
RECORD_YAML_ROOT_STR);
}
return errata;
}
namespace detail
{
std::pair<RecDataT, std::string>
try_deduce_type(YAML::Node const &node)
{
// Using the tag.
std::string_view tag = node.Tag();
if (tag == ts::Yaml::YAML_FLOAT_TAG_URI) {
return {RecDataT::RECD_FLOAT, {}};
} else if (tag == ts::Yaml::YAML_INT_TAG_URI) {
return {RecDataT::RECD_INT, {}};
} else if (tag == ts::Yaml::YAML_STR_TAG_URI) {
return {RecDataT::RECD_STRING, {}};
} else if (tag == ts::Yaml::YAML_INT_TAG_URI) {
return {RecDataT::RECD_INT, {}};
} else if (tag == ts::Yaml::YAML_BOOL_TAG_URI) {
return {RecDataT::RECD_INT, {}};
} else if (tag == ts::Yaml::YAML_NULL_TAG_URI) {
return {RecDataT::RECD_NULL, {}};
}
std::string text;
return {RecDataT::RECD_NULL, swoc::bwprint(text, "Unknown tag type '{}'", tag)};
}
} // namespace detail
namespace swoc
{
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const & /* spec ATS_UNUSED */, YAML::NodeType::value type)
{
return w.write(Node_Type_to_Str[type]);
}
BufferWriter &
bwformat(BufferWriter &w, bwf::Spec const & /* spec ATS_UNUSED */, YAML::Node const &node)
{
return w.write(node.as<std::string>());
}
} // namespace swoc