Skip to content

admin: Add an API on Server to initiate a programmatic admin request.#3667

Merged
htuch merged 12 commits intoenvoyproxy:masterfrom
jmarantz:admin-port-as-callback
Jun 25, 2018
Merged

admin: Add an API on Server to initiate a programmatic admin request.#3667
htuch merged 12 commits intoenvoyproxy:masterfrom
jmarantz:admin-port-as-callback

Conversation

@jmarantz
Copy link
Contributor

@jmarantz jmarantz commented Jun 19, 2018

Description:

Adds a programmatic API so an Envoy integration can provide admin content without exposing it on a network port. This partially addresses #3674 . That will be completely addressed by creating a mechanism to turn off the admin port.

Risk Level: Medium

Testing: //test/...

Docs Changes: will be done with the PR that enables turning off admin.
Release Notes: N/A

jmarantz added 4 commits June 19, 2018 10:21
Signed-off-by: Joshua Marantz <jmarantz@google.com>
Signed-off-by: Joshua Marantz <jmarantz@google.com>
Signed-off-by: Joshua Marantz <jmarantz@google.com>
Signed-off-by: Joshua Marantz <jmarantz@google.com>
@jmarantz jmarantz changed the title WIP admin: Add an API on Server to initiate a programmatic admin request. admin: Add an API on Server to initiate a programmatic admin request. Jun 19, 2018
* Construct a flattened string from a buffer.
* @return the flattened string.
*/
virtual std::string toString() const PURE;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: this method was previously implemented as a test utility; I made it slightly faster by right-sizing the string buffer first, and moving it into source/...


namespace Envoy {
namespace Http {
namespace Utility {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note: I changed this from a class with all static methods into a namespace in order to be able to extract QueryParams into type that was accessible from interfaces in include/...

Textually it looks like this whole file changed but that's just because it lost 2 chars of indent and a lot of 'static' keywords. Really it just lost QueryParams, which is now in include/envoy/http/query_params.h

Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the heads up, this is useful.

Http::HeaderMapImpl response_headers;
std::string body;
Http::Code code = admin_->request(path_copy, params, method_copy, response_headers, body);
response_handler(code, response_headers, body);
Copy link
Member

Choose a reason for hiding this comment

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

Are you sure this is the pattern we want here? The callback is going to execute on some other thread (i.e. the main thread) to that of the caller. So, there will need to be some cross-thread synchronization happening. This has been a total nightmare in the FakeUpstream integration test code, and might lead to problematic main thread blocking.

An alternative would be to have the calling thread allocate a buffer, pass it to the post, wait using a CondVar for a response, and then take the buffer. That works nicely if the calling thread is not on an event loop. Alternatively, you post the response buffer to the event loop of the calling thread when done if async.

Copy link
Contributor Author

@jmarantz jmarantz Jun 19, 2018

Choose a reason for hiding this comment

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

per f2f:

  1. I will rename adminRequest to be adminRequestAsync, and add adminRequestBlocking, which will not use a function-pointer but block on the calling thread until the admin thread responds.
  2. I could also implement this by replacing the network socket with some other sort of local socket, such as socketpair(). I think that might be a similar amount of work or slightly less, but might present an OS porting barrier in the future.

Copy link
Member

Choose a reason for hiding this comment

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

Why isn't this always just happening on the main thread? Do we really need to support post here at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The caller will likely come from another thread, at least in our case.

Copy link
Member

Choose a reason for hiding this comment

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

WDYT about handling the threading complexity in our private code then? I think for most people a use of this would be on the main thread...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It sounds reasonable to have this PR just define Admin::request() requiring it be called from the main thread, so there's no complexity in this PR.

And I realize now that sever()->dispatcher()->post() provides all the infrastructure I'd need to call this from a different thread. I could make Admin::request() call DispatcherImpl::isThreadSafe() I suppose, if that were public, but if that's not generally going to happen I suppose it's sufficient to just document it.

I'm a little reluctant to do the post/block in this PR because that would tie up the calling thread, when the system we are merging it into might want to continue to do other work on that thread while the post is being processed.

@htuch htuch self-assigned this Jun 19, 2018
* @param path The admin request URL path as a string.
* @param params map of parameter names and values.
*/
virtual void adminRequest(absl::string_view path, const Http::Utility::QueryParams& params,
Copy link
Member

Choose a reason for hiding this comment

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

The server instance already exposes an Admin object. Seems like this isn't needed and should actually get routed through the admin object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fair point, that shouldn simplify the PR. Thanks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It turns out not to dramatically simplify the PR as I just pushed the mocking boilerplate out of server and into admin :)

jmarantz added 2 commits June 20, 2018 09:40
Signed-off-by: Joshua Marantz <jmarantz@google.com>
…d expose Admin::request() as an interface.

Signed-off-by: Joshua Marantz <jmarantz@google.com>
hooks_, restart_, stats_store_, fakelock_, component_factory_,
std::make_unique<NiceMock<Runtime::MockRandomGenerator>>(), thread_local_)),
EnvoyException, "unable to read file: ")
EnvoyException, "unable to read file: ");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how server_test.cc got checked in without the semi-colon, because without it, the auto-formatter scrambles this test-method, or the 'format' test should fail.

Copy link
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

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

This approach LGTM. Will defer to someone else for detailed review.

Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

Looks good modulo comments.


OwnedImpl::OwnedImpl(const void* data, uint64_t size) : OwnedImpl() { add(data, size); }

std::string OwnedImpl::toString() const {
Copy link
Member

Choose a reason for hiding this comment

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

How is this different to linearize()? Do we need both variants?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I looked at linearize and though they seem similar, I don't see how either can be efficiently implemented on the other.

Copy link
Member

Choose a reason for hiding this comment

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

well, you could linearize and then create a std;;string with the now linearized buffer. This would involve two copies, but is still O(n) and might be fine for the applications we would use this for, with the assumption being that you don't want to do toString() or linearize for anything performance sensitive anyway. There's also the option of just doing an absl::string_view around the linearized buffer.

Copy link
Contributor Author

@jmarantz jmarantz Jun 25, 2018

Choose a reason for hiding this comment

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

linearize mutates the buffer IIUC; I thought it made sense to have a simple observer that can return the fully flattened string.

Moreover it exists in the codebase now, in a test helper, and I'm mostly just moving it. However per your other comment -- I don't really need this in the interface, as everywhere in tests and in admin.cc where it's needed, I have an OwnedImpl. Removed.

Copy link
Member

Choose a reason for hiding this comment

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

I think mutating is a benefit here, it's kind of like defragging. At least naively, it seems you should be able to implement it about as efficiently as the toString() operation.


namespace Envoy {
namespace Http {
namespace Utility {
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the heads up, this is useful.

const Http::StreamDecoderFilterCallbacks& getDecoderFilterCallbacks() const override;
const Http::HeaderMap& getRequestHeaders() const override;

static void populateFallbackResponseHeaders(Http::Code code, Http::HeaderMap& header_map);
Copy link
Member

Choose a reason for hiding this comment

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

Should this just be in the anonymous namespace in the .cc?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, done.

filter.decodeHeaders(request_headers, false);

// TODO(jmarantz): make QueryParams a real class and put this serializer there,
// along with proper URL escaping of the name and value.
Copy link
Member

Choose a reason for hiding this comment

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

Yeah, ++escaping.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

TBH I think what I'd really like to do is change admin filter callbacks to take a const QueryParam& as an arg, rather than having each one parse it. Then serializing/parsing will be skipped and escaping will be unnecessary. Changing TODO. I don't want to tackle that in this PR though; will follow up.

Http::HeaderMapImpl response_headers;
std::string body;
EXPECT_EQ(Http::Code::OK,
admin_.request("/stats", Http::Utility::QueryParams({{"format", "json"}}), "GET",
Copy link
Member

Choose a reason for hiding this comment

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

Probably should have a test for > 1 query param, since there is slightly different logic (switches to & separator).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't find an admin handler that dealt with >1 query param, but I factored out the serialization to an http utility function and added tests for that to cover all the cases.

jmarantz added 2 commits June 22, 2018 09:28
Signed-off-by: Joshua Marantz <jmarantz@google.com>
Signed-off-by: Joshua Marantz <jmarantz@google.com>
@jmarantz jmarantz requested a review from zuercher as a code owner June 22, 2018 14:17
@jmarantz
Copy link
Contributor Author

@mrice32 @kmyerson @akonradi FYI

Signed-off-by: Joshua Marantz <jmarantz@google.com>
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

LGTM, but I'm still somewhat skeptical we need toString() added to the buffer virtual interface. Needs mater merge.


OwnedImpl::OwnedImpl(const void* data, uint64_t size) : OwnedImpl() { add(data, size); }

std::string OwnedImpl::toString() const {
Copy link
Member

Choose a reason for hiding this comment

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

well, you could linearize and then create a std;;string with the now linearized buffer. This would involve two copies, but is still O(n) and might be fine for the applications we would use this for, with the assumption being that you don't want to do toString() or linearize for anything performance sensitive anyway. There's also the option of just doing an absl::string_view around the linearized buffer.

EXPECT_EQ(0, buffer.length());
}

TEST_F(OwnedImplTest, toString) {
Copy link
Member

Choose a reason for hiding this comment

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

Nit: s/toString/ToString/

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, in this particular file most of the test methods start with lower-case letters, which may be more Envoyish? But lots of Envoy tests have test-methods starting with an upper-case letter. I'll just change all of them in this file to upper-case. SG?

Copy link
Member

Choose a reason for hiding this comment

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

Sure.

auto append = [&buffer](absl::string_view str) { buffer.add(str.data(), str.size()); };
append("Hello, ");
append("world!");
EXPECT_EQ("Hello, world!", buffer.toString());
Copy link
Member

Choose a reason for hiding this comment

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

Should we have more corner cases for the conversion?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question: what would you suggest? I actually don't know a ton about the underlying structure here.

Copy link
Member

Choose a reason for hiding this comment

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

At least empty, single span, two spans..

jmarantz added 3 commits June 24, 2018 22:23
… simplfy abstract Buffer interface.

Signed-off-by: Joshua Marantz <jmarantz@google.com>
…OwnedImpl.

Signed-off-by: Joshua Marantz <jmarantz@google.com>
Signed-off-by: Joshua Marantz <jmarantz@google.com>
Copy link
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

LGTM

@htuch htuch merged commit bfd2615 into envoyproxy:master Jun 25, 2018
@jmarantz jmarantz deleted the admin-port-as-callback branch June 25, 2018 21:49
*/
static std::string bufferToString(const Buffer::Instance& buffer);
static std::string bufferToString(const Buffer::OwnedImpl& buffer) {
// TODO(jmarantz): remove this indirection and update all ~53 call sites.
Copy link
Member

Choose a reason for hiding this comment

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

@jmarantz +1 can we do this as a cleanup sooner rather than later? Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants