commit 7d11a0a4cac996451d6464308921e3f2d1f08140 Author: Arches Date: Tue May 31 11:20:06 2016 +0200 initial repository creation diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e78b607 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required (VERSION 3.1.3 FATAL_ERROR) +project (etcd-cpp-api) + +find_library(CPPREST_LIB NAMES cpprest) +find_package(Boost REQUIRED COMPONENTS system thread locale random) + +set (etcd-cpp-api_VERSION_MAJOR 0) +set (etcd-cpp-api_VERSION_MINOR 1) + +enable_testing() + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror") + +add_subdirectory(src) +add_subdirectory(tst) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..2b08510 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,24 @@ +Copyright (c) 2015, Nokia +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted +provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions +and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +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 HOLDER 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c617eb5 --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +etcd-cpp-api is a C++ API for [etcd](https://github.com/coreos/etcd). + +## Requirements + + * [C++ REST SDK](http://casablanca.codeplex.com/) + * Boost libraries + * [Catch](https://github.com/philsquared/Catch) for testing + +## generic notes + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response response = etcd.get("/test/key1").get(); + std::cout << response.value().as_string(); +``` + +Methods of the etcd client object are sending the corresponding HTTP requests and are returning +immediatelly with a ```pplx::task``` object. The task object is responsible for handling the +reception of the HTTP response as well as parsing the JSON body of the response. All of this is done +asynchronously in a background thread so you can continue your code to do other operations while the +current etcd operation is executing in the background or you can wait for the response with the +```wait()``` or ```get()``` methods if a synchron behaviour is enough for your needs. These methods +are blocking until the HTTP response arrives or some error situation happens. ```get()``` method +also returns the ```etcd::Response``` object. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + pplx::task response_task = etcd.get("/test/key1").get(); + // ... do something else + etcd::Response response = response_task.get(); + std::cout << response.value().as_string(); +``` + +The pplx library allows to do even more. You can attach continuation ojects to the task if you do +not care about when the response is coming you only want to specify what to do then. This +can be achieved by calling the ```then``` method of the task, giving a funcion object parameter to +it that can be used as a callback when the response is arrived and processed. The parameter of this +callback should be either a ```etcd::Response``` or a ```pplx::task```. You should +probably use a C++ lambda funcion here as a callback. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.get("/test/key1").then([](etcd::Response response) + { + std::cout << response.value().as_string(); + }); + + // ... your code can continue here without any delay +``` + +Your lambda function should have a parameter of type ```etcd::Response``` or +```pplx::task```. In the latter case you can get the actual ```etcd::Response``` +object with the ```get()``` function of the task. Calling get can raise exeptions so this is the way +how you can catch the errors generated by the REST interface. The ```get()``` call will not block in +this case since the respose has been already arrived (we are inside the callback). + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.get("/test/key1").then([](pplx::task response_task) + { + try + { + etcd::Response response = response.task.get(); // can throw + std::cout << response.value().as_string(); + } + catch (std::ecxeption const & ex) + { + std::cerr << ex.what(); + } + }); + + // ... your code can continue here without any delay +``` + +## etcd operations + +### reading a value + +You can read a value with the ```get``` method of the clinent instance. The only parameter is the +key to be read. If the read operation is successful then the value of the key can be acquired with +the ```value()``` method of the response. Success of the operation can be checked with the +```is_ok()``` method of the response. In case of an error, the ```error_code()``` and +```error_message()``` methods can be called for some further detail. + +Please note that there can be two kind of error situations. There can be some problem with the +communication between the client and the etcd server. In this case the ```get()``` method of the +response task will throw an exception as shown above. If the communication is ok but there is some +problem with the content of the actual operation, like attemp to read a non-existing key then the +response object will give you all the details. Let's see this in an example. + +The Value object of the response also holds some extra information besides the string value of the +key. You can also get the index number of the creation and the last modification of this key with +the ```created_index()``` and the ```modofied_index()``` methods. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + pplx::task response_task = etcd.get("/test/key1"); + + try + { + etcd::Response response = response_task.get(); // can throw + if (response.is_ok()) + std::cout << "successful read, value=" << response.value().as_string(); + else + std::cout << "operation failed, details: " << response.error_message(); + } + catch (std::ecxeption const & ex) + { + std::cerr << "communication problem, details: " << ex.what(); + } +``` + +### modifying a value + +Setting the value of a key can be done with the ```set()``` method of the client. You simply pass +the key and the value as string parameters and you are done. The newly set value object can be asked +from the response object exactly the same way as in case of the reading (with the ```value()``` +method). This way you can check for example the index value of your modification. You can also check +what was the previous value that this operation was overwritten. You can do that with the +```prev_value()``` method of the response object. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + pplx::task response_task = etcd.set("/test/key1", "42"); + + try + { + etcd::Response response = response_task.get(); + if (response.is_ok()) + std::cout << "The new value is successfully set, previous value was " + << response.prev_value().as_string(); + else + std::cout << "operation failed, details: " << response.error_message(); + } + catch (std::ecxeption const & ex) + { + std::cerr << "communication problem, details: " << ex.what(); + } +``` + +The set method creates a new leaf node if it weren't exists already or modifies an existing one. +There are a couple of other modification methods that are executing the write operation only upon +some specific conditions. + + * ```add(key, value)``` creates a new value if it's key does not exists and returns a "Key + already exists" error otherwise (error code 105) + * ```modify(key, value)``` modifies an already existing value or returns a "Key not found" error + otherwise (error code 100) + * ```modify_if(key, value, old_value)``` modifies an already existing value but only if the previous + value equals with old_value. If the values does not match returns with "Compare failed" error + (code 101) + * ```modify_if(key, value, old_index)``` modifies an already existing value but only if the index of + the previous value equals with old_index. If the indices does not match returns with "Compare + failed" error (code 101) + +### deleting a value + +Values can be deleted with the ```rm``` method passing the key to be deleted as a parameter. The key +should point to an existing value. There are conditional variations for deletion too. + + * ```rm_if(key, value, old_value)``` deletes an already existing value but only if the previous + value equals with old_value. If the values does not match returns with "Compare failed" error + (code 101) + * ```rm_if(key, value, old_index)``` deletes an already existing value but only if the index of + the previous value equals with old_index. If the indices does not match returns with "Compare + failed" error (code 101) + +### handling directory nodes + +Directory nodes can be created, listed and deleted with the mkdir, ls and rmdir methods. For +directory creation you just have to specify the full path of the new directory. Naturally the parent +has to exists and it has to be another directory. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.mkdir("/test").get(); +``` + +When you list a directory the response object's ```keys()``` and ```values()``` methods gives you a +vector of directory entry names and values. The ```value()``` method with an integer parameter also +returns with the i-th element of the values vector, so ```response.values()[i] == +response.value(i)```. Entry names in the keys vector are relative to the parent directory. Elements +in the values vector can be subdirectories or actual string values. To decide which one you can use +the ```is_dir()``` method. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.ls("/test/new_dir").get(); + for (int i = 0; i < resp.keys().size(); ++i) + { + std::cout << resp.keys(i); + if (resp.value(i).is_dir()) + std::cout << "/" << std::endl; + else + std::cout << " = " << resp.value(i).as_string() << std::endl; + } +``` + +Directories can only be deleted if they are empty by default. If you want the delete recursively +then you have to pass a second ```true``` parameter to rmdir. This parameter defaults to ```false```. + +```c++ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.rmdir("/test", true).get(); +``` + +### watching for changes + +Watching for a change is possible with the ```watch()``` operation of the client. The watch method +simply does not deliver a response object until the watched value changes in any way (modified or +deleted). When a change happens the returned result object will be the same as the result object of +the modification operation. So if the change is triggered by a value change, then +```response.action()``` will return "set" or "modify", ```response.value()``` will hold the new +value and ```response.prev_value()``` will contain the previous value. In case of a delete +```response.action()``` will return "delete", ```response.value()``` will be empty and should not be +called at all and ```response.prev_value()``` will contain the deleted value. + +It is also possible to watch a whole directory subtree for changes with passing ```true``` to the second +```recursive``` parameter of ```watch``` (this parameter defaults to ```false``` if omitted). In +this case the modified value object's ```key()``` method can be handy to determine what key is +actually changed. Since this can be a long lasting operation you have to be prepared that is +terminated by an exception and you have to restart the watch operation. + +The watch also accepts an index parameter that specifies what is the first change we are interested +about. Since etcd stores the last couple of modifications with this feature you can ensure that your +client does not miss a single change. + +Here is an example how you can watch continuously for changes of one specific key. + +```c++ +void watch_for_changes() +{ + etcd.watch("/nodes", index + 1, true).then([this](pplx::task resp_task) + { + try + { + etcd::Response resp = resp_task.get(); + index = resp.index(); + std::cout << resp.action() << " " << resp.value().as_string() << std::endl; + } + catch(...) {} + watch_for_changes(); + }); +} +``` + +At first glance it seems that ```watch_for_changes()``` calls itself on every value change but in +fact it just sends the asynchron request, sets up a callback for the response and then returns. The +callback is executed by some thread from the pplx library's thread pool and the callback (in this +case a small lambda function actually) will call ```watch_for_changes``` again from there. diff --git a/etcd/Client.hpp b/etcd/Client.hpp new file mode 100644 index 0000000..4c77388 --- /dev/null +++ b/etcd/Client.hpp @@ -0,0 +1,138 @@ +#ifndef __ETCD_CLIENT_HPP__ +#define __ETCD_CLIENT_HPP__ + +#include "etcd/Response.hpp" + +#include +#include + +namespace etcd +{ + /** + * Client is responsible for maintaining a connection towards an etcd server. + * Etcd operations can be reached via the methods of the client. + */ + class Client + { + public: + /** + * Constructs an etcd client object. + * @param etcd_url is the url of the etcd server to connect to, like "http://127.0.0.1:4001" + */ + Client(std::string const & etcd_url); + + /** + * Sends a get request to the etcd server + * @param key is the key to be read + */ + pplx::task get(std::string const & key); + + /** + * Sets the value of a key. The key will be modified if already exists or created + * if it does not exists. + * @param key is the key to be created or modified + * @param value is the new value to be set + */ + pplx::task set(std::string const & key, std::string const & value); + + /** + * Creates a new key and sets it's value. Fails if the key already exists. + * @param key is the key to be created + * @param value is the value to be set + */ + pplx::task add(std::string const & key, std::string const & value); + + /** + * Modifies an existing key. Fails if the key does not exists. + * @param key is the key to be modified + * @param value is the new value to be set + */ + pplx::task modify(std::string const & key, std::string const & value); + + /** + * Modifies an existing key only if it has a specific value. Fails if the key does not exists + * or the original value differs from the expected one. + * @param key is the key to be modified + * @param value is the new value to be set + * @param old_value is the value to be replaced + */ + pplx::task modify_if(std::string const & key, std::string const & value, std::string const & old_value); + + /** + * Modifies an existing key only if it has a specific modification index value. Fails if the key + * does not exists or the modification index of the previous value differs from the expected one. + * @param key is the key to be modified + * @param value is the new value to be set + * @param old_index is the expected index of the original value + */ + pplx::task modify_if(std::string const & key, std::string const & value, int old_index); + + /** + * Removes a single key. The key has to point to a plain, non directory entry. + * @param key is the key to be deleted + */ + pplx::task rm(std::string const & key); + + /** + * Removes a single key but only if it has a specific value. Fails if the key does not exists + * or the its value differs from the expected one. + * @param key is the key to be deleted + */ + pplx::task rm_if(std::string const & key, std::string const & old_value); + + /** + * Removes an existing key only if it has a specific modification index value. Fails if the key + * does not exists or the modification index of it differs from the expected one. + * @param key is the key to be deleted + * @param old_index is the expected index of the existing value + */ + pplx::task rm_if(std::string const & key, int old_index); + + /** + * Gets a directory listing of the directory identified by the key. + * @param key is the key to be listed + */ + pplx::task ls(std::string const & key); + + /** + * Creates a new directory node. Fails if the parent directory dos not exists or not a directory. + * @param key is the directory to be created to be listed + */ + pplx::task mkdir(std::string const & key); + + /** + * Removes a directory node. Fails if the parent directory dos not exists or not a directory. + * @param key is the directory to be created to be listed + * @param recursive if true then delete a whole subtree, otherwise deletes only an empty directory. + */ + pplx::task rmdir(std::string const & key, bool recursive = false); + + /** + * Watches for changes of a key or a subtree. Please note that if you watch e.g. "/testdir" and + * a new key is created, like "/testdir/newkey" then no change happened in the value of + * "/testdir" so your watch will not detect this. If you want to detect addition and deletion of + * directory entries then you have to do a recursive watch. + * @param key is the value or directory to be watched + * @param recursive if true watch a whole subtree + */ + pplx::task watch(std::string const & key, bool recursive = false); + + /** + * Watches for changes of a key or a subtree from a specific index. The index value can be in the "past". + * @param key is the value or directory to be watched + * @param fromIndex the first index we are interested in + * @param recursive if true watch a whole subtree + */ + pplx::task watch(std::string const & key, int fromIndex, bool recursive = false); + + protected: + + pplx::task send_get_request(web::http::uri_builder & uri); + pplx::task send_del_request(web::http::uri_builder & uri); + pplx::task send_put_request(web::http::uri_builder & uri, std::string const & key, std::string const & value); + + web::http::client::http_client client; + }; +} + +#endif diff --git a/etcd/Response.hpp b/etcd/Response.hpp new file mode 100644 index 0000000..818b5a4 --- /dev/null +++ b/etcd/Response.hpp @@ -0,0 +1,93 @@ +#ifndef __ETCD_RESPONSE_HPP__ +#define __ETCD_RESPONSE_HPP__ + +#include +#include +#include + +#include "etcd/Value.hpp" + +namespace etcd +{ + typedef std::vector Keys; + + /** + * The Reponse object received for the requests of etcd::Client + */ + class Response + { + public: + static pplx::task create(pplx::task response_task); + + Response(); + + /** + * Returns true if this is a successful response + */ + bool is_ok() const; + + /** + * Returns the error code received from the etcd server. In case of success the error code is 0. + */ + int error_code() const; + + /** + * Returns the string representation of the error code + */ + std::string const & error_message() const; + + /** + * Returns the action type of the operation that this response belongs to. + */ + std::string const & action() const; + + /** + * Returns the current index value of etcd + */ + int index() const; + + /** + * Returns the value object of the response to a get/set/modify operation. + */ + Value const & value() const; + + /** + * Returns the previous value object of the response to a set/modify/rm operation. + */ + Value const & prev_value() const; + + /** + * Returns the index-th value of the response to an 'ls' operation. Equivalent to values()[index] + */ + Value const & value(int index) const; + + /** + * Returns the vector of values in a directory in response to an 'ls' operation. + */ + Values const & values() const; + + /** + * Returns the vector of keys in a directory in response to an 'ls' operation. + */ + Keys const & keys() const; + + /** + * Returns the index-th key in a directory listing. Same as keys()[index] + */ + std::string const & key(int index) const; + + protected: + Response(web::http::http_response http_response, web::json::value json_value); + + int _error_code; + std::string _error_message; + int _index; + std::string _action; + Value _value; + Value _prev_value; + Values _values; + Keys _keys; + }; +} + +#endif diff --git a/etcd/Value.hpp b/etcd/Value.hpp new file mode 100644 index 0000000..ec28dce --- /dev/null +++ b/etcd/Value.hpp @@ -0,0 +1,56 @@ +#ifndef __ETCD_VECTOR_HPP__ +#define __ETCD_VECTOR_HPP__ + +#include +#include +#include + +namespace etcd +{ + /** + * Represents a value object received from the etcd server + */ + class Value + { + public: + /** + * Returns true if this value represents a directory on the server. If true the as_string() + * method is meaningless. + */ + bool is_dir() const; + + /** + * Returns the key of this value as an "absolute path". + */ + std::string const & key() const; + + /** + * Returns the string representation of the value + */ + std::string const & as_string() const; + + /** + * Returns the creation index of this value. + */ + int created_index() const; + + /** + * Returns the last modification's index of this value. + */ + int modified_index() const; + + protected: + friend class Response; + Value(); + Value(web::json::value const & json_value); + std::string _key; + bool dir; + std::string value; + int created; + int modified; + }; + + typedef std::vector Values; +} + +#endif diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..fc9f4de --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(etcd-cpp-api SHARED Client.cpp Response.cpp Value.cpp json_constants.cpp) +set_property(TARGET etcd-cpp-api PROPERTY CXX_STANDARD 11) + +target_link_libraries(etcd-cpp-api ${CPPREST_LIB}) + +install (TARGETS etcd-cpp-api DESTINATION lib) +install (FILES ../etcd/Client.hpp + ../etcd/Response.hpp + ../etcd/Value.hpp + DESTINATION include/etcd) diff --git a/src/Client.cpp b/src/Client.cpp new file mode 100644 index 0000000..60e9d0b --- /dev/null +++ b/src/Client.cpp @@ -0,0 +1,127 @@ +#include "etcd/Client.hpp" + +etcd::Client::Client(std::string const & address) + : client(address) +{ +} + +pplx::task etcd::Client::send_get_request(web::http::uri_builder & uri) +{ + return Response::create(client.request(web::http::methods::GET, uri.to_string())); +} + +pplx::task etcd::Client::send_del_request(web::http::uri_builder & uri) +{ + return Response::create(client.request(web::http::methods::DEL, uri.to_string())); +} + +pplx::task etcd::Client::send_put_request(web::http::uri_builder & uri, std::string const & key, std::string const & value) +{ + std::string data = key + "=" + value; + std::string content_type = "application/x-www-form-urlencoded; param=" + key; + return Response::create(client.request(web::http::methods::PUT, uri.to_string(), data.c_str(), content_type.c_str())); +} + +pplx::task etcd::Client::get(std::string const & key) +{ + web::http::uri_builder uri("/v2/keys" + key); + return send_get_request(uri); +} + +pplx::task etcd::Client::set(std::string const & key, std::string const & value) +{ + web::http::uri_builder uri("/v2/keys" + key); + return send_put_request(uri, "value", value); +} + +pplx::task etcd::Client::add(std::string const & key, std::string const & value) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("prevExist=false"); + return send_put_request(uri, "value", value); +} + +pplx::task etcd::Client::modify(std::string const & key, std::string const & value) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("prevExist=true"); + return send_put_request(uri, "value", value); +} + +pplx::task etcd::Client::modify_if(std::string const & key, std::string const & value, std::string const & old_value) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("prevValue", old_value); + return send_put_request(uri, "value", value); +} + +pplx::task etcd::Client::modify_if(std::string const & key, std::string const & value, int old_index) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("prevIndex", old_index); + return send_put_request(uri, "value", value); +} + +pplx::task etcd::Client::rm(std::string const & key) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("dir=false"); + return Response::create(client.request("DELETE", uri.to_string())); +} + +pplx::task etcd::Client::rm_if(std::string const & key, std::string const & old_value) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("dir=false"); + uri.append_query("prevValue", old_value); + return send_del_request(uri); +} + +pplx::task etcd::Client::rm_if(std::string const & key, int old_index) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("dir=false"); + uri.append_query("prevIndex", old_index); + return send_del_request(uri); +} + +pplx::task etcd::Client::mkdir(std::string const & key) +{ + web::http::uri_builder uri("/v2/keys" + key); + return send_put_request(uri, "dir", "true"); +} + +pplx::task etcd::Client::rmdir(std::string const & key, bool recursive) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("dir=true"); + if (recursive) + uri.append_query("recursive=true"); + return send_del_request(uri); +} + +pplx::task etcd::Client::ls(std::string const & key) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("sorted=true"); + return send_get_request(uri); +} + +pplx::task etcd::Client::watch(std::string const & key, bool recursive) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("wait=true"); + if (recursive) + uri.append_query("recursive=true"); + return send_get_request(uri); +} + +pplx::task etcd::Client::watch(std::string const & key, int fromIndex, bool recursive) +{ + web::http::uri_builder uri("/v2/keys" + key); + uri.append_query("wait=true"); + uri.append_query("waitIndex", fromIndex); + if (recursive) + uri.append_query("recursive=true"); + return send_get_request(uri); +} diff --git a/src/Response.cpp b/src/Response.cpp new file mode 100644 index 0000000..ae32089 --- /dev/null +++ b/src/Response.cpp @@ -0,0 +1,106 @@ +#include "etcd/Response.hpp" +#include "json_constants.hpp" + +pplx::task etcd::Response::create(pplx::task response_task) +{ + return pplx::task ([response_task](){ + auto json_task = response_task.get().extract_json(); + return etcd::Response(response_task.get(), json_task.get()); + }); +} + +etcd::Response::Response() + : _error_code(0), + _index(0) +{ +} + +etcd::Response::Response(web::http::http_response http_response, web::json::value json_value) + : _error_code(0), + _index(0) +{ + if (http_response.headers().has(JSON_ETCD_INDEX)) + _index = atoi(http_response.headers()[JSON_ETCD_INDEX].c_str()); + + if (json_value.has_field(JSON_ERROR_CODE)) + { + _error_code = json_value[JSON_ERROR_CODE].as_number().to_int64(); + _error_message = json_value[JSON_MESSAGE].as_string(); + } + + if (json_value.has_field(JSON_ACTION)) + _action = json_value[JSON_ACTION].as_string(); + + if (json_value.has_field(JSON_NODE)) + { + if (json_value[JSON_NODE].has_field(JSON_NODES)) + { + std::string prefix = json_value[JSON_NODE][JSON_KEY].as_string(); + for (auto & node : json_value[JSON_NODE][JSON_NODES].as_array()) + { + _values.push_back(Value(node)); + _keys.push_back(node[JSON_KEY].as_string().substr(prefix.length() + 1)); + } + } + else + _value = Value(json_value.at(JSON_NODE)); + } + + if (json_value.has_field(JSON_PREV_NODE)) + _prev_value = Value(json_value.at(JSON_PREV_NODE)); +} + +int etcd::Response::error_code() const +{ + return _error_code; +} + +std::string const & etcd::Response::error_message() const +{ + return _error_message; +} + +int etcd::Response::index() const +{ + return _index; +} + +std::string const & etcd::Response::action() const +{ + return _action; +} + +bool etcd::Response::is_ok() const +{ + return error_code() == 0; +} + +etcd::Value const & etcd::Response::value() const +{ + return _value; +} + +etcd::Value const & etcd::Response::prev_value() const +{ + return _prev_value; +} + +etcd::Values const & etcd::Response::values() const +{ + return _values; +} + +etcd::Value const & etcd::Response::value(int index) const +{ + return _values[index]; +} + +etcd::Keys const & etcd::Response::keys() const +{ + return _keys; +} + +std::string const & etcd::Response::key(int index) const +{ + return _keys[index]; +} diff --git a/src/Value.cpp b/src/Value.cpp new file mode 100644 index 0000000..2e5c2c9 --- /dev/null +++ b/src/Value.cpp @@ -0,0 +1,43 @@ +#include "etcd/Value.hpp" +#include "json_constants.hpp" + +etcd::Value::Value() + : dir(false), + created(0), + modified(0) +{ +} + +etcd::Value::Value(web::json::value const & json_value) + : _key(json_value.has_field(JSON_KEY) ? json_value.at(JSON_KEY).as_string() : ""), + dir(json_value.has_field(JSON_DIR)), + value(json_value.has_field(JSON_VALUE) ? json_value.at(JSON_VALUE).as_string() : ""), + created(json_value.has_field(JSON_CREATED) ? json_value.at(JSON_CREATED).as_number().to_int64() : 0), + modified(json_value.has_field(JSON_MODIFIED) ? json_value.at(JSON_MODIFIED).as_number().to_int64() : 0) +{ +} + +std::string const & etcd::Value::key() const +{ + return _key; +} + +bool etcd::Value::is_dir() const +{ + return dir; +} + +std::string const & etcd::Value::as_string() const +{ + return value; +} + +int etcd::Value::created_index() const +{ + return created; +} + +int etcd::Value::modified_index() const +{ + return modified; +} diff --git a/src/json_constants.cpp b/src/json_constants.cpp new file mode 100644 index 0000000..015b3a8 --- /dev/null +++ b/src/json_constants.cpp @@ -0,0 +1,14 @@ +#include "json_constants.hpp" + +char const * etcd::JSON_KEY = "key"; +char const * etcd::JSON_DIR = "dir"; +char const * etcd::JSON_VALUE = "value"; +char const * etcd::JSON_CREATED = "createdIndex"; +char const * etcd::JSON_MODIFIED = "modifiedIndex"; +char const * etcd::JSON_ERROR_CODE = "errorCode"; +char const * etcd::JSON_MESSAGE = "message"; +char const * etcd::JSON_ACTION = "action"; +char const * etcd::JSON_NODE = "node"; +char const * etcd::JSON_NODES = "nodes"; +char const * etcd::JSON_PREV_NODE = "prevNode"; +char const * etcd::JSON_ETCD_INDEX = "X-Etcd-Index"; diff --git a/src/json_constants.hpp b/src/json_constants.hpp new file mode 100644 index 0000000..334e2ed --- /dev/null +++ b/src/json_constants.hpp @@ -0,0 +1,20 @@ +#ifndef __ETCD_JSON_CONSTANTS_HPP__ +#define __ETCD_JSON_CONSTANTS_HPP__ + +namespace etcd +{ + extern char const * JSON_KEY; + extern char const * JSON_DIR; + extern char const * JSON_VALUE; + extern char const * JSON_CREATED; + extern char const * JSON_MODIFIED; + extern char const * JSON_ERROR_CODE; + extern char const * JSON_MESSAGE; + extern char const * JSON_ACTION; + extern char const * JSON_NODE; + extern char const * JSON_NODES; + extern char const * JSON_PREV_NODE; + extern char const * JSON_ETCD_INDEX; +}; + +#endif diff --git a/tst/CMakeLists.txt b/tst/CMakeLists.txt new file mode 100644 index 0000000..6c44391 --- /dev/null +++ b/tst/CMakeLists.txt @@ -0,0 +1,6 @@ +add_executable(etcd_test EtcdTest.cpp) +set_property(TARGET etcd_test PROPERTY CXX_STANDARD 11) + +target_link_libraries(etcd_test etcd-cpp-api) + +add_test(etcd_test etcd_test) diff --git a/tst/EtcdTest.cpp b/tst/EtcdTest.cpp new file mode 100644 index 0000000..244e768 --- /dev/null +++ b/tst/EtcdTest.cpp @@ -0,0 +1,251 @@ +#define CATCH_CONFIG_MAIN +#include + +#include "etcd/Client.hpp" + +TEST_CASE("setup") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.rmdir("/test", true).wait(); +} + +TEST_CASE("add a new key") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.add("/test/key1", "42").get(); + REQUIRE(0 == resp.error_code()); + CHECK("create" == resp.action()); + etcd::Value const & val = resp.value(); + CHECK("42" == val.as_string()); + CHECK("/test/key1" == val.key()); + CHECK(!val.is_dir()); + CHECK(0 < val.created_index()); + CHECK(0 < val.modified_index()); + CHECK(0 < resp.index()); // X-Etcd-Index header value + CHECK(105 == etcd.add("/test/key1", "43").get().error_code()); // Key already exists + CHECK(105 == etcd.add("/test/key1", "42").get().error_code()); // Key already exists + CHECK("Key already exists" == etcd.add("/test/key1", "42").get().error_message()); +} + +TEST_CASE("read a value from etcd") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.get("/test/key1").get(); + CHECK("get" == resp.action()); + REQUIRE(resp.is_ok()); + REQUIRE(0 == resp.error_code()); + CHECK("42" == resp.value().as_string()); + + CHECK("" == etcd.get("/test").get().value().as_string()); // key points to a directory +} + +TEST_CASE("simplified read") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + CHECK("42" == etcd.get("/test/key1").get().value().as_string()); + CHECK(100 == etcd.get("/test/key2").get().error_code()); // Key not found +} + +TEST_CASE("modify a key") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.modify("/test/key1", "43").get(); + REQUIRE(0 == resp.error_code()); // overwrite + CHECK("update" == resp.action()); + CHECK(100 == etcd.modify("/test/key2", "43").get().error_code()); // Key not found + CHECK("43" == etcd.modify("/test/key1", "42").get().prev_value().as_string()); +} + +TEST_CASE("set a key") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.set("/test/key1", "43").get(); + REQUIRE(0 == resp.error_code()); // overwrite + CHECK("set" == resp.action()); + CHECK(0 == etcd.set("/test/key2", "43").get().error_code()); // create new + CHECK("43" == etcd.set("/test/key2", "44").get().prev_value().as_string()); + CHECK("" == etcd.set("/test/key3", "44").get().prev_value().as_string()); + CHECK(102 == etcd.set("/test", "42").get().error_code()); // Not a file +} + +TEST_CASE("delete a value") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + CHECK(3 == etcd.ls("/test").get().keys().size()); + etcd::Response resp = etcd.rm("/test/key1").get(); + CHECK("43" == resp.prev_value().as_string()); + CHECK("delete" == resp.action()); + CHECK(2 == etcd.ls("/test").get().keys().size()); +} + +TEST_CASE("create a directory") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd::Response resp = etcd.mkdir("/test/new_dir").get(); + CHECK("set" == resp.action()); + CHECK(resp.value().is_dir()); +} + +TEST_CASE("list a directory") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + CHECK(0 == etcd.ls("/test/new_dir").get().keys().size()); + + etcd.set("/test/new_dir/key1", "value1").wait(); + etcd.set("/test/new_dir/key2", "value2").wait(); + etcd.mkdir("/test/new_dir/sub_dir").wait(); + + etcd::Response resp = etcd.ls("/test/new_dir").get(); + CHECK("get" == resp.action()); + REQUIRE(3 == resp.keys().size()); + CHECK("key1" == resp.key(0)); + CHECK("key2" == resp.key(1)); + CHECK("sub_dir" == resp.key(2)); + CHECK("value1" == resp.value(0).as_string()); + CHECK("value2" == resp.value(1).as_string()); + CHECK(resp.values()[2].is_dir()); + + CHECK(0 == etcd.ls("/test/new_dir/key1").get().error_code()); +} + +TEST_CASE("delete a directory") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + CHECK(108 == etcd.rmdir("/test/new_dir").get().error_code()); // Directory not empty + CHECK(0 == etcd.rmdir("/test/new_dir", true).get().error_code()); +} + +TEST_CASE("wait for a value change") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.set("/test/key1", "42").wait(); + + pplx::task res = etcd.watch("/test/key1"); + CHECK(!res.is_done()); + sleep(1); + CHECK(!res.is_done()); + + etcd.set("/test/key1", "43").get(); + sleep(1); + REQUIRE(res.is_done()); + REQUIRE("set" == res.get().action()); + CHECK("43" == res.get().value().as_string()); +} + +TEST_CASE("wait for a directory change") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + + pplx::task res = etcd.watch("/test", true); + CHECK(!res.is_done()); + sleep(1); + CHECK(!res.is_done()); + + etcd.add("/test/key4", "44").wait(); + sleep(1); + REQUIRE(res.is_done()); + CHECK("create" == res.get().action()); + CHECK("44" == res.get().value().as_string()); + + pplx::task res2 = etcd.watch("/test", true); + CHECK(!res2.is_done()); + sleep(1); + CHECK(!res2.is_done()); + + etcd.set("/test/key4", "45").wait(); + REQUIRE(res2.is_done()); + CHECK("set" == res2.get().action()); + CHECK("45" == res2.get().value().as_string()); +} + +TEST_CASE("watch changes in the past") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + + int index = etcd.set("/test/key1", "42").get().index(); + + etcd.set("/test/key1", "43").wait(); + etcd.set("/test/key1", "44").wait(); + etcd.set("/test/key1", "45").wait(); + + etcd::Response res = etcd.watch("/test/key1", ++index).get(); + CHECK("set" == res.action()); + CHECK("43" == res.value().as_string()); + + res = etcd.watch("/test/key1", ++index).get(); + CHECK("set" == res.action()); + CHECK("44" == res.value().as_string()); + + res = etcd.watch("/test", ++index, true).get(); + CHECK("set" == res.action()); + CHECK("45" == res.value().as_string()); +} + +TEST_CASE("atomic compare-and-swap") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.set("/test/key1", "42").wait(); + + // modify success + etcd::Response res = etcd.modify_if("/test/key1", "43", "42").get(); + int index = res.index(); + REQUIRE(res.is_ok()); + CHECK("compareAndSwap" == res.action()); + CHECK("43" == res.value().as_string()); + + // modify fails the second time + res = etcd.modify_if("/test/key1", "44", "42").get(); + CHECK(!res.is_ok()); + CHECK(101 == res.error_code()); + CHECK("Compare failed" == res.error_message()); + + // succes with the correct index + res = etcd.modify_if("/test/key1", "44", index).get(); + REQUIRE(res.is_ok()); + CHECK("compareAndSwap" == res.action()); + CHECK("44" == res.value().as_string()); + + // index changes so second modify fails + res = etcd.modify_if("/test/key1", "45", index).get(); + CHECK(!res.is_ok()); + CHECK(101 == res.error_code()); + CHECK("Compare failed" == res.error_message()); +} + +TEST_CASE("atomic compare-and-delete based on prevValue") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + etcd.set("/test/key1", "42").wait(); + + etcd::Response res = etcd.rm_if("/test/key1", "43").get(); + CHECK(!res.is_ok()); + CHECK(101 == res.error_code()); + CHECK("Compare failed" == res.error_message()); + + res = etcd.rm_if("/test/key1", "42").get(); + REQUIRE(res.is_ok()); + CHECK("compareAndDelete" == res.action()); + CHECK("42" == res.prev_value().as_string()); +} + +TEST_CASE("atomic compare-and-delete based on prevIndex") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + int index = etcd.set("/test/key1", "42").get().index(); + + etcd::Response res = etcd.rm_if("/test/key1", index - 1).get(); + CHECK(!res.is_ok()); + CHECK(101 == res.error_code()); + CHECK("Compare failed" == res.error_message()); + + res = etcd.rm_if("/test/key1", index).get(); + REQUIRE(res.is_ok()); + CHECK("compareAndDelete" == res.action()); + CHECK("42" == res.prev_value().as_string()); +} + +TEST_CASE("cleanup") +{ + etcd::Client etcd("http://127.0.0.1:4001"); + REQUIRE(0 == etcd.rmdir("/test", true).get().error_code()); +}