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

router: vhost/route/w.cluster local filter configuration #3045

Merged
merged 14 commits into from
Apr 16, 2018

Conversation

rodaine
Copy link
Member

@rodaine rodaine commented Apr 10, 2018

This patch adds support for resolving virtual host-, route-, and weighted cluster-local configuration for filters. See https://github.com/envoyproxy/data-plane-api/issues/540 for discussion. Configs are provided via RDS as opaque structs (similar to the existing LDS-provided configs), resolved to concrete message types only once, and exposed by the VirtualHost, Route and RouteEntry types. These RDS-scoped configs can be a different pb message type than the base configuration, but default to being identical. Filters can optionally access these configs to change their behavior accordingly.

I will be following up to this PR with making the buffer filter route-aware. @rshriram will also be working on the fault and lua filters to support this.

Risk Level: Low-Medium
Testing: unit
API/Docs Changes: envoyproxy/data-plane-api#605
Release Notes: (will update in this PR)

@rshriram
Copy link
Member

cc @lizan @qiwzhang

* empty proto. By default, this method returns the same value as createEmptyConfigProto,
* and can be optionally overridden in implementations.
*/
virtual ProtobufTypes::MessagePtr createEmptyRDSConfigProto() { return createEmptyConfigProto(); }
Copy link
Member

Choose a reason for hiding this comment

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

s/RDS/Route/ ?
The route config can come from static config as well.

Copy link
Member

Choose a reason for hiding this comment

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

+1

envoy_api = dict(
commit = "2dcc435e8ae1d35f8c3a4fa9f132778482fb1a78",
commit = "c15104fce713856203fa4ad948d752e9a1f657c1",
Copy link
Member

Choose a reason for hiding this comment

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

revert this file other than this line? those seems unrelated.

Copy link
Member Author

Choose a reason for hiding this comment

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

the vscode bazel plugin seems a bit overzealous in its linting...

@@ -219,6 +219,20 @@ class Utility {
return config;
}

template <class ProtoMessage, class Factory>
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you need the first template parameter, it is always Struct.

Copy link
Member Author

Choose a reason for hiding this comment

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

was being consistent with the other translate function but will update!

auto& factory = Envoy::Config::Utility::getAndCheckFactory<
Server::Configuration::NamedHttpFilterConfigFactory>(name);

configs_[name] = Envoy::Config::Utility::translateToFactoryRDSConfig(struct_config, factory);
Copy link
Member

Choose a reason for hiding this comment

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

Need to perform validation when PerFilterConfigs are loaded too.

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can do this unfortunately. What type would we validate? (This is related to me asking very nicely for Validate() to be a method on the base proto message which is virtual and empty and can be overriden...

Copy link
Member

Choose a reason for hiding this comment

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

I meant Validate above, but on second thought, we need a method in Factory to pre-process the route-config at loading time, not in the data path. Currently in this PR there is no way to do pre-processing of the filter config in routes. (for configs in listeners, it is done in createFilterFactoryFromProto)

Copy link
Member

Choose a reason for hiding this comment

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

Pre-processing is being treated as an additional feature. That will need more thinking. We won't be doing that as part of this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

we'll likely need to delegate this to the factory(factory) where we already ask it for the pb message. will think about this some more!

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Generally looks good, thank you. Some comments to get started.


/**
* @return const Protobuf::Message* the per-filter config for the given
* filter name configured for this virtual host. If none is present, the
Copy link
Member

Choose a reason for hiding this comment

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

super nit: "the nullptr" is not typically said. Just "nullptr." Same below.

Copy link
Member Author

Choose a reason for hiding this comment

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

thought it sounded funny. Thanks, the @mattklein123!

* empty proto. By default, this method returns the same value as createEmptyConfigProto,
* and can be optionally overridden in implementations.
*/
virtual ProtobufTypes::MessagePtr createEmptyRDSConfigProto() { return createEmptyConfigProto(); }
Copy link
Member

Choose a reason for hiding this comment

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

+1

Factory& factory) {
ProtobufTypes::MessagePtr config = factory.createEmptyRDSConfigProto();

if (config == nullptr) {
Copy link
Member

Choose a reason for hiding this comment

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

I would just RELEASE_ASSERT() this like I recently did for the other instances of this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I was being consistent with the translate method above. Shall I update both?

Copy link
Member

Choose a reason for hiding this comment

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

Yes please. I don't think it's really needed to throw an exception here.

@@ -22,6 +22,7 @@ envoy_cc_library(
"//include/envoy/http:header_map_interface",
"//include/envoy/router:router_interface",
"//include/envoy/runtime:runtime_interface",
"//include/envoy/server:filter_config_interface",
Copy link
Member

Choose a reason for hiding this comment

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

In general we try to not have common depend on server, though this isn't really enforced explicitly. I would add a TODO here and in code to figure out a better way to break this cycle.

Copy link
Member Author

Choose a reason for hiding this comment

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

sg. i imagine this could be done with bazel visibility rules?

Copy link
Member

Choose a reason for hiding this comment

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

I need to think about it. Will look at it as part of repo reorg work.

auto& factory = Envoy::Config::Utility::getAndCheckFactory<
Server::Configuration::NamedHttpFilterConfigFactory>(name);

configs_[name] = Envoy::Config::Utility::translateToFactoryRDSConfig(struct_config, factory);
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we can do this unfortunately. What type would we validate? (This is related to me asking very nicely for Validate() to be a method on the base proto message which is virtual and empty and can be overriden...

proto_cfgs[factory.name()] = buffer_cfg;
PerFilterConfigs configs{proto_cfgs};

ASSERT_EQ(nullptr, configs.get("unknown.filter"))
Copy link
Member

Choose a reason for hiding this comment

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

s/ASSERT/EXPECT in all places for the most part.

@@ -13,10 +16,13 @@
#include "common/network/address_impl.h"
#include "common/router/config_impl.h"

#include "server/config/http/buffer.h"
Copy link
Member

Choose a reason for hiding this comment

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

This has been moved and will need a master merge.

}

TEST(PerFilterConfigsTest, RouteLocalConfig) {
Server::Configuration::BufferFilterConfig factory;
Copy link
Member

Choose a reason for hiding this comment

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

Instead of using buffer filter, can you register a test filter for this test, so that you can correctly verify that you can override to be a different type for the filter data? If you need to you can have a proto just for testing.

Copy link
Member Author

@rodaine rodaine Apr 12, 2018

Choose a reason for hiding this comment

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

is there an existing one I can use for this or should I create one? Should I create the test proto in envoy or data plane?

Copy link
Member

Choose a reason for hiding this comment

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

Just make one local to the test.

Chris Roche added 10 commits April 11, 2018 15:11
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
Signed-off-by: Chris Roche <croche@lyft.com>
@rodaine rodaine force-pushed the route-local-config branch from b13c20c to 118c728 Compare April 12, 2018 01:37
* filter name configured for this virtual host. If none is present,
* nullptr is returned.
*/
virtual const Protobuf::Message* perFilterConfig(const std::string& name) const PURE;
Copy link
Contributor

Choose a reason for hiding this comment

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

This suggestion is trying to address this issue: #3008
We can do it in a separate PR. But it is so simple, you can also put in in this PR too.

In these VHost/Route/Cluster, add another function

// If perFitlerConfig exists for this filter, get its config hash value to detect if config is updated.
// return 0 if no config for the filter.
size_t perFitlerConfigHash(const std::string& name) const PURE;

Copy link
Member

Choose a reason for hiding this comment

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

I don't quite understand how this will help you. In the filter are you going to check this on a per-route basis? What you really want is another callback which allows a filter to update global structures on a route update.

Copy link
Contributor

Choose a reason for hiding this comment

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

This is to help filters need to pre-process per_route_config. per_route_config can be dynamically changed anytime. But you don't want to pre-process the config for each request. This hash will help to detect config change, and skip pre_processing step:

This is how it can be used, For each request in decodeHeader()

auto hash = route()->perFilterConfigHash("my_filter_name");
if (hash == 0) // handle not per_route_config case

 obj =  global_obj_map.find(hash);
 if (obj == null) {
         obj = pre_process_config( route()->perFilterConfig("my_filter_name") );
         global_obj_map[hash] = obj;
 }

This will also achieve: different routes with same config can share the same pre_processed object.

Copy link
Member

Choose a reason for hiding this comment

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

This is not how I would recommend implementing this. (The above is not thread safe.). I have a pretty good idea of how to efficiently implement this and give you the functionality you need. Let's do this in a follow up.

Copy link
Contributor

@qiwzhang qiwzhang Apr 12, 2018

Choose a reason for hiding this comment

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

In istio mixer-client filter, the "global_map" actually is per_thread_global_map. so this approach works for us.
the filter_factory callback approach is better, the global_map can be used by all worker threads. But it is not easy for Istio to use it since its "object_map" object is way deep inside a per_thread object.
My request is: can we support both: 1) calculate the hash of per_route_config at config update time and filter can access it. 2) implements factory callback to support a config change update.

auto& factory = Envoy::Config::Utility::getAndCheckFactory<
Server::Configuration::NamedHttpFilterConfigFactory>(name);

configs_[name] = Envoy::Config::Utility::translateToFactoryRouteConfig(struct_config, factory);
Copy link
Contributor

@qiwzhang qiwzhang Apr 12, 2018

Choose a reason for hiding this comment

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

create another map

config_hash_ to store hash of config

The hash can be calculated by
https://github.com/envoyproxy/envoy/blob/master/source/common/protobuf/utility.h#L127

Copy link
Member

Choose a reason for hiding this comment

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

Same comment as above. I don't think this is very helpful and I would rather sort out filter update callbacks as a separate feature?

Signed-off-by: Chris Roche <croche@lyft.com>
@rodaine
Copy link
Member Author

rodaine commented Apr 13, 2018

Ok, changes made. PTAL @mattklein123

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

LGTM, few small comments. Thank you and nice work!

Protobuf::Map<std::string, ProtobufWkt::Struct> proto_cfgs;
proto_cfgs["unknown.filter"] = some_cfg;

EXPECT_THROW(PerFilterConfigs{proto_cfgs}, EnvoyException)
Copy link
Member

Choose a reason for hiding this comment

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

I would use EXPECT_THROW_WITH_MESSAGE here for better checking. Same elsewhere if applicable.

<< source;
}

std::unique_ptr<Registry::InjectFactory<Server::Configuration::NamedHttpFilterConfigFactory>>
Copy link
Member

Choose a reason for hiding this comment

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

nit: I think this can just be stack allocated with the test? Do you need a unique_ptr?

Copy link
Member Author

@rodaine rodaine Apr 16, 2018

Choose a reason for hiding this comment

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

It's a bit self-referential, so I was having issues creating it in the constructor, but probably can do it in the setup. Let me give that a shot.

Copy link
Member Author

@rodaine rodaine Apr 16, 2018

Choose a reason for hiding this comment

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

Ended up just splitting the filter into a nested class of the fixture

}) << "returned message should be castable to known type";
}

TEST_F(PerFilterConfigsTest, UnknownFilter) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: This would be a better test if you had a full route config that got parsed that then resulted in the exception.

const Protobuf::Message* PerFilterConfigs::get(const std::string& name) const {
auto cfg = configs_.find(name);

if (cfg == configs_.end()) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: I would just use the ternary operator here to collapse the rest of these lines into one.

@zuercher
Copy link
Member

lgtm, modulo pending comments.

Chris Roche and others added 2 commits April 16, 2018 11:34
Signed-off-by: Chris Roche <croche@lyft.com>
@rodaine
Copy link
Member Author

rodaine commented Apr 16, 2018

@mattklein123 nits knitted!

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

Very nice, thanks.

@mattklein123 mattklein123 merged commit 9d6b432 into envoyproxy:master Apr 16, 2018
@rodaine rodaine deleted the route-local-config branch April 16, 2018 20:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants