Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions docs/root/configuration/overview/extension.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ to the common subscription statistics, it also provides the following:

config_reload, Counter, Total number of successful configuration updates
config_fail, Counter, Total number of failed configuration updates
config_conflict, Counter, Total number of conflicting applications of configuration updates; this may happen when a new listener cannot reuse a subscribed extension configuration due to an invalid type URL.
9 changes: 0 additions & 9 deletions include/envoy/config/extension_config_provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,6 @@ template <class Factory, class FactoryCallback> class ExtensionConfigProvider {
*/
virtual absl::optional<FactoryCallback> config() PURE;

/**
* Validate that the configuration is applicable in the context of the provider. If an exception
* is thrown by any of the config providers for an update, the extension configuration update is
* rejected.
* @param proto_config is the candidate configuration update.
* @param factory used to instantiate an extension config.
*/
virtual void validateConfig(const ProtobufWkt::Any& proto_config, Factory& factory) PURE;

/**
* Update the provider with a new configuration.
* @param config is an extension factory callback to replace the existing configuration.
Expand Down
12 changes: 4 additions & 8 deletions include/envoy/filter/http/filter_config_provider.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,15 @@ class FilterConfigProviderManager {
/**
* Get an FilterConfigProviderPtr for a filter config. The config providers may share
* the underlying subscriptions to the filter config discovery service.
* @param config_source supplies the configuration source for the filter configs.
* @param config_source supplies the extension configuration source for the filter configs.
* @param filter_config_name the filter config resource name.
* @param require_type_urls enforces that the typed filter config must have a certain type URL.
* @param factory_context is the context to use for the filter config provider.
* @param stat_prefix supplies the stat_prefix to use for the provider stats.
* @param apply_without_warming initializes immediately with the default config and starts the
* subscription.
*/
virtual FilterConfigProviderPtr createDynamicFilterConfigProvider(
const envoy::config::core::v3::ConfigSource& config_source,
const std::string& filter_config_name, const std::set<std::string>& require_type_urls,
Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix,
bool apply_without_warming) PURE;
const envoy::config::core::v3::ExtensionConfigSource& config_source,
const std::string& filter_config_name, Server::Configuration::FactoryContext& factory_context,
const std::string& stat_prefix) PURE;

/**
* Get an FilterConfigProviderPtr for a statically inlined filter config.
Expand Down
95 changes: 72 additions & 23 deletions source/common/filter/http/filter_config_discovery_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ namespace Filter {
namespace Http {

DynamicFilterConfigProviderImpl::DynamicFilterConfigProviderImpl(
FilterConfigSubscriptionSharedPtr&& subscription,
const std::set<std::string>& require_type_urls,
FilterConfigSubscriptionSharedPtr& subscription,
const absl::flat_hash_set<std::string>& require_type_urls,
Server::Configuration::FactoryContext& factory_context)
: subscription_(std::move(subscription)), require_type_urls_(require_type_urls),
: subscription_(subscription), require_type_urls_(require_type_urls),
tls_(factory_context.threadLocal()),
init_target_("DynamicFilterConfigProviderImpl", [this]() {
subscription_->start();
Expand All @@ -34,21 +34,19 @@ DynamicFilterConfigProviderImpl::~DynamicFilterConfigProviderImpl() {
subscription_->filter_config_providers_.erase(this);
}

void DynamicFilterConfigProviderImpl::validateTypeUrl(const std::string& type_url) const {
if (!require_type_urls_.contains(type_url)) {
throw EnvoyException(fmt::format("Error: filter config has type URL {} but expect {}.",
type_url, absl::StrJoin(require_type_urls_, ", ")));
}
}

const std::string& DynamicFilterConfigProviderImpl::name() { return subscription_->name(); }

absl::optional<Envoy::Http::FilterFactoryCb> DynamicFilterConfigProviderImpl::config() {
return tls_->config_;
}

void DynamicFilterConfigProviderImpl::validateConfig(
const ProtobufWkt::Any& proto_config, Server::Configuration::NamedHttpFilterConfigFactory&) {
auto type_url = Config::Utility::getFactoryType(proto_config);
if (require_type_urls_.count(type_url) == 0) {
throw EnvoyException(fmt::format("Error: filter config has type URL {} but expect {}.",
type_url, absl::StrJoin(require_type_urls_, ", ")));
}
}

void DynamicFilterConfigProviderImpl::onConfigUpdate(Envoy::Http::FilterFactoryCb config,
const std::string&,
Config::ConfigAppliedCb cb) {
Expand Down Expand Up @@ -124,8 +122,9 @@ void FilterConfigSubscription::onConfigUpdate(
// Ensure that the filter config is valid in the filter chain context once the proto is processed.
// Validation happens before updating to prevent a partial update application. It might be
// possible that the providers have distinct type URL constraints.
const auto type_url = Config::Utility::getFactoryType(filter_config.typed_config());
for (auto* provider : filter_config_providers_) {
provider->validateConfig(filter_config.typed_config(), factory);
provider->validateTypeUrl(type_url);
}
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
filter_config.typed_config(), validator_, factory);
Expand All @@ -142,6 +141,9 @@ void FilterConfigSubscription::onConfigUpdate(
});
}
last_config_hash_ = new_hash;
last_config_ = factory_callback;
last_type_url_ = type_url;
last_version_info_ = version_info;
}

void FilterConfigSubscription::onConfigUpdate(
Expand Down Expand Up @@ -173,6 +175,8 @@ FilterConfigSubscription::~FilterConfigSubscription() {
filter_config_provider_manager_.subscriptions_.erase(subscription_id_);
}

void FilterConfigSubscription::incrementConflictCounter() { stats_.config_conflict_.inc(); }

std::shared_ptr<FilterConfigSubscription> FilterConfigProviderManagerImpl::getSubscription(
const envoy::config::core::v3::ConfigSource& config_source, const std::string& name,
Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix) {
Expand All @@ -196,24 +200,69 @@ std::shared_ptr<FilterConfigSubscription> FilterConfigProviderManagerImpl::getSu
}

FilterConfigProviderPtr FilterConfigProviderManagerImpl::createDynamicFilterConfigProvider(
const envoy::config::core::v3::ConfigSource& config_source,
const std::string& filter_config_name, const std::set<std::string>& require_type_urls,
Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix,
bool apply_without_warming) {
auto subscription =
getSubscription(config_source, filter_config_name, factory_context, stat_prefix);
const envoy::config::core::v3::ExtensionConfigSource& config_source,
const std::string& filter_config_name, Server::Configuration::FactoryContext& factory_context,
const std::string& stat_prefix) {
auto subscription = getSubscription(config_source.config_source(), filter_config_name,
factory_context, stat_prefix);
// For warming, wait until the subscription receives the first response to indicate readiness.
// Otherwise, mark ready immediately and start the subscription on initialization. A default
// config is expected in the latter case.
if (!apply_without_warming) {
if (!config_source.apply_default_config_without_warming()) {
factory_context.initManager().add(subscription->initTarget());
}
auto provider = std::make_unique<DynamicFilterConfigProviderImpl>(
std::move(subscription), require_type_urls, factory_context);
absl::flat_hash_set<std::string> require_type_urls;
for (const auto& type_url : config_source.type_urls()) {
auto factory_type_url = TypeUtil::typeUrlToDescriptorFullName(type_url);
require_type_urls.emplace(factory_type_url);
}
auto provider = std::make_unique<DynamicFilterConfigProviderImpl>(subscription, require_type_urls,
factory_context);
// Ensure the subscription starts if it has not already.
if (apply_without_warming) {
if (config_source.apply_default_config_without_warming()) {
factory_context.initManager().add(provider->init_target_);
}

// If the subscription already received a config, attempt to apply it.
// It is possible that the received extension config fails to satisfy the listener
// type URL constraints. This may happen if ECDS and LDS updates are racing, and the LDS
// update arrives first. In this case, use the default config, increment a metric,
// and the applied config eventually converges once ECDS update arrives.
bool last_config_valid = false;
if (subscription->lastConfig().has_value()) {
try {
provider->validateTypeUrl(subscription->lastTypeUrl());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would there be a value in moving this to returning true/false?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I considered it, but the exception contains the list of required type URLs, which is useful. Bool would be less actionable.

last_config_valid = true;
} catch (const EnvoyException& e) {
ENVOY_LOG(debug, "ECDS subscription {} is invalid in a listener context: {}.",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you express this in a way that leads to actionable outcome, i.e. what is the likely cause of this error?

Also, should we have some stat bumped? Logs will get lost in production settings.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thinking through this more broadly, I kind of find this corner case a bit icky. This relates to my validate config comment above; if all we can do is validate configs, can we pull in the type URL validation into the core here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'll take it away from base interfaces. The likely cause is pushing LDS and ECDS together and LDS update arriving first. Do you have any recommendation for a metric?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are you making the argument here that since the last config has already been validated and is known to work, the only possible way it could fail here is if the type URLs mismatch somehow? Do we even need onConfigUpdate in try/catch if we validate?

In terms of stats, probably something in https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/stats#config-http-conn-man-stats namespace makes sense.

Copy link
Copy Markdown
Contributor Author

@kyessenov kyessenov Mar 11, 2021

Choose a reason for hiding this comment

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

@htuch I'm looking for guidance how to deal with inconsistency between ECDS and LDS. The scenario is the following:

  1. LDS add listener L1 with config_discovery for x with type URL {T1, T2}
  2. ECDS update for x with type URL T1 (acked)
  3. LDS add listener L2 with config_discovery for x with type URL T2.

I think this is similar to a delayed NACK situation since we want the management server to push a new ECDS update for x with type URL T2, that validates both L1 and L2. This PR will make L2 to use the default config, but it does not indicate to the management server than a new ECDS update is needed. Granted, this is a fairly convoluted example. The metric would be useful, but is the assumption here, that some higher level controller (human?) is going to look for it and force push ECDS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added "config_conflict" stat for ECDS resource. I think keying by ECDS is a bit more actionable, since it is clear why resource is the conflict (LDS tends to be more source-of-truth). LMK.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In your example, is "x" actually different in step 4? I.e. it has been deliberately changed by the management server and when server in hypothetical step 4, it's really just a problem of 3 and 4 racing? Or is "x" somehow polymorphic and could be T1 or T2 and it gets decided based on client state?

This seems a pretty weird situation but I'm likely missing something. Feel free to ping me IM so we can figure this out with lower RTT.

filter_config_name, e.what());
subscription->incrementConflictCounter();
}
if (last_config_valid) {
provider->onConfigUpdate(subscription->lastConfig().value(), subscription->lastVersionInfo(),
nullptr);
}
}

// Apply the default config if none has been applied.
if (config_source.has_default_config() && !last_config_valid) {
auto* default_factory =
Config::Utility::getFactoryByType<Server::Configuration::NamedHttpFilterConfigFactory>(
config_source.default_config());
if (default_factory == nullptr) {
throw EnvoyException(fmt::format("Error: cannot find filter factory {} for default filter "
"configuration with type URL {}.",
filter_config_name,
config_source.default_config().type_url()));
}
provider->validateTypeUrl(Config::Utility::getFactoryType(config_source.default_config()));
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
config_source.default_config(), factory_context.messageValidationVisitor(),
*default_factory);
Envoy::Http::FilterFactoryCb default_config =
default_factory->createFilterFactoryFromProto(*message, stat_prefix, factory_context);
provider->onConfigUpdate(default_config, "", nullptr);
}
return provider;
}

Expand Down
34 changes: 19 additions & 15 deletions source/common/filter/http/filter_config_discovery_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ using FilterConfigSubscriptionSharedPtr = std::shared_ptr<FilterConfigSubscripti
**/
class DynamicFilterConfigProviderImpl : public FilterConfigProvider {
public:
DynamicFilterConfigProviderImpl(FilterConfigSubscriptionSharedPtr&& subscription,
const std::set<std::string>& require_type_urls,
DynamicFilterConfigProviderImpl(FilterConfigSubscriptionSharedPtr& subscription,
const absl::flat_hash_set<std::string>& require_type_urls,
Server::Configuration::FactoryContext& factory_context);
~DynamicFilterConfigProviderImpl() override;

void validateTypeUrl(const std::string& type_url) const;

// Config::ExtensionConfigProvider
const std::string& name() override;
absl::optional<Envoy::Http::FilterFactoryCb> config() override;
void validateConfig(const ProtobufWkt::Any& proto_config,
Server::Configuration::NamedHttpFilterConfigFactory&) override;
void onConfigUpdate(Envoy::Http::FilterFactoryCb config, const std::string&,
Config::ConfigAppliedCb cb) override;

Expand All @@ -51,7 +51,7 @@ class DynamicFilterConfigProviderImpl : public FilterConfigProvider {
};

FilterConfigSubscriptionSharedPtr subscription_;
const std::set<std::string> require_type_urls_;
const absl::flat_hash_set<std::string> require_type_urls_;
// Currently applied configuration to ensure that the main thread deletes the last reference to
// it.
absl::optional<Envoy::Http::FilterFactoryCb> current_config_{absl::nullopt};
Expand All @@ -69,7 +69,8 @@ class DynamicFilterConfigProviderImpl : public FilterConfigProvider {
*/
#define ALL_EXTENSION_CONFIG_DISCOVERY_STATS(COUNTER) \
COUNTER(config_reload) \
COUNTER(config_fail)
COUNTER(config_fail) \
COUNTER(config_conflict)

/**
* Struct definition for all extension config discovery stats. @see stats_macros.h
Expand Down Expand Up @@ -98,6 +99,10 @@ class FilterConfigSubscription

const Init::SharedTargetImpl& initTarget() { return init_target_; }
const std::string& name() { return filter_config_name_; }
const absl::optional<Envoy::Http::FilterFactoryCb>& lastConfig() { return last_config_; }
const std::string& lastTypeUrl() { return last_type_url_; }
const std::string& lastVersionInfo() { return last_version_info_; }
void incrementConflictCounter();

private:
void start();
Expand All @@ -113,6 +118,9 @@ class FilterConfigSubscription

const std::string filter_config_name_;
uint64_t last_config_hash_{0ul};
absl::optional<Envoy::Http::FilterFactoryCb> last_config_{absl::nullopt};
std::string last_type_url_;
std::string last_version_info_;
Server::Configuration::FactoryContext& factory_context_;
ProtobufMessage::ValidationVisitor& validator_;

Expand Down Expand Up @@ -146,10 +154,6 @@ class StaticFilterConfigProviderImpl : public FilterConfigProvider {
// Config::ExtensionConfigProvider
const std::string& name() override { return filter_config_name_; }
absl::optional<Envoy::Http::FilterFactoryCb> config() override { return config_; }
void validateConfig(const ProtobufWkt::Any&,
Server::Configuration::NamedHttpFilterConfigFactory&) override {
NOT_REACHED_GCOVR_EXCL_LINE;
}
void onConfigUpdate(Envoy::Http::FilterFactoryCb, const std::string&,
Config::ConfigAppliedCb) override {
NOT_REACHED_GCOVR_EXCL_LINE;
Expand All @@ -164,13 +168,13 @@ class StaticFilterConfigProviderImpl : public FilterConfigProvider {
* An implementation of FilterConfigProviderManager.
*/
class FilterConfigProviderManagerImpl : public FilterConfigProviderManager,
public Singleton::Instance {
public Singleton::Instance,
Logger::Loggable<Logger::Id::filter> {
public:
FilterConfigProviderPtr createDynamicFilterConfigProvider(
const envoy::config::core::v3::ConfigSource& config_source,
const std::string& filter_config_name, const std::set<std::string>& require_type_urls,
Server::Configuration::FactoryContext& factory_context, const std::string& stat_prefix,
bool apply_without_warming) override;
const envoy::config::core::v3::ExtensionConfigSource& config_source,
const std::string& filter_config_name, Server::Configuration::FactoryContext& factory_context,
const std::string& stat_prefix) override;

FilterConfigProviderPtr
createStaticFilterConfigProvider(const Envoy::Http::FilterFactoryCb& config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -535,10 +535,8 @@ void HttpConnectionManagerConfig::processDynamicFilterConfig(
throw EnvoyException(fmt::format(
"Error: filter config {} applied without warming but has no default config.", name));
}
std::set<std::string> require_type_urls;
for (const auto& type_url : config_discovery.type_urls()) {
auto factory_type_url = TypeUtil::typeUrlToDescriptorFullName(type_url);
require_type_urls.emplace(factory_type_url);
auto* factory = Registry::FactoryRegistry<
Server::Configuration::NamedHttpFilterConfigFactory>::getFactoryByType(factory_type_url);
if (factory == nullptr) {
Expand All @@ -550,24 +548,7 @@ void HttpConnectionManagerConfig::processDynamicFilterConfig(
last_filter_in_current_config);
}
auto filter_config_provider = filter_config_provider_manager_.createDynamicFilterConfigProvider(
config_discovery.config_source(), name, require_type_urls, context_, stats_prefix_,
config_discovery.apply_default_config_without_warming());
if (config_discovery.has_default_config()) {
auto* default_factory =
Config::Utility::getFactoryByType<Server::Configuration::NamedHttpFilterConfigFactory>(
config_discovery.default_config());
if (default_factory == nullptr) {
throw EnvoyException(fmt::format("Error: cannot find filter factory {} for default filter "
"configuration with type URL {}.",
name, config_discovery.default_config().type_url()));
}
filter_config_provider->validateConfig(config_discovery.default_config(), *default_factory);
ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig(
config_discovery.default_config(), context_.messageValidationVisitor(), *default_factory);
Http::FilterFactoryCb default_config =
default_factory->createFilterFactoryFromProto(*message, stats_prefix_, context_);
filter_config_provider->onConfigUpdate(default_config, "", nullptr);
}
config_discovery, name, context_, stats_prefix_);
filter_factories.push_back(std::move(filter_config_provider));
}

Expand Down
22 changes: 17 additions & 5 deletions test/common/filter/http/filter_config_discovery_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,23 @@ class FilterConfigDiscoveryImplTest : public FilterConfigDiscoveryTestBase {

FilterConfigProviderPtr createProvider(std::string name, bool warm) {
EXPECT_CALL(init_manager_, add(_));
envoy::config::core::v3::ConfigSource config_source;
TestUtility::loadFromYaml("ads: {}", config_source);
envoy::config::core::v3::ExtensionConfigSource config_source;
TestUtility::loadFromYaml(R"EOF(
config_source: { ads: {} }
type_urls:
- envoy.extensions.filters.http.router.v3.Router
)EOF",
config_source);
if (!warm) {
config_source.set_apply_default_config_without_warming(true);
TestUtility::loadFromYaml(R"EOF(
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
)EOF",
*config_source.mutable_default_config());
}

return filter_config_provider_manager_->createDynamicFilterConfigProvider(
config_source, name, {"envoy.extensions.filters.http.router.v3.Router"}, factory_context_,
"xds.", !warm);
config_source, name, factory_context_, "xds.");
}

void setup(bool warm = true) {
Expand Down Expand Up @@ -238,7 +250,7 @@ TEST_F(FilterConfigDiscoveryImplTest, ApplyWithoutWarming) {
InSequence s;
setup(false);
EXPECT_EQ("foo", provider_->name());
EXPECT_EQ(absl::nullopt, provider_->config());
EXPECT_NE(absl::nullopt, provider_->config());
EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_reload").value());
EXPECT_EQ(0UL, scope_.counter("xds.extension_config_discovery.foo.config_fail").value());
}
Expand Down
Loading