From 2520824fe6d49ef5becdc6db6617f09f44bb1e85 Mon Sep 17 00:00:00 2001 From: Mark van Renswoude Date: Fri, 3 Jul 2020 07:07:40 +0200 Subject: [PATCH] Proof of concept --- .gitignore | 4 + README.md | 7 +- config/included.example.js | 4 + config/index.example.js | 105 ++++++++++++ consulwatcher.service | 12 ++ index.js | 9 + lib/consulcatalog.js | 332 +++++++++++++++++++++++++++++++++++++ lib/logger.js | 9 + package-lock.json | 282 +++++++++++++++++++++++++++++++ package.json | 29 ++++ 10 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 config/included.example.js create mode 100644 config/index.example.js create mode 100644 consulwatcher.service create mode 100644 index.js create mode 100644 lib/consulcatalog.js create mode 100644 lib/logger.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5e79b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules + +config/* +!config/*.example.js \ No newline at end of file diff --git a/README.md b/README.md index 2d78d5d..2248ad5 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/config/included.example.js b/config/included.example.js new file mode 100644 index 0000000..bc646c0 --- /dev/null +++ b/config/included.example.js @@ -0,0 +1,4 @@ +module.exports = async (catalog, logger) => +{ + // Use catalog parameter to generate output +}; \ No newline at end of file diff --git a/config/index.example.js b/config/index.example.js new file mode 100644 index 0000000..cdcf929 --- /dev/null +++ b/config/index.example.js @@ -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; \ No newline at end of file diff --git a/consulwatcher.service b/consulwatcher.service new file mode 100644 index 0000000..09619dd --- /dev/null +++ b/consulwatcher.service @@ -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 diff --git a/index.js b/index.js new file mode 100644 index 0000000..72e50e9 --- /dev/null +++ b/index.js @@ -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 diff --git a/lib/consulcatalog.js b/lib/consulcatalog.js new file mode 100644 index 0000000..441aa1d --- /dev/null +++ b/lib/consulcatalog.js @@ -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; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000..56033a0 --- /dev/null +++ b/lib/logger.js @@ -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; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1f275fc --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..028155e --- /dev/null +++ b/package.json @@ -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" + } +}