initial repository creation

This commit is contained in:
Arches 2016-05-31 11:20:06 +02:00
commit 7d11a0a4ca
14 changed files with 1154 additions and 0 deletions

16
CMakeLists.txt Normal file
View File

@ -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)

24
LICENSE.txt Normal file
View File

@ -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.

250
README.md Normal file
View File

@ -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<etcd::Response> 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<etcd:Response>```. 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<etcd::Response>```. 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<etcd::Response> 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<etcd::Response> 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<etcd::Response> 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<etcd::Response> 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.

138
etcd/Client.hpp Normal file
View File

@ -0,0 +1,138 @@
#ifndef __ETCD_CLIENT_HPP__
#define __ETCD_CLIENT_HPP__
#include "etcd/Response.hpp"
#include <cpprest/http_client.h>
#include <string>
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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> 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<Response> watch(std::string const & key, int fromIndex, bool recursive = false);
protected:
pplx::task<Response> send_get_request(web::http::uri_builder & uri);
pplx::task<Response> send_del_request(web::http::uri_builder & uri);
pplx::task<Response> send_put_request(web::http::uri_builder & uri, std::string const & key, std::string const & value);
web::http::client::http_client client;
};
}
#endif

93
etcd/Response.hpp Normal file
View File

@ -0,0 +1,93 @@
#ifndef __ETCD_RESPONSE_HPP__
#define __ETCD_RESPONSE_HPP__
#include <cpprest/http_client.h>
#include <string>
#include <vector>
#include "etcd/Value.hpp"
namespace etcd
{
typedef std::vector<std::string> Keys;
/**
* The Reponse object received for the requests of etcd::Client
*/
class Response
{
public:
static pplx::task<Response> create(pplx::task<web::http::http_response> 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

56
etcd/Value.hpp Normal file
View File

@ -0,0 +1,56 @@
#ifndef __ETCD_VECTOR_HPP__
#define __ETCD_VECTOR_HPP__
#include <cpprest/http_client.h>
#include <string>
#include <vector>
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<Value> Values;
}
#endif

10
src/CMakeLists.txt Normal file
View File

@ -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)

127
src/Client.cpp Normal file
View File

@ -0,0 +1,127 @@
#include "etcd/Client.hpp"
etcd::Client::Client(std::string const & address)
: client(address)
{
}
pplx::task<etcd::Response> 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::Response> 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::Response> 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::Response> etcd::Client::get(std::string const & key)
{
web::http::uri_builder uri("/v2/keys" + key);
return send_get_request(uri);
}
pplx::task<etcd::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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::Response> 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);
}

106
src/Response.cpp Normal file
View File

@ -0,0 +1,106 @@
#include "etcd/Response.hpp"
#include "json_constants.hpp"
pplx::task<etcd::Response> etcd::Response::create(pplx::task<web::http::http_response> response_task)
{
return pplx::task<etcd::Response> ([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];
}

43
src/Value.cpp Normal file
View File

@ -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;
}

14
src/json_constants.cpp Normal file
View File

@ -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";

20
src/json_constants.hpp Normal file
View File

@ -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

6
tst/CMakeLists.txt Normal file
View File

@ -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)

251
tst/EtcdTest.cpp Normal file
View File

@ -0,0 +1,251 @@
#define CATCH_CONFIG_MAIN
#include <catch.hpp>
#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<etcd::Response> 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<etcd::Response> 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<etcd::Response> 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());
}