├── .gitignore ├── History.md ├── Makefile ├── README.md ├── examples └── basics.js ├── index.js ├── lib ├── client.js ├── result.js └── utils.js ├── package.json └── test ├── cluster.sh └── commands.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib-cov 3 | tmp 4 | coverage.html 5 | *.log 6 | *.tern-port -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 0.1.0 / 2013-12-09 2 | ================== 3 | 4 | * initial release. 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | TESTS ?= $(wildcard test/*.test.js) 3 | 4 | test: 5 | @NODE_ENV=test ./node_modules/.bin/mocha $(TESTS) \ 6 | --require "should" \ 7 | --timeout 2000 \ 8 | --reporter $(REPORTER) \ 9 | --growl \ 10 | 11 | test-cov: lib-cov 12 | @ETCD_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 13 | 14 | lib-cov: 15 | @jscoverage lib lib-cov 16 | 17 | clean: 18 | @rm -rf lib-cov coverage.html 19 | 20 | .PHONY: test lib-cov test-cov clean -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-etcd 2 | 3 | Another (!!) etcd library for nodejs. This is formerly based on etcd-node, but has since evolved to a full-fledged new library with etcd v2 support. 4 | 5 | [![NPM](https://nodei.co/npm/nodejs-etcd.png)](https://nodei.co/npm/nodejs-etcd/) 6 | 7 | ## Notice 8 | 9 | This is not stable at the moment. Development will follow closely the development of etcd and changes in its API. minor-version changes will be kept in sync. 10 | 11 | ## Install 12 | 13 | ```sh 14 | $ npm install nodejs-etcd 15 | ``` 16 | 17 | ## Configuring. 18 | 19 | The client only need to be configured very simply by providing the base url of the etcd service. 20 | 21 | 22 | ```js 23 | var etcd = require('nodejs-etcd'); 24 | 25 | var e = new etcd({ 26 | url: 'https://node01.example.com:4001' 27 | }) 28 | ``` 29 | 30 | 31 | ## Commands 32 | 33 | Nodejs-etcd supports the full v2 api specification. 34 | 35 | ### .read(options, [callback]) 36 | 37 | Reads from etcd. All paths you may want to read start with '/' as the etcd hierarchy strictly mimics the one of a filesystem 38 | 39 | ```js 40 | e.read({'key': '/hello'}, function (err, result, body) { 41 | if (err) throw err; 42 | assert(result.value); 43 | }); 44 | ``` 45 | 46 | All etcd flags are supported here as well; the valid options are: 47 | 48 | - `recursive` (boolean) it set to true, fetches all subdirectories 49 | - `wait` (boolean) if set to true, the request will wait until the value changes. 50 | - `wait_index` (integer) if set toghether with wait, will wait to return until the marked index is reached 51 | 52 | 53 | ### .generator(err_cb, resp_cb) 54 | 55 | The callback can be encapsulated using this method. It will return a valid callback for the other methods that will: 56 | 57 | - Manage HTTP response codes 58 | - Populate a standard `EtcdResult` object (see `result.js`) 59 | - Apply resp_cb to this result. 60 | 61 | Let's say we just want to output the value of the key: 62 | 63 | ```js 64 | cb = e.generator( 65 | function () { console.log('An error has occurred')}, 66 | function (result) { console.log('We found the key, it has value ' + result.value)} 67 | ) 68 | e.read( 69 | {key: '/hello'}, 70 | cb 71 | ) 72 | ``` 73 | By default, if no callback is declared nodejs-etcd will log some important values of the response to the console. 74 | 75 | 76 | ### .write(options, [callback]) 77 | 78 | Writes a key or dir to the cluster. Simplest form: 79 | 80 | ```js 81 | e.write({ 82 | key: 'hello', 83 | value: 'world', 84 | }, function (err,resp, body) { 85 | if (err) throw err; 86 | console.log(body); 87 | }); 88 | ``` 89 | 90 | All etcd flags to a write operation are supported and must be added to the `options` object. 91 | 92 | Accepted options: 93 | 94 | - `ttl` (integer) sets a TTL on the key 95 | - `dir` (boolean) will write a directory. dont pass a value if this is true. 96 | - `prev_exists` (boolean) key gets written only if it is being created. 97 | - `prev_index` (integer) sets the key only if the actual index is exactly this one. 98 | - `prev_value` (string) sets the key only if the actual value is this one. 99 | 100 | ### .del(options, [callback]) 101 | 102 | Deletes a key from etcd. If the `recursive` option is set to true, it will allow to remove directories. 103 | 104 | ```js 105 | e.del('hello', function (err) { 106 | if (err) throw err; 107 | }); 108 | ``` 109 | 110 | 111 | ### .machines(callback) 112 | 113 | ```js 114 | etcd.machines(function (err, list) { 115 | if (err) throw err; 116 | }); 117 | ``` 118 | 119 | ### .leader(callback) 120 | 121 | ```js 122 | etcd.leader(function (err, host) { 123 | if (err) throw err; 124 | }); 125 | ``` 126 | 127 | ## License 128 | 129 | MIT 130 | -------------------------------------------------------------------------------- /examples/basics.js: -------------------------------------------------------------------------------- 1 | process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; 2 | var etcd = require('..'); 3 | var sleep = require('sleep') 4 | 5 | e = new etcd({ url: 'http://localhost:4001'}) 6 | 7 | 8 | //basic example, *can* run into race conditions. 9 | e 10 | .write({key:'/foo/example', value:'hi'}) 11 | .read({key:'/foo/example'}); 12 | 13 | e.write({key:'/foo/example2', value:'there'}) 14 | 15 | 16 | //now try to create a valid callback structure to handle the etcd response. 17 | 18 | var set_config = function (result) { 19 | if ('errorCode' in result) 20 | console.log(result) 21 | else { 22 | global.config = result.getChildren() 23 | console.log('config set!') 24 | console.log(global.config) 25 | } 26 | } 27 | 28 | var set_config_and_exit = e.generator( 29 | function () { console.log(arguments)}, 30 | set_config 31 | ) 32 | 33 | var watch_endpoint = function (cb) { 34 | e.read( 35 | {key: '/foo', recursive: true, wait: true}, 36 | cb 37 | ) 38 | } 39 | 40 | 41 | var set_config_and_watch = e.generator( 42 | function () { console.log(arguments)}, 43 | function (result) { 44 | if (result.action == 'get') { 45 | set_config(result) 46 | } else { 47 | e.read({key: '/foo', recursive: true}, set_config_and_exit) 48 | } 49 | watch_endpoint(set_config_and_watch) 50 | } 51 | ) 52 | 53 | 54 | // this will sit forever waiting for changes in the /foo dir and changing global.config for you. 55 | // Note that no recursive call is made here as all calls to set_config_and_watch 56 | // are done via callback, hence from the event loop itself and not from the calling function. 57 | console.log('We will run forever now waiting for changes to happen in the config...') 58 | e 59 | .read( 60 | { 61 | key: '/foo', 62 | recursive: true 63 | }, 64 | set_config_and_watch 65 | ) 66 | 67 | e 68 | .write({key:'/foo/example3', value:'meoww2'}, 69 | function () { 70 | sleep.sleep(10); 71 | e.del({key:'/foo/example2'}); 72 | console.log(global.config); 73 | }) 74 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/client') 2 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies. 3 | */ 4 | 5 | var utils = require('./utils') 6 | var request = require('request') 7 | var debug = require('debug')('nodejs-etcd') 8 | var https = require('https') 9 | 10 | /** 11 | * Initialize a new client. 12 | * 13 | * @see configure() 14 | */ 15 | 16 | function Client(opts) { 17 | this.version = 'v2' 18 | this.configure(opts || {}) 19 | } 20 | 21 | 22 | 23 | Client.prototype.generator = require('./result').handle_generator 24 | 25 | 26 | /** 27 | * Configure connection options. 28 | * 29 | * Settings: 30 | * 31 | * - port 32 | * - host 33 | * 34 | * 35 | * @param {Object} opts 36 | * @return {Client} 37 | * @public 38 | */ 39 | 40 | Client.prototype.configure = function (settings) { 41 | //TODO: validate the url? 42 | this.baseurl = settings.url + '/' + this.version 43 | //ssl client certificate support. 44 | //Set up HttpsAgent if sslopts {ca, key, cert} are given 45 | if ('ssloptions' in settings) { 46 | this.agent = new https.HttpsAgent(settings.ssloptions) 47 | } else { 48 | this.agent = false 49 | } 50 | return this; 51 | }; 52 | 53 | 54 | /** 55 | * Internal method for calling the server. 56 | * 57 | * @param {Object} options 58 | * @param {Function} cb 59 | * @return {Object} 60 | * @private 61 | * 62 | */ 63 | Client.prototype._call = function (options, callback) { 64 | // by default, just log to console the result. 65 | cb = callback || this.generator() 66 | var blocking = ('blocking' in options) && options.blocking 67 | delete options.blocking 68 | url = this.url('keys', options.key) 69 | 70 | delete options.key 71 | if (blocking) { 72 | //TODO: make a syncronoush http call in this case. 73 | } 74 | if (this.agent) { 75 | options.agent = this.agent 76 | } 77 | request(url, options, cb) 78 | return this; 79 | }; 80 | 81 | 82 | /** 83 | * Machines. 84 | * 85 | * TODO: look into `res.error`. 86 | * 87 | * @param {Function} cb 88 | * @public 89 | */ 90 | 91 | Client.prototype.machines = function (cb) { 92 | return request.get(this.url('machines'), cb) 93 | }; 94 | 95 | /** 96 | * Leader. 97 | * 98 | * TODO: look into `res.error`. 99 | * 100 | * @param {Function} cb 101 | * @public 102 | */ 103 | 104 | Client.prototype.leader = function (cb) { 105 | return request.get(this.url('leader'), cb) 106 | }; 107 | 108 | 109 | /** 110 | * Read. 111 | * 112 | * @param {Object} options 113 | * @return {Object} 114 | * @public 115 | */ 116 | 117 | Client.prototype.read = function (options, cb) { 118 | if (!options) options = {} 119 | 120 | var opts = {} 121 | opts.method = 'GET' 122 | opts.key = options.key || '/' 123 | 124 | opts.qs = {} 125 | if ('recursive' in options) opts.qs.recursive = options.recursive 126 | if ('wait' in options) opts.qs.wait = options.wait 127 | if ('wait_index' in options) opts.qs.waitIndex = options.wait_index 128 | if ('sorted' in options) opts.qs.sorted = options.sorted 129 | return this._call(opts, cb) 130 | }; 131 | 132 | /** 133 | * Get. 134 | * 135 | * @param {String} key 136 | * @param {Function} cb 137 | * @return {Client} 138 | * @public 139 | */ 140 | 141 | Client.prototype.get = function (key, cb) { 142 | return this.read({'key': key}, cb) 143 | }; 144 | 145 | /** 146 | * Delete. 147 | * 148 | * @param {String} key 149 | * @param {Function} cb 150 | * @return {Client} 151 | * @public 152 | */ 153 | 154 | Client.prototype.del = function (options, cb) { 155 | var opts = {'method': 'DELETE'} 156 | opts.key = options.key 157 | 158 | if ('recursive' in options) opts.recursive = options.recursive 159 | if ('dir' in options) opts.dir = options.dir 160 | // Still unsupported, but they may work soon. 161 | if ('prev_value' in options) opts.prevValue = options.prev_value 162 | if ('prev_index' in options) opts.prevIndex = options.prev_index 163 | return this._call(opts, cb) 164 | }; 165 | 166 | 167 | /** 168 | * Write. 169 | * 170 | * @param {Object} options 171 | * @param {Function} cb 172 | * @return {Mixed} 173 | * @public 174 | */ 175 | 176 | Client.prototype.write = function (options, cb) { 177 | var opts = {} 178 | 179 | opts.method = ('method' in options) && options.method || 'PUT' 180 | opts.key = options.key || '/' 181 | opts.form = {'value': options.value} 182 | opts.qs = {}; 183 | 184 | if ('ttl' in options) opts.form.ttl = options.ttl 185 | if ('dir' in options) opts.qs.dir = options.dir 186 | if ('prev_exists' in options) opts.form.prevExists = options.prev_exists 187 | if ('prev_index' in options) opts.form.prevIndex = options.prev_index 188 | if ('prev_value' in options) opts.form.prevValue = options.prev_value 189 | 190 | return this._call(opts, cb) 191 | } 192 | 193 | /** 194 | * Append. 195 | * 196 | * @param {Object} options 197 | * @param {Function} cb 198 | * @return {Mixed} 199 | * @public 200 | */ 201 | 202 | Client.prototype.append = function (options, cb) { 203 | options.method = 'POST' 204 | return this.write(options, cb) 205 | } 206 | 207 | /** 208 | * Endpoint utility. 209 | * 210 | * @return {String} 211 | * @private 212 | */ 213 | 214 | Client.prototype.url = function () { 215 | var route = [].slice.call(arguments).join('/').replace('//','/') 216 | return this.baseurl + '/' + route 217 | }; 218 | 219 | 220 | 221 | module.exports = Client 222 | -------------------------------------------------------------------------------- /lib/result.js: -------------------------------------------------------------------------------- 1 | function EtcdResult(data) { 2 | console.log(data) 3 | this.fetch(data) 4 | } 5 | 6 | 7 | EtcdResult.prototype.fetch = function (data) { 8 | this.action = data.action 9 | for (k in data.node) { 10 | this[k] = data.node[k] 11 | } 12 | } 13 | 14 | EtcdResult.prototype.getChildren = function () { 15 | var res = {} 16 | if (! this.dir) { 17 | return res 18 | } 19 | for (i=0; i < this.nodes.length; i++) { 20 | if (this.nodes[i].dir) continue 21 | var key = this.nodes[i].key 22 | var value = this.nodes[i].value 23 | res[key] = value 24 | } 25 | return res 26 | } 27 | 28 | 29 | 30 | 31 | function noop(obj){ 32 | console.log('###') 33 | console.log('Action: ' + obj.action) 34 | if (obj.dir) { 35 | console.log('Directory: ' + obj.key) 36 | } else { 37 | console.log('Key: ' + obj.key) 38 | console.log('Value: ' + obj.value) 39 | } 40 | console.log('Index: ' + obj.modifiedIndex) 41 | console.log('###') 42 | } 43 | 44 | exports.handle_generator = function (err_cb, resp_cb) { 45 | var exc = err_cb || noop 46 | var resp = resp_cb || noop 47 | return function (error, response, body) { 48 | if (error) { 49 | console.log('An error occured when trying to execute your request') 50 | console.log(error) 51 | return exc(error) 52 | } 53 | if (response.statusCode == 200 || response.statusCode == 201) { 54 | //this response was ok 55 | var container = {} 56 | var data = JSON.parse(body) 57 | var result = new EtcdResult(data) 58 | if (response.statusCode == 201) result.new_key = true 59 | resp(result) 60 | } else { 61 | var json = JSON.parse(body); 62 | var error = new Error(json.message); 63 | 64 | error.code = json.code; 65 | error.cause = json.cause; 66 | 67 | resp(error); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Errors codes we can safely ignore. 4 | * 5 | * See: https://github.com/coreos/etcd/blob/master/error/error.go 6 | */ 7 | 8 | var ignored = [100]; 9 | 10 | /** 11 | * Parses the error text, decides what we 12 | * want to ignore or not. If this never gets 13 | * more complicated than this, we can just 14 | * move this back into client. 15 | * 16 | * @param {String} text 17 | * @return {Error} 18 | * @private 19 | */ 20 | 21 | exports.error = function (text) { 22 | var json = JSON.parse(text); 23 | var error = new Error(json.message); 24 | 25 | error.code = json.code; 26 | error.cause = json.cause; 27 | error.ignore = !!~ignored.indexOf(error.code); 28 | 29 | return error; 30 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-etcd", 3 | "version": "0.1.1", 4 | "description": "etcd client for node", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "examples", 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "make test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git@github.com:lavagetto/nodejs-etcd" 16 | }, 17 | "keywords": [ 18 | "etcd", 19 | "coreos", 20 | "raft" 21 | ], 22 | "author": "Giuseppe Lavagetto", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/lavagetto/nodejs-etcd/issues" 26 | }, 27 | "dependencies": { 28 | "request": "~2.29.0", 29 | "debug": "~0.7.2" 30 | }, 31 | "devDependencies": { 32 | "should": "~2.0.1", 33 | "mocha": "~1.13.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/cluster.sh: -------------------------------------------------------------------------------- 1 | 2 | # I assume you have a similar setup in my tests... 3 | 4 | ./etcd -s 127.0.0.1:7001 -c 127.0.0.1:4001 -d nodes/node1 -n node1 & 5 | ./etcd -s 127.0.0.1:7002 -c 127.0.0.1:4002 -C 127.0.0.1:7001 -d nodes/node2 -n node2 & 6 | ./etcd -s 127.0.0.1:7003 -c 127.0.0.1:4003 -C 127.0.0.1:7001 -d nodes/node3 -n node3 7 | -------------------------------------------------------------------------------- /test/commands.test.js: -------------------------------------------------------------------------------- 1 | var etcd = require('..') 2 | var should = require('should') 3 | var e = new etcd({url: 'http://localhost:4001'}) 4 | describe('Commands', function () { 5 | 6 | describe('SET', function () { 7 | it('should set key with value', function (done) { 8 | e.write( 9 | {key: '/hello', value: 'world'}, 10 | e.generator( 11 | function () { should.not.exist(arguments); done()}, 12 | function (result) { 13 | result.should.have.property('key', '/hello'); 14 | result.should.have.property('action', 'set'); 15 | result.should.have.property('value', 'world'); 16 | done(); 17 | } 18 | ) 19 | ); 20 | }); 21 | 22 | it('should set a key with ttl', function (done) { 23 | e.write( 24 | {key: '/hi', value: 'there', ttl: 10}, 25 | e.generator( 26 | function () { should.not.exist(arguments); done()}, 27 | function (result) { 28 | result.should.have.property('key', '/hi'); 29 | result.should.have.property('action', 'set'); 30 | result.should.have.property('ttl', 10); 31 | done(); 32 | } 33 | 34 | ) 35 | ) 36 | }); 37 | }); 38 | 39 | 40 | describe('GET', function () { 41 | it('should get key with value', function (done) { 42 | e.write({ key: '/hi', value: 'bye'}, function () { 43 | e.read( 44 | {key: '/hi'}, 45 | e.generator( 46 | function () { should.not.exist(arguments); done()}, 47 | function (result) { 48 | result.should.have.property('key', '/hi'); 49 | result.should.have.property('action', 'get'); 50 | result.should.have.property('value', 'bye'); 51 | done(); 52 | } 53 | ) 54 | ); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('DEL', function () { 60 | it('should delete key', function (done) { 61 | e.write( 62 | { key: '/yoo', value: 'bye'}, 63 | function () { 64 | e.del('/yoo', 65 | e.generator( 66 | function () { should.not.exist(arguments); done()}, 67 | function (result) { 68 | result.should.have.property('action', 'delete'); 69 | result.should.have.property('key', '/yoo'); 70 | result.should.have.property('prevValue', 'bye'); 71 | done(); 72 | } 73 | ) 74 | ) 75 | }); 76 | }); 77 | }); 78 | 79 | }); 80 | 81 | /** 82 | * Little utility. 83 | */ 84 | 85 | function times(n, fn) { 86 | return function () { 87 | --n || fn.apply(null, arguments); 88 | }; 89 | } 90 | --------------------------------------------------------------------------------