diff --git a/envoy_build_config/extensions_build_config.bzl b/envoy_build_config/extensions_build_config.bzl index 325a6bf335..b061112096 100644 --- a/envoy_build_config/extensions_build_config.bzl +++ b/envoy_build_config/extensions_build_config.bzl @@ -15,6 +15,7 @@ EXTENSIONS = { "envoy.filters.http.router": "//source/extensions/filters/http/router:config", "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", "envoy.http.original_ip_detection.xff": "//source/extensions/http/original_ip_detection/xff:config", + "envoy.key_value.platform": "@envoy_mobile//library/common/extensions/key_value/platform:config", "envoy.network.dns_resolver.apple": "//source/extensions/network/dns_resolver/apple:config", "envoy.retry.options.network_configuration": "@envoy_mobile//library/common/extensions/retry/options/network_configuration:config", "envoy.stat_sinks.metrics_service": "//source/extensions/stat_sinks/metrics_service:config", diff --git a/library/common/extensions/key_value/platform/BUILD b/library/common/extensions/key_value/platform/BUILD new file mode 100644 index 0000000000..4ffc2b6a74 --- /dev/null +++ b/library/common/extensions/key_value/platform/BUILD @@ -0,0 +1,31 @@ +load( + "@envoy//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_extension_package", + "envoy_proto_library", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_proto_library( + name = "platform", + srcs = ["platform.proto"], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + repository = "@envoy", + deps = [ + ":platform_cc_proto", + "@envoy//envoy/common:key_value_store_interface", + "@envoy//envoy/event:dispatcher_interface", + "@envoy//envoy/filesystem:filesystem_interface", + "@envoy//envoy/registry", + "@envoy//source/common/common:key_value_store_lib", + "@envoy_api//envoy/config/common/key_value/v3:pkg_cc_proto", + ], +) diff --git a/library/common/extensions/key_value/platform/config.cc b/library/common/extensions/key_value/platform/config.cc new file mode 100644 index 0000000000..2756c93380 --- /dev/null +++ b/library/common/extensions/key_value/platform/config.cc @@ -0,0 +1,52 @@ +#include "library/common/extensions/key_value/platform/config.h" + +#include "envoy/config/common/key_value/v3/config.pb.h" +#include "envoy/config/common/key_value/v3/config.pb.validate.h" +#include "envoy/registry/registry.h" + +namespace Envoy { +namespace Extensions { +namespace KeyValue { + +PlatformKeyValueStore::PlatformKeyValueStore(Event::Dispatcher& dispatcher, + std::chrono::milliseconds save_interval, + PlatformInterface& platform_interface, + const std::string& key) + : KeyValueStoreBase(dispatcher, save_interval), platform_interface_(platform_interface), + key_(key) { + const std::string contents = platform_interface.read(key); + if (!parseContents(contents, store_)) { + ENVOY_LOG(warn, "Failed to parse key value store contents {}", key); + } +} + +void PlatformKeyValueStore::flush() { + std::string output; + for (const auto& it : store_) { + absl::StrAppend(&output, it.first.length(), "\n", it.first, it.second.length(), "\n", + it.second); + } + platform_interface_.save(key_, output); +} + +KeyValueStorePtr +PlatformKeyValueStoreFactory::createStore(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor, + Event::Dispatcher& dispatcher, Filesystem::Instance&) { + const auto& typed_config = MessageUtil::downcastAndValidate< + const ::envoy::config::common::key_value::v3::KeyValueStoreConfig&>(config, + validation_visitor); + const auto file_config = MessageUtil::anyConvertAndValidate< + envoymobile::extensions::key_value::platform::PlatformKeyValueStoreConfig>( + typed_config.config().typed_config(), validation_visitor); + auto milliseconds = + std::chrono::milliseconds(DurationUtil::durationToMilliseconds(file_config.save_interval())); + return std::make_unique( + dispatcher, milliseconds, platform_interface_.value().get(), file_config.key()); +} + +REGISTER_FACTORY(PlatformKeyValueStoreFactory, KeyValueStoreFactory); + +} // namespace KeyValue +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/key_value/platform/config.h b/library/common/extensions/key_value/platform/config.h new file mode 100644 index 0000000000..6f2b6d03fb --- /dev/null +++ b/library/common/extensions/key_value/platform/config.h @@ -0,0 +1,64 @@ +#include "envoy/common/key_value_store.h" + +#include "source/common/common/key_value_store_base.h" + +#include "library/common/extensions/key_value/platform/platform.pb.h" +#include "library/common/extensions/key_value/platform/platform.pb.validate.h" + +namespace Envoy { +namespace Extensions { +namespace KeyValue { + +class PlatformInterface { +public: + virtual ~PlatformInterface() {} + // Save the contents to the key provided. This may be done asynchronously. + virtual void save(const std::string& key, const std::string& contents) PURE; + // Read the contents of the key provided. + virtual std::string read(const std::string& key) const PURE; +}; + +// A platform file based key value store, which reads from and saves from based on +// a key. An example implementation would be flushing to the android prefs file. +// +// All keys and values are flushed to a single entry as +// [length]\n[key][length]\n[value] +class PlatformKeyValueStore : public KeyValueStoreBase { +public: + PlatformKeyValueStore(Event::Dispatcher& dispatcher, std::chrono::milliseconds save_interval, + PlatformInterface& platform_interface, const std::string& key); + // KeyValueStore + void flush() override; + +private: + PlatformInterface& platform_interface_; + const std::string key_; +}; + +class PlatformKeyValueStoreFactory : public KeyValueStoreFactory { +public: + PlatformKeyValueStoreFactory() {} + PlatformKeyValueStoreFactory(PlatformInterface& platform_interface) + : platform_interface_(platform_interface) {} + + // KeyValueStoreFactory + KeyValueStorePtr createStore(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor, + Event::Dispatcher& dispatcher, + Filesystem::Instance& file_system) override; + + // TypedFactory + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return ProtobufTypes::MessagePtr{ + new envoymobile::extensions::key_value::platform::PlatformKeyValueStoreConfig()}; + } + + std::string name() const override { return "envoy.key_value.platform"; } + // TODO(alyssawilk, goaway) the default PlatformInterface should do up calls through Java and this + // can be moved to a non-optional reference. + OptRef platform_interface_; +}; + +} // namespace KeyValue +} // namespace Extensions +} // namespace Envoy diff --git a/library/common/extensions/key_value/platform/platform.proto b/library/common/extensions/key_value/platform/platform.proto new file mode 100644 index 0000000000..3a73ca1594 --- /dev/null +++ b/library/common/extensions/key_value/platform/platform.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package envoymobile.extensions.key_value.platform; + +import "google/protobuf/duration.proto"; + +import "validate/validate.proto"; + +message PlatformKeyValueStoreConfig { + // The key to save the contents to. + string key = 1 [(validate.rules).string = {min_len: 1}]; + + // The interval at which the key value store should be saved. + google.protobuf.Duration save_interval = 2; +} diff --git a/test/common/extensions/key_value/platform/BUILD b/test/common/extensions/key_value/platform/BUILD new file mode 100644 index 0000000000..830f173196 --- /dev/null +++ b/test/common/extensions/key_value/platform/BUILD @@ -0,0 +1,22 @@ +load("@envoy//bazel:envoy_build_system.bzl", "envoy_package") +load( + "@envoy//test/extensions:extensions_build_system.bzl", + "envoy_extension_cc_test", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_extension_cc_test( + name = "platform_store_test", + srcs = ["platform_store_test.cc"], + extension_names = ["envoy.key_value.platform"], + repository = "@envoy", + deps = [ + "//library/common/extensions/key_value/platform:config", + "@envoy//source/common/common:key_value_store_lib", + "@envoy//test/mocks/event:event_mocks", + "@envoy//test/test_common:file_system_for_test_lib", + ], +) diff --git a/test/common/extensions/key_value/platform/platform_store_test.cc b/test/common/extensions/key_value/platform/platform_store_test.cc new file mode 100644 index 0000000000..7ef606eaed --- /dev/null +++ b/test/common/extensions/key_value/platform/platform_store_test.cc @@ -0,0 +1,120 @@ +#include +#include + +#include "test/mocks/event/mocks.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "library/common/extensions/key_value/platform/config.h" +#include "library/common/extensions/key_value/platform/platform.pb.h" + +using testing::NiceMock; + +namespace Envoy { +namespace Extensions { +namespace KeyValue { +namespace { + +class TestPlatformInterface : public PlatformInterface { + virtual void save(const std::string& key, const std::string& contents) { + store_.erase(key); + store_.emplace(key, contents); + } + virtual std::string read(const std::string& key) const { + auto it = store_.find(key); + if (it == store_.end()) { + return ""; + } + return it->second; + } + + absl::flat_hash_map store_; +}; + +class PlatformStoreTest : public testing::Test { +protected: + PlatformStoreTest() { createStore(); } + + void createStore() { + flush_timer_ = new NiceMock(&dispatcher_); + store_ = + std::make_unique(dispatcher_, save_interval_, mock_platform_, key_); + } + NiceMock dispatcher_; + std::string key_{"key"}; + std::unique_ptr store_{}; + std::chrono::seconds save_interval_{5}; + Event::MockTimer* flush_timer_ = nullptr; + TestPlatformInterface mock_platform_; +}; + +TEST_F(PlatformStoreTest, Basic) { + EXPECT_EQ(absl::nullopt, store_->get("foo")); + store_->addOrUpdate("foo", "bar"); + EXPECT_EQ("bar", store_->get("foo").value()); + store_->addOrUpdate("foo", "eep"); + EXPECT_EQ("eep", store_->get("foo").value()); + store_->remove("foo"); + EXPECT_EQ(absl::nullopt, store_->get("foo")); +} + +TEST_F(PlatformStoreTest, Persist) { + store_->addOrUpdate("foo", "bar"); + store_->addOrUpdate("by\nz", "ee\np"); + ASSERT_TRUE(flush_timer_->enabled_); + flush_timer_->invokeCallback(); // flush + EXPECT_TRUE(flush_timer_->enabled_); + // Not flushed as 5ms didn't pass. + store_->addOrUpdate("baz", "eep"); + + save_interval_ = std::chrono::seconds(0); + createStore(); + KeyValueStore::ConstIterateCb validate = [](const std::string& key, const std::string&) { + EXPECT_TRUE(key == "foo" || key == "by\nz"); + return KeyValueStore::Iterate::Continue; + }; + + EXPECT_EQ("bar", store_->get("foo").value()); + EXPECT_EQ("ee\np", store_->get("by\nz").value()); + EXPECT_FALSE(store_->get("baz").has_value()); + store_->iterate(validate); + + // This will flush due to 0ms flush interval + store_->addOrUpdate("baz", "eep"); + createStore(); + EXPECT_TRUE(store_->get("baz").has_value()); + + // This will flush due to 0ms flush interval + store_->remove("bar"); + createStore(); + EXPECT_FALSE(store_->get("bar").has_value()); +} + +TEST_F(PlatformStoreTest, Iterate) { + store_->addOrUpdate("foo", "bar"); + store_->addOrUpdate("baz", "eep"); + + int full_counter = 0; + KeyValueStore::ConstIterateCb validate = [&full_counter](const std::string& key, + const std::string&) { + ++full_counter; + EXPECT_TRUE(key == "foo" || key == "baz"); + return KeyValueStore::Iterate::Continue; + }; + store_->iterate(validate); + EXPECT_EQ(2, full_counter); + + int stop_early_counter = 0; + KeyValueStore::ConstIterateCb stop_early = [&stop_early_counter](const std::string&, + const std::string&) { + ++stop_early_counter; + return KeyValueStore::Iterate::Break; + }; + store_->iterate(stop_early); + EXPECT_EQ(1, stop_early_counter); +} + +} // namespace +} // namespace KeyValue +} // namespace Extensions +} // namespace Envoy