diff --git a/Makefile b/Makefile index 574c63bbfb..ca64aaa4bb 100644 --- a/Makefile +++ b/Makefile @@ -203,6 +203,15 @@ BRPC_PROTOS = $(filter %.proto,$(BRPC_SOURCES)) BRPC_CFAMILIES = $(filter-out %.proto %.pb.cc,$(BRPC_SOURCES)) BRPC_OBJS = $(BRPC_PROTOS:.proto=.pb.o) $(addsuffix .o, $(basename $(BRPC_CFAMILIES))) +ETCD_CLIENT_PROTOS = src/etcd_client/proto/kv.proto \ + src/etcd_client/proto/auth.proto \ + src/etcd_client/proto/rpc.proto \ + src/etcd_client/proto/gogoproto/gogo.proto\ + src/etcd_client/proto/google/api/annotations.proto \ + src/etcd_client/proto/google/api/http.proto +ETCD_CLIENT_SOURCES = src/etcd_client/etcd_client.cpp +ETCD_CLIENT_OBJS = $(ETCD_CLIENT_PROTOS:.proto=.pb.o) $(addsuffix .o, $(basename $(ETCD_CLIENT_SOURCES))) + MCPACK2PB_SOURCES = \ src/mcpack2pb/field_type.cpp \ src/mcpack2pb/mcpack2pb.cpp \ @@ -214,12 +223,12 @@ ifeq (ENABLE_THRIFT_FRAMED_PROTOCOL, $(findstring ENABLE_THRIFT_FRAMED_PROTOCOL, THRIFT_OBJS = $(addsuffix .o, $(basename $(THRIFT_SOURCES))) endif -OBJS=$(BUTIL_OBJS) $(BVAR_OBJS) $(BTHREAD_OBJS) $(JSON2PB_OBJS) $(MCPACK2PB_OBJS) $(BRPC_OBJS) $(THRIFT_OBJS) +OBJS=$(BUTIL_OBJS) $(BVAR_OBJS) $(BTHREAD_OBJS) $(JSON2PB_OBJS) $(MCPACK2PB_OBJS) $(BRPC_OBJS) $(THRIFT_OBJS) $(ETCD_CLIENT_OBJS) BVAR_DEBUG_OBJS=$(BUTIL_OBJS:.o=.dbg.o) $(BVAR_OBJS:.o=.dbg.o) DEBUG_OBJS = $(OBJS:.o=.dbg.o) -PROTOS=$(BRPC_PROTOS) src/idl_options.proto +PROTOS=$(BRPC_PROTOS) src/idl_options.proto $(ETCD_CLIENT_PROTOS) .PHONY:all all: protoc-gen-mcpack libbrpc.a libbrpc.$(SOEXT) output/include output/lib output/bin diff --git a/example/etcdctl_c++/etcdctl.cpp b/example/etcdctl_c++/etcdctl.cpp new file mode 100644 index 0000000000..c60f412b6b --- /dev/null +++ b/example/etcdctl_c++/etcdctl.cpp @@ -0,0 +1,91 @@ +// 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 +#include +#include +#include +#include +#include +#include "butil/base64.h" +#include "google/protobuf/util/json_util.h" +#include "etcd_client/proto/rpc.pb.h" +#include "etcd_client/etcd_client.h" + +DEFINE_string(url, "localhost:2379", "connect to etcd server"); +DEFINE_string(api, "", "etcd api"); +DEFINE_string(key, "", "key"); +DEFINE_string(value, "", "value"); +DEFINE_string(range_end, "", "range end"); + + +class MyWatcher : public brpc::Watcher { + public: + void OnEventResponse(const ::etcdserverpb::WatchResponse& response) override { + LOG(INFO) << response.DebugString(); + } + void OnParseError() { + LOG(INFO) << "parser error"; + } +}; + +int main(int argc, char* argv[]) { + // Parse gflags. We recommend you to use gflags as well. + gflags::ParseCommandLineFlags(&argc, &argv, true); + + brpc::EtcdClient client; + LOG(INFO) << "FLAGS_url " << FLAGS_url; + const char* url = FLAGS_url.c_str(); + client.Init(url); + if (FLAGS_api == "watch") { + std::shared_ptr watcher(new MyWatcher); + ::etcdserverpb::WatchRequest request; + auto* create_request = request.mutable_create_request(); + create_request->set_key(FLAGS_key); + if (!FLAGS_range_end.empty()) { + create_request->set_range_end(FLAGS_range_end); + } + client.Watch(request, watcher); + while (1) { + // ctrl c to exit + sleep(10); + } + } else if (FLAGS_api == "put") { + ::etcdserverpb::PutRequest request; + ::etcdserverpb::PutResponse response; + request.set_key(FLAGS_key); + if (!FLAGS_value.empty()) { + request.set_value(FLAGS_value); + } + LOG(INFO) << "Put Request: " << request.DebugString(); + client.Put(request, &response); + LOG(INFO) << "Put response: " << response.DebugString(); + } else if (FLAGS_api == "range") { + ::etcdserverpb::RangeRequest request; + ::etcdserverpb::RangeResponse response; + request.set_key(FLAGS_key); + if (!FLAGS_range_end.empty()) { + request.set_range_end(FLAGS_range_end); + } + LOG(INFO) << "Range Request: " << request.DebugString(); + client.Range(request, &response); + LOG(INFO) << "Range response: " << response.DebugString(); + } else { + LOG(ERROR) << "unknown api " << FLAGS_api; + } + return 0; +} diff --git a/src/etcd_client/etcd_client.cpp b/src/etcd_client/etcd_client.cpp new file mode 100644 index 0000000000..68a41ee49b --- /dev/null +++ b/src/etcd_client/etcd_client.cpp @@ -0,0 +1,265 @@ +// 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 "etcd_client/etcd_client.h" +#include +#include "butil/string_splitter.h" +#include "json2pb/pb_to_json.h" +#include "json2pb/json_to_pb.h" + +namespace brpc { + +EtcdClient::EtcdClient() { +} + +EtcdClient::~EtcdClient() { +} + +bool EtcdClient::Init(const std::string& url, const std::string& lb) { + brpc::ChannelOptions options; + options.protocol = "http"; + if (_channel.Init(url.c_str(), lb.c_str(), &options) != 0) { + LOG(ERROR) << "Fail to initialize channel with " << url; + return false; + } + return true; +} + +bool EtcdClient::Init(butil::EndPoint server_addr_and_port) { + brpc::ChannelOptions options; + options.protocol = "http"; + if (_channel.Init(server_addr_and_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel with " << server_addr_and_port; + return false; + } + return true; +} + +bool EtcdClient::Init(const char* server_addr_and_port) { + brpc::ChannelOptions options; + options.protocol = "http"; + if (_channel.Init(server_addr_and_port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel with " << server_addr_and_port; + return false; + } + return true; +} + +bool EtcdClient::Init(const char* server_addr, int port) { + brpc::ChannelOptions options; + options.protocol = "http"; + if (_channel.Init(server_addr, port, &options) != 0) { + LOG(ERROR) << "Fail to initialize channel with " << server_addr + << ":" << port; + return false; + } + return true; +} + +enum EtcdOps { + RANGE = 0, + PUT, + DELETE_RANGE, + TXN, + COMPACT, + LEASE_GRANT, + LEASE_REVOKE, + LEASE_KEEPALIVE, + LEASE_TTL, + LEASE_LEASES, +}; + +template +static bool EtcdOp(brpc::Channel* channel, EtcdOps op, const Request& request, Response* response) { + // invoker will make sure the op is valid + std::string path = ""; + switch (op) { + case RANGE : path = "/v3/kv/range"; break; + case PUT : path = "/v3/kv/put"; break; + case DELETE_RANGE : path = "/v3/kv/deleterange"; break; + case TXN : path = "/v3/kv/txn"; break; + case COMPACT : path = "/v3/kv/compaction"; break; + case LEASE_GRANT : path = "/v3/lease/grant"; break; + case LEASE_REVOKE : path = "/v3/lease/revoke"; break; + case LEASE_KEEPALIVE : path = "/v3/lease/keepalive"; break; + case LEASE_TTL : path = "/v3/lease/timetolive"; break; + case LEASE_LEASES : path = "/v3/lease/leases"; break; + default: + LOG(ERROR) << "Invalid operation " << op << " with " << request.ShortDebugString(); + return false; + } + Controller cntl; + brpc::URI& uri = cntl.http_request().uri(); + uri.set_path(path); + cntl.http_request().set_method(brpc::HTTP_METHOD_POST); + std::string output; + struct json2pb::Pb2JsonOptions pb2json_option; + pb2json_option.bytes_to_base64 = true; + if (!json2pb::ProtoMessageToJson(request, &output, pb2json_option)) { + LOG(ERROR) << "convert PutRequest " << request.ShortDebugString() << " to json fail."; + return false; + } + cntl.request_attachment().append(output); + channel->CallMethod(NULL, &cntl, NULL, NULL, NULL); + if (cntl.Failed()) { + LOG(ERROR) << "request " << request.ShortDebugString() << " to etcd server fail." + << "error message : " << cntl.ErrorText(); + return false; + } + struct json2pb::Json2PbOptions json2pb_option; + json2pb_option.base64_to_bytes = true; + if (!json2pb::JsonToProtoMessage(cntl.response_attachment().to_string(), response, json2pb_option)) { + LOG(ERROR) << "Json to proto fail for " << cntl.response_attachment().to_string(); + return false; + } + return true; +} + +bool EtcdClient::Put(const ::etcdserverpb::PutRequest& request, + ::etcdserverpb::PutResponse* response) { + return EtcdOp(&_channel, EtcdOps::PUT, request, response); +} + +bool EtcdClient::Range(const ::etcdserverpb::RangeRequest& request, + ::etcdserverpb::RangeResponse* response) { + return EtcdOp(&_channel, EtcdOps::RANGE, request, response); +} + +bool EtcdClient::DeleteRange(const ::etcdserverpb::DeleteRangeRequest& request, + ::etcdserverpb::DeleteRangeResponse* response) { + return EtcdOp(&_channel, DELETE_RANGE, request, response); +} + +bool EtcdClient::Txn(const ::etcdserverpb::TxnRequest& request, + ::etcdserverpb::TxnResponse* response) { + return EtcdOp(&_channel, EtcdOps::TXN, request, response); +} + +bool EtcdClient::Compact(const ::etcdserverpb::CompactionRequest& request, + ::etcdserverpb::CompactionResponse* response) { + return EtcdOp(&_channel, EtcdOps::COMPACT, request, response); +} + +bool EtcdClient::LeaseGrant(const ::etcdserverpb::LeaseGrantRequest& request, + ::etcdserverpb::LeaseGrantResponse* response) { + return EtcdOp(&_channel, EtcdOps::LEASE_GRANT, request, response); +} + +bool EtcdClient::LeaseRevoke(const ::etcdserverpb::LeaseRevokeRequest& request, + ::etcdserverpb::LeaseRevokeResponse* response) { + return EtcdOp(&_channel, EtcdOps::LEASE_REVOKE, request, response); +} + +bool EtcdClient::LeaseTimeToLive(const ::etcdserverpb::LeaseTimeToLiveRequest& request, + ::etcdserverpb::LeaseTimeToLiveResponse* response) { + return EtcdOp(&_channel, EtcdOps::LEASE_TTL, request, response); +} + +bool EtcdClient::LeaseLeases(const ::etcdserverpb::LeaseLeasesRequest& request, + ::etcdserverpb::LeaseLeasesResponse* response) { + return EtcdOp(&_channel, EtcdOps::LEASE_LEASES, request, response); +} + +bool EtcdClient::LeaseKeepAlive(const ::etcdserverpb::LeaseKeepAliveRequest& request, + ::etcdserverpb::LeaseKeepAliveResponse* response) { + return EtcdOp(&_channel, EtcdOps::LEASE_KEEPALIVE, request, response); +} + +class ReadBody : public brpc::ProgressiveReader, public brpc::SharedObject { + public: + ReadBody() : _destroyed(false) { + butil::intrusive_ptr(this).detach(); + } + + butil::Status OnReadOnePart(const void* data, size_t length) { + if (length > 0) { + butil::IOBuf os; + os.append(data, length); + ::etcdserverpb::WatchResponseEx response; + struct json2pb::Json2PbOptions option; + option.base64_to_bytes = true; + option.allow_remaining_bytes_after_parsing = true; + std::string err; + if (!json2pb::JsonToProtoMessage(os.to_string(), &response, option, &err)) { + _watcher->OnParseError(); + LOG(WARNING) << "watch parse " << os.to_string() << " fail. " + << "error msg " << err; + } else { + _watcher->OnEventResponse(response.result()); + } + } + return butil::Status::OK(); + } + + void OnEndOfMessage(const butil::Status& st) { + butil::intrusive_ptr(this, false); + _destroyed = true; + _destroying_st = st; + return; + } + + bool destroyed() const { + return _destroyed; + } + + const butil::Status& destroying_status() const { + return _destroying_st; + } + + void set_watcher(std::shared_ptr watcher) { + _watcher = watcher; + } + + private: + bool _destroyed; + butil::Status _destroying_st; + std::shared_ptr _watcher; +}; + +bool EtcdClient::Watch(const ::etcdserverpb::WatchRequest& request, WatcherPtr watcher) { + if (!watcher.get()) { + LOG(ERROR) << "watcher is nullptr"; + return false; + } + brpc::Controller cntl; + cntl.http_request().uri().set_path("/v3/watch"); + cntl.http_request().set_method(brpc::HTTP_METHOD_POST); + std::string output; + struct json2pb::Pb2JsonOptions pb2json_option; + pb2json_option.bytes_to_base64 = true; + if (!json2pb::ProtoMessageToJson(request, &output, pb2json_option)) { + LOG(ERROR) << "convert WatchRequest " << request.ShortDebugString() << " to json fail."; + return false; + } + cntl.request_attachment().append(output); + cntl.response_will_be_read_progressively(); + _channel.CallMethod(NULL, &cntl, NULL, NULL, NULL); + if (cntl.Failed()) { + LOG(ERROR) << "watch request " << request.ShortDebugString() << " to etcd server fail." + << "error message : " << cntl.ErrorText(); + return false; + } + butil::intrusive_ptr reader; + reader.reset(new ReadBody); + reader->set_watcher(watcher); + cntl.ReadProgressiveAttachmentBy(reader.get()); + return true; +} + +} // namespace brpc + diff --git a/src/etcd_client/etcd_client.h b/src/etcd_client/etcd_client.h new file mode 100644 index 0000000000..e6cf232c01 --- /dev/null +++ b/src/etcd_client/etcd_client.h @@ -0,0 +1,84 @@ +// 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. + +#ifndef SRC_ETCD_CLIENT_ETCD_CLIENT_H_ +#define SRC_ETCD_CLIENT_ETCD_CLIENT_H_ +#include +#include +#include "brpc/channel.h" +#include "etcd_client/proto/rpc.pb.h" +#include "butil/endpoint.h" + +namespace brpc { + +// The routine of Watcher will be invoked by different event read thread +// Users should ensure thread safe on the process of event +class Watcher { + public: + virtual void OnEventResponse(const ::etcdserverpb::WatchResponse& response) {} + virtual void OnParseError() {} +}; +typedef std::shared_ptr WatcherPtr; + +class EtcdClient { + public: + EtcdClient(); + ~EtcdClient(); + bool Init(const std::string& url, const std::string& lb = "rr"); + bool Init(butil::EndPoint server_addr_and_port); + bool Init(const char* server_addr_and_port); + bool Init(const char* server_addr, int port); + + bool Put(const ::etcdserverpb::PutRequest& request, + ::etcdserverpb::PutResponse* response); + + bool Range(const ::etcdserverpb::RangeRequest& request, + ::etcdserverpb::RangeResponse* response); + + bool DeleteRange(const ::etcdserverpb::DeleteRangeRequest& request, + ::etcdserverpb::DeleteRangeResponse* response); + + bool Txn(const ::etcdserverpb::TxnRequest& request, + ::etcdserverpb::TxnResponse* response); + + bool Compact(const ::etcdserverpb::CompactionRequest& request, + ::etcdserverpb::CompactionResponse* response); + + bool LeaseGrant(const ::etcdserverpb::LeaseGrantRequest& request, + ::etcdserverpb::LeaseGrantResponse* response); + + bool LeaseRevoke(const ::etcdserverpb::LeaseRevokeRequest& request, + ::etcdserverpb::LeaseRevokeResponse* response); + + bool LeaseTimeToLive(const ::etcdserverpb::LeaseTimeToLiveRequest& request, + ::etcdserverpb::LeaseTimeToLiveResponse* response); + + bool LeaseLeases(const ::etcdserverpb::LeaseLeasesRequest& request, + ::etcdserverpb::LeaseLeasesResponse* response); + + bool LeaseKeepAlive(const ::etcdserverpb::LeaseKeepAliveRequest& request, + ::etcdserverpb::LeaseKeepAliveResponse* response); + + bool Watch(const ::etcdserverpb::WatchRequest& request, WatcherPtr watcher); + + private: + brpc::Channel _channel; +}; + +} // namespace brpc + +#endif // SRC_ETCD_CLIENT_ETCD_CLIENT_H_ diff --git a/src/etcd_client/proto/auth.proto b/src/etcd_client/proto/auth.proto new file mode 100644 index 0000000000..4cfbf32496 --- /dev/null +++ b/src/etcd_client/proto/auth.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; +package authpb; + +import "etcd_client/proto/gogoproto/gogo.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.sizer_all) = true; +option (gogoproto.unmarshaler_all) = true; +option (gogoproto.goproto_getters_all) = false; +option (gogoproto.goproto_enum_prefix_all) = false; + +message UserAddOptions { + bool no_password = 1; +}; + +// User is a single entry in the bucket authUsers +message User { + bytes name = 1; + bytes password = 2; + repeated string roles = 3; + UserAddOptions options = 4; +} + +// Permission is a single entity +message Permission { + enum Type { + READ = 0; + WRITE = 1; + READWRITE = 2; + } + Type permType = 1; + + bytes key = 2; + bytes range_end = 3; +} + +// Role is a single entry in the bucket authRoles +message Role { + bytes name = 1; + + repeated Permission keyPermission = 2; +} diff --git a/src/etcd_client/proto/etcdserver.proto b/src/etcd_client/proto/etcdserver.proto new file mode 100644 index 0000000000..740bbd7de7 --- /dev/null +++ b/src/etcd_client/proto/etcdserver.proto @@ -0,0 +1,34 @@ +syntax = "proto2"; +package etcdserverpb; + +import "etcd_client/proto/gogoproto/gogo.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.sizer_all) = true; +option (gogoproto.unmarshaler_all) = true; +option (gogoproto.goproto_getters_all) = false; + +message Request { + optional uint64 ID = 1 [(gogoproto.nullable) = false]; + optional string Method = 2 [(gogoproto.nullable) = false]; + optional string Path = 3 [(gogoproto.nullable) = false]; + optional string Val = 4 [(gogoproto.nullable) = false]; + optional bool Dir = 5 [(gogoproto.nullable) = false]; + optional string PrevValue = 6 [(gogoproto.nullable) = false]; + optional uint64 PrevIndex = 7 [(gogoproto.nullable) = false]; + optional bool PrevExist = 8 [(gogoproto.nullable) = true]; + optional int64 Expiration = 9 [(gogoproto.nullable) = false]; + optional bool Wait = 10 [(gogoproto.nullable) = false]; + optional uint64 Since = 11 [(gogoproto.nullable) = false]; + optional bool Recursive = 12 [(gogoproto.nullable) = false]; + optional bool Sorted = 13 [(gogoproto.nullable) = false]; + optional bool Quorum = 14 [(gogoproto.nullable) = false]; + optional int64 Time = 15 [(gogoproto.nullable) = false]; + optional bool Stream = 16 [(gogoproto.nullable) = false]; + optional bool Refresh = 17 [(gogoproto.nullable) = true]; +} + +message Metadata { + optional uint64 NodeID = 1 [(gogoproto.nullable) = false]; + optional uint64 ClusterID = 2 [(gogoproto.nullable) = false]; +} diff --git a/src/etcd_client/proto/gogoproto/gogo.proto b/src/etcd_client/proto/gogoproto/gogo.proto new file mode 100644 index 0000000000..b80c85653f --- /dev/null +++ b/src/etcd_client/proto/gogoproto/gogo.proto @@ -0,0 +1,144 @@ +// Protocol Buffers for Go with Gadgets +// +// Copyright (c) 2013, The GoGo Authors. All rights reserved. +// http://github.com/gogo/protobuf +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +syntax = "proto2"; +package gogoproto; + +import "google/protobuf/descriptor.proto"; + +option java_package = "com.google.protobuf"; +option java_outer_classname = "GoGoProtos"; +option go_package = "github.com/gogo/protobuf/gogoproto"; + +extend google.protobuf.EnumOptions { + optional bool goproto_enum_prefix = 62001; + optional bool goproto_enum_stringer = 62021; + optional bool enum_stringer = 62022; + optional string enum_customname = 62023; + optional bool enumdecl = 62024; +} + +extend google.protobuf.EnumValueOptions { + optional string enumvalue_customname = 66001; +} + +extend google.protobuf.FileOptions { + optional bool goproto_getters_all = 63001; + optional bool goproto_enum_prefix_all = 63002; + optional bool goproto_stringer_all = 63003; + optional bool verbose_equal_all = 63004; + optional bool face_all = 63005; + optional bool gostring_all = 63006; + optional bool populate_all = 63007; + optional bool stringer_all = 63008; + optional bool onlyone_all = 63009; + + optional bool equal_all = 63013; + optional bool description_all = 63014; + optional bool testgen_all = 63015; + optional bool benchgen_all = 63016; + optional bool marshaler_all = 63017; + optional bool unmarshaler_all = 63018; + optional bool stable_marshaler_all = 63019; + + optional bool sizer_all = 63020; + + optional bool goproto_enum_stringer_all = 63021; + optional bool enum_stringer_all = 63022; + + optional bool unsafe_marshaler_all = 63023; + optional bool unsafe_unmarshaler_all = 63024; + + optional bool goproto_extensions_map_all = 63025; + optional bool goproto_unrecognized_all = 63026; + optional bool gogoproto_import = 63027; + optional bool protosizer_all = 63028; + optional bool compare_all = 63029; + optional bool typedecl_all = 63030; + optional bool enumdecl_all = 63031; + + optional bool goproto_registration = 63032; + optional bool messagename_all = 63033; + + optional bool goproto_sizecache_all = 63034; + optional bool goproto_unkeyed_all = 63035; +} + +extend google.protobuf.MessageOptions { + optional bool goproto_getters = 64001; + optional bool goproto_stringer = 64003; + optional bool verbose_equal = 64004; + optional bool face = 64005; + optional bool gostring = 64006; + optional bool populate = 64007; + optional bool stringer = 67008; + optional bool onlyone = 64009; + + optional bool equal = 64013; + optional bool description = 64014; + optional bool testgen = 64015; + optional bool benchgen = 64016; + optional bool marshaler = 64017; + optional bool unmarshaler = 64018; + optional bool stable_marshaler = 64019; + + optional bool sizer = 64020; + + optional bool unsafe_marshaler = 64023; + optional bool unsafe_unmarshaler = 64024; + + optional bool goproto_extensions_map = 64025; + optional bool goproto_unrecognized = 64026; + + optional bool protosizer = 64028; + optional bool compare = 64029; + + optional bool typedecl = 64030; + + optional bool messagename = 64033; + + optional bool goproto_sizecache = 64034; + optional bool goproto_unkeyed = 64035; +} + +extend google.protobuf.FieldOptions { + optional bool nullable = 65001; + optional bool embed = 65002; + optional string customtype = 65003; + optional string customname = 65004; + optional string jsontag = 65005; + optional string moretags = 65006; + optional string casttype = 65007; + optional string castkey = 65008; + optional string castvalue = 65009; + + optional bool stdtime = 65010; + optional bool stdduration = 65011; + optional bool wktpointer = 65012; + +} diff --git a/src/etcd_client/proto/google/api/annotations.proto b/src/etcd_client/proto/google/api/annotations.proto new file mode 100644 index 0000000000..54f7d685ae --- /dev/null +++ b/src/etcd_client/proto/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright (c) 2015, Google Inc. +// +// Licensed 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. + +syntax = "proto3"; + +package google.api; + +import "etcd_client/proto/google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/src/etcd_client/proto/google/api/http.proto b/src/etcd_client/proto/google/api/http.proto new file mode 100644 index 0000000000..b2977f5147 --- /dev/null +++ b/src/etcd_client/proto/google/api/http.proto @@ -0,0 +1,376 @@ +// Copyright 2019 Google LLC. +// +// Licensed 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. +// + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/src/etcd_client/proto/kv.proto b/src/etcd_client/proto/kv.proto new file mode 100644 index 0000000000..becadf9f73 --- /dev/null +++ b/src/etcd_client/proto/kv.proto @@ -0,0 +1,49 @@ +syntax = "proto3"; +package mvccpb; + +// import "gogoproto/gogo.proto"; + +// option (gogoproto.marshaler_all) = true; +// option (gogoproto.sizer_all) = true; +// option (gogoproto.unmarshaler_all) = true; +// option (gogoproto.goproto_getters_all) = false; +// option (gogoproto.goproto_enum_prefix_all) = false; + +message KeyValue { + // key is the key in bytes. An empty key is not allowed. + bytes key = 1; + // create_revision is the revision of last creation on this key. + int64 create_revision = 2; + // mod_revision is the revision of last modification on this key. + int64 mod_revision = 3; + // version is the version of the key. A deletion resets + // the version to zero and any modification of the key + // increases its version. + int64 version = 4; + // value is the value held by the key, in bytes. + bytes value = 5; + // lease is the ID of the lease that attached to key. + // When the attached lease expires, the key will be deleted. + // If lease is 0, then no lease is attached to the key. + int64 lease = 6; +} + +message Event { + enum EventType { + PUT = 0; + DELETE = 1; + } + // type is the kind of event. If type is a PUT, it indicates + // new data has been stored to the key. If type is a DELETE, + // it indicates the key was deleted. + EventType type = 1; + // kv holds the KeyValue for the event. + // A PUT event contains current kv pair. + // A PUT event with kv.Version=1 indicates the creation of a key. + // A DELETE/EXPIRE event contains the deleted key with + // its modification revision set to the revision of deletion. + KeyValue kv = 2; + + // prev_kv holds the key-value pair before the event happens. + KeyValue prev_kv = 3; +} diff --git a/src/etcd_client/proto/rpc.proto b/src/etcd_client/proto/rpc.proto new file mode 100644 index 0000000000..361206765c --- /dev/null +++ b/src/etcd_client/proto/rpc.proto @@ -0,0 +1,1150 @@ +syntax = "proto3"; +package etcdserverpb; + +import "etcd_client/proto/gogoproto/gogo.proto"; +import "etcd_client/proto/kv.proto"; +import "etcd_client/proto/auth.proto"; + +// for grpc-gateway +import "etcd_client/proto/google/api/annotations.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.unmarshaler_all) = true; + +service KV { + // Range gets the keys in the range from the key-value store. + rpc Range(RangeRequest) returns (RangeResponse) { + option (google.api.http) = { + post: "/v3/kv/range" + body: "*" + }; + } + + // Put puts the given key into the key-value store. + // A put request increments the revision of the key-value store + // and generates one event in the event history. + rpc Put(PutRequest) returns (PutResponse) { + option (google.api.http) = { + post: "/v3/kv/put" + body: "*" + }; + } + + // DeleteRange deletes the given range from the key-value store. + // A delete request increments the revision of the key-value store + // and generates a delete event in the event history for every deleted key. + rpc DeleteRange(DeleteRangeRequest) returns (DeleteRangeResponse) { + option (google.api.http) = { + post: "/v3/kv/deleterange" + body: "*" + }; + } + + // Txn processes multiple requests in a single transaction. + // A txn request increments the revision of the key-value store + // and generates events with the same revision for every completed request. + // It is not allowed to modify the same key several times within one txn. + rpc Txn(TxnRequest) returns (TxnResponse) { + option (google.api.http) = { + post: "/v3/kv/txn" + body: "*" + }; + } + + // Compact compacts the event history in the etcd key-value store. The key-value + // store should be periodically compacted or the event history will continue to grow + // indefinitely. + rpc Compact(CompactionRequest) returns (CompactionResponse) { + option (google.api.http) = { + post: "/v3/kv/compaction" + body: "*" + }; + } +} + +service Watch { + // Watch watches for events happening or that have happened. Both input and output + // are streams; the input stream is for creating and canceling watchers and the output + // stream sends events. One watch RPC can watch on multiple key ranges, streaming events + // for several watches at once. The entire event history can be watched starting from the + // last compaction revision. + rpc Watch(stream WatchRequest) returns (stream WatchResponse) { + option (google.api.http) = { + post: "/v3/watch" + body: "*" + }; + } +} + +service Lease { + // LeaseGrant creates a lease which expires if the server does not receive a keepAlive + // within a given time to live period. All keys attached to the lease will be expired and + // deleted if the lease expires. Each expired key generates a delete event in the event history. + rpc LeaseGrant(LeaseGrantRequest) returns (LeaseGrantResponse) { + option (google.api.http) = { + post: "/v3/lease/grant" + body: "*" + }; + } + + // LeaseRevoke revokes a lease. All keys attached to the lease will expire and be deleted. + rpc LeaseRevoke(LeaseRevokeRequest) returns (LeaseRevokeResponse) { + option (google.api.http) = { + post: "/v3/lease/revoke" + body: "*" + additional_bindings { + post: "/v3/kv/lease/revoke" + body: "*" + } + }; + } + + // LeaseKeepAlive keeps the lease alive by streaming keep alive requests from the client + // to the server and streaming keep alive responses from the server to the client. + rpc LeaseKeepAlive(stream LeaseKeepAliveRequest) returns (stream LeaseKeepAliveResponse) { + option (google.api.http) = { + post: "/v3/lease/keepalive" + body: "*" + }; + } + + // LeaseTimeToLive retrieves lease information. + rpc LeaseTimeToLive(LeaseTimeToLiveRequest) returns (LeaseTimeToLiveResponse) { + option (google.api.http) = { + post: "/v3/lease/timetolive" + body: "*" + additional_bindings { + post: "/v3/kv/lease/timetolive" + body: "*" + } + }; + } + + // LeaseLeases lists all existing leases. + rpc LeaseLeases(LeaseLeasesRequest) returns (LeaseLeasesResponse) { + option (google.api.http) = { + post: "/v3/lease/leases" + body: "*" + additional_bindings { + post: "/v3/kv/lease/leases" + body: "*" + } + }; + } +} + +service Cluster { + // MemberAdd adds a member into the cluster. + rpc MemberAdd(MemberAddRequest) returns (MemberAddResponse) { + option (google.api.http) = { + post: "/v3/cluster/member/add" + body: "*" + }; + } + + // MemberRemove removes an existing member from the cluster. + rpc MemberRemove(MemberRemoveRequest) returns (MemberRemoveResponse) { + option (google.api.http) = { + post: "/v3/cluster/member/remove" + body: "*" + }; + } + + // MemberUpdate updates the member configuration. + rpc MemberUpdate(MemberUpdateRequest) returns (MemberUpdateResponse) { + option (google.api.http) = { + post: "/v3/cluster/member/update" + body: "*" + }; + } + + // MemberList lists all the members in the cluster. + rpc MemberList(MemberListRequest) returns (MemberListResponse) { + option (google.api.http) = { + post: "/v3/cluster/member/list" + body: "*" + }; + } + + // MemberPromote promotes a member from raft learner (non-voting) to raft voting member. + rpc MemberPromote(MemberPromoteRequest) returns (MemberPromoteResponse) { + option (google.api.http) = { + post: "/v3/cluster/member/promote" + body: "*" + }; + } +} + +service Maintenance { + // Alarm activates, deactivates, and queries alarms regarding cluster health. + rpc Alarm(AlarmRequest) returns (AlarmResponse) { + option (google.api.http) = { + post: "/v3/maintenance/alarm" + body: "*" + }; + } + + // Status gets the status of the member. + rpc Status(StatusRequest) returns (StatusResponse) { + option (google.api.http) = { + post: "/v3/maintenance/status" + body: "*" + }; + } + + // Defragment defragments a member's backend database to recover storage space. + rpc Defragment(DefragmentRequest) returns (DefragmentResponse) { + option (google.api.http) = { + post: "/v3/maintenance/defragment" + body: "*" + }; + } + + // Hash computes the hash of whole backend keyspace, + // including key, lease, and other buckets in storage. + // This is designed for testing ONLY! + // Do not rely on this in production with ongoing transactions, + // since Hash operation does not hold MVCC locks. + // Use "HashKV" API instead for "key" bucket consistency checks. + rpc Hash(HashRequest) returns (HashResponse) { + option (google.api.http) = { + post: "/v3/maintenance/hash" + body: "*" + }; + } + + // HashKV computes the hash of all MVCC keys up to a given revision. + // It only iterates "key" bucket in backend storage. + rpc HashKV(HashKVRequest) returns (HashKVResponse) { + option (google.api.http) = { + post: "/v3/maintenance/hash" + body: "*" + }; + } + + // Snapshot sends a snapshot of the entire backend from a member over a stream to a client. + rpc Snapshot(SnapshotRequest) returns (stream SnapshotResponse) { + option (google.api.http) = { + post: "/v3/maintenance/snapshot" + body: "*" + }; + } + + // MoveLeader requests current leader node to transfer its leadership to transferee. + rpc MoveLeader(MoveLeaderRequest) returns (MoveLeaderResponse) { + option (google.api.http) = { + post: "/v3/maintenance/transfer-leadership" + body: "*" + }; + } +} + +service Auth { + // AuthEnable enables authentication. + rpc AuthEnable(AuthEnableRequest) returns (AuthEnableResponse) { + option (google.api.http) = { + post: "/v3/auth/enable" + body: "*" + }; + } + + // AuthDisable disables authentication. + rpc AuthDisable(AuthDisableRequest) returns (AuthDisableResponse) { + option (google.api.http) = { + post: "/v3/auth/disable" + body: "*" + }; + } + + // Authenticate processes an authenticate request. + rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) { + option (google.api.http) = { + post: "/v3/auth/authenticate" + body: "*" + }; + } + + // UserAdd adds a new user. User name cannot be empty. + rpc UserAdd(AuthUserAddRequest) returns (AuthUserAddResponse) { + option (google.api.http) = { + post: "/v3/auth/user/add" + body: "*" + }; + } + + // UserGet gets detailed user information. + rpc UserGet(AuthUserGetRequest) returns (AuthUserGetResponse) { + option (google.api.http) = { + post: "/v3/auth/user/get" + body: "*" + }; + } + + // UserList gets a list of all users. + rpc UserList(AuthUserListRequest) returns (AuthUserListResponse) { + option (google.api.http) = { + post: "/v3/auth/user/list" + body: "*" + }; + } + + // UserDelete deletes a specified user. + rpc UserDelete(AuthUserDeleteRequest) returns (AuthUserDeleteResponse) { + option (google.api.http) = { + post: "/v3/auth/user/delete" + body: "*" + }; + } + + // UserChangePassword changes the password of a specified user. + rpc UserChangePassword(AuthUserChangePasswordRequest) returns (AuthUserChangePasswordResponse) { + option (google.api.http) = { + post: "/v3/auth/user/changepw" + body: "*" + }; + } + + // UserGrant grants a role to a specified user. + rpc UserGrantRole(AuthUserGrantRoleRequest) returns (AuthUserGrantRoleResponse) { + option (google.api.http) = { + post: "/v3/auth/user/grant" + body: "*" + }; + } + + // UserRevokeRole revokes a role of specified user. + rpc UserRevokeRole(AuthUserRevokeRoleRequest) returns (AuthUserRevokeRoleResponse) { + option (google.api.http) = { + post: "/v3/auth/user/revoke" + body: "*" + }; + } + + // RoleAdd adds a new role. Role name cannot be empty. + rpc RoleAdd(AuthRoleAddRequest) returns (AuthRoleAddResponse) { + option (google.api.http) = { + post: "/v3/auth/role/add" + body: "*" + }; + } + + // RoleGet gets detailed role information. + rpc RoleGet(AuthRoleGetRequest) returns (AuthRoleGetResponse) { + option (google.api.http) = { + post: "/v3/auth/role/get" + body: "*" + }; + } + + // RoleList gets lists of all roles. + rpc RoleList(AuthRoleListRequest) returns (AuthRoleListResponse) { + option (google.api.http) = { + post: "/v3/auth/role/list" + body: "*" + }; + } + + // RoleDelete deletes a specified role. + rpc RoleDelete(AuthRoleDeleteRequest) returns (AuthRoleDeleteResponse) { + option (google.api.http) = { + post: "/v3/auth/role/delete" + body: "*" + }; + } + + // RoleGrantPermission grants a permission of a specified key or range to a specified role. + rpc RoleGrantPermission(AuthRoleGrantPermissionRequest) returns (AuthRoleGrantPermissionResponse) { + option (google.api.http) = { + post: "/v3/auth/role/grant" + body: "*" + }; + } + + // RoleRevokePermission revokes a key or range permission of a specified role. + rpc RoleRevokePermission(AuthRoleRevokePermissionRequest) returns (AuthRoleRevokePermissionResponse) { + option (google.api.http) = { + post: "/v3/auth/role/revoke" + body: "*" + }; + } +} + +message ResponseHeader { + // cluster_id is the ID of the cluster which sent the response. + uint64 cluster_id = 1; + // member_id is the ID of the member which sent the response. + uint64 member_id = 2; + // revision is the key-value store revision when the request was applied. + // For watch progress responses, the header.revision indicates progress. All future events + // recieved in this stream are guaranteed to have a higher revision number than the + // header.revision number. + int64 revision = 3; + // raft_term is the raft term when the request was applied. + uint64 raft_term = 4; +} + +message RangeRequest { + enum SortOrder { + NONE = 0; // default, no sorting + ASCEND = 1; // lowest target value first + DESCEND = 2; // highest target value first + } + enum SortTarget { + KEY = 0; + VERSION = 1; + CREATE = 2; + MOD = 3; + VALUE = 4; + } + + // key is the first key for the range. If range_end is not given, the request only looks up key. + bytes key = 1; + // range_end is the upper bound on the requested range [key, range_end). + // If range_end is '\0', the range is all keys >= key. + // If range_end is key plus one (e.g., "aa"+1 == "ab", "a\xff"+1 == "b"), + // then the range request gets all keys prefixed with key. + // If both key and range_end are '\0', then the range request returns all keys. + bytes range_end = 2; + // limit is a limit on the number of keys returned for the request. When limit is set to 0, + // it is treated as no limit. + int64 limit = 3; + // revision is the point-in-time of the key-value store to use for the range. + // If revision is less or equal to zero, the range is over the newest key-value store. + // If the revision has been compacted, ErrCompacted is returned as a response. + int64 revision = 4; + + // sort_order is the order for returned sorted results. + SortOrder sort_order = 5; + + // sort_target is the key-value field to use for sorting. + SortTarget sort_target = 6; + + // serializable sets the range request to use serializable member-local reads. + // Range requests are linearizable by default; linearizable requests have higher + // latency and lower throughput than serializable requests but reflect the current + // consensus of the cluster. For better performance, in exchange for possible stale reads, + // a serializable range request is served locally without needing to reach consensus + // with other nodes in the cluster. + bool serializable = 7; + + // keys_only when set returns only the keys and not the values. + bool keys_only = 8; + + // count_only when set returns only the count of the keys in the range. + bool count_only = 9; + + // min_mod_revision is the lower bound for returned key mod revisions; all keys with + // lesser mod revisions will be filtered away. + int64 min_mod_revision = 10; + + // max_mod_revision is the upper bound for returned key mod revisions; all keys with + // greater mod revisions will be filtered away. + int64 max_mod_revision = 11; + + // min_create_revision is the lower bound for returned key create revisions; all keys with + // lesser create revisions will be filtered away. + int64 min_create_revision = 12; + + // max_create_revision is the upper bound for returned key create revisions; all keys with + // greater create revisions will be filtered away. + int64 max_create_revision = 13; +} + +message RangeResponse { + ResponseHeader header = 1; + // kvs is the list of key-value pairs matched by the range request. + // kvs is empty when count is requested. + repeated mvccpb.KeyValue kvs = 2; + // more indicates if there are more keys to return in the requested range. + bool more = 3; + // count is set to the number of keys within the range when requested. + int64 count = 4; +} + +message PutRequest { + // key is the key, in bytes, to put into the key-value store. + bytes key = 1; + // value is the value, in bytes, to associate with the key in the key-value store. + bytes value = 2; + // lease is the lease ID to associate with the key in the key-value store. A lease + // value of 0 indicates no lease. + int64 lease = 3; + + // If prev_kv is set, etcd gets the previous key-value pair before changing it. + // The previous key-value pair will be returned in the put response. + bool prev_kv = 4; + + // If ignore_value is set, etcd updates the key using its current value. + // Returns an error if the key does not exist. + bool ignore_value = 5; + + // If ignore_lease is set, etcd updates the key using its current lease. + // Returns an error if the key does not exist. + bool ignore_lease = 6; +} + +message PutResponse { + ResponseHeader header = 1; + // if prev_kv is set in the request, the previous key-value pair will be returned. + mvccpb.KeyValue prev_kv = 2; +} + +message DeleteRangeRequest { + // key is the first key to delete in the range. + bytes key = 1; + // range_end is the key following the last key to delete for the range [key, range_end). + // If range_end is not given, the range is defined to contain only the key argument. + // If range_end is one bit larger than the given key, then the range is all the keys + // with the prefix (the given key). + // If range_end is '\0', the range is all keys greater than or equal to the key argument. + bytes range_end = 2; + + // If prev_kv is set, etcd gets the previous key-value pairs before deleting it. + // The previous key-value pairs will be returned in the delete response. + bool prev_kv = 3; +} + +message DeleteRangeResponse { + ResponseHeader header = 1; + // deleted is the number of keys deleted by the delete range request. + int64 deleted = 2; + // if prev_kv is set in the request, the previous key-value pairs will be returned. + repeated mvccpb.KeyValue prev_kvs = 3; +} + +message RequestOp { + // request is a union of request types accepted by a transaction. + oneof request { + RangeRequest request_range = 1; + PutRequest request_put = 2; + DeleteRangeRequest request_delete_range = 3; + TxnRequest request_txn = 4; + } +} + +message ResponseOp { + // response is a union of response types returned by a transaction. + oneof response { + RangeResponse response_range = 1; + PutResponse response_put = 2; + DeleteRangeResponse response_delete_range = 3; + TxnResponse response_txn = 4; + } +} + +message Compare { + enum CompareResult { + EQUAL = 0; + GREATER = 1; + LESS = 2; + NOT_EQUAL = 3; + } + enum CompareTarget { + VERSION = 0; + CREATE = 1; + MOD = 2; + VALUE = 3; + LEASE = 4; + } + // result is logical comparison operation for this comparison. + CompareResult result = 1; + // target is the key-value field to inspect for the comparison. + CompareTarget target = 2; + // key is the subject key for the comparison operation. + bytes key = 3; + oneof target_union { + // version is the version of the given key + int64 version = 4; + // create_revision is the creation revision of the given key + int64 create_revision = 5; + // mod_revision is the last modified revision of the given key. + int64 mod_revision = 6; + // value is the value of the given key, in bytes. + bytes value = 7; + // lease is the lease id of the given key. + int64 lease = 8; + // leave room for more target_union field tags, jump to 64 + } + + // range_end compares the given target to all keys in the range [key, range_end). + // See RangeRequest for more details on key ranges. + bytes range_end = 64; + // TODO: fill out with most of the rest of RangeRequest fields when needed. +} + +// From google paxosdb paper: +// Our implementation hinges around a powerful primitive which we call MultiOp. All other database +// operations except for iteration are implemented as a single call to MultiOp. A MultiOp is applied atomically +// and consists of three components: +// 1. A list of tests called guard. Each test in guard checks a single entry in the database. It may check +// for the absence or presence of a value, or compare with a given value. Two different tests in the guard +// may apply to the same or different entries in the database. All tests in the guard are applied and +// MultiOp returns the results. If all tests are true, MultiOp executes t op (see item 2 below), otherwise +// it executes f op (see item 3 below). +// 2. A list of database operations called t op. Each operation in the list is either an insert, delete, or +// lookup operation, and applies to a single database entry. Two different operations in the list may apply +// to the same or different entries in the database. These operations are executed +// if guard evaluates to +// true. +// 3. A list of database operations called f op. Like t op, but executed if guard evaluates to false. +message TxnRequest { + // compare is a list of predicates representing a conjunction of terms. + // If the comparisons succeed, then the success requests will be processed in order, + // and the response will contain their respective responses in order. + // If the comparisons fail, then the failure requests will be processed in order, + // and the response will contain their respective responses in order. + repeated Compare compare = 1; + // success is a list of requests which will be applied when compare evaluates to true. + repeated RequestOp success = 2; + // failure is a list of requests which will be applied when compare evaluates to false. + repeated RequestOp failure = 3; +} + +message TxnResponse { + ResponseHeader header = 1; + // succeeded is set to true if the compare evaluated to true or false otherwise. + bool succeeded = 2; + // responses is a list of responses corresponding to the results from applying + // success if succeeded is true or failure if succeeded is false. + repeated ResponseOp responses = 3; +} + +// CompactionRequest compacts the key-value store up to a given revision. All superseded keys +// with a revision less than the compaction revision will be removed. +message CompactionRequest { + // revision is the key-value store revision for the compaction operation. + int64 revision = 1; + // physical is set so the RPC will wait until the compaction is physically + // applied to the local database such that compacted entries are totally + // removed from the backend database. + bool physical = 2; +} + +message CompactionResponse { + ResponseHeader header = 1; +} + +message HashRequest { +} + +message HashKVRequest { + // revision is the key-value store revision for the hash operation. + int64 revision = 1; +} + +message HashKVResponse { + ResponseHeader header = 1; + // hash is the hash value computed from the responding member's MVCC keys up to a given revision. + uint32 hash = 2; + // compact_revision is the compacted revision of key-value store when hash begins. + int64 compact_revision = 3; +} + +message HashResponse { + ResponseHeader header = 1; + // hash is the hash value computed from the responding member's KV's backend. + uint32 hash = 2; +} + +message SnapshotRequest { +} + +message SnapshotResponse { + // header has the current key-value store information. The first header in the snapshot + // stream indicates the point in time of the snapshot. + ResponseHeader header = 1; + + // remaining_bytes is the number of blob bytes to be sent after this message + uint64 remaining_bytes = 2; + + // blob contains the next chunk of the snapshot in the snapshot stream. + bytes blob = 3; +} + +message WatchRequest { + // request_union is a request to either create a new watcher or cancel an existing watcher. + oneof request_union { + WatchCreateRequest create_request = 1; + WatchCancelRequest cancel_request = 2; + WatchProgressRequest progress_request = 3; + } +} + +message WatchCreateRequest { + // key is the key to register for watching. + bytes key = 1; + + // range_end is the end of the range [key, range_end) to watch. If range_end is not given, + // only the key argument is watched. If range_end is equal to '\0', all keys greater than + // or equal to the key argument are watched. + // If the range_end is one bit larger than the given key, + // then all keys with the prefix (the given key) will be watched. + bytes range_end = 2; + + // start_revision is an optional revision to watch from (inclusive). No start_revision is "now". + int64 start_revision = 3; + + // progress_notify is set so that the etcd server will periodically send a WatchResponse with + // no events to the new watcher if there are no recent events. It is useful when clients + // wish to recover a disconnected watcher starting from a recent known revision. + // The etcd server may decide how often it will send notifications based on current load. + bool progress_notify = 4; + + enum FilterType { + // filter out put event. + NOPUT = 0; + // filter out delete event. + NODELETE = 1; + } + + // filters filter the events at server side before it sends back to the watcher. + repeated FilterType filters = 5; + + // If prev_kv is set, created watcher gets the previous KV before the event happens. + // If the previous KV is already compacted, nothing will be returned. + bool prev_kv = 6; + + // If watch_id is provided and non-zero, it will be assigned to this watcher. + // Since creating a watcher in etcd is not a synchronous operation, + // this can be used ensure that ordering is correct when creating multiple + // watchers on the same stream. Creating a watcher with an ID already in + // use on the stream will cause an error to be returned. + int64 watch_id = 7; + + // fragment enables splitting large revisions into multiple watch responses. + bool fragment = 8; +} + +message WatchCancelRequest { + // watch_id is the watcher id to cancel so that no more events are transmitted. + int64 watch_id = 1; +} + +// Requests the a watch stream progress status be sent in the watch response stream as soon as +// possible. +message WatchProgressRequest { +} + +message WatchResponseEx { + WatchResponse result = 1; +}; + +message WatchResponse { + ResponseHeader header = 1; + // watch_id is the ID of the watcher that corresponds to the response. + int64 watch_id = 2; + + // created is set to true if the response is for a create watch request. + // The client should record the watch_id and expect to receive events for + // the created watcher from the same stream. + // All events sent to the created watcher will attach with the same watch_id. + bool created = 3; + + // canceled is set to true if the response is for a cancel watch request. + // No further events will be sent to the canceled watcher. + bool canceled = 4; + + // compact_revision is set to the minimum index if a watcher tries to watch + // at a compacted index. + // + // This happens when creating a watcher at a compacted revision or the watcher cannot + // catch up with the progress of the key-value store. + // + // The client should treat the watcher as canceled and should not try to create any + // watcher with the same start_revision again. + int64 compact_revision = 5; + + // cancel_reason indicates the reason for canceling the watcher. + string cancel_reason = 6; + + // framgment is true if large watch response was split over multiple responses. + bool fragment = 7; + + repeated mvccpb.Event events = 11; +} + +message LeaseGrantRequest { + // TTL is the advisory time-to-live in seconds. Expired lease will return -1. + int64 TTL = 1; + // ID is the requested ID for the lease. If ID is set to 0, the lessor chooses an ID. + int64 ID = 2; +} + +message LeaseGrantResponse { + ResponseHeader header = 1; + // ID is the lease ID for the granted lease. + int64 ID = 2; + // TTL is the server chosen lease time-to-live in seconds. + int64 TTL = 3; + string error = 4; +} + +message LeaseRevokeRequest { + // ID is the lease ID to revoke. When the ID is revoked, all associated keys will be deleted. + int64 ID = 1; +} + +message LeaseRevokeResponse { + ResponseHeader header = 1; +} + +message LeaseCheckpoint { + // ID is the lease ID to checkpoint. + int64 ID = 1; + + // Remaining_TTL is the remaining time until expiry of the lease. + int64 remaining_TTL = 2; +} + +message LeaseCheckpointRequest { + repeated LeaseCheckpoint checkpoints = 1; +} + +message LeaseCheckpointResponse { + ResponseHeader header = 1; +} + +message LeaseKeepAliveRequest { + // ID is the lease ID for the lease to keep alive. + int64 ID = 1; +} + +message LeaseKeepAliveResponse { + ResponseHeader header = 1; + // ID is the lease ID from the keep alive request. + int64 ID = 2; + // TTL is the new time-to-live for the lease. + int64 TTL = 3; +} + +message LeaseTimeToLiveRequest { + // ID is the lease ID for the lease. + int64 ID = 1; + // keys is true to query all the keys attached to this lease. + bool keys = 2; +} + +message LeaseTimeToLiveResponse { + ResponseHeader header = 1; + // ID is the lease ID from the keep alive request. + int64 ID = 2; + // TTL is the remaining TTL in seconds for the lease; the lease will expire in under TTL+1 seconds. + int64 TTL = 3; + // GrantedTTL is the initial granted time in seconds upon lease creation/renewal. + int64 grantedTTL = 4; + // Keys is the list of keys attached to this lease. + repeated bytes keys = 5; +} + +message LeaseLeasesRequest { +} + +message LeaseStatus { + int64 ID = 1; + // TODO: int64 TTL = 2; +} + +message LeaseLeasesResponse { + ResponseHeader header = 1; + repeated LeaseStatus leases = 2; +} + +message Member { + // ID is the member ID for this member. + uint64 ID = 1; + // name is the human-readable name of the member. If the member is not started, the name will be an empty string. + string name = 2; + // peerURLs is the list of URLs the member exposes to the cluster for communication. + repeated string peerURLs = 3; + // clientURLs is the list of URLs the member exposes to clients for communication. If the member is not started, clientURLs will be empty. + repeated string clientURLs = 4; + // isLearner indicates if the member is raft learner. + bool isLearner = 5; +} + +message MemberAddRequest { + // peerURLs is the list of URLs the added member will use to communicate with the cluster. + repeated string peerURLs = 1; + // isLearner indicates if the added member is raft learner. + bool isLearner = 2; +} + +message MemberAddResponse { + ResponseHeader header = 1; + // member is the member information for the added member. + Member member = 2; + // members is a list of all members after adding the new member. + repeated Member members = 3; +} + +message MemberRemoveRequest { + // ID is the member ID of the member to remove. + uint64 ID = 1; +} + +message MemberRemoveResponse { + ResponseHeader header = 1; + // members is a list of all members after removing the member. + repeated Member members = 2; +} + +message MemberUpdateRequest { + // ID is the member ID of the member to update. + uint64 ID = 1; + // peerURLs is the new list of URLs the member will use to communicate with the cluster. + repeated string peerURLs = 2; +} + +message MemberUpdateResponse{ + ResponseHeader header = 1; + // members is a list of all members after updating the member. + repeated Member members = 2; +} + +message MemberListRequest { +} + +message MemberListResponse { + ResponseHeader header = 1; + // members is a list of all members associated with the cluster. + repeated Member members = 2; +} + +message MemberPromoteRequest { + // ID is the member ID of the member to promote. + uint64 ID = 1; +} + +message MemberPromoteResponse { + ResponseHeader header = 1; + // members is a list of all members after promoting the member. + repeated Member members = 2; +} + +message DefragmentRequest { +} + +message DefragmentResponse { + ResponseHeader header = 1; +} + +message MoveLeaderRequest { + // targetID is the node ID for the new leader. + uint64 targetID = 1; +} + +message MoveLeaderResponse { + ResponseHeader header = 1; +} + +enum AlarmType { + NONE = 0; // default, used to query if any alarm is active + NOSPACE = 1; // space quota is exhausted + CORRUPT = 2; // kv store corruption detected +} + +message AlarmRequest { + enum AlarmAction { + GET = 0; + ACTIVATE = 1; + DEACTIVATE = 2; + } + // action is the kind of alarm request to issue. The action + // may GET alarm statuses, ACTIVATE an alarm, or DEACTIVATE a + // raised alarm. + AlarmAction action = 1; + // memberID is the ID of the member associated with the alarm. If memberID is 0, the + // alarm request covers all members. + uint64 memberID = 2; + // alarm is the type of alarm to consider for this request. + AlarmType alarm = 3; +} + +message AlarmMember { + // memberID is the ID of the member associated with the raised alarm. + uint64 memberID = 1; + // alarm is the type of alarm which has been raised. + AlarmType alarm = 2; +} + +message AlarmResponse { + ResponseHeader header = 1; + // alarms is a list of alarms associated with the alarm request. + repeated AlarmMember alarms = 2; +} + +message StatusRequest { +} + +message StatusResponse { + ResponseHeader header = 1; + // version is the cluster protocol version used by the responding member. + string version = 2; + // dbSize is the size of the backend database physically allocated, in bytes, of the responding member. + int64 dbSize = 3; + // leader is the member ID which the responding member believes is the current leader. + uint64 leader = 4; + // raftIndex is the current raft committed index of the responding member. + uint64 raftIndex = 5; + // raftTerm is the current raft term of the responding member. + uint64 raftTerm = 6; + // raftAppliedIndex is the current raft applied index of the responding member. + uint64 raftAppliedIndex = 7; + // errors contains alarm/health information and status. + repeated string errors = 8; + // dbSizeInUse is the size of the backend database logically in use, in bytes, of the responding member. + int64 dbSizeInUse = 9; + // isLearner indicates if the member is raft learner. + bool isLearner = 10; +} + +message AuthEnableRequest { +} + +message AuthDisableRequest { +} + +message AuthenticateRequest { + string name = 1; + string password = 2; +} + +message AuthUserAddRequest { + string name = 1; + string password = 2; + authpb.UserAddOptions options = 3; +} + +message AuthUserGetRequest { + string name = 1; +} + +message AuthUserDeleteRequest { + // name is the name of the user to delete. + string name = 1; +} + +message AuthUserChangePasswordRequest { + // name is the name of the user whose password is being changed. + string name = 1; + // password is the new password for the user. + string password = 2; +} + +message AuthUserGrantRoleRequest { + // user is the name of the user which should be granted a given role. + string user = 1; + // role is the name of the role to grant to the user. + string role = 2; +} + +message AuthUserRevokeRoleRequest { + string name = 1; + string role = 2; +} + +message AuthRoleAddRequest { + // name is the name of the role to add to the authentication system. + string name = 1; +} + +message AuthRoleGetRequest { + string role = 1; +} + +message AuthUserListRequest { +} + +message AuthRoleListRequest { +} + +message AuthRoleDeleteRequest { + string role = 1; +} + +message AuthRoleGrantPermissionRequest { + // name is the name of the role which will be granted the permission. + string name = 1; + // perm is the permission to grant to the role. + authpb.Permission perm = 2; +} + +message AuthRoleRevokePermissionRequest { + string role = 1; + bytes key = 2; + bytes range_end = 3; +} + +message AuthEnableResponse { + ResponseHeader header = 1; +} + +message AuthDisableResponse { + ResponseHeader header = 1; +} + +message AuthenticateResponse { + ResponseHeader header = 1; + // token is an authorized token that can be used in succeeding RPCs + string token = 2; +} + +message AuthUserAddResponse { + ResponseHeader header = 1; +} + +message AuthUserGetResponse { + ResponseHeader header = 1; + + repeated string roles = 2; +} + +message AuthUserDeleteResponse { + ResponseHeader header = 1; +} + +message AuthUserChangePasswordResponse { + ResponseHeader header = 1; +} + +message AuthUserGrantRoleResponse { + ResponseHeader header = 1; +} + +message AuthUserRevokeRoleResponse { + ResponseHeader header = 1; +} + +message AuthRoleAddResponse { + ResponseHeader header = 1; +} + +message AuthRoleGetResponse { + ResponseHeader header = 1; + + repeated authpb.Permission perm = 2; +} + +message AuthRoleListResponse { + ResponseHeader header = 1; + + repeated string roles = 2; +} + +message AuthUserListResponse { + ResponseHeader header = 1; + + repeated string users = 2; +} + +message AuthRoleDeleteResponse { + ResponseHeader header = 1; +} + +message AuthRoleGrantPermissionResponse { + ResponseHeader header = 1; +} + +message AuthRoleRevokePermissionResponse { + ResponseHeader header = 1; +} diff --git a/src/etcd_client/proto/v3election.proto b/src/etcd_client/proto/v3election.proto new file mode 100644 index 0000000000..9392fe36e1 --- /dev/null +++ b/src/etcd_client/proto/v3election.proto @@ -0,0 +1,119 @@ +syntax = "proto3"; +package v3electionpb; + +import "etcd_client/proto/gogoproto/gogo.proto"; +import "etcd_client/proto/rpc.proto"; +import "etcd_client/proto/kv.proto"; + +// for grpc-gateway +import "etcd_client/proto/google/api/annotations.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.unmarshaler_all) = true; + +// The election service exposes client-side election facilities as a gRPC interface. +service Election { + // Campaign waits to acquire leadership in an election, returning a LeaderKey + // representing the leadership if successful. The LeaderKey can then be used + // to issue new values on the election, transactionally guard API requests on + // leadership still being held, and resign from the election. + rpc Campaign(CampaignRequest) returns (CampaignResponse) { + option (google.api.http) = { + post: "/v3/election/campaign" + body: "*" + }; + } + // Proclaim updates the leader's posted value with a new value. + rpc Proclaim(ProclaimRequest) returns (ProclaimResponse) { + option (google.api.http) = { + post: "/v3/election/proclaim" + body: "*" + }; + } + // Leader returns the current election proclamation, if any. + rpc Leader(LeaderRequest) returns (LeaderResponse) { + option (google.api.http) = { + post: "/v3/election/leader" + body: "*" + }; + } + // Observe streams election proclamations in-order as made by the election's + // elected leaders. + rpc Observe(LeaderRequest) returns (stream LeaderResponse) { + option (google.api.http) = { + post: "/v3/election/observe" + body: "*" + }; + } + // Resign releases election leadership so other campaigners may acquire + // leadership on the election. + rpc Resign(ResignRequest) returns (ResignResponse) { + option (google.api.http) = { + post: "/v3/election/resign" + body: "*" + }; + } +} + +message CampaignRequest { + // name is the election's identifier for the campaign. + bytes name = 1; + // lease is the ID of the lease attached to leadership of the election. If the + // lease expires or is revoked before resigning leadership, then the + // leadership is transferred to the next campaigner, if any. + int64 lease = 2; + // value is the initial proclaimed value set when the campaigner wins the + // election. + bytes value = 3; +} + +message CampaignResponse { + etcdserverpb.ResponseHeader header = 1; + // leader describes the resources used for holding leadereship of the election. + LeaderKey leader = 2; +} + +message LeaderKey { + // name is the election identifier that correponds to the leadership key. + bytes name = 1; + // key is an opaque key representing the ownership of the election. If the key + // is deleted, then leadership is lost. + bytes key = 2; + // rev is the creation revision of the key. It can be used to test for ownership + // of an election during transactions by testing the key's creation revision + // matches rev. + int64 rev = 3; + // lease is the lease ID of the election leader. + int64 lease = 4; +} + +message LeaderRequest { + // name is the election identifier for the leadership information. + bytes name = 1; +} + +message LeaderResponse { + etcdserverpb.ResponseHeader header = 1; + // kv is the key-value pair representing the latest leader update. + mvccpb.KeyValue kv = 2; +} + +message ResignRequest { + // leader is the leadership to relinquish by resignation. + LeaderKey leader = 1; +} + +message ResignResponse { + etcdserverpb.ResponseHeader header = 1; +} + +message ProclaimRequest { + // leader is the leadership hold on the election. + LeaderKey leader = 1; + // value is an update meant to overwrite the leader's current value. + bytes value = 2; +} + +message ProclaimResponse { + etcdserverpb.ResponseHeader header = 1; +} diff --git a/src/etcd_client/proto/v3lock.proto b/src/etcd_client/proto/v3lock.proto new file mode 100644 index 0000000000..b959d4e93d --- /dev/null +++ b/src/etcd_client/proto/v3lock.proto @@ -0,0 +1,65 @@ +syntax = "proto3"; +package v3lockpb; + +import "etcd_client/proto/gogoproto/gogo.proto"; +import "etcd_client/proto/rpc.proto"; + +// for grpc-gateway +import "proto/google/api/annotations.proto"; + +option (gogoproto.marshaler_all) = true; +option (gogoproto.unmarshaler_all) = true; + +// The lock service exposes client-side locking facilities as a gRPC interface. +service Lock { + // Lock acquires a distributed shared lock on a given named lock. + // On success, it will return a unique key that exists so long as the + // lock is held by the caller. This key can be used in conjunction with + // transactions to safely ensure updates to etcd only occur while holding + // lock ownership. The lock is held until Unlock is called on the key or the + // lease associate with the owner expires. + rpc Lock(LockRequest) returns (LockResponse) { + option (google.api.http) = { + post: "/v3/lock/lock" + body: "*" + }; + } + + // Unlock takes a key returned by Lock and releases the hold on lock. The + // next Lock caller waiting for the lock will then be woken up and given + // ownership of the lock. + rpc Unlock(UnlockRequest) returns (UnlockResponse) { + option (google.api.http) = { + post: "/v3/lock/unlock" + body: "*" + }; + } +} + +message LockRequest { + // name is the identifier for the distributed shared lock to be acquired. + bytes name = 1; + // lease is the ID of the lease that will be attached to ownership of the + // lock. If the lease expires or is revoked and currently holds the lock, + // the lock is automatically released. Calls to Lock with the same lease will + // be treated as a single acquisition; locking twice with the same lease is a + // no-op. + int64 lease = 2; +} + +message LockResponse { + etcdserverpb.ResponseHeader header = 1; + // key is a key that will exist on etcd for the duration that the Lock caller + // owns the lock. Users should not modify this key or the lock may exhibit + // undefined behavior. + bytes key = 2; +} + +message UnlockRequest { + // key is the lock ownership key granted by Lock. + bytes key = 1; +} + +message UnlockResponse { + etcdserverpb.ResponseHeader header = 1; +} diff --git a/test/Makefile b/test/Makefile index 871a99ed88..6d5a31cecd 100644 --- a/test/Makefile +++ b/test/Makefile @@ -173,7 +173,9 @@ TEST_BRPC_OBJS = $(addsuffix .o, $(basename $(TEST_BRPC_SOURCES))) TEST_PROTO_SOURCES = $(wildcard *.proto) TEST_PROTO_OBJS = $(TEST_PROTO_SOURCES:.proto=.pb.o) -TEST_BINS = test_butil test_bvar $(TEST_BTHREAD_SOURCES:.cpp=) $(TEST_BRPC_SOURCES:.cpp=) +TEST_ETCD_CLIENT_SOURCES = brpc_etcd_client_unittest.cpp + +TEST_BINS = test_butil test_bvar $(TEST_BTHREAD_SOURCES:.cpp=) $(TEST_BRPC_SOURCES:.cpp=) brpc_etcd_client_unittest .PHONY:all all: $(TEST_BINS) @@ -230,6 +232,14 @@ else ifeq ($(SYSTEM),Darwin) $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) $^ $(STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) endif +brpc_etcd_client_unittest:$(TEST_PROTO_OBJS) brpc_etcd_client_unittest.o + @echo "> Linking $@" +ifeq ($(SYSTEM),Linux) + $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) -Xlinker "-(" $^ -Xlinker "-)" $(STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) +else ifeq ($(SYSTEM),Darwin) + $(CXX) -o $@ $(LIBPATHS) $(SOPATHS) $^ $(STATIC_LINKINGS) $(UT_DYNAMIC_LINKINGS) +endif + %.pb.cc %.pb.h:%.proto @echo "> Generating $@" $(PROTOC) --cpp_out=. --proto_path=. --proto_path=../src --proto_path=$(PROTOBUF_HDR) $< diff --git a/test/brpc_etcd_client_unittest.cpp b/test/brpc_etcd_client_unittest.cpp new file mode 100644 index 0000000000..f2188d17e4 --- /dev/null +++ b/test/brpc_etcd_client_unittest.cpp @@ -0,0 +1,590 @@ +// 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. + +// brpc - A framework to host and access services throughout Baidu. + +// Date: Sun Jul 13 15:04:18 CST 2014 + +#include +#include +#include +#include +#include +#include +#include +#include +#include "butil/time.h" +#include "butil/macros.h" +#include "butil/files/scoped_file.h" +#include "butil/fd_guard.h" +#include "butil/file_util.h" +#include "brpc/socket.h" +#include "brpc/acceptor.h" +#include "brpc/server.h" +#include "brpc/channel.h" +#include "brpc/policy/most_common_message.h" +#include "brpc/controller.h" +#include "echo.pb.h" +#include "brpc/policy/http_rpc_protocol.h" +#include "brpc/policy/http2_rpc_protocol.h" +#include "json2pb/pb_to_json.h" +#include "json2pb/json_to_pb.h" +#include "brpc/details/method_status.h" +#include "etcd_client/etcd_client.h" + +namespace { + +class MockEtcdService : public ::test::EtcdService { + public: + void Range(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"110\"," + "\"raft_term\":\"12\"" + "}," + "\"kvs\":[{" + "\"key\":\"L3NlcnZpY2UvMTI3LjAuMC4x\"," + "\"create_revision\":\"109\"," + "\"mod_revision\":\"110\"," + "\"version\":\"2\"," + "\"value\":\"MTIzNA==\"" + "}]," + "\"count\":\"1\"" + "}"; + cntl->response_attachment().append(output); + } + void RangeFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock Range fail"); + } + void Put(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\" : {" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"110\"," + "\"raft_term\":\"12\"" + "}}"; + cntl->response_attachment().append(output); + } + void PutFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock put fail"); + } + void DeleteRange(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"113\"," + "\"raft_term\":\"12\"" + "}," + "\"deleted\":\"1\"" + "}"; + cntl->response_attachment().append(output); + } + void DeleteRangeFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock delete range fail"); + } + void Txn(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"122\"," + "\"raft_term\":\"12\"" + "}," + "\"succeeded\":true," + "\"responses\":[{" + "\"response_put\":{" + "\"header\":{" + "\"revision\":\"122\"" + "}" + "}" + "}]" + "}"; + cntl->response_attachment().append(output); + } + void TxnFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock txn fail"); + } + void Compact(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}" + "}"; + cntl->response_attachment().append(output); + } + void CompactFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock compact fail"); + } + void LeaseGrant(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}," + "\"ID\":\"23459870987\"," + "\"TTL\":\"2\"" + "}"; + cntl->response_attachment().append(output); + } + void LeaseGrantFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock lease grant fail"); + } + void LeaseRevoke(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}" + "}"; + cntl->response_attachment().append(output); + } + void LeaseRevokeFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock lease revoke fail"); + } + void LeaseKeepAlive(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}," + "\"ID\":\"23459870987\"," + "\"TTL\":\"2\"" + "}"; + cntl->response_attachment().append(output); + } + void LeaseKeepAliveFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock lease keep alive fail"); + } + void LeaseTimeToLive(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}," + "\"ID\":\"23459870987\"," + "\"TTL\":\"2\"," + "\"keys\":[\"YWJj\", \"MTIz\"]" + "}"; + cntl->response_attachment().append(output); + } + void LeaseTimeToLiveFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock lease time to live fail"); + } + void LeaseLeases(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + std::string output = "{" + "\"header\":{" + "\"cluster_id\":\"14841639068965178418\"," + "\"member_id\":\"10276657743932975437\"," + "\"revision\":\"128\"," + "\"raft_term\":\"12\"" + "}," + "\"leases\":[" + "{\"ID\":\"7587863094269227633\"}," + "{\"ID\":\"7587863094269227635\"}" + "]}"; + cntl->response_attachment().append(output); + } + void LeaseLeasesFail(::google::protobuf::RpcController* cntl_base, + const ::test::HttpRequest* req, + ::test::HttpResponse* res, + ::google::protobuf::Closure* done) { + brpc::ClosureGuard done_guard(done); + brpc::Controller* cntl = static_cast(cntl_base); + cntl->SetFailed("mock lease leases fail"); + } +}; + +class EtcdClientTest : public ::testing::Test { + protected: + EtcdClientTest() { + EXPECT_EQ(0, _success_server.AddService(&_success_svc, brpc::SERVER_DOESNT_OWN_SERVICE, + "/v3/kv/range => Range," + "/v3/kv/put => Put," + "/v3/kv/deleterange => DeleteRange," + "/v3/kv/txn => Txn," + "/v3/kv/compaction => Compact," + "/v3/lease/grant => LeaseGrant," + "/v3/lease/revoke => LeaseRevoke," + "/v3/lease/keepalive => LeaseKeepAlive," + "/v3/lease/timetolive => LeaseTimeToLive," + "/v3/lease/leases => LeaseLeases")); + + EXPECT_EQ(0, _success_server.Start(2389, nullptr)); + EXPECT_EQ(0, _fail_server.AddService(&_fail_svc, brpc::SERVER_DOESNT_OWN_SERVICE, + "/v3/kv/range => RangeFail," + "/v3/kv/put => PutFail," + "/v3/kv/deleterange => DeleteRangeFail," + "/v3/kv/txn => TxnFail," + "/v3/kv/compaction => CompactFail," + "/v3/lease/grant => LeaseGrantFail," + "/v3/lease/revoke => LeaseRevokeFail," + "/v3/lease/keepalive => LeaseKeepAliveFail," + "/v3/lease/timetolive => LeaseTimeToLiveFail," + "/v3/lease/leases => LeaseLeasesFail")); + EXPECT_EQ(0, _fail_server.Start(2399, nullptr)); + } + virtual void TearDown() { + } + brpc::Server _success_server; + MockEtcdService _success_svc; + brpc::Server _fail_server; + MockEtcdService _fail_svc; +}; + +TEST_F(EtcdClientTest, etcd_client_init) { + brpc::EtcdClient client; + EXPECT_TRUE(client.Init("localhost:2389")); +} + +TEST_F(EtcdClientTest, etcd_op_success) { + brpc::EtcdClient client; + EXPECT_TRUE(client.Init("localhost:2389")); + { + // test Put + etcdserverpb::PutRequest request; + request.set_key("/service/127.0.0.1"); + request.set_value("1"); + etcdserverpb::PutResponse response; + EXPECT_TRUE(client.Put(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 110); + EXPECT_EQ(response.header().raft_term(), 12u); + } + { + // test Range + etcdserverpb::RangeRequest request; + request.set_key("/service/127.0.0.1"); + etcdserverpb::RangeResponse response; + EXPECT_TRUE(client.Range(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 110); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.kvs_size(), 1); + EXPECT_EQ(response.kvs(0).key(), "/service/127.0.0.1"); + EXPECT_EQ(response.kvs(0).value(), "1234"); + } + { + // test DeleteRange + etcdserverpb::DeleteRangeRequest request; + request.set_key("/service/127.0.0.1"); + etcdserverpb::DeleteRangeResponse response; + EXPECT_TRUE(client.DeleteRange(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 113); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.deleted(), 1); + } + { + // test Txn + etcdserverpb::TxnRequest request; + auto* compare = request.add_compare(); + compare->set_result(::etcdserverpb::Compare::EQUAL); + compare->set_target(::etcdserverpb::Compare::VALUE); + compare->set_key("/service/127.0.0.1"); + compare->set_value("1234"); + auto* put = request.add_success()->mutable_request_put(); + put->set_key("/service/127.0.0.1"); + put->set_value("9876"); + etcdserverpb::TxnResponse response; + EXPECT_TRUE(client.Txn(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 122); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.succeeded(), 1); + } + { + // test Compaction + etcdserverpb::CompactionRequest request; + request.set_revision(126); + etcdserverpb::CompactionResponse response; + EXPECT_TRUE(client.Compact(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + } + { + // test LeaseGrant + etcdserverpb::LeaseGrantRequest request; + request.set_ttl(2); + request.set_id(23459870987); + etcdserverpb::LeaseGrantResponse response; + EXPECT_TRUE(client.LeaseGrant(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.id(), 23459870987); + EXPECT_EQ(response.ttl(), 2u); + } + { + // test revoke + etcdserverpb::LeaseRevokeRequest request; + request.set_id(23459870987); + etcdserverpb::LeaseRevokeResponse response; + EXPECT_TRUE(client.LeaseRevoke(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + } + { + // test keepalive + etcdserverpb::LeaseKeepAliveRequest request; + request.set_id(23459870987); + etcdserverpb::LeaseKeepAliveResponse response; + EXPECT_TRUE(client.LeaseKeepAlive(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.id(), 23459870987); + EXPECT_EQ(response.ttl(), 2u); + } + { + // test time to live + etcdserverpb::LeaseTimeToLiveRequest request; + request.set_id(23459870987); + request.set_keys(true); + etcdserverpb::LeaseTimeToLiveResponse response; + EXPECT_TRUE(client.LeaseTimeToLive(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.id(), 23459870987); + EXPECT_EQ(response.ttl(), 2u); + EXPECT_EQ(response.keys_size(), 2); + EXPECT_EQ(response.keys(0), "abc"); + EXPECT_EQ(response.keys(1), "123"); + } + { + // test lease leases + etcdserverpb::LeaseLeasesRequest request; + etcdserverpb::LeaseLeasesResponse response; + EXPECT_TRUE(client.LeaseLeases(request, &response)); + EXPECT_EQ(response.header().cluster_id(), 14841639068965178418u); + EXPECT_EQ(response.header().member_id(), 10276657743932975437u); + EXPECT_EQ(response.header().revision(), 128); + EXPECT_EQ(response.header().raft_term(), 12u); + EXPECT_EQ(response.leases(0).id(), 7587863094269227633); + EXPECT_EQ(response.leases(1).id(), 7587863094269227635); + } +} + +TEST_F(EtcdClientTest, etcd_op_fail) { + brpc::EtcdClient client; + EXPECT_TRUE(client.Init("localhost:2399")); + { + // Put fail + etcdserverpb::PutRequest request; + request.set_key("/service/127.0.0.1"); + request.set_value("1"); + etcdserverpb::PutResponse response; + EXPECT_FALSE(client.Put(request, &response)); + } + { + // Range fail + etcdserverpb::RangeRequest request; + request.set_key("/service/127.0.0.1"); + etcdserverpb::RangeResponse response; + EXPECT_FALSE(client.Range(request, &response)); + } + { + // DeleteRange fail + etcdserverpb::DeleteRangeRequest request; + request.set_key("/service/127.0.0.1"); + etcdserverpb::DeleteRangeResponse response; + EXPECT_FALSE(client.DeleteRange(request, &response)); + } + { + // Txn fail + etcdserverpb::TxnRequest request; + auto* compare = request.add_compare(); + compare->set_result(::etcdserverpb::Compare::EQUAL); + compare->set_target(::etcdserverpb::Compare::VALUE); + compare->set_key("/service/127.0.0.1"); + compare->set_value("1234"); + auto* put = request.add_success()->mutable_request_put(); + put->set_key("/service/127.0.0.1"); + put->set_value("9876"); + etcdserverpb::TxnResponse response; + EXPECT_FALSE(client.Txn(request, &response)); + } + { + // Compact fail + etcdserverpb::CompactionRequest request; + request.set_revision(126); + etcdserverpb::CompactionResponse response; + EXPECT_FALSE(client.Compact(request, &response)); + } + { + // LeaseGrant fail + etcdserverpb::LeaseGrantRequest request; + request.set_ttl(2); + request.set_id(23459870987); + etcdserverpb::LeaseGrantResponse response; + EXPECT_FALSE(client.LeaseGrant(request, &response)); + } + { + // LeaseRevoke fail + etcdserverpb::LeaseRevokeRequest request; + request.set_id(23459870987); + etcdserverpb::LeaseRevokeResponse response; + EXPECT_FALSE(client.LeaseRevoke(request, &response)); + } + { + // test keepalive + etcdserverpb::LeaseKeepAliveRequest request; + request.set_id(23459870987); + etcdserverpb::LeaseKeepAliveResponse response; + EXPECT_FALSE(client.LeaseKeepAlive(request, &response)); + } + { + // test time to live + etcdserverpb::LeaseTimeToLiveRequest request; + request.set_id(23459870987); + etcdserverpb::LeaseTimeToLiveResponse response; + EXPECT_FALSE(client.LeaseTimeToLive(request, &response)); + } + { + etcdserverpb::LeaseLeasesRequest request; + etcdserverpb::LeaseLeasesResponse response; + EXPECT_FALSE(client.LeaseLeases(request, &response)); + } + { + // test lease leases + etcdserverpb::LeaseLeasesRequest request; + etcdserverpb::LeaseLeasesResponse response; + EXPECT_FALSE(client.LeaseLeases(request, &response)); + } +} +} // namespace diff --git a/test/echo.proto b/test/echo.proto index 2a47b234e9..11278b5b99 100644 --- a/test/echo.proto +++ b/test/echo.proto @@ -65,6 +65,30 @@ service DownloadService { rpc DownloadFailed(HttpRequest) returns (HttpResponse); } +service EtcdService { + rpc Range(HttpRequest) returns (HttpResponse); + rpc RangeFail(HttpRequest) returns (HttpResponse); + rpc Put(HttpRequest) returns (HttpResponse); + rpc PutFail(HttpRequest) returns (HttpResponse); + rpc DeleteRange(HttpRequest) returns (HttpResponse); + rpc DeleteRangeFail(HttpRequest) returns (HttpResponse); + rpc Txn(HttpRequest) returns (HttpResponse); + rpc TxnFail(HttpRequest) returns (HttpResponse); + rpc Compact(HttpRequest) returns (HttpResponse); + rpc CompactFail(HttpRequest) returns (HttpResponse); + rpc LeaseGrant(HttpRequest) returns (HttpResponse); + rpc LeaseGrantFail(HttpRequest) returns (HttpResponse); + rpc LeaseRevoke(HttpRequest) returns (HttpResponse); + rpc LeaseRevokeFail(HttpRequest) returns (HttpResponse); + rpc LeaseTimeToLive(HttpRequest) returns (HttpResponse); + rpc LeaseTimeToLiveFail(HttpRequest) returns (HttpResponse); + rpc LeaseLeases(HttpRequest) returns (HttpResponse); + rpc LeaseLeasesFail(HttpRequest) returns (HttpResponse); + rpc LeaseKeepAlive(HttpRequest) returns (HttpResponse); + rpc LeaseKeepAliveFail(HttpRequest) returns (HttpResponse); + +} + service UserNamingService { rpc ListNames(HttpRequest) returns (HttpResponse); rpc Touch(HttpRequest) returns (HttpResponse);