diff --git a/include/envoy/runtime/runtime.h b/include/envoy/runtime/runtime.h index 9fc144244419..c85d7033a740 100644 --- a/include/envoy/runtime/runtime.h +++ b/include/envoy/runtime/runtime.h @@ -55,6 +55,25 @@ class Snapshot { absl::optional uint_value_; }; + /** + * A provider of runtime values. One or more of these compose the snapshot's source of values, + * where successive layers override the previous ones. + */ + class OverrideLayer { + public: + virtual ~OverrideLayer() {} + /** + * @return const std::unordered_map& the values in this layer. + */ + virtual const std::unordered_map& values() const PURE; + /** + * @return const std::string& a user-friendly alias for this layer, e.g. "admin" or "disk". + */ + virtual const std::string& name() const PURE; + }; + + typedef std::unique_ptr OverrideLayerConstPtr; + /** * Test if a feature is enabled using the built in random generator. This is done by generating * a random number in the range 0-99 and seeing if this number is < the value stored in the @@ -116,10 +135,13 @@ class Snapshot { virtual uint64_t getInteger(const std::string& key, uint64_t default_value) const PURE; /** - * Fetch the raw runtime entries map. The map data is safe only for the lifetime of the Snapshot. - * @return const std::unordered_map& the raw map of loaded values. + * Fetch the OverrideLayers that provide values in this snapshot. Layers are ordered from bottom + * to top; for instance, the second layer's entries override the first layer's entries, and so on. + * Any layer can add a key in addition to overriding keys in layers below. The layer vector is + * safe only for the lifetime of the Snapshot. + * @return const std::vector& the raw map of loaded values. */ - virtual const std::unordered_map& getAll() const PURE; + virtual const std::vector& getLayers() const PURE; }; /** @@ -135,6 +157,13 @@ class Loader { * fetched again when needed. */ virtual Snapshot& snapshot() PURE; + + /** + * Merge the given map of key-value pairs into the runtime's state. To remove a previous merge for + * a key, use an empty string as the value. + * @param values the values to merge + */ + virtual void mergeValues(const std::unordered_map& values) PURE; }; typedef std::unique_ptr LoaderPtr; diff --git a/source/common/runtime/runtime_impl.cc b/source/common/runtime/runtime_impl.cc index d6700e59b02d..ddd5d470e3c1 100644 --- a/source/common/runtime/runtime_impl.cc +++ b/source/common/runtime/runtime_impl.cc @@ -146,26 +146,26 @@ std::string RandomGeneratorImpl::uuid() { return std::string(uuid, UUID_LENGTH); } -SnapshotImpl::SnapshotImpl(const std::string& root_path, const std::string& override_path, - RuntimeStats& stats, RandomGenerator& generator, - Api::OsSysCalls& os_sys_calls) - : generator_(generator), os_sys_calls_(os_sys_calls) { - try { - walkDirectory(root_path, ""); - if (Filesystem::directoryExists(override_path)) { - walkDirectory(override_path, ""); - stats.override_dir_exists_.inc(); - } else { - stats.override_dir_not_exists_.inc(); - } +bool SnapshotImpl::featureEnabled(const std::string& key, uint64_t default_value, + uint64_t random_value, uint64_t num_buckets) const { + return random_value % num_buckets < std::min(getInteger(key, default_value), num_buckets); +} - stats.load_success_.inc(); - } catch (EnvoyException& e) { - stats.load_error_.inc(); - ENVOY_LOG(debug, "error creating runtime snapshot: {}", e.what()); +bool SnapshotImpl::featureEnabled(const std::string& key, uint64_t default_value) const { + // Avoid PNRG if we know we don't need it. + uint64_t cutoff = std::min(getInteger(key, default_value), static_cast(100)); + if (cutoff == 0) { + return false; + } else if (cutoff == 100) { + return true; + } else { + return generator_.random() % 100 < cutoff; } +} - stats.num_keys_.set(values_.size()); +bool SnapshotImpl::featureEnabled(const std::string& key, uint64_t default_value, + uint64_t random_value) const { + return featureEnabled(key, default_value, random_value, 100); } const std::string& SnapshotImpl::get(const std::string& key) const { @@ -186,11 +186,50 @@ uint64_t SnapshotImpl::getInteger(const std::string& key, uint64_t default_value } } -const std::unordered_map& SnapshotImpl::getAll() const { - return values_; +const std::vector& SnapshotImpl::getLayers() const { + return layers_; } -void SnapshotImpl::walkDirectory(const std::string& path, const std::string& prefix) { +SnapshotImpl::SnapshotImpl(RandomGenerator& generator, RuntimeStats& stats, + std::vector&& layers) + : layers_{std::move(layers)}, generator_{generator} { + for (const auto& layer : layers_) { + for (const auto& kv : layer->values()) { + values_.erase(kv.first); + values_.emplace(kv.first, kv.second); + } + } + stats.num_keys_.set(values_.size()); +} + +Snapshot::Entry SnapshotImpl::createEntry(const std::string& value) { + Entry entry{value, absl::nullopt}; + // As a perf optimization, attempt to convert the entry's string into an integer. If we don't + // succeed that's fine. + uint64_t converted; + if (StringUtil::atoul(entry.string_value_.c_str(), converted)) { + entry.uint_value_ = converted; + } + return entry; +} + +void AdminLayer::mergeValues(const std::unordered_map& values) { + for (const auto& kv : values) { + values_.erase(kv.first); + if (!kv.second.empty()) { + values_.emplace(kv.first, SnapshotImpl::createEntry(kv.second)); + } + } + stats_.admin_overrides_active_.set(values_.empty() ? 0 : 1); +} + +DiskLayer::DiskLayer(const std::string& name, const std::string& path, + Api::OsSysCalls& os_sys_calls) + : OverrideLayerImpl{name}, os_sys_calls_(os_sys_calls) { + walkDirectory(path, ""); +} + +void DiskLayer::walkDirectory(const std::string& path, const std::string& prefix) { ENVOY_LOG(debug, "walking directory: {}", path); Directory current_dir(path); while (true) { @@ -226,7 +265,7 @@ void SnapshotImpl::walkDirectory(const std::string& path, const std::string& pre // for small files. Also, as noted elsewhere, none of this is non-blocking which could // theoretically lead to issues. ENVOY_LOG(debug, "reading file: {}", full_path); - Entry entry; + std::string value; // Read the file and remove any comments. A comment is a line starting with a '#' character. // Comments are useful for placeholder files with no value. @@ -238,39 +277,65 @@ void SnapshotImpl::walkDirectory(const std::string& path, const std::string& pre } if (line == lines.back()) { const absl::string_view trimmed = StringUtil::rtrim(line); - entry.string_value_.append(trimmed.data(), trimmed.size()); + value.append(trimmed.data(), trimmed.size()); } else { - entry.string_value_.append(std::string{line} + "\n"); + value.append(std::string{line} + "\n"); } } - - // As a perf optimization, attempt to convert the string into an integer. If we don't - // succeed that's fine. - uint64_t converted; - if (StringUtil::atoul(entry.string_value_.c_str(), converted)) { - entry.uint_value_ = converted; - } - // Separate erase/insert calls required due to the value type being constant; this prevents // the use of the [] operator. Can leverage insert_or_assign in C++17 in the future. values_.erase(full_prefix); - values_.insert({full_prefix, entry}); + values_.insert({full_prefix, SnapshotImpl::createEntry(value)}); } } } -LoaderImpl::LoaderImpl(Event::Dispatcher& dispatcher, ThreadLocal::SlotAllocator& tls, - const std::string& root_symlink_path, const std::string& subdir, - const std::string& override_dir, Stats::Store& store, - RandomGenerator& generator, Api::OsSysCallsPtr os_sys_calls) - : watcher_(dispatcher.createFilesystemWatcher()), tls_(tls.allocateSlot()), - generator_(generator), root_path_(root_symlink_path + "/" + subdir), - override_path_(root_symlink_path + "/" + override_dir), stats_(generateStats(store)), +LoaderImpl::LoaderImpl(RandomGenerator& generator, Stats::Store& store, + ThreadLocal::SlotAllocator& tls) + : LoaderImpl(DoNotLoadSnapshot{}, generator, store, tls) { + loadNewSnapshot(); +} + +LoaderImpl::LoaderImpl(DoNotLoadSnapshot /* unused */, RandomGenerator& generator, + Stats::Store& store, ThreadLocal::SlotAllocator& tls) + : generator_(generator), stats_(generateStats(store)), admin_layer_(stats_), + tls_(tls.allocateSlot()) {} + +std::unique_ptr LoaderImpl::createNewSnapshot() { + std::vector layers; + layers.emplace_back(std::make_unique(admin_layer_)); + return std::make_unique(generator_, stats_, std::move(layers)); +} + +void LoaderImpl::loadNewSnapshot() { + ThreadLocal::ThreadLocalObjectSharedPtr ptr = createNewSnapshot(); + tls_->set([ptr = std::move(ptr)](Event::Dispatcher&)->ThreadLocal::ThreadLocalObjectSharedPtr { + return ptr; + }); +} + +Snapshot& LoaderImpl::snapshot() { return tls_->getTyped(); } + +void LoaderImpl::mergeValues(const std::unordered_map& values) { + admin_layer_.mergeValues(values); + loadNewSnapshot(); +} + +DiskBackedLoaderImpl::DiskBackedLoaderImpl(Event::Dispatcher& dispatcher, + ThreadLocal::SlotAllocator& tls, + const std::string& root_symlink_path, + const std::string& subdir, + const std::string& override_dir, Stats::Store& store, + RandomGenerator& generator, + Api::OsSysCallsPtr os_sys_calls) + : LoaderImpl(DoNotLoadSnapshot{}, generator, store, tls), + watcher_(dispatcher.createFilesystemWatcher()), root_path_(root_symlink_path + "/" + subdir), + override_path_(root_symlink_path + "/" + override_dir), os_sys_calls_(std::move(os_sys_calls)) { watcher_->addWatch(root_symlink_path, Filesystem::Watcher::Events::MovedTo, - [this](uint32_t) -> void { onSymlinkSwap(); }); + [this](uint32_t) -> void { loadNewSnapshot(); }); - onSymlinkSwap(); + loadNewSnapshot(); } RuntimeStats LoaderImpl::generateStats(Stats::Store& store) { @@ -280,16 +345,24 @@ RuntimeStats LoaderImpl::generateStats(Stats::Store& store) { return stats; } -void LoaderImpl::onSymlinkSwap() { - current_snapshot_.reset( - new SnapshotImpl(root_path_, override_path_, stats_, generator_, *os_sys_calls_)); - ThreadLocal::ThreadLocalObjectSharedPtr ptr_copy = current_snapshot_; - tls_->set([ptr_copy](Event::Dispatcher&) -> ThreadLocal::ThreadLocalObjectSharedPtr { - return ptr_copy; - }); +std::unique_ptr DiskBackedLoaderImpl::createNewSnapshot() { + std::vector layers; + try { + layers.push_back(std::make_unique("root", root_path_, *os_sys_calls_)); + if (Filesystem::directoryExists(override_path_)) { + layers.push_back(std::make_unique("override", override_path_, *os_sys_calls_)); + stats_.override_dir_exists_.inc(); + } else { + stats_.override_dir_not_exists_.inc(); + } + } catch (EnvoyException& e) { + layers.clear(); + stats_.load_error_.inc(); + ENVOY_LOG(debug, "error loading runtime values from disk: {}", e.what()); + } + layers.push_back(std::make_unique(admin_layer_)); + return std::make_unique(generator_, stats_, std::move(layers)); } -Snapshot& LoaderImpl::snapshot() { return tls_->getTyped(); } - } // namespace Runtime } // namespace Envoy diff --git a/source/common/runtime/runtime_impl.h b/source/common/runtime/runtime_impl.h index e08605f9edf6..c04e29981115 100644 --- a/source/common/runtime/runtime_impl.h +++ b/source/common/runtime/runtime_impl.h @@ -44,7 +44,8 @@ class RandomGeneratorImpl : public RandomGenerator { COUNTER(override_dir_not_exists) \ COUNTER(override_dir_exists) \ COUNTER(load_success) \ - GAUGE (num_keys) + GAUGE (num_keys) \ + GAUGE (admin_overrides_active) // clang-format on /** @@ -55,41 +56,78 @@ struct RuntimeStats { }; /** - * Implementation of Snapshot that reads from disk. + * Implementation of Snapshot whose source is the vector of layers passed to the constructor. */ -class SnapshotImpl : public Snapshot, - public ThreadLocal::ThreadLocalObject, - Logger::Loggable { +class SnapshotImpl : public Snapshot, public ThreadLocal::ThreadLocalObject { public: - SnapshotImpl(const std::string& root_path, const std::string& override_path, RuntimeStats& stats, - RandomGenerator& generator, Api::OsSysCalls& os_sys_calls); + SnapshotImpl(RandomGenerator& generator, RuntimeStats& stats, + std::vector&& layers); // Runtime::Snapshot bool featureEnabled(const std::string& key, uint64_t default_value, uint64_t random_value, - uint64_t num_buckets) const override { - return random_value % num_buckets < std::min(getInteger(key, default_value), num_buckets); - } + uint64_t num_buckets) const override; + bool featureEnabled(const std::string& key, uint64_t default_value) const override; + bool featureEnabled(const std::string& key, uint64_t default_value, + uint64_t random_value) const override; + const std::string& get(const std::string& key) const override; + uint64_t getInteger(const std::string& key, uint64_t default_value) const override; + const std::vector& getLayers() const override; - bool featureEnabled(const std::string& key, uint64_t default_value) const override { - // Avoid PNRG if we know we don't need it. - uint64_t cutoff = std::min(getInteger(key, default_value), static_cast(100)); - if (cutoff == 0) { - return false; - } else if (cutoff == 100) { - return true; - } else { - return generator_.random() % 100 < cutoff; - } + static Entry createEntry(const std::string& value); + +private: + const std::vector layers_; + std::unordered_map values_; + RandomGenerator& generator_; +}; + +/** + * Base implementation of OverrideLayer that by itself provides an empty values map. + */ +class OverrideLayerImpl : public Snapshot::OverrideLayer { +public: + explicit OverrideLayerImpl(const std::string& name) : name_{name} {} + const std::unordered_map& values() const override { + return values_; } + const std::string& name() const override { return name_; } - bool featureEnabled(const std::string& key, uint64_t default_value, - uint64_t random_value) const override { - return featureEnabled(key, default_value, random_value, 100); +protected: + std::unordered_map values_; + const std::string name_; +}; + +/** + * Extension of OverrideLayerImpl that maintains an in-memory set of values. These values can be + * modified programmatically via mergeValues(). AdminLayer is so named because it can be accessed + * and manipulated by Envoy's admin interface. + */ +class AdminLayer : public OverrideLayerImpl { +public: + explicit AdminLayer(RuntimeStats& stats) : OverrideLayerImpl{"admin"}, stats_{stats} {} + /** + * Copy-constructible so that it can snapshotted. + */ + AdminLayer(const AdminLayer& admin_layer) : AdminLayer{admin_layer.stats_} { + values_ = admin_layer.values(); } - const std::string& get(const std::string& key) const override; - uint64_t getInteger(const std::string&, uint64_t default_value) const override; - const std::unordered_map& getAll() const override; + /** + * Merge the provided values into our entry map. An empty value indicates that a key should be + * removed from our map. + */ + void mergeValues(const std::unordered_map& values); + +private: + RuntimeStats& stats_; +}; + +/** + * Extension of OverrideLayerImpl that loads values from the file system upon construction. + */ +class DiskLayer : public OverrideLayerImpl, Logger::Loggable { +public: + DiskLayer(const std::string& name, const std::string& path, Api::OsSysCalls& os_sys_calls); private: struct Directory { @@ -107,91 +145,65 @@ class SnapshotImpl : public Snapshot, void walkDirectory(const std::string& path, const std::string& prefix); - std::unordered_map values_; - RandomGenerator& generator_; + const std::string path_; Api::OsSysCalls& os_sys_calls_; }; /** - * Implementation of Loader that watches a symlink for swapping and loads a specified subdirectory - * from disk. A single snapshot is shared among all threads and referenced by shared_ptr such that + * Implementation of Loader that provides Snapshots of values added via mergeValues(). + * A single snapshot is shared among all threads and referenced by shared_ptr such that * a new runtime can be swapped in by the main thread while workers are still using the previous * version. */ class LoaderImpl : public Loader { public: - LoaderImpl(Event::Dispatcher& dispatcher, ThreadLocal::SlotAllocator& tls, - const std::string& root_symlink_path, const std::string& subdir, - const std::string& override_dir, Stats::Store& store, RandomGenerator& generator, - Api::OsSysCallsPtr os_sys_calls); + LoaderImpl(RandomGenerator& generator, Stats::Store& stats, ThreadLocal::SlotAllocator& tls); // Runtime::Loader Snapshot& snapshot() override; + void mergeValues(const std::unordered_map& values) override; + +protected: + // Identical the the public constructor but does not call loadSnapshot(). Subclasses must call + // loadSnapshot() themselves to create the initial snapshot, since loadSnapshot calls the virtual + // function createNewSnapshot() and is therefore unsuitable for use in a superclass constructor. + struct DoNotLoadSnapshot {}; + LoaderImpl(DoNotLoadSnapshot /* unused */, RandomGenerator& generator, Stats::Store& stats, + ThreadLocal::SlotAllocator& tls); + + // Create a new Snapshot + virtual std::unique_ptr createNewSnapshot(); + // Load a new Snapshot into TLS + void loadNewSnapshot(); + + RandomGenerator& generator_; + RuntimeStats stats_; + AdminLayer admin_layer_; private: RuntimeStats generateStats(Stats::Store& store); - void onSymlinkSwap(); - Filesystem::WatcherPtr watcher_; ThreadLocal::SlotPtr tls_; - RandomGenerator& generator_; - std::string root_path_; - std::string override_path_; - std::shared_ptr current_snapshot_; - RuntimeStats stats_; - Api::OsSysCallsPtr os_sys_calls_; }; /** - * Null implementation of runtime if another runtime source is not configured. + * Extension of LoaderImpl that watches a symlink for swapping and loads a specified subdirectory + * from disk. Values added via mergeValues() are secondary to those loaded from disk. */ -class NullLoaderImpl : public Loader { +class DiskBackedLoaderImpl : public LoaderImpl, Logger::Loggable { public: - NullLoaderImpl(RandomGenerator& generator) : snapshot_(generator) {} - - // Runtime::Loader - Snapshot& snapshot() override { return snapshot_; } + DiskBackedLoaderImpl(Event::Dispatcher& dispatcher, ThreadLocal::SlotAllocator& tls, + const std::string& root_symlink_path, const std::string& subdir, + const std::string& override_dir, Stats::Store& store, + RandomGenerator& generator, Api::OsSysCallsPtr os_sys_calls); private: - struct NullSnapshotImpl : public Snapshot { - NullSnapshotImpl(RandomGenerator& generator) : generator_(generator) {} - - // Runtime::Snapshot - bool featureEnabled(const std::string&, uint64_t default_value, uint64_t random_value, - uint64_t num_buckets) const override { - return random_value % num_buckets < std::min(default_value, num_buckets); - } - - bool featureEnabled(const std::string& key, uint64_t default_value) const override { - if (default_value == 0) { - return false; - } else if (default_value == 100) { - return true; - } else { - return featureEnabled(key, default_value, generator_.random()); - } - } - - bool featureEnabled(const std::string& key, uint64_t default_value, - uint64_t random_value) const override { - return featureEnabled(key, default_value, random_value, 100); - } - - const std::string& get(const std::string&) const override { return EMPTY_STRING; } - - uint64_t getInteger(const std::string&, uint64_t default_value) const override { - return default_value; - } - - const std::unordered_map& getAll() const override { - return values_; - } - - RandomGenerator& generator_; - std::unordered_map values_; - }; + std::unique_ptr createNewSnapshot() override; - NullSnapshotImpl snapshot_; + const Filesystem::WatcherPtr watcher_; + const std::string root_path_; + const std::string override_path_; + const Api::OsSysCallsPtr os_sys_calls_; }; } // namespace Runtime diff --git a/source/server/http/admin.cc b/source/server/http/admin.cc index b71a0d9a62f8..6d70eee40ab8 100644 --- a/source/server/http/admin.cc +++ b/source/server/http/admin.cc @@ -542,46 +542,63 @@ Http::Code AdminImpl::handlerCerts(absl::string_view, Http::HeaderMap&, Http::Code AdminImpl::handlerRuntime(absl::string_view url, Http::HeaderMap& response_headers, Buffer::Instance& response) { - Http::Code rc = Http::Code::OK; const Http::Utility::QueryParams params = Http::Utility::parseQueryString(url); - const auto& entries = server_.runtime().snapshot().getAll(); - const auto pairs = sortedRuntime(entries); + response_headers.insertContentType().value().setReference( + Http::Headers::get().ContentTypeValues.Json); - if (params.size() == 0) { - for (const auto& entry : pairs) { - response.add(fmt::format("{}: {}\n", entry.first, entry.second.string_value_)); - } - } else { - if (params.begin()->first == "format" && params.begin()->second == "json") { - response_headers.insertContentType().value().setReference( - Http::Headers::get().ContentTypeValues.Json); - response.add(runtimeAsJson(pairs)); - response.add("\n"); - } else { - response.add("usage: /runtime?format=json\n"); - rc = Http::Code::BadRequest; + // TODO(jsedgwick) Use proto to structure this output instead of arbitrary JSON + rapidjson::Document document; + document.SetObject(); + auto& allocator = document.GetAllocator(); + std::map entry_objects; + rapidjson::Value layer_names{rapidjson::kArrayType}; + const auto& layers = server_.runtime().snapshot().getLayers(); + + for (const auto& layer : layers) { + rapidjson::Value layer_name; + layer_name.SetString(layer->name().c_str(), allocator); + layer_names.PushBack(std::move(layer_name), allocator); + for (const auto& kv : layer->values()) { + rapidjson::Value entry_object{rapidjson::kObjectType}; + const auto it = entry_objects.find(kv.first); + if (it == entry_objects.end()) { + rapidjson::Value entry_object{rapidjson::kObjectType}; + entry_object.AddMember("layer_values", rapidjson::Value{kArrayType}, allocator); + entry_object.AddMember("final_value", "", allocator); + entry_objects.emplace(kv.first, std::move(entry_object)); + } } } + document.AddMember("layers", std::move(layer_names), allocator); - return rc; -} + for (const auto& layer : layers) { + for (auto& kv : entry_objects) { + const auto it = layer->values().find(kv.first); + const auto& entry_value = it == layer->values().end() ? "" : it->second.string_value_; + rapidjson::Value entry_value_object; + entry_value_object.SetString(entry_value.c_str(), allocator); + if (!entry_value.empty()) { + kv.second["final_value"] = rapidjson::Value{entry_value_object, allocator}; + } + kv.second["layer_values"].PushBack(entry_value_object, allocator); + } + } -const std::vector> AdminImpl::sortedRuntime( - const std::unordered_map& entries) { - std::vector> pairs(entries.begin(), - entries.end()); + rapidjson::Value value_arrays_obj{rapidjson::kObjectType}; + for (auto& kv : entry_objects) { + value_arrays_obj.AddMember(rapidjson::StringRef(kv.first.c_str()), std::move(kv.second), + allocator); + } - std::sort(pairs.begin(), pairs.end(), - [](const std::pair& a, - const std::pair& b) -> bool { - return a.first < b.first; - }); + document.AddMember("entries", std::move(value_arrays_obj), allocator); - return pairs; + rapidjson::StringBuffer strbuf; + rapidjson::PrettyWriter writer(strbuf); + document.Accept(writer); + response.add(strbuf.GetString()); + return Http::Code::OK; } -ConfigTracker& AdminImpl::getConfigTracker() { return config_tracker_; } - std::string AdminImpl::runtimeAsJson( const std::vector>& entries) { rapidjson::Document document; @@ -611,6 +628,23 @@ std::string AdminImpl::runtimeAsJson( return strbuf.GetString(); } +Http::Code AdminImpl::handlerRuntimeModify(absl::string_view url, Http::HeaderMap&, + Buffer::Instance& response) { + const Http::Utility::QueryParams params = Http::Utility::parseQueryString(url); + if (params.empty()) { + response.add("usage: /runtime_modify?key1=value1&key2=value2&keyN=valueN\n"); + response.add("use an empty value to remove a previously added override"); + return Http::Code::BadRequest; + } + std::unordered_map overrides; + overrides.insert(params.begin(), params.end()); + server_.runtime().mergeValues(overrides); + response.add("OK\n"); + return Http::Code::OK; +} + +ConfigTracker& AdminImpl::getConfigTracker() { return config_tracker_; } + void AdminFilter::onComplete() { absl::string_view path = request_headers_->Path()->value().getStringView(); ENVOY_STREAM_LOG(debug, "request complete: path: {}", *callbacks_, path); @@ -681,7 +715,10 @@ AdminImpl::AdminImpl(const std::string& access_log_path, const std::string& prof MAKE_ADMIN_HANDLER(handlerPrometheusStats), false, false}, {"/listeners", "print listener addresses", MAKE_ADMIN_HANDLER(handlerListenerInfo), false, false}, - {"/runtime", "print runtime values", MAKE_ADMIN_HANDLER(handlerRuntime), false, false}}, + {"/runtime", "print runtime values", MAKE_ADMIN_HANDLER(handlerRuntime), false, false}, + {"/runtime_modify", "modify runtime values", MAKE_ADMIN_HANDLER(handlerRuntimeModify), + false, true}}, + // TODO(jsedgwick) add /runtime_reset endpoint that removes all admin-set values listener_(*this, std::move(listener_scope)) { if (!address_out_path.empty()) { diff --git a/source/server/http/admin.h b/source/server/http/admin.h index 4f5f3f13efe9..c8972651529d 100644 --- a/source/server/http/admin.h +++ b/source/server/http/admin.h @@ -181,6 +181,8 @@ class AdminImpl : public Admin, Http::HeaderMap& response_headers, Buffer::Instance& response); Http::Code handlerRuntime(absl::string_view path_and_query, Http::HeaderMap& response_headers, Buffer::Instance& response); + Http::Code handlerRuntimeModify(absl::string_view path_and_query, + Http::HeaderMap& response_headers, Buffer::Instance& response); class AdminListener : public Network::ListenerConfig { public: diff --git a/source/server/server.cc b/source/server/server.cc index 90c58c9ec4be..3cc6a2a8a22e 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -289,12 +289,13 @@ Runtime::LoaderPtr InstanceUtil::createRuntime(Instance& server, ENVOY_LOG(info, "runtime override subdirectory: {}", override_subdirectory); Api::OsSysCallsPtr os_sys_calls(new Api::OsSysCallsImpl); - return Runtime::LoaderPtr{new Runtime::LoaderImpl( + return std::make_unique( server.dispatcher(), server.threadLocal(), config.runtime()->symlinkRoot(), config.runtime()->subdirectory(), override_subdirectory, server.stats(), server.random(), - std::move(os_sys_calls))}; + std::move(os_sys_calls)); } else { - return Runtime::LoaderPtr{new Runtime::NullLoaderImpl(server.random())}; + return std::make_unique(server.random(), server.stats(), + server.threadLocal()); } } diff --git a/test/common/runtime/runtime_impl_test.cc b/test/common/runtime/runtime_impl_test.cc index c2bce70d267e..c772a86de9bd 100644 --- a/test/common/runtime/runtime_impl_test.cc +++ b/test/common/runtime/runtime_impl_test.cc @@ -64,7 +64,7 @@ TEST(UUID, sanityCheckOfUniqueness) { EXPECT_EQ(num_of_uuids, uuids.size()); } -class RuntimeImplTest : public testing::Test { +class DiskBackedLoaderImplTest : public testing::Test { public: static void SetUpTestCase() { TestEnvironment::exec( @@ -83,8 +83,9 @@ class RuntimeImplTest : public testing::Test { void run(const std::string& primary_dir, const std::string& override_dir) { Api::OsSysCallsPtr os_sys_calls(os_sys_calls_); - loader.reset(new LoaderImpl(dispatcher, tls, TestEnvironment::temporaryPath(primary_dir), - "envoy", override_dir, store, generator, std::move(os_sys_calls))); + loader.reset(new DiskBackedLoaderImpl(dispatcher, tls, + TestEnvironment::temporaryPath(primary_dir), "envoy", + override_dir, store, generator, std::move(os_sys_calls))); } Event::MockDispatcher dispatcher; @@ -96,7 +97,7 @@ class RuntimeImplTest : public testing::Test { std::unique_ptr loader; }; -TEST_F(RuntimeImplTest, All) { +TEST_F(DiskBackedLoaderImplTest, All) { setup(); run("test/common/runtime/test_data/current", "envoy_override"); @@ -134,59 +135,114 @@ TEST_F(RuntimeImplTest, All) { EXPECT_EQ("hello override", loader->snapshot().get("file1")); } -TEST_F(RuntimeImplTest, GetAll) { +TEST_F(DiskBackedLoaderImplTest, GetLayers) { setup(); run("test/common/runtime/test_data/current", "envoy_override"); - - auto values = loader->snapshot().getAll(); - - auto entry = values.find("file1"); - EXPECT_FALSE(entry == values.end()); - EXPECT_EQ("hello override", entry->second.string_value_); - EXPECT_FALSE(entry->second.uint_value_); - - entry = values.find("file2"); - EXPECT_FALSE(entry == values.end()); - EXPECT_EQ("world", entry->second.string_value_); - EXPECT_FALSE(entry->second.uint_value_); - - entry = values.find("file3"); - EXPECT_FALSE(entry == values.end()); - EXPECT_EQ("2", entry->second.string_value_); - EXPECT_TRUE(entry->second.uint_value_); - EXPECT_EQ(2UL, entry->second.uint_value_.value()); - - entry = values.find("invalid"); - EXPECT_TRUE(entry == values.end()); + const auto& layers = loader->snapshot().getLayers(); + EXPECT_EQ(3, layers.size()); + EXPECT_EQ("hello", layers[0]->values().find("file1")->second.string_value_); + EXPECT_EQ("hello override", layers[1]->values().find("file1")->second.string_value_); + // Admin should be last + EXPECT_NE(nullptr, dynamic_cast(layers.back().get())); + EXPECT_TRUE(layers[2]->values().empty()); + + loader->mergeValues({{"foo", "bar"}}); + // The old snapshot and its layers should have been invalidated. Refetch. + const auto& new_layers = loader->snapshot().getLayers(); + EXPECT_EQ("bar", new_layers[2]->values().find("foo")->second.string_value_); } -TEST_F(RuntimeImplTest, BadDirectory) { +TEST_F(DiskBackedLoaderImplTest, BadDirectory) { setup(); run("/baddir", "/baddir"); } -TEST_F(RuntimeImplTest, BadStat) { +TEST_F(DiskBackedLoaderImplTest, BadStat) { setup(); EXPECT_CALL(*os_sys_calls_, stat(_, _)).WillOnce(Return(-1)); run("test/common/runtime/test_data/current", "envoy_override"); EXPECT_EQ(store.counter("runtime.load_error").value(), 1); + // We should still have the admin layer + const auto& layers = loader->snapshot().getLayers(); + EXPECT_EQ(1, layers.size()); + EXPECT_NE(nullptr, dynamic_cast(layers.back().get())); } -TEST_F(RuntimeImplTest, OverrideFolderDoesNotExist) { +TEST_F(DiskBackedLoaderImplTest, OverrideFolderDoesNotExist) { setup(); run("test/common/runtime/test_data/current", "envoy_override_does_not_exist"); EXPECT_EQ("hello", loader->snapshot().get("file1")); } -TEST(NullRuntimeImplTest, All) { +void testNewOverrides(Loader& loader, Stats::Store& store) { + // New string + loader.mergeValues({{"foo", "bar"}}); + EXPECT_EQ("bar", loader.snapshot().get("foo")); + EXPECT_EQ(1, store.gauge("runtime.admin_overrides_active").value()); + + // Remove new string + loader.mergeValues({{"foo", ""}}); + EXPECT_EQ("", loader.snapshot().get("foo")); + EXPECT_EQ(0, store.gauge("runtime.admin_overrides_active").value()); + + // New integer + loader.mergeValues({{"baz", "42"}}); + EXPECT_EQ(42, loader.snapshot().getInteger("baz", 0)); + EXPECT_EQ(1, store.gauge("runtime.admin_overrides_active").value()); + + // Remove new integer + loader.mergeValues({{"baz", ""}}); + EXPECT_EQ(0, loader.snapshot().getInteger("baz", 0)); + EXPECT_EQ(0, store.gauge("runtime.admin_overrides_active").value()); +} + +TEST_F(DiskBackedLoaderImplTest, mergeValues) { + setup(); + run("test/common/runtime/test_data/current", "envoy_override"); + testNewOverrides(*loader, store); + + // Override string + loader->mergeValues({{"file2", "new world"}}); + EXPECT_EQ("new world", loader->snapshot().get("file2")); + EXPECT_EQ(1, store.gauge("runtime.admin_overrides_active").value()); + + // Remove overridden string + loader->mergeValues({{"file2", ""}}); + EXPECT_EQ("world", loader->snapshot().get("file2")); + EXPECT_EQ(0, store.gauge("runtime.admin_overrides_active").value()); + + // Override integer + loader->mergeValues({{"file3", "42"}}); + EXPECT_EQ(42, loader->snapshot().getInteger("file3", 1)); + EXPECT_EQ(1, store.gauge("runtime.admin_overrides_active").value()); + + // Remove overridden integer + loader->mergeValues({{"file3", ""}}); + EXPECT_EQ(2, loader->snapshot().getInteger("file3", 1)); + EXPECT_EQ(0, store.gauge("runtime.admin_overrides_active").value()); + + // Override override string + loader->mergeValues({{"file1", "hello overridden override"}}); + EXPECT_EQ("hello overridden override", loader->snapshot().get("file1")); + EXPECT_EQ(1, store.gauge("runtime.admin_overrides_active").value()); + + // Remove overridden override string + loader->mergeValues({{"file1", ""}}); + EXPECT_EQ("hello override", loader->snapshot().get("file1")); + EXPECT_EQ(0, store.gauge("runtime.admin_overrides_active").value()); +} + +TEST(LoaderImplTest, All) { MockRandomGenerator generator; - NullLoaderImpl loader(generator); + NiceMock tls; + Stats::IsolatedStoreImpl store; + LoaderImpl loader(generator, store, tls); EXPECT_EQ("", loader.snapshot().get("foo")); EXPECT_EQ(1UL, loader.snapshot().getInteger("foo", 1)); EXPECT_CALL(generator, random()).WillOnce(Return(49)); EXPECT_TRUE(loader.snapshot().featureEnabled("foo", 50)); - EXPECT_TRUE(loader.snapshot().getAll().empty()); + testNewOverrides(loader, store); } } // namespace Runtime diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index c53e331ce4ce..164526729faf 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -240,7 +240,7 @@ TEST_P(IntegrationAdminTest, Admin) { downstreamProtocol(), version_); EXPECT_TRUE(response->complete()); EXPECT_STREQ("200", response->headers().Status()->value().c_str()); - EXPECT_STREQ("text/plain; charset=UTF-8", ContentType(response)); + EXPECT_STREQ("application/json", ContentType(response)); response = IntegrationUtil::makeSingleRequest(lookupPort("admin"), "GET", "/runtime?format=json", "", downstreamProtocol(), version_); diff --git a/test/mocks/runtime/mocks.h b/test/mocks/runtime/mocks.h index e9a28be80786..7b2292da4d7a 100644 --- a/test/mocks/runtime/mocks.h +++ b/test/mocks/runtime/mocks.h @@ -34,7 +34,7 @@ class MockSnapshot : public Snapshot { uint64_t random_value, uint64_t num_buckets)); MOCK_CONST_METHOD1(get, const std::string&(const std::string& key)); MOCK_CONST_METHOD2(getInteger, uint64_t(const std::string& key, uint64_t default_value)); - MOCK_CONST_METHOD0(getAll, const std::unordered_map&()); + MOCK_CONST_METHOD0(getLayers, const std::vector&()); }; class MockLoader : public Loader { @@ -43,9 +43,16 @@ class MockLoader : public Loader { ~MockLoader(); MOCK_METHOD0(snapshot, Snapshot&()); + MOCK_METHOD1(mergeValues, void(const std::unordered_map&)); testing::NiceMock snapshot_; }; +class MockOverrideLayer : public Snapshot::OverrideLayer { +public: + MOCK_CONST_METHOD0(name, const std::string&()); + MOCK_CONST_METHOD0(values, const std::unordered_map&()); +}; + } // namespace Runtime } // namespace Envoy diff --git a/test/server/http/admin_test.cc b/test/server/http/admin_test.cc index 1c939b7ff9ed..09b11bdf4b12 100644 --- a/test/server/http/admin_test.cc +++ b/test/server/http/admin_test.cc @@ -18,6 +18,7 @@ #include "test/test_common/printers.h" #include "test/test_common/utility.h" +#include "absl/strings/match.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -235,69 +236,91 @@ TEST_P(AdminInstanceTest, Runtime) { Http::HeaderMapImpl header_map; Buffer::OwnedImpl response; - std::unordered_map entries{ - {"string_key", {"foo", {}}}, {"int_key", {"1", {1}}}, {"other_key", {"bar", {}}}}; Runtime::MockSnapshot snapshot; Runtime::MockLoader loader; + auto layer1 = std::make_unique>(); + auto layer2 = std::make_unique>(); + std::unordered_map entries1{ + {"string_key", {"foo", {}}}, {"int_key", {"1", {1}}}, {"other_key", {"bar", {}}}}; + std::unordered_map entries2{ + {"string_key", {"override", {}}}, {"extra_key", {"bar", {}}}}; + + ON_CALL(*layer1, name()).WillByDefault(testing::ReturnRefOfCopy(std::string{"layer1"})); + ON_CALL(*layer1, values()).WillByDefault(testing::ReturnRef(entries1)); + ON_CALL(*layer2, name()).WillByDefault(testing::ReturnRefOfCopy(std::string{"layer2"})); + ON_CALL(*layer2, values()).WillByDefault(testing::ReturnRef(entries2)); + + std::vector layers; + layers.push_back(std::move(layer1)); + layers.push_back(std::move(layer2)); + EXPECT_CALL(snapshot, getLayers()).WillRepeatedly(testing::ReturnRef(layers)); + + const std::string expected_json = R"EOF({ + "layers": [ + "layer1", + "layer2" + ], + "entries": { + "extra_key": { + "layer_values": [ + "", + "bar" + ], + "final_value": "bar" + }, + "int_key": { + "layer_values": [ + "1", + "" + ], + "final_value": "1" + }, + "other_key": { + "layer_values": [ + "bar", + "" + ], + "final_value": "bar" + }, + "string_key": { + "layer_values": [ + "foo", + "override" + ], + "final_value": "override" + } + } +})EOF"; - EXPECT_CALL(snapshot, getAll()).WillRepeatedly(testing::ReturnRef(entries)); EXPECT_CALL(loader, snapshot()).WillRepeatedly(testing::ReturnPointee(&snapshot)); EXPECT_CALL(server_, runtime()).WillRepeatedly(testing::ReturnPointee(&loader)); - EXPECT_EQ(Http::Code::OK, admin_.runCallback("/runtime", header_map, response)); - EXPECT_EQ("int_key: 1\nother_key: bar\nstring_key: foo\n", TestUtility::bufferToString(response)); + EXPECT_EQ(expected_json, TestUtility::bufferToString(response)); } -TEST_P(AdminInstanceTest, RuntimeJSON) { +TEST_P(AdminInstanceTest, RuntimeModify) { Http::HeaderMapImpl header_map; Buffer::OwnedImpl response; - std::unordered_map entries{ - {"string_key", {"foo", {}}}, {"int_key", {"1", {1}}}, {"other_key", {"bar", {}}}}; - Runtime::MockSnapshot snapshot; Runtime::MockLoader loader; - - EXPECT_CALL(snapshot, getAll()).WillRepeatedly(testing::ReturnRef(entries)); - EXPECT_CALL(loader, snapshot()).WillRepeatedly(testing::ReturnPointee(&snapshot)); EXPECT_CALL(server_, runtime()).WillRepeatedly(testing::ReturnPointee(&loader)); - EXPECT_EQ(Http::Code::OK, admin_.runCallback("/runtime?format=json", header_map, response)); - - std::string output = TestUtility::bufferToString(response); - Json::ObjectSharedPtr json = Json::Factory::loadFromString(output); - - EXPECT_TRUE(json->hasObject("runtime")); - std::vector pairs = json->getObjectArray("runtime"); - EXPECT_EQ(3, pairs.size()); - - Json::ObjectSharedPtr pair = pairs[0]; - EXPECT_EQ("int_key", pair->getString("name", "")); - EXPECT_EQ(1, pair->getInteger("value", -1)); - - pair = pairs[1]; - EXPECT_EQ("other_key", pair->getString("name", "")); - EXPECT_EQ("bar", pair->getString("value", "")); - - pair = pairs[2]; - EXPECT_EQ("string_key", pair->getString("name", "")); - EXPECT_EQ("foo", pair->getString("value", "")); + std::unordered_map overrides; + overrides["foo"] = "bar"; + overrides["x"] = "42"; + overrides["nothing"] = ""; + EXPECT_CALL(loader, mergeValues(overrides)).Times(1); + EXPECT_EQ(Http::Code::OK, + admin_.runCallback("/runtime_modify?foo=bar&x=42¬hing=", header_map, response)); + EXPECT_EQ("OK\n", TestUtility::bufferToString(response)); } -TEST_P(AdminInstanceTest, RuntimeBadFormat) { +TEST_P(AdminInstanceTest, RuntimeModifyNoArguments) { Http::HeaderMapImpl header_map; Buffer::OwnedImpl response; - std::unordered_map entries; - Runtime::MockSnapshot snapshot; - Runtime::MockLoader loader; - - EXPECT_CALL(snapshot, getAll()).WillRepeatedly(testing::ReturnRef(entries)); - EXPECT_CALL(loader, snapshot()).WillRepeatedly(testing::ReturnPointee(&snapshot)); - EXPECT_CALL(server_, runtime()).WillRepeatedly(testing::ReturnPointee(&loader)); - - EXPECT_EQ(Http::Code::BadRequest, - admin_.runCallback("/runtime?format=foo", header_map, response)); - EXPECT_EQ("usage: /runtime?format=json\n", TestUtility::bufferToString(response)); + EXPECT_EQ(Http::Code::BadRequest, admin_.runCallback("/runtime_modify", header_map, response)); + EXPECT_TRUE(absl::StartsWith(TestUtility::bufferToString(response), "usage:")); } TEST(PrometheusStatsFormatter, MetricName) {