Proof of concept

This commit is contained in:
Mark van Renswoude 2020-07-03 07:07:40 +02:00
parent e12143dc28
commit 2520824fe6
10 changed files with 791 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
config/*
!config/*.example.js

View File

@ -1,2 +1,5 @@
# ConsulTemplate-NodeJS
Like Consul-template, but different. And written in Node.js.
# ConsulWatcher
This is a much simplified version of HashiCorp's Consul-Template, because what it accomplishes is awesome but I really dislike the Go template syntax. All configuration in this version are plain JavaScript.
It's main purpose is creating output files based on a Consul catalog, much like Consul-Template, but since the update handlers are just JavaScript functions you are free to do whatever you want, like calling a webservice or using a template library.

View File

@ -0,0 +1,4 @@
module.exports = async (catalog, logger) =>
{
// Use catalog parameter to generate output
};

105
config/index.example.js Normal file
View File

@ -0,0 +1,105 @@
const config = {
onUpdate: [],
afterUpdate: null
};
/*
Consul agent configuration
Determines the agent or server which will be queried and
monitored for the service catalog.
Recommended to be a local agent connected to the cluster.
Passed to the initialization of the Node Consul client.
For all options, see: https://github.com/silas/node-consul#init
*/
config.consul = {
host: 'localhost'
}
/*
Logging
See: https://github.com/winstonjs/winston#transports
*/
const winston = require('winston');
config.logging = {
transports: [
new winston.transports.Console({
level: 'debug',
timestamp: true,
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
};
/*
onUpdate handlers
When a change occurs in the Consul catalog, each handler is called in order.
Callbacks may return a Promise. Note that when using multiple handlers,
they are not awaited immediately but at the end using Promise.all(), which
means the handlers effectively run in parallel.
The catalog parameter has a services property to enumerate the registered
services. See the Readme for the documentation of the ConsulCatalog class.
For more information about a service, including it's address and health,
an additional call to Consul is required. Calling these methods will result
in the service being watched for changes as well.
The second parameter is a reference to the Winston logger instance.
*/
const fs = require('fs').promises;
config.onUpdate.push((catalog, logger) =>
{
// Use catalog parameter to generate output
let output = '';
for (const service of catalog.services)
{
output +=
`Service: ${service.name}
Tags: ${JSON.stringify(service.tags)}
Address: ${await service.getAddress()}
Port: ${await service.getPort()}
`;
};
await fs.writeFile('example-output.txt', output);
});
/*
afterUpdate handler
This is a single handler which is called after all the onUpdate handlers
have finished, including any Promises returned.
It can be used for example to reload a proxy server after the configuration
changes have been written in onUpdate.
*/
config.afterUpdate = (catalog, logger) =>
{
// Call a reload script
}
module.exports = config;

12
consulwatcher.service Normal file
View File

@ -0,0 +1,12 @@
[Unit]
Description=ConsulWatcher
Requires=network-online.target
After=network-online.target
[Service]
Restart=on-failure
ExecStart=/usr/local/bin/node /srv/consulwatcher/index.js
KillSignal=SIGINT
[Install]
WantedBy=multi-user.target

9
index.js Normal file
View File

@ -0,0 +1,9 @@
const ConsulCatalog = require('./lib/consulcatalog');
const logger = require('./lib/logger');
const config = require('./config');
const catalog = new ConsulCatalog(logger, config);
// TODO detect if the connection is down for too long, allow a custom notification to be sent
// TODO provide a way to easily switch between configs, for multiple environments

332
lib/consulcatalog.js Normal file
View File

@ -0,0 +1,332 @@
const debounce = require('debounce');
// TODO support multiple instances of the same service name
function sameTags(a, b)
{
if (a.length != b.length)
return false;
for (let i = 0, l = a.length; i < l; i++)
{
if (!b.includes(a[i]))
return false;
}
return true;
}
class ConsulCatalog
{
constructor(logger, config)
{
const self = this;
self.rawData = null;
// Always use promises, the code relies on it
config.consul.promisify = true;
self._config = config;
self._logger = logger;
self._consul = require('consul')(config.consul);
self._services = [];
self._updating = false;
self._requireUpdate = false;
self._debouncedUpdate = null;
this._logger.info('Starting watch for catalog service list');
self._watch = self._consul.watch({
method: self._consul.catalog.service.list,
options: {}
});
self._watch.on('change', (data, res) =>
{
if (self._applyCatalogData(data))
self._doUpdate();
});
self._watch.on('error', err =>
{
// TODO better exception handling
self._logger.error('Error while watching catalog service list', err);
});
}
get services()
{
return this._services;
}
serviceByName(name)
{
return this._services.find(service => service.name == name) || null;
}
servicesByTag(tag)
{
return this._services.filter(service => service.tags.includes(tag));
}
servicesByTags(tags)
{
return this._services.filter(service => tags.every(tag => service.tags.includes(tag)));
}
_applyCatalogData(data)
{
const self = this;
self.rawData = data;
let changed = false;
// Remove services that no longer exist
const serviceNames = Object.keys(data);
self._services = self._services.filter(service =>
{
const serviceIndex = serviceNames.indexOf(service.name);
if (serviceIndex == -1)
{
// Previously detected service no longer appears in Consul, remove
// any watches that may be present and remove it from the list
service._delete();
changed = true;
return false;
}
if (service._applyCatalogData(data[service.name]))
changed = true;
// Remove from serviceNames to indicate it has already been applied
serviceNames.splice(serviceIndex, 1);
return true;
});
// All remaining entries in serviceNames are new
serviceNames.forEach(name =>
{
self._logger.debug(`Found new service: ${name}`)
self._services.push(new ConsulService(self, name, data[name]));
changed = true;
});
return changed;
}
_doUpdate()
{
const self = this;
if (self._updating)
{
self._logger.debug('Update already running, will re-run after it is finished');
self._requireUpdate = true;
return;
}
if (self._debouncedUpdate == null)
{
self._debouncedUpdate = debounce(() =>
{
self._updating = true;
self._requireUpdate = false;
self._logger.info('Running update handlers');
const handlerPromises = [];
self._config.onUpdate.forEach(handler =>
{
const handlerPromise = Promise.resolve(handler(self, self._logger));
handlerPromises.push(handlerPromise);
});
Promise.all(handlerPromises)
.then(() =>
{
self._logger.info('Running after-update handler');
Promise.resolve(self._config.afterUpdate(self, self._logger))
.then(() =>
{
self._logger.info('Update completed');
self._updating = false;
if (self._requireUpdate)
{
self._logger.debug('Update re-run requested');
self._doUpdate();
}
});
});
}, 500);
}
self._debouncedUpdate();
}
}
class ConsulService
{
constructor(catalog, name, tags)
{
this._catalog = catalog;
this._consul = catalog._consul;
this._logger = catalog._logger;
this._watch = null;
this._rawData = null;
this._rawDataPromise = null;
this.name = name;
this.tags = tags;
}
async getAddress()
{
var rawData;
try
{
rawData = await this._getRawData();
}
catch(e)
{
// TODO better exception handling
this._logger.error('Error while retrieving service status', err);
return null;
}
if (rawData.length == 0)
return null;
if (rawData[0].Service.Address != '')
return rawData[0].Service.Address;
return rawData[0].Node.Address;
}
async getPort()
{
var rawData;
try
{
rawData = await this._getRawData();
}
catch(e)
{
// TODO better exception handling
this._logger.error('Error while retrieving service status', err);
return null;
}
return rawData.length > 0 ? rawData[0].Service.Port : null;
}
// TODO getHealth
_getRawData()
{
const self = this;
if (self._rawDataPromise !== null)
return self._rawDataPromise;
// Get status information for the service and start watching it
self._rawDataPromise = new Promise((resolve, reject) =>
{
let firstResponse = true;
self._logger.debug(`Starting watch for service: ${this.name}`);
self._watch = self._consul.watch({
method: self._consul.health.service,
options: { service: self.name }
});
self._watch.on('change', (data, res) =>
{
if (self._applyHealthData(data))
self._catalog._doUpdate();
if (firstResponse)
{
firstResponse = false;
resolve(self._rawData);
}
});
self._watch.on('error', err =>
{
// TODO better error handling
self._logger.error(`Error while watching status for service: ${this.name}`, err);
if (firstResponse)
{
firstResponse = false;
reject(err);
}
// Try again the next time
self._rawDataPromise = null;
});
});
return self._rawDataPromise;
}
_delete()
{
if (this._watch !== null)
{
this._logger.debug(`Stopping watch for service: ${this.name}`);
this._watch.end();
}
}
_applyCatalogData(data)
{
if (sameTags(this.tags, data))
return false;
this._logger.info(`${this.name}: tags changed from ${JSON.stringify(this.tags)} to ${JSON.stringify(data)}`);
this.tags = data;
return true;
}
_applyHealthData(data)
{
if (data == this._rawData)
return false;
const isUpdate = this._rawData != null;
this._rawData = data;
// If this is the first time we've received data, it is guaranteed to be the result of
// an update handler requesting this data and the handlers do not need to be called again.
return isUpdate;
}
}
module.exports = ConsulCatalog;

9
lib/logger.js Normal file
View File

@ -0,0 +1,9 @@
const winston = require('winston');
const config = require('../config');
// TODO make configurable
let logger = winston.createLogger({
transports: config.logging.transports,
});
module.exports = logger;

282
package-lock.json generated Normal file
View File

@ -0,0 +1,282 @@
{
"name": "consulwatcher",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@dabh/diagnostics": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz",
"integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==",
"requires": {
"colorspace": "1.1.x",
"enabled": "2.0.x",
"kuler": "^2.0.0"
}
},
"async": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz",
"integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw=="
},
"color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz",
"integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==",
"requires": {
"color-convert": "^1.9.1",
"color-string": "^1.5.2"
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"color-string": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz",
"integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==",
"requires": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
},
"colorspace": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz",
"integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==",
"requires": {
"color": "3.0.x",
"text-hex": "1.0.x"
}
},
"consul": {
"version": "0.37.0",
"resolved": "https://registry.npmjs.org/consul/-/consul-0.37.0.tgz",
"integrity": "sha512-8V5hUcKK0osd8lShVFEgcYAGaJZmJIYq+sBy/5i4isyJ3Ud159V1PGT2eEvfib7Tu0c9kJ7Uesicc7KEnqj0yA==",
"requires": {
"papi": "^0.29.0"
}
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debounce": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
"integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg=="
},
"dedent": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz",
"integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw="
},
"enabled": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
"fast-safe-stringify": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz",
"integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA=="
},
"fecha": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz",
"integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg=="
},
"fn.name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"is-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
"integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="
},
"logform": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
"integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==",
"requires": {
"colors": "^1.2.1",
"fast-safe-stringify": "^2.0.4",
"fecha": "^4.2.0",
"ms": "^2.1.1",
"triple-beam": "^1.3.0"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"one-time": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
"integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
"requires": {
"fn.name": "1.x.x"
}
},
"papi": {
"version": "0.29.1",
"resolved": "https://registry.npmjs.org/papi/-/papi-0.29.1.tgz",
"integrity": "sha512-Y9ipSMfWuuVFO3zY9PlxOmEg+bQ7CeJ28sa9/a0veYNynLf9fwjR3+3fld5otEy7okUaEOUuCHVH62MyTmACXQ=="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
},
"simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
"requires": {
"is-arrayish": "^0.3.1"
}
},
"stack-trace": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
"integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"requires": {
"safe-buffer": "~5.2.0"
}
},
"supervisor": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/supervisor/-/supervisor-0.12.0.tgz",
"integrity": "sha1-3n5jNwFbKRhRwQ81OMSn8EkX7ME=",
"dev": true
},
"text-hex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
"integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="
},
"triple-beam": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz",
"integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw=="
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"winston": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz",
"integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==",
"requires": {
"@dabh/diagnostics": "^2.0.2",
"async": "^3.1.0",
"is-stream": "^2.0.0",
"logform": "^2.2.0",
"one-time": "^1.0.0",
"readable-stream": "^3.4.0",
"stack-trace": "0.0.x",
"triple-beam": "^1.3.0",
"winston-transport": "^4.4.0"
}
},
"winston-transport": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz",
"integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==",
"requires": {
"readable-stream": "^2.3.7",
"triple-beam": "^1.2.0"
},
"dependencies": {
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
}
}
}
}
}

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "consulwatcher",
"version": "1.0.0",
"description": "Like Consul-template, but different. And written in Node.js.",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "supervisor --watch .,lib index.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/MvRens/ConsulWatcher.git"
},
"author": "Mark van Renswoude",
"license": "Unlicense",
"bugs": {
"url": "https://github.com/MvRens/ConsulWatcher/issues"
},
"homepage": "https://github.com/MvRens/ConsulWatcher#readme",
"dependencies": {
"consul": "^0.37.0",
"debounce": "^1.2.0",
"dedent": "^0.7.0",
"winston": "^3.3.3"
},
"devDependencies": {
"supervisor": "^0.12.0"
}
}