Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate deprecated fields via validation visitor #10853

Merged
merged 29 commits into from
Jun 5, 2020
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion include/envoy/protobuf/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ envoy_package()
envoy_cc_library(
name = "message_validator_interface",
hdrs = ["message_validator.h"],
deps = ["//source/common/protobuf"],
deps = [
"//source/common/protobuf",
],
)
21 changes: 19 additions & 2 deletions include/envoy/protobuf/message_validator.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,22 @@ namespace ProtobufMessage {

/**
* Exception class for reporting validation errors due to the presence of unknown
* fields in a protobuf
* fields in a protobuf.
*/
class UnknownProtoFieldException : public EnvoyException {
public:
UnknownProtoFieldException(const std::string& message) : EnvoyException(message) {}
};

/**
* Exception class for reporting validation errors due to the presence of deprecated
* fields in a protobuf.
*/
class DeprecatedProtoFieldException : public EnvoyException {
public:
DeprecatedProtoFieldException(const std::string& message) : EnvoyException(message) {}
};

/**
* Visitor interface for a Protobuf::Message. The methods of ValidationVisitor are invoked to
* perform validation based on events encountered during or after the parsing of proto binary
Expand All @@ -30,7 +39,7 @@ class ValidationVisitor {

/**
* Invoked when an unknown field is encountered.
* @param description human readable description of the field
* @param description human readable description of the field.
*/
virtual void onUnknownField(absl::string_view description) PURE;

Expand All @@ -39,6 +48,14 @@ class ValidationVisitor {
* possible.
**/
virtual bool skipValidation() PURE;

/**
* Invoked when deprecated field is encountered.
* @param description human readable description of the field.
* @param soft_deprecation is set to true, visitor would log a warning message, otherwise would
* throw an exception.
*/
virtual void onDeprecatedField(absl::string_view description, bool soft_deprecation) PURE;
};

class ValidationContext {
Expand Down
1 change: 1 addition & 0 deletions source/common/protobuf/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ envoy_cc_library(
deps = [
"//include/envoy/protobuf:message_validator_interface",
"//include/envoy/stats:stats_interface",
"//source/common/common:documentation_url_lib",
"//source/common/common:hash_lib",
"//source/common/common:logger_lib",
"//source/common/common:macros",
Expand Down
35 changes: 27 additions & 8 deletions source/common/protobuf/message_validator_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
namespace Envoy {
namespace ProtobufMessage {

void WarningValidationVisitorImpl::setCounter(Stats::Counter& counter) {
ASSERT(counter_ == nullptr);
counter_ = &counter;
counter.add(prestats_count_);
void WarningValidationVisitorImpl::setUnknownCounter(Stats::Counter& counter) {
ASSERT(unknown_counter_ == nullptr);
unknown_counter_ = &counter;
counter.add(prestats_unknown_count_);
}

void WarningValidationVisitorImpl::onUnknownField(absl::string_view description) {
Expand All @@ -24,12 +24,22 @@ void WarningValidationVisitorImpl::onUnknownField(absl::string_view description)
if (!it.second) {
return;
}

// It's a new field, log and bump stat.
ENVOY_LOG(warn, "Unknown field: {}", description);
if (counter_ == nullptr) {
++prestats_count_;
ENVOY_LOG(warn, "Unexpected field: {}", description);
nezdolik marked this conversation as resolved.
Show resolved Hide resolved
if (unknown_counter_ == nullptr) {
++prestats_unknown_count_;
} else {
unknown_counter_->inc();
}
}

void WarningValidationVisitorImpl::onDeprecatedField(absl::string_view description,
bool soft_deprecation) {
if (soft_deprecation) {
ENVOY_LOG_MISC(warn, "Unexpected field: {}", absl::StrCat(description, deprecation_error));
} else {
counter_->inc();
throw DeprecatedProtoFieldException(absl::StrCat(description, deprecation_error));
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come the common method is inline now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@htuch if i understand your question correctly, you mean previous version of PR where both visitors shared common method for incrementing stats and logging message? i've changed impl, there is no longer common method. There is little benefit to have common method as deprecated field validation does not increment counters and log messages are different.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but looking at the two implementations, I'm not seeing a stats increment in either, and still unclear why the warnings are different.

}

Expand All @@ -38,6 +48,15 @@ void StrictValidationVisitorImpl::onUnknownField(absl::string_view description)
absl::StrCat("Protobuf message (", description, ") has unknown fields"));
}

void StrictValidationVisitorImpl::onDeprecatedField(absl::string_view description,
bool soft_deprecation) {
if (soft_deprecation) {
ENVOY_LOG_MISC(warn, "Unexpected field: {}", absl::StrCat(description, deprecation_error));
nezdolik marked this conversation as resolved.
Show resolved Hide resolved
} else {
throw DeprecatedProtoFieldException(absl::StrCat(description, deprecation_error));
}
}

ValidationVisitor& getNullValidationVisitor() {
MUTABLE_CONSTRUCT_ON_FIRST_USE(NullValidationVisitorImpl);
}
Expand Down
18 changes: 13 additions & 5 deletions source/common/protobuf/message_validator_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,23 @@
#include "envoy/protobuf/message_validator.h"
#include "envoy/stats/stats.h"

#include "common/common/documentation_url.h"
#include "common/common/logger.h"

#include "absl/container/flat_hash_set.h"

namespace Envoy {
namespace ProtobufMessage {

const char deprecation_error[] = " If continued use of this field is absolutely necessary, "
"see " ENVOY_DOC_URL_RUNTIME_OVERRIDE_DEPRECATED " for "
"how to apply a temporary and highly discouraged override.";

class NullValidationVisitorImpl : public ValidationVisitor {
public:
// Envoy::ProtobufMessage::ValidationVisitor
void onUnknownField(absl::string_view) override {}
void onDeprecatedField(absl::string_view, bool) override {}

// Envoy::ProtobufMessage::ValidationVisitor
bool skipValidation() override { return true; }
Expand All @@ -24,10 +30,11 @@ ValidationVisitor& getNullValidationVisitor();
class WarningValidationVisitorImpl : public ValidationVisitor,
public Logger::Loggable<Logger::Id::config> {
public:
void setCounter(Stats::Counter& counter);
void setUnknownCounter(Stats::Counter& counter);

// Envoy::ProtobufMessage::ValidationVisitor
void onUnknownField(absl::string_view description) override;
void onDeprecatedField(absl::string_view description, bool soft_deprecation) override;

// Envoy::ProtobufMessage::ValidationVisitor
bool skipValidation() override { return false; }
Expand All @@ -36,10 +43,10 @@ class WarningValidationVisitorImpl : public ValidationVisitor,
// Track hashes of descriptions we've seen, to avoid log spam. A hash is used here to avoid
// wasting memory with unused strings.
absl::flat_hash_set<uint64_t> descriptions_;
// This can be late initialized via setCounter(), enabling the server bootstrap loading which
// occurs prior to the initialization of the stats subsystem.
Stats::Counter* counter_{};
uint64_t prestats_count_{};
// This can be late initialized via setUnknownCounter(), enabling the server bootstrap loading
// which occurs prior to the initialization of the stats subsystem.
Stats::Counter* unknown_counter_{};
uint64_t prestats_unknown_count_{};
};

class StrictValidationVisitorImpl : public ValidationVisitor {
Expand All @@ -49,6 +56,7 @@ class StrictValidationVisitorImpl : public ValidationVisitor {

// Envoy::ProtobufMessage::ValidationVisitor
bool skipValidation() override { return false; }
void onDeprecatedField(absl::string_view description, bool soft_deprecation) override;
};

ValidationVisitor& getStrictValidationVisitor();
Expand Down
30 changes: 13 additions & 17 deletions source/common/protobuf/utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ void tryWithApiBoosting(MessageXformFn f, Protobuf::Message& message) {
// otherwise fatal field. Throws a warning on use of a fatal by default field.
void deprecatedFieldHelper(Runtime::Loader* runtime, bool proto_annotated_as_deprecated,
bool proto_annotated_as_disallowed, const std::string& feature_name,
std::string error, const Protobuf::Message& message) {
std::string error, const Protobuf::Message& message,
ProtobufMessage::ValidationVisitor& validation_visitor) {
// This option is for Envoy builds with --define deprecated_features=disabled
// The build options CI then verifies that as Envoy developers deprecate fields,
// that they update canonical configs and unit tests to not use those deprecated
Expand Down Expand Up @@ -196,14 +197,9 @@ void deprecatedFieldHelper(Runtime::Loader* runtime, bool proto_annotated_as_dep
std::string with_overridden = fmt::format(
error,
(runtime_overridden ? "runtime overrides to continue using now fatal-by-default " : ""));
if (warn_only) {
nezdolik marked this conversation as resolved.
Show resolved Hide resolved
ENVOY_LOG_MISC(warn, "{}", with_overridden);
} else {
const char fatal_error[] = " If continued use of this field is absolutely necessary, "
"see " ENVOY_DOC_URL_RUNTIME_OVERRIDE_DEPRECATED " for how "
"to apply a temporary and highly discouraged override.";
throw ProtoValidationException(with_overridden + fatal_error, message);
nezdolik marked this conversation as resolved.
Show resolved Hide resolved
}

validation_visitor.onDeprecatedField("type " + message.GetTypeName() + " " + with_overridden,
warn_only);
}

} // namespace
Expand Down Expand Up @@ -385,11 +381,10 @@ void MessageUtil::loadFromFile(const std::string& path, Protobuf::Message& messa

namespace {

void checkForDeprecatedNonRepeatedEnumValue(const Protobuf::Message& message,
absl::string_view filename,
const Protobuf::FieldDescriptor* field,
const Protobuf::Reflection* reflection,
Runtime::Loader* runtime) {
void checkForDeprecatedNonRepeatedEnumValue(
const Protobuf::Message& message, absl::string_view filename,
const Protobuf::FieldDescriptor* field, const Protobuf::Reflection* reflection,
Runtime::Loader* runtime, ProtobufMessage::ValidationVisitor& validation_visitor) {
// Repeated fields will be handled by recursion in checkForUnexpectedFields.
if (field->is_repeated() || field->cpp_type() != Protobuf::FieldDescriptor::CPPTYPE_ENUM) {
return;
Expand All @@ -412,7 +407,7 @@ void checkForDeprecatedNonRepeatedEnumValue(const Protobuf::Message& message,
runtime, true /*deprecated*/,
enum_value_descriptor->options().GetExtension(envoy::annotations::disallowed_by_default_enum),
absl::StrCat("envoy.deprecated_features:", enum_value_descriptor->full_name()), error,
message);
message, validation_visitor);
}

class UnexpectedFieldProtoVisitor : public ProtobufMessage::ConstProtoVisitor {
Expand All @@ -428,7 +423,8 @@ class UnexpectedFieldProtoVisitor : public ProtobufMessage::ConstProtoVisitor {

// Before we check to see if the field is in use, see if there's a
// deprecated default enum value.
checkForDeprecatedNonRepeatedEnumValue(message, filename, &field, reflection, runtime_);
checkForDeprecatedNonRepeatedEnumValue(message, filename, &field, reflection, runtime_,
validation_visitor_);

// If this field is not in use, continue.
if ((field.is_repeated() && reflection->FieldSize(message, &field) == 0) ||
Expand All @@ -446,7 +442,7 @@ class UnexpectedFieldProtoVisitor : public ProtobufMessage::ConstProtoVisitor {
deprecatedFieldHelper(runtime_, true /*deprecated*/,
field.options().GetExtension(envoy::annotations::disallowed_by_default),
absl::StrCat("envoy.deprecated_features:", field.full_name()), warning,
message);
message, validation_visitor_);
}
return nullptr;
}
Expand Down
4 changes: 2 additions & 2 deletions source/server/server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,9 @@ void InstanceImpl::initialize(const Options& options,
ServerStats{ALL_SERVER_STATS(POOL_COUNTER_PREFIX(stats_store_, server_stats_prefix),
POOL_GAUGE_PREFIX(stats_store_, server_stats_prefix),
POOL_HISTOGRAM_PREFIX(stats_store_, server_stats_prefix))});
validation_context_.static_warning_validation_visitor().setCounter(
validation_context_.static_warning_validation_visitor().setUnknownCounter(
server_stats_->static_unknown_fields_);
validation_context_.dynamic_warning_validation_visitor().setCounter(
validation_context_.dynamic_warning_validation_visitor().setUnknownCounter(
server_stats_->dynamic_unknown_fields_);

initialization_timer_ = std::make_unique<Stats::HistogramCompletableTimespanImpl>(
Expand Down
3 changes: 3 additions & 0 deletions test/common/http/conn_manager_impl_fuzz_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,9 @@ DEFINE_PROTO_FUZZER(const test::common::http::ConnManagerImplTestCase& input) {
} catch (const ProtoValidationException& e) {
ENVOY_LOG_MISC(debug, "ProtoValidationException: {}", e.what());
return;
} catch (const Envoy::ProtobufMessage::DeprecatedProtoFieldException& e) {
ENVOY_LOG_MISC(debug, "DeprecatedProtoFieldException: {}", e.what());
return;
}

FuzzConfig config;
Expand Down
18 changes: 9 additions & 9 deletions test/common/protobuf/message_validator_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ TEST(NullValidationVisitorImpl, UnknownField) {
// The warning validation visitor logs and bumps stats on unknown fields
TEST(WarningValidationVisitorImpl, UnknownField) {
Stats::TestUtil::TestStore stats;
Stats::Counter& counter = stats.counter("counter");
Stats::Counter& unknown_counter = stats.counter("counter");
WarningValidationVisitorImpl warning_validation_visitor;
// we want to be executed.
EXPECT_FALSE(warning_validation_visitor.skipValidation());
// First time around we should log.
EXPECT_LOG_CONTAINS("warn", "Unknown field: foo",
EXPECT_LOG_CONTAINS("warn", "Unexpected field: foo",
warning_validation_visitor.onUnknownField("foo"));
// Duplicate descriptions don't generate a log the second time around.
EXPECT_LOG_NOT_CONTAINS("warn", "Unknown field: foo",
EXPECT_LOG_NOT_CONTAINS("warn", "Unexpected field: foo",
warning_validation_visitor.onUnknownField("foo"));
// Unrelated variable increments.
EXPECT_LOG_CONTAINS("warn", "Unknown field: bar",
EXPECT_LOG_CONTAINS("warn", "Unexpected field: bar",
warning_validation_visitor.onUnknownField("bar"));
// When we set the stats counter, the above increments are transferred.
EXPECT_EQ(0, counter.value());
warning_validation_visitor.setCounter(counter);
EXPECT_EQ(2, counter.value());
EXPECT_EQ(0, unknown_counter.value());
warning_validation_visitor.setUnknownCounter(unknown_counter);
EXPECT_EQ(2, unknown_counter.value());
// A third unknown field is tracked in stats post-initialization.
EXPECT_LOG_CONTAINS("warn", "Unknown field: baz",
EXPECT_LOG_CONTAINS("warn", "Unexpected field: baz",
warning_validation_visitor.onUnknownField("baz"));
EXPECT_EQ(3, counter.value());
EXPECT_EQ(3, unknown_counter.value());
}

// The strict validation visitor throws on unknown fields.
Expand Down
14 changes: 8 additions & 6 deletions test/common/protobuf/utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1468,7 +1468,7 @@ TEST_P(DeprecatedFieldsTest, DEPRECATED_FEATURE_TEST(IndividualFieldDisallowed))
envoy::test::deprecation_test::Base base;
base.set_is_deprecated_fatal("foo");
EXPECT_THROW_WITH_REGEX(
checkForDeprecation(base), ProtoValidationException,
checkForDeprecation(base), Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using deprecated option 'envoy.test.deprecation_test.Base.is_deprecated_fatal'");
}

Expand All @@ -1479,7 +1479,7 @@ TEST_P(DeprecatedFieldsTest,

// Make sure this is set up right.
EXPECT_THROW_WITH_REGEX(
checkForDeprecation(base), ProtoValidationException,
checkForDeprecation(base), Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using deprecated option 'envoy.test.deprecation_test.Base.is_deprecated_fatal'");
// The config will be rejected, so the feature will not be used.
EXPECT_EQ(0, runtime_deprecated_feature_use_.value());
Expand Down Expand Up @@ -1512,7 +1512,7 @@ TEST_P(DeprecatedFieldsTest, DEPRECATED_FEATURE_TEST(DisallowViaRuntime)) {
{{"envoy.deprecated_features:envoy.test.deprecation_test.Base.is_deprecated", " false"}});

EXPECT_THROW_WITH_REGEX(
checkForDeprecation(base), ProtoValidationException,
checkForDeprecation(base), Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using deprecated option 'envoy.test.deprecation_test.Base.is_deprecated'");
EXPECT_EQ(1, runtime_deprecated_feature_use_.value());
}
Expand All @@ -1527,7 +1527,7 @@ TEST_P(DeprecatedFieldsTest, DEPRECATED_FEATURE_TEST(MixOfFatalAndWarnings)) {
EXPECT_LOG_CONTAINS(
"warning", "Using deprecated option 'envoy.test.deprecation_test.Base.is_deprecated'", {
EXPECT_THROW_WITH_REGEX(
checkForDeprecation(base), ProtoValidationException,
checkForDeprecation(base), Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using deprecated option 'envoy.test.deprecation_test.Base.is_deprecated_fatal'");
});
}
Expand Down Expand Up @@ -1620,7 +1620,8 @@ TEST_P(DeprecatedFieldsTest, DEPRECATED_FEATURE_TEST(RuntimeOverrideEnumDefault)
{{"envoy.deprecated_features:envoy.test.deprecation_test.Base.DEPRECATED_DEFAULT", "false"}});

// Make sure this is set up right.
EXPECT_THROW_WITH_REGEX(checkForDeprecation(base), ProtoValidationException,
EXPECT_THROW_WITH_REGEX(checkForDeprecation(base),
Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using the default now-deprecated value DEPRECATED_DEFAULT");
}

Expand All @@ -1629,7 +1630,8 @@ TEST_P(DeprecatedFieldsTest, DEPRECATED_FEATURE_TEST(FatalEnum)) {
envoy::test::deprecation_test::Base base;
base.mutable_enum_container()->set_deprecated_enum(
envoy::test::deprecation_test::Base::DEPRECATED_FATAL);
EXPECT_THROW_WITH_REGEX(checkForDeprecation(base), ProtoValidationException,
EXPECT_THROW_WITH_REGEX(checkForDeprecation(base),
Envoy::ProtobufMessage::DeprecatedProtoFieldException,
"Using deprecated value DEPRECATED_FATAL");

Runtime::LoaderSingleton::getExisting()->mergeValues(
Expand Down
4 changes: 3 additions & 1 deletion test/mocks/protobuf/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ envoy_cc_mock(
name = "protobuf_mocks",
srcs = ["mocks.cc"],
hdrs = ["mocks.h"],
deps = ["//include/envoy/protobuf:message_validator_interface"],
deps = [
"//include/envoy/protobuf:message_validator_interface",
],
)
1 change: 1 addition & 0 deletions test/mocks/protobuf/mocks.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class MockValidationVisitor : public ValidationVisitor {
~MockValidationVisitor() override;

MOCK_METHOD(void, onUnknownField, (absl::string_view));
MOCK_METHOD(void, onDeprecatedField, (absl::string_view, bool));

bool skipValidation() override { return skip_validation_; }

Expand Down
Loading