diff --git a/docs/operations/cli.rst b/docs/operations/cli.rst index 9dc1f8f6c2e43..fd88f987826be 100644 --- a/docs/operations/cli.rst +++ b/docs/operations/cli.rst @@ -10,6 +10,10 @@ following are the command line options that Envoy supports. *(required)* The path to the :ref:`JSON configuration file `. +.. option:: --admin-address-path + + *(optional)* The output file path where the admin address and port will be written. + .. option:: --base-id *(optional)* The base ID to use when allocating shared memory regions. Envoy uses shared memory diff --git a/include/envoy/server/options.h b/include/envoy/server/options.h index 08d0642e8de61..eb77829531033 100644 --- a/include/envoy/server/options.h +++ b/include/envoy/server/options.h @@ -40,6 +40,11 @@ class Options { */ virtual const std::string& configPath() PURE; + /** + * @return const std::string& the admin address output file. + */ + virtual const std::string& adminAddressPath() PURE; + /** * @return spdlog::level::level_enum the default log level for the server. */ diff --git a/source/common/json/json_loader.cc b/source/common/json/json_loader.cc index d608dfd9a5a9e..310b4b5516e6d 100644 --- a/source/common/json/json_loader.cc +++ b/source/common/json/json_loader.cc @@ -239,4 +239,17 @@ ObjectPtr Factory::LoadFromString(const std::string& json) { return ObjectPtr{new ObjectImplRoot(std::move(document))}; } +const std::string Factory::listAsJsonString(const std::list& items) { + rapidjson::StringBuffer writer_string_buffer; + rapidjson::Writer writer(writer_string_buffer); + + writer.StartArray(); + for (const std::string& item : items) { + writer.String(item.c_str()); + } + writer.EndArray(); + + return writer_string_buffer.GetString(); +} + } // Json diff --git a/source/common/json/json_loader.h b/source/common/json/json_loader.h index 205d455df042c..0c812f9fb8740 100644 --- a/source/common/json/json_loader.h +++ b/source/common/json/json_loader.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/json/json_object.h" @@ -8,6 +9,7 @@ namespace Json { class Factory { public: + // TODO(hennna): Cleanup function names - i.e. s/LoadFromFile/loadFromFile/. /* * Constructs a Json Object from a File. */ @@ -17,6 +19,8 @@ class Factory { * Constructs a Json Object from a String. */ static ObjectPtr LoadFromString(const std::string& json); + + static const std::string listAsJsonString(const std::list& items); }; } // Json diff --git a/source/server/http/admin.cc b/source/server/http/admin.cc index fe5a351bb7715..6c9b9c83df074 100644 --- a/source/server/http/admin.cc +++ b/source/server/http/admin.cc @@ -1,6 +1,7 @@ #include "server/http/admin.h" #include +#include #include #include @@ -23,6 +24,7 @@ #include "common/http/header_map_impl.h" #include "common/http/headers.h" #include "common/http/http1/codec_impl.h" +#include "common/json/json_loader.h" #include "common/network/listen_socket_impl.h" #include "common/profiler/profiler.h" #include "common/router/config_impl.h" @@ -284,6 +286,16 @@ Http::Code AdminImpl::handlerQuitQuitQuit(const std::string&, Buffer::Instance& return Http::Code::OK; } +Http::Code AdminImpl::handlerListenerInfo(const std::string&, Buffer::Instance& response) { + std::list listeners; + int listener_index = 0; + while (auto listen_socket = server_.getListenSocketByIndex(listener_index++)) { + listeners.push_back(listen_socket->localAddress()->asString()); + } + response.add(Json::Factory::listAsJsonString(listeners)); + return Http::Code::OK; +} + Http::Code AdminImpl::handlerCerts(const std::string&, Buffer::Instance& response) { // This set is used to track distinct certificates. We may have multiple listeners, upstreams, etc // using the same cert. @@ -322,6 +334,7 @@ AdminImpl::NullRouteConfigProvider::NullRouteConfigProvider() : config_(new Router::NullConfigImpl()) {} AdminImpl::AdminImpl(const std::string& access_log_path, const std::string& profile_path, + const std::string& address_out_path, Network::Address::InstanceConstSharedPtr address, Server::Instance& server) : server_(server), profile_path_(profile_path), socket_(new Network::TcpListenSocket(address, true)), @@ -343,7 +356,17 @@ AdminImpl::AdminImpl(const std::string& access_log_path, const std::string& prof {"/reset_counters", "reset all counters to zero", MAKE_HANDLER(handlerResetCounters)}, {"/server_info", "print server version/status information", MAKE_HANDLER(handlerServerInfo)}, - {"/stats", "print server stats", MAKE_HANDLER(handlerStats)}} { + {"/stats", "print server stats", MAKE_HANDLER(handlerStats)}, + {"/listeners", "print listener addresses", MAKE_HANDLER(handlerListenerInfo)}} { + + if (!address_out_path.empty()) { + std::ofstream address_out_file(address_out_path); + if (!address_out_file) { + log().critical("cannot open admin address output file {} for writing.", address_out_path); + } else { + address_out_file << socket_->localAddress()->asString(); + } + } access_logs_.emplace_back(new Http::AccessLog::InstanceImpl( access_log_path, {}, Http::AccessLog::AccessLogFormatUtils::defaultAccessLogFormatter(), diff --git a/source/server/http/admin.h b/source/server/http/admin.h index 32fcd6ecf4be2..c5757eeb63a80 100644 --- a/source/server/http/admin.h +++ b/source/server/http/admin.h @@ -29,7 +29,8 @@ class AdminImpl : public Admin, Logger::Loggable { public: AdminImpl(const std::string& access_log_path, const std::string& profiler_path, - Network::Address::InstanceConstSharedPtr address, Server::Instance& server); + const std::string& address_out_path, Network::Address::InstanceConstSharedPtr address, + Server::Instance& server); Http::Code runCallback(const std::string& path, Buffer::Instance& response); const Network::ListenSocket& socket() override { return *socket_; } @@ -118,6 +119,7 @@ class AdminImpl : public Admin, Http::Code handlerServerInfo(const std::string& url, Buffer::Instance& response); Http::Code handlerStats(const std::string& url, Buffer::Instance& response); Http::Code handlerQuitQuitQuit(const std::string& url, Buffer::Instance& response); + Http::Code handlerListenerInfo(const std::string& url, Buffer::Instance& response); Server::Instance& server_; std::list access_logs_; diff --git a/source/server/options_impl.cc b/source/server/options_impl.cc index 016b2ac3ae553..29206b5c57a27 100644 --- a/source/server/options_impl.cc +++ b/source/server/options_impl.cc @@ -29,6 +29,8 @@ OptionsImpl::OptionsImpl(int argc, char** argv, const std::string& hot_restart_v std::thread::hardware_concurrency(), "uint32_t", cmd); TCLAP::ValueArg config_path("c", "config-path", "Path to configuration file", false, "", "string", cmd); + TCLAP::ValueArg admin_address_path("", "admin-address-path", "Admin address path", + false, "", "string", cmd); TCLAP::ValueArg log_level("l", "log-level", log_levels_string, false, spdlog::level::level_names[default_log_level], "string", cmd); @@ -74,6 +76,7 @@ OptionsImpl::OptionsImpl(int argc, char** argv, const std::string& hot_restart_v base_id_ = base_id.getValue() * 10; concurrency_ = concurrency.getValue(); config_path_ = config_path.getValue(); + admin_address_path_ = admin_address_path.getValue(); restart_epoch_ = restart_epoch.getValue(); service_cluster_ = service_cluster.getValue(); service_node_ = service_node.getValue(); diff --git a/source/server/options_impl.h b/source/server/options_impl.h index e29265691c160..48b92d5f4f840 100644 --- a/source/server/options_impl.h +++ b/source/server/options_impl.h @@ -24,6 +24,7 @@ class OptionsImpl : public Server::Options { uint64_t baseId() override { return base_id_; } uint32_t concurrency() override { return concurrency_; } const std::string& configPath() override { return config_path_; } + const std::string& adminAddressPath() override { return admin_address_path_; } std::chrono::seconds drainTime() override { return drain_time_; } spdlog::level::level_enum logLevel() override { return log_level_; } std::chrono::seconds parentShutdownTime() override { return parent_shutdown_time_; } @@ -34,6 +35,7 @@ class OptionsImpl : public Server::Options { uint64_t base_id_; uint32_t concurrency_; std::string config_path_; + std::string admin_address_path_; spdlog::level::level_enum log_level_; uint64_t restart_epoch_; std::string service_cluster_; diff --git a/source/server/server.cc b/source/server/server.cc index 321d61e705c8e..86223f7b02837 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -189,8 +189,9 @@ void InstanceImpl::initialize(Options& options, TestHooks& hooks, restarter_.shutdownParentAdmin(info); original_start_time_ = info.original_start_time_; admin_.reset(new AdminImpl(initial_config.admin().accessLogPath(), - initial_config.admin().profilePath(), initial_config.admin().address(), - *this)); + initial_config.admin().profilePath(), options.adminAddressPath(), + initial_config.admin().address(), *this)); + admin_scope_ = stats_store_.createScope("listener.admin."); handler_.addListener(*admin_, admin_->mutable_socket(), *admin_scope_, Network::ListenerOptions::listenerOptionsWithBindToPort()); diff --git a/test/common/json/json_loader_test.cc b/test/common/json/json_loader_test.cc index 3af55a5dbf86e..b6716b75cfb0c 100644 --- a/test/common/json/json_loader_test.cc +++ b/test/common/json/json_loader_test.cc @@ -96,6 +96,7 @@ TEST(JsonLoaderTest, Basic) { ObjectPtr config = Factory::LoadFromString(json); EXPECT_EQ(2U, config->getObjectArray("descriptors")[0]->asObjectArray().size()); + EXPECT_EQ(1U, config->getObjectArray("descriptors")[1]->asObjectArray().size()); } @@ -227,4 +228,42 @@ TEST(JsonLoaderTest, AsString) { }); } +TEST(JsonLoaderTest, ListAsString) { + { + std::list list = {}; + Json::ObjectPtr json = Json::Factory::LoadFromString(Json::Factory::listAsJsonString(list)); + std::vector output = json->asObjectArray(); + EXPECT_TRUE(output.empty()); + } + + { + std::list list = {"one"}; + Json::ObjectPtr json = Json::Factory::LoadFromString(Json::Factory::listAsJsonString(list)); + std::vector output = json->asObjectArray(); + EXPECT_EQ(1, output.size()); + EXPECT_EQ("one", output[0]->asString()); + } + + { + std::list list = {"one", "two", "three", "four"}; + Json::ObjectPtr json = Json::Factory::LoadFromString(Json::Factory::listAsJsonString(list)); + std::vector output = json->asObjectArray(); + EXPECT_EQ(4, output.size()); + EXPECT_EQ("one", output[0]->asString()); + EXPECT_EQ("two", output[1]->asString()); + EXPECT_EQ("three", output[2]->asString()); + EXPECT_EQ("four", output[3]->asString()); + } + + { + std::list list = {"127.0.0.1:46465", "127.0.0.1:52211", "127.0.0.1:58941"}; + Json::ObjectPtr json = Json::Factory::LoadFromString(Json::Factory::listAsJsonString(list)); + std::vector output = json->asObjectArray(); + EXPECT_EQ(3, output.size()); + EXPECT_EQ("127.0.0.1:46465", output[0]->asString()); + EXPECT_EQ("127.0.0.1:52211", output[1]->asString()); + EXPECT_EQ("127.0.0.1:58941", output[2]->asString()); + } +} + } // Json diff --git a/test/integration/BUILD b/test/integration/BUILD index c48bb8eb179d8..38f64147fb731 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -16,6 +16,7 @@ envoy_sh_test( "//test/common/runtime:filesystem_setup.sh", "//test/common/runtime:filesystem_test_data", "//test/config/integration:server_config_files", + "//tools:socket_passing", ], # Need to disable test execution sandboxing for this test, since it uses shmem. # TODO(htuch): When most folks are using a Bazel with the recent diff --git a/test/integration/hotrestart_test.sh b/test/integration/hotrestart_test.sh index 6b9698eca9c4c..e86637cfb7d4d 100755 --- a/test/integration/hotrestart_test.sh +++ b/test/integration/hotrestart_test.sh @@ -16,11 +16,19 @@ cat "${TEST_RUNDIR}"/test/config/integration/server.json | # Now start the real server, hot restart it twice, and shut it all down as a basic hot restart # sanity test. echo "Starting epoch 0" +ADMIN_ADDRESS_PATH_0="${TEST_TMPDIR}"/admin_0.address "${ENVOY_BIN}" -c "${HOT_RESTART_JSON}" \ - --restart-epoch 0 --base-id 1 --service-cluster cluster --service-node node & + --restart-epoch 0 --base-id 1 --service-cluster cluster --service-node node \ + --admin-address-path "${ADMIN_ADDRESS_PATH_0}" & FIRST_SERVER_PID=$! sleep 3 + +echo "Updating original config json listener addresses" +UPDATED_HOT_RESTART_JSON="${TEST_TMPDIR}"/hot_restart_updated.json +"${TEST_RUNDIR}"/tools/socket_passing.py "-o" "${HOT_RESTART_JSON}" "-a" "${ADMIN_ADDRESS_PATH_0}" \ + "-u" "${UPDATED_HOT_RESTART_JSON}" + # Send SIGUSR1 signal to the first server, this should not kill it. Also send SIGHUP which should # get eaten. echo "Sending SIGUSR1/SIGHUP to first server" @@ -29,20 +37,38 @@ kill -SIGHUP ${FIRST_SERVER_PID} sleep 3 echo "Starting epoch 1" -"${ENVOY_BIN}" -c "${HOT_RESTART_JSON}" \ - --restart-epoch 1 --base-id 1 --service-cluster cluster --service-node node & +ADMIN_ADDRESS_PATH_1="${TEST_TMPDIR}"/admin_1.address +"${ENVOY_BIN}" -c "${UPDATED_HOT_RESTART_JSON}" \ + --restart-epoch 1 --base-id 1 --service-cluster cluster --service-node node \ + --admin-address-path "${ADMIN_ADDRESS_PATH_1}" & SECOND_SERVER_PID=$! # Wait for stat flushing sleep 7 +echo "Checking that listener addresses have not changed" +HOT_RESTART_JSON_1="${TEST_TMPDIR}"/hot_restart_1.json +"${TEST_RUNDIR}"/tools/socket_passing.py "-o" "${UPDATED_HOT_RESTART_JSON}" "-a" "${ADMIN_ADDRESS_PATH_1}" \ + "-u" "${HOT_RESTART_JSON_1}" +CONFIG_DIFF=$(diff -Z "${UPDATED_HOT_RESTART_JSON}" "${HOT_RESTART_JSON_1}") +[[ -z "${CONFIG_DIFF}" ]] + +ADMIN_ADDRESS_PATH_2="${TEST_TMPDIR}"/admin_2.address echo "Starting epoch 2" -"${ENVOY_BIN}" -c "${HOT_RESTART_JSON}" \ - --restart-epoch 2 --base-id 1 --service-cluster cluster --service-node node & +"${ENVOY_BIN}" -c "${UPDATED_HOT_RESTART_JSON}" \ + --restart-epoch 2 --base-id 1 --service-cluster cluster --service-node node \ + --admin-address-path "${ADMIN_ADDRESS_PATH_2}" & THIRD_SERVER_PID=$! sleep 3 +echo "Checking that listener addresses have not changed" +HOT_RESTART_JSON_2="${TEST_TMPDIR}"/hot_restart_2.json +"${TEST_RUNDIR}"/tools/socket_passing.py "-o" "${UPDATED_HOT_RESTART_JSON}" "-a" "${ADMIN_ADDRESS_PATH_2}" \ + "-u" "${HOT_RESTART_JSON_2}" +CONFIG_DIFF=$(diff -Z "${UPDATED_HOT_RESTART_JSON}" "${HOT_RESTART_JSON_2}") +[[ -z "${CONFIG_DIFF}" ]] + # First server should already be gone. echo "Waiting for epoch 0" wait ${FIRST_SERVER_PID} diff --git a/test/integration/integration_admin_test.cc b/test/integration/integration_admin_test.cc index d0c825c114174..a175d5a3a3477 100644 --- a/test/integration/integration_admin_test.cc +++ b/test/integration/integration_admin_test.cc @@ -1,5 +1,7 @@ #include "envoy/http/header_map.h" +#include "common/json/json_loader.h" + #include "test/integration/integration_test.h" #include "test/integration/utility.h" @@ -122,6 +124,18 @@ TEST_F(IntegrationTest, Admin) { Http::CodecClient::Type::HTTP1); EXPECT_TRUE(response->complete()); EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + + response = IntegrationUtil::makeSingleRequest(lookupPort("admin"), "GET", "/listeners", "", + Http::CodecClient::Type::HTTP1); + EXPECT_TRUE(response->complete()); + EXPECT_STREQ("200", response->headers().Status()->value().c_str()); + + Json::ObjectPtr json = Json::Factory::LoadFromString(response->body()); + std::vector listener_info = json->asObjectArray(); + for (std::size_t index = 0; index < listener_info.size(); index++) { + EXPECT_EQ(test_server_->server().getListenSocketByIndex(index)->localAddress()->asString(), + listener_info[index]->asString()); + } } // Successful call to startProfiler requires tcmalloc. diff --git a/test/integration/server.h b/test/integration/server.h index 66b0e0946833c..d8b4a135f1d2b 100644 --- a/test/integration/server.h +++ b/test/integration/server.h @@ -34,6 +34,7 @@ class TestOptionsImpl : public Options { uint64_t baseId() override { return 0; } uint32_t concurrency() override { return 1; } const std::string& configPath() override { return config_path_; } + const std::string& adminAddressPath() override { return admin_address_path_; } std::chrono::seconds drainTime() override { return std::chrono::seconds(0); } spdlog::level::level_enum logLevel() override { NOT_IMPLEMENTED; } std::chrono::seconds parentShutdownTime() override { return std::chrono::seconds(0); } @@ -44,6 +45,7 @@ class TestOptionsImpl : public Options { private: const std::string config_path_; + const std::string admin_address_path_; }; class TestDrainManager : public DrainManager { diff --git a/test/mocks/server/mocks.cc b/test/mocks/server/mocks.cc index 7856d91ade510..56c17582993ad 100644 --- a/test/mocks/server/mocks.cc +++ b/test/mocks/server/mocks.cc @@ -13,8 +13,10 @@ using testing::SaveArg; namespace Server { -MockOptions::MockOptions(const std::string& path) : path_(path) { - ON_CALL(*this, configPath()).WillByDefault(ReturnRef(path_)); +MockOptions::MockOptions(const std::string& config_path) + : config_path_(config_path), admin_address_path_("") { + ON_CALL(*this, configPath()).WillByDefault(ReturnRef(config_path_)); + ON_CALL(*this, adminAddressPath()).WillByDefault(ReturnRef(admin_address_path_)); } MockOptions::~MockOptions() {} diff --git a/test/mocks/server/mocks.h b/test/mocks/server/mocks.h index 3e030bac2cb8e..98c206480cf11 100644 --- a/test/mocks/server/mocks.h +++ b/test/mocks/server/mocks.h @@ -45,13 +45,15 @@ class MockOptions : public Options { MOCK_METHOD0(baseId, uint64_t()); MOCK_METHOD0(concurrency, uint32_t()); MOCK_METHOD0(configPath, const std::string&()); + MOCK_METHOD0(adminAddressPath, const std::string&()); MOCK_METHOD0(drainTime, std::chrono::seconds()); MOCK_METHOD0(logLevel, spdlog::level::level_enum()); MOCK_METHOD0(parentShutdownTime, std::chrono::seconds()); MOCK_METHOD0(restartEpoch, uint64_t()); MOCK_METHOD0(fileFlushIntervalMsec, std::chrono::milliseconds()); - std::string path_; + std::string config_path_; + std::string admin_address_path_; }; class MockAdmin : public Admin { diff --git a/test/server/http/BUILD b/test/server/http/BUILD index 725cc76a2de80..fbaff7ffbc975 100644 --- a/test/server/http/BUILD +++ b/test/server/http/BUILD @@ -15,6 +15,7 @@ envoy_cc_test( "//source/server/http:admin_lib", "//test/mocks/server:server_mocks", "//test/test_common:environment_lib", + "//test/test_common:network_utility_lib", "//test/test_common:utility_lib", ], ) diff --git a/test/server/http/admin_test.cc b/test/server/http/admin_test.cc index 3cbd64d4ac28b..94a9f18aad199 100644 --- a/test/server/http/admin_test.cc +++ b/test/server/http/admin_test.cc @@ -1,3 +1,5 @@ +#include + #include "common/http/message_impl.h" #include "common/profiler/profiler.h" @@ -5,6 +7,7 @@ #include "test/mocks/server/mocks.h" #include "test/test_common/environment.h" +#include "test/test_common/network_utility.h" #include "test/test_common/printers.h" #include "test/test_common/utility.h" @@ -21,6 +24,7 @@ class AdminFilterTest : public testing::Test { // TODO(mattklein123): Switch to mocks and do not bind to a real port. AdminFilterTest() : admin_("/dev/null", TestEnvironment::temporaryPath("envoy.prof"), + TestEnvironment::temporaryPath("admin.address"), Network::Utility::resolveUrl("tcp://127.0.0.1:0"), server_), filter_(admin_), request_headers_{{":path", "/"}} { filter_.setDecoderFilterCallbacks(callbacks_); @@ -53,12 +57,26 @@ TEST_F(AdminFilterTest, Trailers) { filter_.decodeTrailers(request_headers_); } +class AdminInstanceTest : public testing::Test { +public: + AdminInstanceTest() + : address_out_path_(TestEnvironment::temporaryPath("admin.address")), + cpu_profile_path_(TestEnvironment::temporaryPath("envoy.prof")), + admin_("/dev/null", cpu_profile_path_, address_out_path_, + Network::Test::getSomeLoopbackAddress(Network::Address::IpVersion::v4), server_) {} + + std::string address_out_path_; + std::string cpu_profile_path_; + NiceMock server_; + AdminImpl admin_; +}; + // Can only get code coverage of AdminImpl::handlerCpuProfiler stopProfiler with // a real profiler linked in (successful call to startProfiler). startProfiler // requies tcmalloc. #ifdef TCMALLOC -TEST_F(AdminFilterTest, AdminProfiler) { +TEST_F(AdminInstanceTest, AdminProfiler) { Buffer::OwnedImpl data; admin_.runCallback("/cpuprofiler?enable=y", data); EXPECT_TRUE(Profiler::Cpu::profilerEnabled()); @@ -68,13 +86,27 @@ TEST_F(AdminFilterTest, AdminProfiler) { #endif -TEST_F(AdminFilterTest, AdminBadProfiler) { +TEST_F(AdminInstanceTest, AdminBadProfiler) { Buffer::OwnedImpl data; - AdminImpl admin_bad_profile_path("/dev/null", - TestEnvironment::temporaryPath("some/unlikely/bad/path.prof"), - Network::Utility::resolveUrl("tcp://127.0.0.1:0"), server_); + AdminImpl admin_bad_profile_path( + "/dev/null", TestEnvironment::temporaryPath("some/unlikely/bad/path.prof"), "", + Network::Test::getSomeLoopbackAddress(Network::Address::IpVersion::v4), server_); admin_bad_profile_path.runCallback("/cpuprofiler?enable=y", data); EXPECT_FALSE(Profiler::Cpu::profilerEnabled()); } +TEST_F(AdminInstanceTest, WriteAddressToFile) { + std::ifstream address_file(address_out_path_); + std::string address_from_file; + std::getline(address_file, address_from_file); + EXPECT_EQ(admin_.socket().localAddress()->asString(), address_from_file); +} + +TEST_F(AdminInstanceTest, AdminBadAddressOutPath) { + std::string bad_path = TestEnvironment::temporaryPath("some/unlikely/bad/path/admin.address"); + AdminImpl admin_bad_address_out_path( + "/dev/null", cpu_profile_path_, bad_path, + Network::Test::getSomeLoopbackAddress(Network::Address::IpVersion::v4), server_); + EXPECT_FALSE(std::ifstream(bad_path)); +} } // namespace Server diff --git a/test/server/options_impl_test.cc b/test/server/options_impl_test.cc index 6ac9bb227ddf9..d54021140779f 100644 --- a/test/server/options_impl_test.cc +++ b/test/server/options_impl_test.cc @@ -28,11 +28,12 @@ TEST(OptionsImplDeathTest, HotRestartVersion) { TEST(OptionsImplTest, All) { std::unique_ptr options = createOptionsImpl( - "envoy --concurrency 2 -c hello --restart-epoch 1 -l info " + "envoy --concurrency 2 -c hello --admin-address-path path --restart-epoch 1 -l info " "--service-cluster cluster --service-node node --service-zone zone " "--file-flush-interval-msec 9000 --drain-time-s 60 --parent-shutdown-time-s 90"); EXPECT_EQ(2U, options->concurrency()); EXPECT_EQ("hello", options->configPath()); + EXPECT_EQ("path", options->adminAddressPath()); EXPECT_EQ(1U, options->restartEpoch()); EXPECT_EQ(spdlog::level::info, options->logLevel()); EXPECT_EQ("cluster", options->serviceClusterName()); @@ -47,4 +48,5 @@ TEST(OptionsImplTest, DefaultParams) { std::unique_ptr options = createOptionsImpl("envoy -c hello"); EXPECT_EQ(std::chrono::seconds(600), options->drainTime()); EXPECT_EQ(std::chrono::seconds(900), options->parentShutdownTime()); + EXPECT_EQ("", options->adminAddressPath()); } diff --git a/tools/BUILD b/tools/BUILD index 8febcf1a8665e..d516bf1c4ea1f 100644 --- a/tools/BUILD +++ b/tools/BUILD @@ -1,6 +1,7 @@ load( "//bazel:envoy_build_system.bzl", "envoy_package", + "envoy_py_test_binary", ) envoy_package() @@ -9,3 +10,10 @@ exports_files([ "gen_git_sha.sh", "git_sha_rewriter.py", ]) + +envoy_py_test_binary( + name = "socket_passing", + srcs = [ + "socket_passing.py", + ], +) diff --git a/tools/socket_passing.py b/tools/socket_passing.py new file mode 100755 index 0000000000000..86e5e2d3b47f0 --- /dev/null +++ b/tools/socket_passing.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +# This tool is a helper script that queries the admin address for all listener +# addresses after envoy startup. (The admin adress is written out to a file by +# setting the -a flag in the envoy binary.) The script then outputs a new json +# config file with updated listener addresses. This script is currently called +# in the hot restart integration test to update listener addresses bound to +# port 0 in the intial json config file. + +from collections import OrderedDict + +import argparse +import httplib +import json +import os.path +import sys +import time + +# Seconds to wait for the admin address output file to appear. The script exits +# with failure if the file is not found. +ADMIN_FILE_TIMEOUT_SECS = 20 + +def GenerateNewConfig(original_json, admin_address, updated_json): + # Get original listener addresses + with open(original_json, 'r') as original_json_file: + # Import original config file in order to get a deterministic output. This + # allows us to diff the original config file and the updated config file + # output from this script to check for any changes. + parsed_json = json.load(original_json_file, object_pairs_hook=OrderedDict) + original_listeners = parsed_json['listeners'] + + sys.stdout.write('Admin address is ' + admin_address + '\n') + try: + admin_conn = httplib.HTTPConnection(admin_address) + admin_conn.request('GET', '/listeners') + admin_response = admin_conn.getresponse() + if not admin_response.status == 200: + return False + discovered_listeners = json.loads(admin_response.read()) + except Exception as e: + sys.stderr.write('Cannot connect to admin: %s\n' % e) + return False + else: + if len(discovered_listeners) != len(original_listeners): + return False + for discovered, original in zip(discovered_listeners, original_listeners): + original['address'] = 'tcp://' + discovered + with open(updated_json, 'w') as outfile: + json.dump(OrderedDict(parsed_json), outfile, indent=2, separators=(',',':')) + finally: + admin_conn.close() + + return True + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Replace listener addressses in json file.') + parser.add_argument('-o', '--original_json', type=str, required=True, + help='Path of the original config json file') + parser.add_argument('-a', '--admin_address_path', type=str, required=True, + help='Path of the admin address file') + parser.add_argument('-u', '--updated_json', type=str, required=True, + help='Path to output updated json config file') + args = parser.parse_args() + admin_address_path = args.admin_address_path + + # Read admin address from file + counter = 0; + while not os.path.exists(admin_address_path): + time.sleep(1) + counter += 1 + if counter > ADMIN_FILE_TIMEOUT_SECS: + break + + if not os.path.exists(admin_address_path): + sys.exit(1) + + with open(admin_address_path, 'r') as admin_address_file: + admin_address = admin_address_file.read() + + success = GenerateNewConfig(args.original_json, admin_address, args.updated_json) + + if not success: + sys.exit(1)