Merge branch 'nghttpx-configrevision-api'

This commit is contained in:
Tatsuhiro Tsujikawa 2017-02-20 00:15:33 +09:00
commit 102b98813f
8 changed files with 281 additions and 17 deletions

View File

@ -559,6 +559,9 @@ status
code code
HTTP status code HTTP status code
Additionally, depending on the API endpoint, ``data`` key may be
present, and its value contains the API endpoint specific data.
We wrote "normally", since nghttpx may return ordinal HTML response in We wrote "normally", since nghttpx may return ordinal HTML response in
some cases where the error has occurred before reaching API endpoint some cases where the error has occurred before reaching API endpoint
(e.g., header field is too large). (e.g., header field is too large).
@ -590,6 +593,23 @@ The one limitation is that only numeric IP address is allowd in
is used while non numeric hostname is allowed in command-line or is used while non numeric hostname is allowed in command-line or
configuration file is read using :option:`--conf`. configuration file is read using :option:`--conf`.
GET /api/v1beta1/configrevision
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This API returns configuration revision of the current nghttpx. The
configuration revision is opaque string, and it changes after each
reloading by SIGHUP. With this API, an external application knows
that whether nghttpx has finished reloading its configuration by
comparing the configuration revisions between before and after
reloading.
This API returns response including ``data`` key. Its value is JSON
object, and it contains at least the following key:
configRevision
The configuration revision of the current nghttpx
SEE ALSO SEE ALSO
-------- --------

View File

@ -974,6 +974,43 @@ backend=127.0.0.1,3011
} }
} }
// TestH1APIConfigrevision tests configrevision API.
func TestH1APIConfigrevision(t *testing.T) {
st := newServerTesterConnectPort([]string{"-f127.0.0.1,3010;api;no-tls"}, t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("request should not be forwarded")
}, 3010)
defer st.Close()
res, err := st.http1(requestParam{
name: "TestH1APIConfigrevision",
path: "/api/v1beta1/configrevision",
method: "GET",
})
if err != nil {
t.Fatalf("Error st.http1() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("res.status: %v; want = %v", got, want)
}
var apiResp APIResponse
d := json.NewDecoder(bytes.NewBuffer(res.body))
d.UseNumber()
err = d.Decode(&apiResp)
if err != nil {
t.Fatalf("Error unmarshalling API response: %v", err)
}
if got, want := apiResp.Status, "Success"; got != want {
t.Errorf("apiResp.Status: %v; want %v", got, want)
}
if got, want := apiResp.Code, 200; got != want {
t.Errorf("apiResp.Status: %v; want %v", got, want)
}
if got, want := apiResp.Data["configRevision"], json.Number("0"); got != want {
t.Errorf(`apiResp.Data["configRevision"]: %v %t; want %v`, got, got, want)
}
}
// TestH1APINotFound exercise backendconfig API endpoint routine when // TestH1APINotFound exercise backendconfig API endpoint routine when
// API endpoint is not found. // API endpoint is not found.
func TestH1APINotFound(t *testing.T) { func TestH1APINotFound(t *testing.T) {

View File

@ -1,6 +1,7 @@
package nghttp2 package nghttp2
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -2071,6 +2072,43 @@ backend=127.0.0.1,3011
} }
} }
// TestH2APIConfigrevision tests configrevision API.
func TestH2APIConfigrevision(t *testing.T) {
st := newServerTesterConnectPort([]string{"-f127.0.0.1,3010;api;no-tls"}, t, func(w http.ResponseWriter, r *http.Request) {
t.Fatalf("request should not be forwarded")
}, 3010)
defer st.Close()
res, err := st.http2(requestParam{
name: "TestH2APIConfigrevision",
path: "/api/v1beta1/configrevision",
method: "GET",
})
if err != nil {
t.Fatalf("Error st.http2() = %v", err)
}
if got, want := res.status, 200; got != want {
t.Errorf("res.status: %v; want = %v", got, want)
}
var apiResp APIResponse
d := json.NewDecoder(bytes.NewBuffer(res.body))
d.UseNumber()
err = d.Decode(&apiResp)
if err != nil {
t.Fatalf("Error unmarshalling API response: %v", err)
}
if got, want := apiResp.Status, "Success"; got != want {
t.Errorf("apiResp.Status: %v; want %v", got, want)
}
if got, want := apiResp.Code, 200; got != want {
t.Errorf("apiResp.Status: %v; want %v", got, want)
}
if got, want := apiResp.Data["configRevision"], json.Number("0"); got != want {
t.Errorf(`apiResp.Data["configRevision"]: %v %t; want %v`, got, got, want)
}
}
// TestH2APINotFound exercise backendconfig API endpoint routine when // TestH2APINotFound exercise backendconfig API endpoint routine when
// API endpoint is not found. // API endpoint is not found.
func TestH2APINotFound(t *testing.T) { func TestH2APINotFound(t *testing.T) {

View File

@ -801,6 +801,7 @@ func cloneHeader(h http.Header) http.Header {
func noopHandler(w http.ResponseWriter, r *http.Request) {} func noopHandler(w http.ResponseWriter, r *http.Request) {}
type APIResponse struct { type APIResponse struct {
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Code int `json:"code,omitempty"` Code int `json:"code,omitempty"`
Data map[string]interface{} `json:"data,omitempty"`
} }

View File

@ -2942,6 +2942,7 @@ void reload_config(WorkerProcess *wp) {
new_config->daemon = cur_config->daemon; new_config->daemon = cur_config->daemon;
// loop is reused, and ev_loop_flags gets ignored // loop is reused, and ev_loop_flags gets ignored
new_config->ev_loop_flags = cur_config->ev_loop_flags; new_config->ev_loop_flags = cur_config->ev_loop_flags;
new_config->config_revision = cur_config->config_revision + 1;
rv = process_options(new_config.get(), suconfig.cmdcfgs); rv = process_options(new_config.get(), suconfig.cmdcfgs);
if (rv != 0) { if (rv != 0) {

View File

@ -33,8 +33,31 @@
namespace shrpx { namespace shrpx {
namespace {
// List of API endpoints
const APIEndpoint apis[] = {
APIEndpoint{
StringRef::from_lit("/api/v1beta1/backendconfig"), true,
(1 << API_METHOD_POST) | (1 << API_METHOD_PUT),
&APIDownstreamConnection::handle_backendconfig,
},
APIEndpoint{
StringRef::from_lit("/api/v1beta1/configrevision"), true,
(1 << API_METHOD_GET), &APIDownstreamConnection::handle_configrevision,
},
};
} // namespace
namespace {
// The method string. This must be same order of APIMethod.
constexpr StringRef API_METHOD_STRING[] = {
StringRef::from_lit("GET"), StringRef::from_lit("POST"),
StringRef::from_lit("PUT"),
};
} // namespace
APIDownstreamConnection::APIDownstreamConnection(Worker *worker) APIDownstreamConnection::APIDownstreamConnection(Worker *worker)
: worker_(worker), abandoned_(false) {} : worker_(worker), api_(nullptr), shutdown_read_(false) {}
APIDownstreamConnection::~APIDownstreamConnection() {} APIDownstreamConnection::~APIDownstreamConnection() {}
@ -63,8 +86,8 @@ enum {
}; };
int APIDownstreamConnection::send_reply(unsigned int http_status, int APIDownstreamConnection::send_reply(unsigned int http_status,
int api_status) { int api_status, const StringRef &data) {
abandoned_ = true; shutdown_read_ = true;
auto upstream = downstream_->get_upstream(); auto upstream = downstream_->get_upstream();
@ -93,7 +116,8 @@ int APIDownstreamConnection::send_reply(unsigned int http_status,
// 3 is the number of digits in http_status, assuming it is 3 digits // 3 is the number of digits in http_status, assuming it is 3 digits
// number. // number.
auto buflen = M1.size() + M2.size() + M3.size() + api_status_str.size() + 3; auto buflen = M1.size() + M2.size() + M3.size() + data.size() +
api_status_str.size() + 3;
auto buf = make_byte_ref(balloc, buflen); auto buf = make_byte_ref(balloc, buflen);
auto p = buf.base; auto p = buf.base;
@ -102,6 +126,7 @@ int APIDownstreamConnection::send_reply(unsigned int http_status,
p = std::copy(std::begin(api_status_str), std::end(api_status_str), p); p = std::copy(std::begin(api_status_str), std::end(api_status_str), p);
p = std::copy(std::begin(M2), std::end(M2), p); p = std::copy(std::begin(M2), std::end(M2), p);
p = util::utos(p, http_status); p = util::utos(p, http_status);
p = std::copy(std::begin(data), std::end(data), p);
p = std::copy(std::begin(M3), std::end(M3), p); p = std::copy(std::begin(M3), std::end(M3), p);
buf.len = p - buf.base; buf.len = p - buf.base;
@ -128,25 +153,68 @@ int APIDownstreamConnection::send_reply(unsigned int http_status,
return 0; return 0;
} }
namespace {
const APIEndpoint *lookup_api(const StringRef &path) {
switch (path.size()) {
case 26:
switch (path[25]) {
case 'g':
if (util::streq_l("/api/v1beta1/backendconfi", std::begin(path), 25)) {
return &apis[0];
}
break;
}
break;
case 27:
switch (path[26]) {
case 'n':
if (util::streq_l("/api/v1beta1/configrevisio", std::begin(path), 26)) {
return &apis[1];
}
break;
}
break;
}
return nullptr;
}
} // namespace
int APIDownstreamConnection::push_request_headers() { int APIDownstreamConnection::push_request_headers() {
auto &req = downstream_->request(); auto &req = downstream_->request();
auto &resp = downstream_->response();
auto path = auto path =
StringRef{std::begin(req.path), StringRef{std::begin(req.path),
std::find(std::begin(req.path), std::end(req.path), '?')}; std::find(std::begin(req.path), std::end(req.path), '?')};
if (path != StringRef::from_lit("/api/v1beta1/backendconfig")) { api_ = lookup_api(path);
if (!api_) {
send_reply(404, API_FAILURE); send_reply(404, API_FAILURE);
return 0; return 0;
} }
if (req.method != HTTP_POST && req.method != HTTP_PUT) { switch (req.method) {
resp.fs.add_header_token(StringRef::from_lit("allow"), case HTTP_GET:
StringRef::from_lit("POST, PUT"), false, -1); if (!(api_->allowed_methods & (1 << API_METHOD_GET))) {
send_reply(405, API_FAILURE); error_method_not_allowed();
return 0;
}
break;
case HTTP_POST:
if (!(api_->allowed_methods & (1 << API_METHOD_POST))) {
error_method_not_allowed();
return 0;
}
break;
case HTTP_PUT:
if (!(api_->allowed_methods & (1 << API_METHOD_PUT))) {
error_method_not_allowed();
return 0;
}
break;
default:
error_method_not_allowed();
return 0; return 0;
} }
@ -161,9 +229,42 @@ int APIDownstreamConnection::push_request_headers() {
return 0; return 0;
} }
int APIDownstreamConnection::error_method_not_allowed() {
auto &resp = downstream_->response();
size_t len = 0;
for (uint8_t i = 0; i < API_METHOD_MAX; ++i) {
if (api_->allowed_methods & (1 << i)) {
// The length of method + ", "
len += API_METHOD_STRING[i].size() + 2;
}
}
assert(len > 0);
auto &balloc = downstream_->get_block_allocator();
auto iov = make_byte_ref(balloc, len + 1);
auto p = iov.base;
for (uint8_t i = 0; i < API_METHOD_MAX; ++i) {
if (api_->allowed_methods & (1 << i)) {
auto &s = API_METHOD_STRING[i];
p = std::copy(std::begin(s), std::end(s), p);
p = std::copy_n(", ", 2, p);
}
}
p -= 2;
*p = '\0';
resp.fs.add_header_token(StringRef::from_lit("allow"), StringRef{iov.base, p},
false, -1);
return send_reply(405, API_FAILURE);
}
int APIDownstreamConnection::push_upload_data_chunk(const uint8_t *data, int APIDownstreamConnection::push_upload_data_chunk(const uint8_t *data,
size_t datalen) { size_t datalen) {
if (abandoned_) { if (shutdown_read_ || !api_->require_body) {
return 0; return 0;
} }
@ -187,10 +288,14 @@ int APIDownstreamConnection::push_upload_data_chunk(const uint8_t *data,
} }
int APIDownstreamConnection::end_upload_data() { int APIDownstreamConnection::end_upload_data() {
if (abandoned_) { if (shutdown_read_) {
return 0; return 0;
} }
return api_->handler(*this);
}
int APIDownstreamConnection::handle_backendconfig() {
auto output = downstream_->get_request_buf(); auto output = downstream_->get_request_buf();
std::array<struct iovec, 2> iov; std::array<struct iovec, 2> iov;
@ -287,6 +392,25 @@ int APIDownstreamConnection::end_upload_data() {
return 0; return 0;
} }
int APIDownstreamConnection::handle_configrevision() {
auto config = get_config();
auto &balloc = downstream_->get_block_allocator();
// Construct the following string:
// ,
// "data":{
// "configRevision": N
// }
auto data = concat_string_ref(
balloc, StringRef::from_lit(R"(,"data":{"configRevision":)"),
util::make_string_ref_uint(balloc, config->config_revision),
StringRef::from_lit("}"));
send_reply(200, API_SUCCESS, data);
return 0;
}
void APIDownstreamConnection::pause_read(IOCtrlReason reason) {} void APIDownstreamConnection::pause_read(IOCtrlReason reason) {}
int APIDownstreamConnection::resume_read(IOCtrlReason reason, size_t consumed) { int APIDownstreamConnection::resume_read(IOCtrlReason reason, size_t consumed) {

View File

@ -26,11 +26,36 @@
#define SHRPX_API_DOWNSTREAM_CONNECTION_H #define SHRPX_API_DOWNSTREAM_CONNECTION_H
#include "shrpx_downstream_connection.h" #include "shrpx_downstream_connection.h"
#include "template.h"
using namespace nghttp2;
namespace shrpx { namespace shrpx {
class Worker; class Worker;
// If new method is added, don't forget to update API_METHOD_STRING as
// well.
enum APIMethod {
API_METHOD_GET,
API_METHOD_POST,
API_METHOD_PUT,
API_METHOD_MAX,
};
class APIDownstreamConnection;
struct APIEndpoint {
// Endpoint path. It must start with "/api/".
StringRef path;
// true if we evaluate request body.
bool require_body;
// Allowed methods. This is bitwise OR of one or more of (1 <<
// APIMethod value).
uint8_t allowed_methods;
std::function<int(APIDownstreamConnection &)> handler;
};
class APIDownstreamConnection : public DownstreamConnection { class APIDownstreamConnection : public DownstreamConnection {
public: public:
APIDownstreamConnection(Worker *worker); APIDownstreamConnection(Worker *worker);
@ -58,11 +83,21 @@ public:
get_downstream_addr_group() const; get_downstream_addr_group() const;
virtual DownstreamAddr *get_addr() const; virtual DownstreamAddr *get_addr() const;
int send_reply(unsigned int http_status, int api_status); int send_reply(unsigned int http_status, int api_status,
const StringRef &data = StringRef{});
int error_method_not_allowed();
// Handles backendconfig API request.
int handle_backendconfig();
// Handles configrevision API request.
int handle_configrevision();
private: private:
Worker *worker_; Worker *worker_;
bool abandoned_; // This points to the requested APIEndpoint struct.
const APIEndpoint *api_;
// true if we stop reading request body.
bool shutdown_read_;
}; };
} // namespace shrpx } // namespace shrpx

View File

@ -850,6 +850,7 @@ struct Config {
conn{}, conn{},
api{}, api{},
dns{}, dns{},
config_revision{0},
num_worker{0}, num_worker{0},
padding{0}, padding{0},
rlimit_nofile{0}, rlimit_nofile{0},
@ -883,6 +884,13 @@ struct Config {
StringRef conf_path; StringRef conf_path;
StringRef user; StringRef user;
StringRef mruby_file; StringRef mruby_file;
// The revision of configuration which is opaque string, and changes
// on each configuration reloading. This does not change on
// backendconfig API call. This value is returned in health check
// as "nghttpx-conf-rev" response header field. The external
// program can check this value to know whether reloading has
// completed or not.
uint64_t config_revision;
size_t num_worker; size_t num_worker;
size_t padding; size_t padding;
size_t rlimit_nofile; size_t rlimit_nofile;