Skip to content

Commit

Permalink
matcher: Generic matching API: configuration validation (envoyproxy#2…
Browse files Browse the repository at this point in the history
…7091)


Signed-off-by: tyxia <[email protected]>
  • Loading branch information
tyxia authored May 9, 2023
1 parent 8a6f65c commit e5326d4
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 7 deletions.
26 changes: 25 additions & 1 deletion envoy/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include "envoy/config/typed_config.h"
#include "envoy/protobuf/message_validator.h"

#include "absl/container/flat_hash_set.h"
#include "absl/strings/string_view.h"
#include "absl/types/optional.h"
#include "xds/type/matcher/v3/matcher.pb.h"
Expand All @@ -25,6 +26,7 @@ class ServerFactoryContext;
namespace Matcher {

using MatchingDataType = absl::variant<absl::monostate, std::string>;
inline constexpr absl::string_view DefaultMatchingDataType = "string";

// This file describes a MatchTree<DataType>, which traverses a tree of matches until it
// either matches (resulting in either an action or a new tree to traverse) or doesn't match.
Expand Down Expand Up @@ -161,10 +163,22 @@ class InputMatcher {

/**
* Whether the provided input is a match.
* @param absl::optional<absl::string_view> the value to match on. Will be absl::nullopt if the
* @param Matcher::MatchingDataType the value to match on. Will be absl::monostate() if the
* lookup failed.
*/
virtual bool match(const Matcher::MatchingDataType& input) PURE;

/**
* A set of data input types supported by InputMatcher.
* String is default supported data input type because almost all the derived objects support
* string only. The name of core types (e.g., std::string, int) is defined string constrant which
* produces human-readable form (e.g., "string", "int").
*
* Override this function to provide matcher specific supported data input types.
*/
virtual absl::flat_hash_set<std::string> supportedDataInputTypes() const {
return absl::flat_hash_set<std::string>{std::string(DefaultMatchingDataType)};
}
};

using InputMatcherPtr = std::unique_ptr<InputMatcher>;
Expand Down Expand Up @@ -237,6 +251,16 @@ template <class DataType> class DataInput {
virtual ~DataInput() = default;

virtual DataInputGetResult get(const DataType& data) const PURE;

/**
* Input type of DataInput.
* String is default data input type since nearly all the DataInput's derived objects' input type
* is string. The name of core types (e.g., std::string, int) is defined string constrant which
* produces human-readable form (e.g., "string", "int").
*
* Override this function to provide matcher specific data input type.
*/
virtual absl::string_view dataInputType() const { return DefaultMatchingDataType; }
};

template <class DataType> using DataInputPtr = std::unique_ptr<DataInput<DataType>>;
Expand Down
13 changes: 12 additions & 1 deletion source/common/matcher/field_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

#include "envoy/matcher/matcher.h"

#include "absl/strings/str_join.h"

namespace Envoy {
namespace Matcher {

Expand Down Expand Up @@ -144,7 +146,16 @@ template <class DataType>
class SingleFieldMatcher : public FieldMatcher<DataType>, Logger::Loggable<Logger::Id::matcher> {
public:
SingleFieldMatcher(DataInputPtr<DataType>&& data_input, InputMatcherPtr&& input_matcher)
: data_input_(std::move(data_input)), input_matcher_(std::move(input_matcher)) {}
: data_input_(std::move(data_input)), input_matcher_(std::move(input_matcher)) {
auto supported_input_types = input_matcher_->supportedDataInputTypes();
if (supported_input_types.find(data_input_->dataInputType()) == supported_input_types.end()) {
std::string supported_types =
absl::StrJoin(supported_input_types.begin(), supported_input_types.end(), ", ");
throw EnvoyException(
absl::StrCat("Unsupported data input type: ", data_input_->dataInputType(),
". The matcher supports input type: ", supported_types));
}
}

FieldMatchResult match(const DataType& data) override {
const auto input = data_input_->get(data);
Expand Down
9 changes: 8 additions & 1 deletion source/common/matcher/map_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ template <class DataType>
class MapMatcher : public MatchTree<DataType>, Logger::Loggable<Logger::Id::matcher> {
public:
MapMatcher(DataInputPtr<DataType>&& data_input, absl::optional<OnMatch<DataType>> on_no_match)
: data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)) {}
: data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)) {
auto input_type = data_input_->dataInputType();
if (input_type != DefaultMatchingDataType) {
throw EnvoyException(
absl::StrCat("Unsupported data input type: ", input_type,
", currently only string type is supported in map matcher"));
}
}

// Adds a child to the map.
virtual void addChild(std::string value, OnMatch<DataType>&& on_match) PURE;
Expand Down
3 changes: 1 addition & 2 deletions source/common/matcher/matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {
}

auto on_no_match = createOnMatch(config.on_no_match());

return [matcher_factories, on_no_match]() {
auto list_matcher = std::make_unique<ListMatcher<DataType>>(
on_no_match ? absl::make_optional((*on_no_match)()) : absl::nullopt);
Expand Down Expand Up @@ -318,7 +317,7 @@ class MatchTreeFactory : public OnMatchFactory<DataType> {
template <class OnMatchType>
absl::optional<OnMatchFactoryCb<DataType>> createOnMatchBase(const OnMatchType& on_match) {
if (on_match.has_matcher()) {
return [matcher_factory = create(on_match.matcher())]() {
return [matcher_factory = std::move(create(on_match.matcher()))]() {
return OnMatch<DataType>{{}, matcher_factory()};
};
} else if (on_match.has_action()) {
Expand Down
9 changes: 8 additions & 1 deletion source/extensions/common/matcher/trie_matcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,14 @@ template <class DataType> class TrieMatcher : public MatchTree<DataType> {
public:
TrieMatcher(DataInputPtr<DataType>&& data_input, absl::optional<OnMatch<DataType>> on_no_match,
const std::shared_ptr<Network::LcTrie::LcTrie<TrieNode<DataType>>>& trie)
: data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), trie_(trie) {}
: data_input_(std::move(data_input)), on_no_match_(std::move(on_no_match)), trie_(trie) {
auto input_type = data_input_->dataInputType();
if (input_type != Envoy::Matcher::DefaultMatchingDataType) {
throw EnvoyException(
absl::StrCat("Unsupported data input type: ", input_type,
", currently only string type is supported in trie matcher"));
}
}

typename MatchTree<DataType>::MatchResult match(const DataType& data) override {
const auto input = data_input_->get(data);
Expand Down
174 changes: 174 additions & 0 deletions test/common/matcher/matcher_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,180 @@ TEST_F(MatcherTest, TestPrefixMatcher) {
EXPECT_NE(result.on_match_->action_cb_, nullptr);
}

TEST_F(MatcherTest, TestInvalidFloatPrefixMapMatcher) {
const std::string yaml = R"EOF(
matcher_tree:
input:
name: outer_input
typed_config:
"@type": type.googleapis.com/google.protobuf.FloatValue
prefix_match_map:
map:
3.14:
matcher:
matcher_list:
matchers:
- on_match:
action:
name: test_action
typed_config:
"@type": type.googleapis.com/google.protobuf.StringValue
value: match!!
predicate:
single_predicate:
input:
name: inner_input
typed_config:
"@type": type.googleapis.com/google.protobuf.BoolValue
value_match:
exact: foo
)EOF";

envoy::config::common::matcher::v3::Matcher matcher;
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());

TestUtility::validate(matcher);
auto outer_input_factory = TestDataInputFloatFactory(3.14);
auto inner_input_factory = TestDataInputBoolFactory("foo");

EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.BoolValue"));
EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.FloatValue"));

auto match_tree = factory_.create(matcher);
std::string error_message = absl::StrCat(
"Unsupported data input type: float, currently only string type is supported in map matcher");

EXPECT_THROW_WITH_MESSAGE(match_tree(), EnvoyException, error_message);
}

TEST_F(MatcherTest, TestInvalidFloatExactMapMatcher) {
const std::string yaml = R"EOF(
matcher_tree:
input:
name: outer_input
typed_config:
"@type": type.googleapis.com/google.protobuf.FloatValue
exact_match_map:
map:
3.14:
matcher:
matcher_list:
matchers:
- on_match:
action:
name: test_action
typed_config:
"@type": type.googleapis.com/google.protobuf.StringValue
value: match!!
predicate:
single_predicate:
input:
name: inner_input
typed_config:
"@type": type.googleapis.com/google.protobuf.BoolValue
value_match:
exact: foo
)EOF";

envoy::config::common::matcher::v3::Matcher matcher;
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());

TestUtility::validate(matcher);
auto outer_input_factory = TestDataInputFloatFactory(3.14);
auto inner_input_factory = TestDataInputBoolFactory("foo");

EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.BoolValue"));
EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.FloatValue"));
auto match_tree = factory_.create(matcher);
std::string error_message = absl::StrCat(
"Unsupported data input type: float, currently only string type is supported in map matcher");
EXPECT_THROW_WITH_MESSAGE(match_tree(), EnvoyException, error_message);
}

TEST_F(MatcherTest, InvalidDataInput) {
const std::string yaml = R"EOF(
matcher_list:
matchers:
- on_match:
action:
name: test_action
typed_config:
"@type": type.googleapis.com/google.protobuf.StringValue
value: match!!
predicate:
single_predicate:
input:
name: generic
typed_config:
"@type": type.googleapis.com/google.protobuf.FloatValue
value_match:
exact: 3.14
)EOF";
envoy::config::common::matcher::v3::Matcher matcher;
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());

TestUtility::validate(matcher);

auto outer_input_factory = TestDataInputFloatFactory(3.14);

EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.FloatValue"));
auto match_tree = factory_.create(matcher);
std::string error_message = absl::StrCat("Unsupported data input type: float.",
" The matcher supports input type: string");
EXPECT_THROW_WITH_MESSAGE(match_tree(), EnvoyException, error_message);
}

TEST_F(MatcherTest, InvalidDataInputInAndMatcher) {
const std::string yaml = R"EOF(
matcher_list:
matchers:
- on_match:
action:
name: test_action
typed_config:
"@type": type.googleapis.com/google.protobuf.StringValue
value: match!!
predicate:
and_matcher:
predicate:
- single_predicate:
input:
name: inner_input
typed_config:
"@type": type.googleapis.com/google.protobuf.FloatValue
value_match:
exact: 3.14
- single_predicate:
input:
name: inner_input
typed_config:
"@type": type.googleapis.com/google.protobuf.FloatValue
value_match:
exact: 3.14
)EOF";
envoy::config::common::matcher::v3::Matcher matcher;
MessageUtil::loadFromYaml(yaml, matcher, ProtobufMessage::getStrictValidationVisitor());

TestUtility::validate(matcher);

auto outer_input_factory = TestDataInputFloatFactory(3.14);

EXPECT_CALL(validation_visitor_,
performDataInputValidation(_, "type.googleapis.com/google.protobuf.FloatValue"))
.Times(2);

std::string error_message = absl::StrCat("Unsupported data input type: float.",
" The matcher supports input type: string");
EXPECT_THROW_WITH_MESSAGE(factory_.create(matcher)(), EnvoyException, error_message);
}

TEST_F(MatcherTest, TestAnyMatcher) {
const std::string yaml = R"EOF(
on_no_match:
Expand Down
31 changes: 30 additions & 1 deletion test/common/matcher/test_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,13 @@ class TestCommonProtocolInputFactory : public CommonProtocolInputFactory {
struct TestInput : public DataInput<TestData> {
explicit TestInput(DataInputGetResult result) : result_(result) {}
DataInputGetResult get(const TestData&) const override { return result_; }
DataInputGetResult result_;
};

struct TestFloatInput : public DataInput<TestData> {
explicit TestFloatInput(DataInputGetResult result) : result_(result) {}
DataInputGetResult get(const TestData&) const override { return result_; }
absl::string_view dataInputType() const override { return "float"; }
DataInputGetResult result_;
};

Expand Down Expand Up @@ -84,6 +90,7 @@ class TestDataInputBoolFactory : public DataInputFactory<TestData> {
{DataInputGetResult::DataAvailability::AllDataAvailable, std::string(data)}) {}
DataInputFactoryCb<TestData>
createDataInputFactoryCb(const Protobuf::Message&, ProtobufMessage::ValidationVisitor&) override {
// Note, here is using `TestInput` same as `TestDataInputStringFactory`.
return [&]() { return std::make_unique<TestInput>(result_); };
}

Expand All @@ -97,12 +104,34 @@ class TestDataInputBoolFactory : public DataInputFactory<TestData> {
Registry::InjectFactory<DataInputFactory<TestData>> injection_;
};

class TestDataInputFloatFactory : public DataInputFactory<TestData> {
public:
TestDataInputFloatFactory(DataInputGetResult result) : result_(result), injection_(*this) {}
TestDataInputFloatFactory(float)
: TestDataInputFloatFactory(
{DataInputGetResult::DataAvailability::AllDataAvailable, absl::monostate()}) {}
DataInputFactoryCb<TestData>
createDataInputFactoryCb(const Protobuf::Message&, ProtobufMessage::ValidationVisitor&) override {
return [&]() { return std::make_unique<TestFloatInput>(result_); };
}

ProtobufTypes::MessagePtr createEmptyConfigProto() override {
return std::make_unique<ProtobufWkt::FloatValue>();
}
std::string name() const override { return "float"; }

private:
const DataInputGetResult result_;
Registry::InjectFactory<DataInputFactory<TestData>> injection_;
};

// A matcher that evaluates to the configured value.
// Note, `BoolMatcher` supports string type data input only as `TestDataInputBoolFactory` is using
// `TestInput` same as `TestDataInputStringFactory`.
struct BoolMatcher : public InputMatcher {
explicit BoolMatcher(bool value) : value_(value) {}

bool match(const MatchingDataType&) override { return value_; }

const bool value_;
};

Expand Down
Loading

0 comments on commit e5326d4

Please sign in to comment.