diff --git a/source/common/formatter/substitution_format_string.cc b/source/common/formatter/substitution_format_string.cc index bddaac039a7f1..5cd4a2f22e355 100644 --- a/source/common/formatter/substitution_format_string.cc +++ b/source/common/formatter/substitution_format_string.cc @@ -12,7 +12,9 @@ namespace Formatter { FormatterPtr SubstitutionFormatStringUtils::createJsonFormatter(const ProtobufWkt::Struct& struct_format, bool preserve_types, bool omit_empty_values) { - return std::make_unique(struct_format, preserve_types, omit_empty_values); + std::vector commands; + return std::make_unique(struct_format, preserve_types, omit_empty_values, + commands); } FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig( @@ -25,6 +27,9 @@ FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig( throw EnvoyException(absl::StrCat("Formatter not found: ", formatter.name())); } auto parser = factory->createCommandParserFromProto(formatter.typed_config()); + if (!parser) { + throw EnvoyException(absl::StrCat("Failed to create command parser: ", formatter.name())); + } commands.push_back(std::move(parser)); } @@ -32,15 +37,16 @@ FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig( case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kTextFormat: return std::make_unique(config.text_format(), config.omit_empty_values(), commands); - case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat: { - return createJsonFormatter(config.json_format(), true, config.omit_empty_values()); + case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat: + return std::make_unique(config.json_format(), true, + config.omit_empty_values(), commands); case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kTextFormatSource: return std::make_unique( Config::DataSource::read(config.text_format_source(), true, api), false, commands); - } default: NOT_REACHED_GCOVR_EXCL_LINE; } + return nullptr; } diff --git a/source/common/formatter/substitution_formatter.cc b/source/common/formatter/substitution_formatter.cc index b8cf77b485736..4a700e1b41a70 100644 --- a/source/common/formatter/substitution_formatter.cc +++ b/source/common/formatter/substitution_formatter.cc @@ -149,14 +149,21 @@ std::string JsonFormatterImpl::format(const Http::RequestHeaderMap& request_head return absl::StrCat(log_line, "\n"); } +StructFormatter::StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, + bool omit_empty_values, + const std::vector& commands) + : omit_empty_values_(omit_empty_values), preserve_types_(preserve_types), + empty_value_(omit_empty_values_ ? EMPTY_STRING : DefaultUnspecifiedValueString), + struct_output_format_(FormatBuilder(commands).toFormatMapValue(format_mapping)) {} + StructFormatter::StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, bool omit_empty_values) : omit_empty_values_(omit_empty_values), preserve_types_(preserve_types), empty_value_(omit_empty_values_ ? EMPTY_STRING : DefaultUnspecifiedValueString), - struct_output_format_(toFormatMapValue(format_mapping)) {} + struct_output_format_(FormatBuilder().toFormatMapValue(format_mapping)) {} StructFormatter::StructFormatMapWrapper -StructFormatter::toFormatMapValue(const ProtobufWkt::Struct& struct_format) const { +StructFormatter::FormatBuilder::toFormatMapValue(const ProtobufWkt::Struct& struct_format) const { auto output = std::make_unique(); for (const auto& pair : struct_format.fields()) { switch (pair.second.kind_case()) { @@ -178,10 +185,10 @@ StructFormatter::toFormatMapValue(const ProtobufWkt::Struct& struct_format) cons } } return {std::move(output)}; -}; +} -StructFormatter::StructFormatListWrapper -StructFormatter::toFormatListValue(const ProtobufWkt::ListValue& list_value_format) const { +StructFormatter::StructFormatListWrapper StructFormatter::FormatBuilder::toFormatListValue( + const ProtobufWkt::ListValue& list_value_format) const { auto output = std::make_unique(); for (const auto& value : list_value_format.values()) { switch (value.kind_case()) { @@ -205,9 +212,10 @@ StructFormatter::toFormatListValue(const ProtobufWkt::ListValue& list_value_form } std::vector -StructFormatter::toFormatStringValue(const std::string& string_format) const { - return SubstitutionFormatParser::parse(string_format); -}; +StructFormatter::FormatBuilder::toFormatStringValue(const std::string& string_format) const { + std::vector commands; + return SubstitutionFormatParser::parse(string_format, commands_.value_or(commands)); +} ProtobufWkt::Value StructFormatter::providersCallback( const std::vector& providers, diff --git a/source/common/formatter/substitution_formatter.h b/source/common/formatter/substitution_formatter.h index d9e2f53b4661f..32d33eab4d3b5 100644 --- a/source/common/formatter/substitution_formatter.h +++ b/source/common/formatter/substitution_formatter.h @@ -135,6 +135,8 @@ class StructFormatter { public: StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, bool omit_empty_values); + StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, + bool omit_empty_values, const std::vector& commands); ProtobufWkt::Struct format(const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, @@ -168,9 +170,19 @@ class StructFormatter { const std::function>; // Methods for building the format map. - std::vector toFormatStringValue(const std::string& string_format) const; - StructFormatMapWrapper toFormatMapValue(const ProtobufWkt::Struct& struct_format) const; - StructFormatListWrapper toFormatListValue(const ProtobufWkt::ListValue& list_value_format) const; + class FormatBuilder { + public: + explicit FormatBuilder(const std::vector& commands) : commands_(commands) {} + explicit FormatBuilder() : commands_(absl::nullopt) {} + std::vector toFormatStringValue(const std::string& string_format) const; + StructFormatMapWrapper toFormatMapValue(const ProtobufWkt::Struct& struct_format) const; + StructFormatListWrapper + toFormatListValue(const ProtobufWkt::ListValue& list_value_format) const; + + private: + using CommandsRef = std::reference_wrapper>; + const absl::optional commands_; + }; // Methods for doing the actual formatting. ProtobufWkt::Value providersCallback(const std::vector& providers, @@ -189,8 +201,9 @@ class StructFormatter { const bool omit_empty_values_; const bool preserve_types_; const std::string empty_value_; + const StructFormatMapWrapper struct_output_format_; -}; // namespace Formatter +}; using StructFormatterPtr = std::unique_ptr; @@ -199,6 +212,9 @@ class JsonFormatterImpl : public Formatter { JsonFormatterImpl(const ProtobufWkt::Struct& format_mapping, bool preserve_types, bool omit_empty_values) : struct_formatter_(format_mapping, preserve_types, omit_empty_values) {} + JsonFormatterImpl(const ProtobufWkt::Struct& format_mapping, bool preserve_types, + bool omit_empty_values, const std::vector& commands) + : struct_formatter_(format_mapping, preserve_types, omit_empty_values, commands) {} // Formatter::format std::string format(const Http::RequestHeaderMap& request_headers, diff --git a/test/common/formatter/command_extension.cc b/test/common/formatter/command_extension.cc index 4dd2faa346255..724650d1811a4 100644 --- a/test/common/formatter/command_extension.cc +++ b/test/common/formatter/command_extension.cc @@ -41,5 +41,54 @@ ProtobufTypes::MessagePtr TestCommandFactory::createEmptyConfigProto() { std::string TestCommandFactory::name() const { return "envoy.formatter.TestFormatter"; } +absl::optional AdditionalFormatter::format(const Http::RequestHeaderMap&, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + return "AdditionalFormatter"; +} + +ProtobufWkt::Value AdditionalFormatter::formatValue(const Http::RequestHeaderMap&, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + return ValueUtil::stringValue(""); +} + +FormatterProviderPtr AdditionalCommandParser::parse(const std::string& token, size_t, + size_t) const { + if (absl::StartsWith(token, "ADDITIONAL_EXTENSION")) { + return std::make_unique(); + } + + return nullptr; +} + +CommandParserPtr AdditionalCommandFactory::createCommandParserFromProto(const Protobuf::Message&) { + return std::make_unique(); +} + +std::string AdditionalCommandFactory::configType() { return "google.protobuf.UInt32Value"; } + +ProtobufTypes::MessagePtr AdditionalCommandFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +std::string AdditionalCommandFactory::name() const { return "envoy.formatter.AdditionalFormatter"; } + +CommandParserPtr FailCommandFactory::createCommandParserFromProto(const Protobuf::Message&) { + return nullptr; +} + +std::string FailCommandFactory::configType() { return "google.protobuf.UInt64Value"; } + +ProtobufTypes::MessagePtr FailCommandFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +std::string FailCommandFactory::name() const { return "envoy.formatter.FailFormatter"; } + } // namespace Formatter } // namespace Envoy diff --git a/test/common/formatter/command_extension.h b/test/common/formatter/command_extension.h index 3afa5e0333747..2abba003d9b2c 100644 --- a/test/common/formatter/command_extension.h +++ b/test/common/formatter/command_extension.h @@ -23,7 +23,6 @@ class TestFormatter : public FormatterProvider { class TestCommandParser : public CommandParser { public: - TestCommandParser() = default; FormatterProviderPtr parse(const std::string& token, size_t, size_t) const override; }; @@ -35,5 +34,37 @@ class TestCommandFactory : public CommandParserFactory { std::string name() const override; }; +class AdditionalFormatter : public FormatterProvider { +public: + // FormatterProvider + absl::optional format(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; + ProtobufWkt::Value formatValue(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; +}; + +class AdditionalCommandParser : public CommandParser { +public: + FormatterProviderPtr parse(const std::string& token, size_t, size_t) const override; +}; + +class AdditionalCommandFactory : public CommandParserFactory { +public: + CommandParserPtr createCommandParserFromProto(const Protobuf::Message&) override; + std::string configType() override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + std::string name() const override; +}; + +class FailCommandFactory : public CommandParserFactory { +public: + CommandParserPtr createCommandParserFromProto(const Protobuf::Message&) override; + std::string configType() override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + std::string name() const override; +}; + } // namespace Formatter } // namespace Envoy diff --git a/test/common/formatter/substitution_format_string_test.cc b/test/common/formatter/substitution_format_string_test.cc index 37e83fe3caa9f..31924dbd19f44 100644 --- a/test/common/formatter/substitution_format_string_test.cc +++ b/test/common/formatter/substitution_format_string_test.cc @@ -119,6 +119,26 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigFormatterExtension) response_trailers_, stream_info_, body_)); } +TEST_F(SubstitutionFormatStringUtilsTest, + TestFromProtoConfigFormatterExtensionFailsToCreateParser) { + FailCommandFactory fail_factory; + Registry::InjectFactory command_register(fail_factory); + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "plain text" + formatters: + - name: envoy.formatter.FailFormatter + typed_config: + "@type": type.googleapis.com/google.protobuf.UInt64Value +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + EXPECT_THROW_WITH_MESSAGE(SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()), + EnvoyException, + "Failed to create command parser: envoy.formatter.FailFormatter"); +} + TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigFormatterExtensionUnknown) { const std::string yaml = R"EOF( text_format_source: @@ -135,5 +155,71 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigFormatterExtensionU "Formatter not found: envoy.formatter.TestFormatterUnknown"); } +TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigJsonWithExtension) { + TestCommandFactory factory; + Registry::InjectFactory command_register(factory); + + const std::string yaml = R"EOF( + json_format: + text: "plain text %COMMAND_EXTENSION()%" + path: "%REQ(:path)% %COMMAND_EXTENSION()%" + code: "%RESPONSE_CODE% %COMMAND_EXTENSION()%" + headers: + content-type: "%REQ(CONTENT-TYPE)% %COMMAND_EXTENSION()%" + formatters: + - name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()); + const auto out_json = formatter->format(request_headers_, response_headers_, response_trailers_, + stream_info_, body_); + + const std::string expected = R"EOF({ + "text": "plain text TestFormatter", + "path": "/bar/foo TestFormatter", + "code": "200 TestFormatter", + "headers": { + "content-type": "application/json TestFormatter" + } +})EOF"; + + EXPECT_TRUE(TestUtility::jsonStringEqual(out_json, expected)); +} + +TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigJsonWithMultipleExtensions) { + TestCommandFactory test_factory; + Registry::InjectFactory test_command_register(test_factory); + AdditionalCommandFactory additional_factory; + Registry::InjectFactory additional_command_register(additional_factory); + + const std::string yaml = R"EOF( + json_format: + text: "plain text %COMMAND_EXTENSION()%" + path: "%REQ(:path)% %ADDITIONAL_EXTENSION()%" + formatters: + - name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue + - name: envoy.formatter.AdditionalFormatter + typed_config: + "@type": type.googleapis.com/google.protobuf.UInt32Value +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()); + const auto out_json = formatter->format(request_headers_, response_headers_, response_trailers_, + stream_info_, body_); + + const std::string expected = R"EOF({ + "text": "plain text TestFormatter", + "path": "/bar/foo AdditionalFormatter", +})EOF"; + + EXPECT_TRUE(TestUtility::jsonStringEqual(out_json, expected)); +} + } // namespace Formatter } // namespace Envoy