Added observe endpoint to master. This changes adds a new HTTP endpoint of observe to master. This allows clients to report health via an HTTP POST. Values are: MONITOR = Monitor for which health is being reported. HOSTS = Comma seperated list of hosts. LEVEL = OK for healthy, anything else for unhealthy.
This also contains a small fix to alphabetize the existing endpoints / help strings. Review: https://reviews.apache.org/r/17255 Project: http://git-wip-us.apache.org/repos/asf/mesos/repo Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/165dbe10 Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/165dbe10 Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/165dbe10 Branch: refs/heads/master Commit: 165dbe101812f50780a802c8eea0dd5579678d96 Parents: 5c2f4f7 Author: Charlie Carson <[email protected]> Authored: Fri Feb 14 17:07:35 2014 -0800 Committer: Vinod Kone <[email protected]> Committed: Fri Feb 14 17:07:35 2014 -0800 ---------------------------------------------------------------------- src/Makefile.am | 1 + src/master/http.cpp | 99 ++++++++++++++++++++++ src/master/master.cpp | 9 +- src/master/master.hpp | 13 ++- src/tests/repair_tests.cpp | 176 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 291 insertions(+), 7 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mesos/blob/165dbe10/src/Makefile.am ---------------------------------------------------------------------- diff --git a/src/Makefile.am b/src/Makefile.am index cfd7416..768c66a 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -870,6 +870,7 @@ mesos_tests_SOURCES = \ tests/paths_tests.cpp \ tests/protobuf_io_tests.cpp \ tests/registrar_tests.cpp \ + tests/repair_tests.cpp \ tests/resource_offers_tests.cpp \ tests/resources_tests.cpp \ tests/sasl_tests.cpp \ http://git-wip-us.apache.org/repos/asf/mesos/blob/165dbe10/src/master/http.cpp ---------------------------------------------------------------------- diff --git a/src/master/http.cpp b/src/master/http.cpp index 966eed6..6aeb257 100644 --- a/src/master/http.cpp +++ b/src/master/http.cpp @@ -22,6 +22,8 @@ #include <string> #include <vector> +#include <boost/array.hpp> + #include <mesos/mesos.hpp> #include <mesos/resources.hpp> @@ -278,6 +280,103 @@ Future<Response> Master::Http::health(const Request& request) return OK(); } +const static string HOSTS_KEY = "hosts"; +const static string LEVEL_KEY = "level"; +const static string MONITOR_KEY = "monitor"; + +const string Master::Http::OBSERVE_HELP = HELP( + TLDR( + "Observe a monitor health state for host(s)."), + USAGE( + "/master/observe"), + DESCRIPTION( + "This endpoint receives information indicating host(s) ", + "health." + "", + "The following fields should be supplied in a POST:", + "1. " + MONITOR_KEY + " - name of the monitor that is being reported", + "2. " + HOSTS_KEY + " - comma seperated list of hosts", + "3. " + LEVEL_KEY + " - OK for healthy, anything else for unhealthy")); + + +Try<string> getFormValue( + const string& key, + const hashmap<string, string>& values) +{ + Option<string> value = values.get(key); + + if (value.isNone()) { + return Error("Missing value for '" + key + "'."); + } + + // HTTP decode the value. + Try<string> decodedValue = http::decode(value.get()); + if (decodedValue.isError()) { + return decodedValue; + } + + // Treat empty string as an error. + if (decodedValue.isSome() && decodedValue.get().empty()) { + return Error("Empty string for '" + key + "'."); + } + + return decodedValue.get(); +} + + +Future<Response> Master::Http::observe(const Request& request) +{ + LOG(INFO) << "HTTP request for '" << request.path << "'"; + + hashmap<string, string> values = + process::http::query::parse(request.body); + + // Build up a JSON object of the values we recieved and send them back + // down the wire as JSON for validation / confirmation. + JSON::Object response; + + // TODO(ccarson): As soon as RepairCoordinator is introduced it will + // consume these values. We should revisit if we still want to send the + // JSON down the wire at that point. + + // Add 'monitor'. + Try<string> monitor = getFormValue(MONITOR_KEY, values); + if (monitor.isError()) { + return BadRequest(monitor.error()); + } + response.values[MONITOR_KEY] = monitor.get(); + + // Add 'hosts'. + Try<string> hostsString = getFormValue(HOSTS_KEY, values); + if (hostsString.isError()) { + return BadRequest(hostsString.error()); + } + + vector<string> hosts = strings::split(hostsString.get(), ","); + JSON::Array hostArray; + hostArray.values.assign(hosts.begin(), hosts.end()); + + response.values[HOSTS_KEY] = hostArray; + + // Add 'isHealthy'. + Try<string> level = getFormValue(LEVEL_KEY, values); + if (level.isError()) { + return BadRequest(level.error()); + } + + bool isHealthy = strings::upper(level.get()) == "OK"; + + // TODO(ccarson): This is a workaround b/c currently a bool is coerced + // into a JSON::Double instead of a JSON::True or JSON::False when + // you assign to a JSON::Value. + // + // SEE: https://issues.apache.org/jira/browse/MESOS-939 + response.values["isHealthy"] = + (isHealthy ? JSON::Value(JSON::True()) : JSON::False()); + + return OK(response); +} + const string Master::Http::REDIRECT_HELP = HELP( TLDR( http://git-wip-us.apache.org/repos/asf/mesos/blob/165dbe10/src/master/master.cpp ---------------------------------------------------------------------- diff --git a/src/master/master.cpp b/src/master/master.cpp index f24df23..f4f5e04 100644 --- a/src/master/master.cpp +++ b/src/master/master.cpp @@ -462,16 +462,19 @@ void Master::initialize() route("/health", Http::HEALTH_HELP, lambda::bind(&Http::health, http, lambda::_1)); + route("/observe", + Http::OBSERVE_HELP, + lambda::bind(&Http::observe, http, lambda::_1)); route("/redirect", Http::REDIRECT_HELP, lambda::bind(&Http::redirect, http, lambda::_1)); - route("/stats.json", + route("/roles.json", None(), - lambda::bind(&Http::stats, http, lambda::_1)); + lambda::bind(&Http::roles, http, lambda::_1)); route("/state.json", None(), lambda::bind(&Http::state, http, lambda::_1)); - route("/roles.json", + route("/stats.json", None(), lambda::bind(&Http::roles, http, lambda::_1)); http://git-wip-us.apache.org/repos/asf/mesos/blob/165dbe10/src/master/master.hpp ---------------------------------------------------------------------- diff --git a/src/master/master.hpp b/src/master/master.hpp index 00d630a..9d1b56c 100644 --- a/src/master/master.hpp +++ b/src/master/master.hpp @@ -280,20 +280,24 @@ private: process::Future<process::http::Response> health( const process::http::Request& request); + // /master/observe + process::Future<process::http::Response> observe( + const process::http::Request& request); + // /master/redirect process::Future<process::http::Response> redirect( const process::http::Request& request); - // /master/stats.json - process::Future<process::http::Response> stats( + // /master/roles.json + process::Future<process::http::Response> roles( const process::http::Request& request); // /master/state.json process::Future<process::http::Response> state( const process::http::Request& request); - // /master/roles.json - process::Future<process::http::Response> roles( + // /master/stats.json + process::Future<process::http::Response> stats( const process::http::Request& request); // /master/tasks.json @@ -301,6 +305,7 @@ private: const process::http::Request& request); const static std::string HEALTH_HELP; + const static std::string OBSERVE_HELP; const static std::string REDIRECT_HELP; const static std::string TASKS_HELP; http://git-wip-us.apache.org/repos/asf/mesos/blob/165dbe10/src/tests/repair_tests.cpp ---------------------------------------------------------------------- diff --git a/src/tests/repair_tests.cpp b/src/tests/repair_tests.cpp new file mode 100644 index 0000000..ba6f50a --- /dev/null +++ b/src/tests/repair_tests.cpp @@ -0,0 +1,176 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> +#include <vector> + +#include <process/future.hpp> +#include <process/gtest.hpp> +#include <process/http.hpp> +#include <process/pid.hpp> +#include <process/process.hpp> + +#include <stout/json.hpp> + +#include "tests/mesos.hpp" + + +using namespace mesos; +using namespace mesos::internal; +using namespace mesos::internal::tests; + +using mesos::internal::master::Master; + +using process::Future; +using process::PID; + +using process::http::BadRequest; +using process::http::OK; +using process::http::Response; + +using std::string; +using std::vector; + +using testing::_; + + +class HealthTest : public MesosTest {}; + + +struct JsonResponse +{ + string monitor; + vector<string> hosts; + bool isHealthy; +}; + + +string stringify(const JsonResponse& response) +{ + JSON::Object object; + object.values["monitor"] = response.monitor; + + JSON::Array hosts; + hosts.values.assign(response.hosts.begin(), response.hosts.end()); + object.values["hosts"] = hosts; + + // TODO(ccarson): This is a workaround b/c currently a bool is coerced + // into a JSON::Double instead of a JSON::True or JSON::False when + // you assign to a JSON::Value. + // + // SEE: https://issues.apache.org/jira/browse/MESOS-939 + object.values["isHealthy"] = + response.isHealthy ? JSON::Value(JSON::True()) : JSON::False(); + + return stringify(object); +} + + +// Using macros instead of a helper function so that we get good line +// numbers from the test run. +#define VALIDATE_BAD_RESPONSE(response, error) \ + AWAIT_READY(response); \ + AWAIT_EXPECT_RESPONSE_STATUS_EQ(BadRequest().status, response); \ + AWAIT_EXPECT_RESPONSE_BODY_EQ(error, response) + +#define VALIDATE_GOOD_RESPONSE(response, jsonResponse) \ + AWAIT_READY(response); \ + AWAIT_EXPECT_RESPONSE_HEADER_EQ( \ + "application/json", \ + "Content-Type", \ + response); \ + AWAIT_EXPECT_RESPONSE_STATUS_EQ(OK().status, response); \ + AWAIT_EXPECT_RESPONSE_BODY_EQ(jsonResponse, response); + + +TEST_F(HealthTest, ObserveEndpoint) +{ + Try<PID<Master> > master = StartMaster(); + ASSERT_SOME(master); + + // Empty get to the observe endpoint. + Future<Response> response = process::http::get(master.get(), "observe"); + VALIDATE_BAD_RESPONSE(response, "Missing value for 'monitor'."); + + // Empty post to the observe endpoint. + response = process::http::post(master.get(), "observe"); + VALIDATE_BAD_RESPONSE(response, "Missing value for 'monitor'."); + + // Query string is ignored. + response = process::http::post(master.get(), "observe?monitor=foo"); + VALIDATE_BAD_RESPONSE(response, "Missing value for 'monitor'."); + + // Malformed value causes error. + response = process::http::post(master.get(), "observe", "monitor=foo%"); + VALIDATE_BAD_RESPONSE(response, "Malformed % escape in 'foo%': '%'"); + + // Empty value causes error. + response = process::http::post(master.get(), "observe", "monitor="); + VALIDATE_BAD_RESPONSE(response, "Empty string for 'monitor'."); + + // Missing hosts. + response = process::http::post(master.get(), "observe", "monitor=a"); + VALIDATE_BAD_RESPONSE(response, "Missing value for 'hosts'."); + + // Missing level. + response = process::http::post(master.get(), "observe", "monitor=a&hosts=b"); + VALIDATE_BAD_RESPONSE(response, "Missing value for 'level'."); + + // Good request is successful. + JsonResponse expected; + expected.monitor = "a"; + expected.hosts.push_back("b"); + expected.isHealthy = true; + + response = + process::http::post(master.get(), "observe", "monitor=a&hosts=b&level=ok"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + // ok is case-insensitive. + response = + process::http::post(master.get(), "observe", "monitor=a&hosts=b&level=Ok"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + response = + process::http::post(master.get(), "observe", "monitor=a&hosts=b&level=oK"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + response = + process::http::post(master.get(), "observe", "monitor=a&hosts=b&level=OK"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + // level != OK is unhealthy. + expected.isHealthy = false; + response = + process::http::post( + master.get(), + "observe", + "monitor=a&hosts=b&level=true"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + // Comma seperated hosts are parsed into an array. + expected.hosts.push_back("e"); + response = + process::http::post( + master.get(), + "observe", + "monitor=a&hosts=b,e&level=true"); + VALIDATE_GOOD_RESPONSE(response, stringify(expected) ); + + Shutdown(); +}
