Repository: jquatier/eureka-js-client
Branch: master
Commit: c83bbfb99810
Files: 30
Total size: 123.6 KB
Directory structure:
gitextract_gdwjdngz/
├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── example.js
├── gulpfile.babel.js
├── package.json
├── src/
│ ├── AwsMetadata.js
│ ├── ConfigClusterResolver.js
│ ├── DnsClusterResolver.js
│ ├── EurekaClient.js
│ ├── Logger.js
│ ├── defaultConfig.js
│ ├── deltaUtils.js
│ └── index.js
└── test/
├── AwsMetadata.test.js
├── ConfigClusterResolver.test.js
├── DnsClusterResolver.test.js
├── EurekaClient.test.js
├── Logger.test.js
├── deltaUtil.test.js
├── eureka-client-test.yml
├── eureka-client.yml
├── fixtures/
│ ├── config.yml
│ └── malformed-config.yml
├── index.test.js
└── integration.test.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"presets": ["es2015-loose"]
}
================================================
FILE: .eslintrc
================================================
{
"extends": "airbnb-base",
"rules": {
"no-param-reassign": [2, {"props": false}],
"consistent-return": 0
},
"env": {
"mocha": true
}
}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
node_modules
lib/
.DS_Store
#IDE
.idea
================================================
FILE: .npmignore
================================================
src/
================================================
FILE: .travis.yml
================================================
language: node_js
sudo: required
services:
- docker
node_js:
- "4"
- "6"
- "8"
script:
- npm run test && npm run integration
after_script:
- npm run coveralls
cache:
directories:
- node_modules
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2015 Jacob Quatier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# eureka-js-client
[](http://badge.fury.io/js/eureka-js-client) [](https://travis-ci.org/jquatier/eureka-js-client) [](https://coveralls.io/github/jquatier/eureka-js-client?branch=master) [](https://david-dm.org/jquatier/eureka-js-client) [](https://www.bithound.io/github/jquatier/eureka-js-client)
A JavaScript implementation of a client for Eureka (https://github.com/Netflix/eureka), the Netflix OSS service registry.

## Usage
First, install the module into your node project:
```shell
npm install eureka-js-client --save
```
### Add Eureka client to a Node application.
The Eureka module exports a JavaScript function that can be constructed.
```javascript
import Eureka from 'eureka-js-client';
// Or, if you're not using a transpiler:
const Eureka = require('eureka-js-client').Eureka;
// example configuration
const client = new Eureka({
// application instance information
instance: {
app: 'jqservice',
hostName: 'localhost',
ipAddr: '127.0.0.1',
port: 8080,
vipAddress: 'jq.test.something.com',
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
// eureka server host / port
host: '192.168.99.100',
port: 32768,
},
});
```
The Eureka client searches for the YAML file `eureka-client.yml` in the current working directory. It further searches for environment specific overrides in the environment specific YAML files (e.g. `eureka-client-test.yml`). The environment is typically `development` or `production`, and is determined by environment variables in this order: `EUREKA_ENV`, if present, or `NODE_ENV`, if present. Otherwise it defaults to `development`. The options passed to the constructor overwrite any values that are set in configuration files.
You can configure a custom directory to load the configuration files from by specifying a `cwd` option in the object passed to the `Eureka` constructor.
```javascript
const client = new Eureka({
cwd: `${__dirname}/config`,
});
```
If you wish, you can also overwrite the name of the file that is loaded with the `filename` property. You can mix the `cwd` and `filename` options.
```javascript
const client = new Eureka({
filename: 'eureka',
cwd: `${__dirname}/config`,
});
```
### Register with Eureka & start application heartbeats
```javascript
client.start();
```
### De-register with Eureka & stop application heartbeats
```javascript
client.stop();
```
### Get Instances By App ID
```javascript
const instances = client.getInstancesByAppId('YOURSERVICE');
```
### Get Instances By Vip Address
```javascript
const instances = client.getInstancesByVipAddress('YOURSERVICEVIP');
```
### Providing Custom Request Middleware
The client exposes the ability to modify the outgoing [request](https://www.npmjs.com/package/request) options object prior to a eureka call. This is useful when adding authentication methods such as OAuth, or other custom headers. This will be called on every eureka request, so it highly suggested that any long-lived external calls made in the middleware are cached or memoized. If the middleware returns anything other than an object, the eureka request will immediately fail and perform a retry if configured.
```javascript
// example using middleware to set-up HTTP authentication
const client = new Eureka({
requestMiddleware: (requestOpts, done) => {
requestOpts.auth = {
user: 'username',
password: 'somepassword'
};
done(requestOpts);
}
});
```
## Configuring for AWS environments
For AWS environments (`dataCenterInfo.name == 'Amazon'`) the client has built-in logic to request the AWS metadata that the Eureka server requires. See [Eureka REST schema](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations) for more information.
```javascript
// example configuration for AWS
const client = new Eureka({
// application instance information
instance: {
app: 'jqservice',
port: 8080,
vipAddress: 'jq.test.something.com',
statusPageUrl: 'http://__HOST__:8080/info',
healthCheckUrl: 'http://__HOST__:8077/healthcheck',
homePageUrl: 'http://__HOST__:8080/',
dataCenterInfo: {
name: 'Amazon',
},
},
eureka: {
// eureka server host / port / EC2 region
host: 'eureka.test.mydomain.com',
port: 80,
},
});
```
Notes:
- Under this configuration, the instance `hostName` and `ipAddr` will be set to the public host and public IP that the AWS metadata provides. You can set `eureka.useLocalMetadata` to `true` to use the private host and private IP address instead.
- If you want to register using the IP address as the hostname, set `eureka.preferIpAddress` to `true`. This may be used in combination with `eureka.useLocalMetadata` for selecting the private or public IP.
- For status and healthcheck URLs, you may use the replacement key of `__HOST__` to use the host from the metadata.
- Metadata fetching can be disabled by setting `config.eureka.fetchMetadata` to `false` if you want to provide your own metadata in AWS environments.
### Looking up Eureka Servers using DNS
If your have multiple availability zones and your DNS entries set up according to the Wiki article [Deploying Eureka Servers in EC2](https://github.com/Netflix/eureka/wiki/Deploying-Eureka-Servers-in-EC2#configuring-eips-using-dns), you'll want to set `config.eureka.useDns` to `true` and set `config.eureka.ec2Region` to the current region (usually this can be pulled into your application via an environment variable, or passed in directly at startup).
This will cause the client to perform a DNS lookup using `config.eureka.host` and `config.eureka.ec2Region`. The naming convention for the DNS TXT records required for this to function is also described in the Wiki article above. This feature will also work in non-EC2 environments as long as the DNS records conform to the same convention. The results of the DNS resolution are cached in memory and refreshed every 5 minutes by default (set `config.eureka.clusterRefreshInterval` to override).
##### Zone Affinity
By default, the client will first try to connect to the Eureka server located in the same availability-zone as it's currently in. If `availability-zone` is not set in the instance metadata, a random server will be chosen. This also applies when statically configuring the cluster (mapped by zone, see below). To disable this feature, set `config.eureka.preferSameZone` to `false`, and a random server will be chosen.
### Statically configuring Eureka server list
While the recommended approach for resolving the Eureka cluster is using DNS (see above), you can also statically configure the list of Eureka servers by zone or just using a simple default list. Make sure to provide the full protocol, host, port, and path to the Eureka REST service (usually `/apps/`) when using this approach.
#### Static cluster configuration (map by zone)
```javascript
// example configuration for AWS (static map of Eureka cluster by availability-zone)
const client = new Eureka({
instance: {
... // application instance information
},
eureka: {
availabilityZones: {
'us-east-1': ['us-east-1c', 'us-east-1d', 'us-east-1e']
},
serviceUrls: {
'us-east-1c': [
'http://ec2-fake-552-627-568-165.compute-1.amazonaws.com:7001/eureka/v2/apps/', 'http://ec2-fake-368-101-182-134.compute-1.amazonaws.com:7001/eureka/v2/apps/'
],
'us-east-1d': [...],
'us-east-1e': [...]
}
},
});
```
#### Static cluster configuration (list)
```javascript
// example configuration (static list of Eureka cluster servers)
const client = new Eureka({
instance: {
... // application instance information
},
eureka: {
serviceUrls: {
default: [
'http://ec2-fake-552-627-568-165.compute-1.amazonaws.com:7001/eureka/v2/apps/', 'http://ec2-fake-368-101-182-134.compute-1.amazonaws.com:7001/eureka/v2/apps/'
]
}
},
});
```
## Advanced Configuration Options
option | default value | description
---- | --- | ---
`requestMiddleware` | noop | Custom middleware function to modify the outgoing [request](https://www.npmjs.com/package/request) to eureka
`logger` | console logging | logger implementation for the client to use
`shouldUseDelta` | false | Experimental mode to fetch deltas from eureka instead of full registry on update
`eureka.maxRetries` | `3` | Number of times to retry all requests to eureka
`eureka.requestRetryDelay` | `500` | milliseconds to wait between retries. This will be multiplied by the # of failed retries.
`eureka.heartbeatInterval` | `30000` | milliseconds to wait between heartbeats
`eureka.registryFetchInterval` | `30000` | milliseconds to wait between registry fetches
`eureka.registerWithEureka` | `true` | enable/disable Eureka registration
`eureka.fetchRegistry` | `true` | enable/disable registry fetching
`eureka.filterUpInstances` | `true` | enable/disable filtering of instances with status === `UP`
`eureka.servicePath` | `/eureka/v2/apps/` | path to eureka REST service
`eureka.ssl` | `false` | enable SSL communication with Eureka server
`eureka.useDns` | `false` | look up Eureka server using DNS, see [Looking up Eureka Servers in AWS using DNS](#looking-up-eureka-servers-in-aws-using-dns)
`eureka.preferSameZone` | `true` | enable/disable zone affinity when locating a Eureka server
`eureka.clusterRefreshInterval` | `300000` | milliseconds to wait between refreshing cluster hosts (DNS resolution only)
`eureka.fetchMetadata` | `true` | fetch AWS metadata when in AWS environment, see [Configuring for AWS environments](#configuring-for-aws-environments)
`eureka.useLocalMetadata` | `false` | use local IP and local hostname from metadata when in an AWS environment.
`eureka.preferIpAddress` | `false` | use IP address (local or public) as the hostname for registration when in an AWS environment.
## Events
Eureka client is an instance of `EventEmitter` and provides the following events for consumption:
event | data provided | description
---- | --- | ---
`started` | N/A | Fired when eureka client is fully registered and all registries have been updated.
`registered` | N/A | Fired when the eureka client is registered with eureka.
`deregistered` | N/A | Fired when the eureka client is deregistered with eureka.
`heartbeat` | N/A | Fired when the eureka client has successfully renewed it's lease with eureka.
`registryUpdated` | N/A | Fired when the eureka client has successfully update it's registries.
## Debugging
The library uses [request](https://github.com/request/request) for all service calls, and debugging can be turned on by passing `NODE_DEBUG=request` when you start node. This allows you you double-check the URL being called as well as other request properties.
```shell
NODE_DEBUG=request node example.js
```
You can also turn on debugging within the library by setting the log level to debug:
```javascript
client.logger.level('debug');
```
## Known Issues
### 400 Bad Request Errors from Eureka Server
Later versions of Eureka require a slightly different JSON POST body on registration. If you are seeing 400 errors on registration it's probably an issue with your configuration and it could be the formatting differences below. The history behind this is unclear and there's a discussion [here](https://github.com/Netflix-Skunkworks/zerotodocker/issues/46). The main differences are:
- `port` is now an object with 2 required fields `$` and `@enabled`.
- `dataCenterInfo` has an `@class` property.
See below for an example:
```javascript
const client = new Eureka({
// application instance information
instance: {
app: 'jqservice',
hostName: 'localhost',
ipAddr: '127.0.0.1',
port: {
'$': 8080,
'@enabled': true,
},
vipAddress: 'jq.test.something.com',
dataCenterInfo: {
'@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',
name: 'MyOwn',
},
},
eureka: {
// eureka server host / port
host: '192.168.99.100',
port: 32768,
},
});
```
If you are planning on connecting to a eureka service in AWS you will need to add the corresponding `dataCenterInfo` information:
```javascript
dataCenterInfo: {
'@class': 'com.netflix.appinfo.AmazonInfo',
name: 'Amazon',
}
```
### 404 Not Found Errors from Eureka Server
This probably means that the Eureka REST service is located on a different path in your environment. The default is `http://<EUREKA_HOST>/eureka/v2/apps`, but depending on your setup you may need to set `eureka.servicePath` in your configuration to another path. The REST service could be hung under `/eureka/apps/` or possibly `/apps/`.
### Usage with Spring Cloud
If you are using Spring Cloud you'll likely need the following settings:
- Set `eureka.servicePath` in your config to `/eureka/apps/`.
- Use the newer style of the configuration [here](#400-bad-request-errors-from-eureka-server) or Spring Cloud Eureka will throw a 500 error.
- Set `statusPageUrl` to a valid URL for your application, Spring Cloud [seems to require this](https://github.com/jquatier/eureka-js-client/issues/113) when the instance information is parsed.
- Put single quotes around boolean `@enabled`. Unfortunately, a 500 error regarding parsing [seems to occur](https://github.com/jquatier/eureka-js-client/issues/63) without that.
Below is an example configuration that should work with Spring Cloud Eureka server:
```javascript
const client = new Eureka({
instance: {
app: 'jqservice',
hostName: 'localhost',
ipAddr: '127.0.0.1',
statusPageUrl: 'http://localhost:8080/info',
port: {
'$': 8080,
'@enabled': 'true',
},
vipAddress: 'jq.test.something.com',
dataCenterInfo: {
'@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo',
name: 'MyOwn',
},
},
eureka: {
host: '192.168.99.100',
port: 32768,
servicePath: '/eureka/apps/'
},
});
```
## Tests
The test for the module are written using mocha and chai. To run the unit tests, you can use the gulp `test` task:
```shell
gulp test
```
If you wish to have the tests watch the `src/` and `test/` directories for changes, you can use the `test:watch` gulp task:
```shell
gulp test:watch
```
================================================
FILE: example.js
================================================
// assuming no transpiler here
const Eureka = require('eureka-js-client').Eureka;
// example configuration
const client = new Eureka({
// application instance information
instance: {
app: 'jqservice',
hostName: 'localhost',
ipAddr: '127.0.0.1',
port: 8080,
vipAddress: 'jq.test.something.com',
dataCenterInfo: {
name: 'MyOwn',
}
},
eureka: {
// eureka server host / port
host: '192.168.99.100',
port: 32768,
}
});
client.logger.level('debug');
client.start(function(error){
console.log(error || 'complete');
});
================================================
FILE: gulpfile.babel.js
================================================
import gulp from 'gulp';
import babel from 'gulp-babel';
import mocha from 'gulp-mocha';
import eslint from 'gulp-eslint';
import { Instrumenter } from 'babel-istanbul';
import istanbul from 'gulp-istanbul';
import env from 'gulp-env';
import request from 'request';
import { spawn, exec } from 'child_process';
gulp.task('build', () => (
gulp.src('src/**/*.js')
.pipe(babel())
.pipe(gulp.dest('lib'))
));
gulp.task('lint', () => (
gulp.src(['src/**/*.js', 'test/**/*.js'])
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failOnError())
));
gulp.task('mocha', (cb) => {
const envs = env.set({
NODE_ENV: 'test',
});
return gulp.src('src/**/*.js')
.pipe(envs)
.pipe(istanbul({
instrumenter: Instrumenter,
})) // Covering files
.pipe(istanbul.hookRequire()) // Force `require` to return covered files
.on('finish', () => {
gulp.src(['test/**/*.js', '!test/integration.test.js'])
.pipe(mocha())
.pipe(istanbul.writeReports())
.pipe(istanbul.enforceThresholds({ thresholds: { global: 0 } }))
.pipe(envs.reset)
.on('end', cb);
});
});
const EUREKA_INIT_TIMEOUT = 60000;
const EUREKA_IMAGE = 'netflixoss/eureka:1.1.147';
const DOCKER_PORT = '8080';
const DOCKER_NAME = 'eureka-js-client';
const DOCKER_RUN_ARGS = [
'run', '-d', '-p', `${DOCKER_PORT}:8080`, '--name', DOCKER_NAME, EUREKA_IMAGE,
];
const DOCKER_START_ARGS = [
'start', DOCKER_NAME,
];
let startTime;
function waitForEureka(cb) {
if (!startTime) startTime = +new Date();
else if ((+new Date() - startTime) > EUREKA_INIT_TIMEOUT) {
return cb(new Error('Eureka failed to start before timeout'));
}
request.get({ url: `http://localhost:${DOCKER_PORT}/eureka` }, (err) => {
if (err) {
if (err.code === 'ECONNRESET') {
console.log('Eureka connection not ready. Waiting..'); // eslint-disable-line
setTimeout(() => waitForEureka(cb), 1000);
} else {
cb(err);
}
} else {
cb();
}
});
}
gulp.task('docker:run', (cb) => {
exec(`docker ps -a | grep '\\b${DOCKER_NAME}\\b' | wc -l`, (error, stdout) => {
const DOCKER_ARGS = (stdout.trim() === '1') ? DOCKER_START_ARGS : DOCKER_RUN_ARGS;
const child = spawn('docker', DOCKER_ARGS, { stdio: 'inherit' });
child.on('close', (code) => {
if (code > 0) {
cb(new Error('Failed to start docker image'));
} else {
waitForEureka(cb);
}
});
});
});
gulp.task('test:integration', ['docker:run'], () => (
gulp.src('test/integration.test.js')
.pipe(mocha({ timeout: 120000 }))
));
gulp.task('test', ['lint', 'mocha']);
gulp.task('test:watch', () => (
gulp.watch(['src/**/*.js', 'test/**/*.test.js'], ['test'])
));
gulp.task('default', ['build']);
================================================
FILE: package.json
================================================
{
"name": "eureka-js-client",
"version": "4.5.0",
"description": "A JavaScript implementation the Netflix OSS service registry, Eureka.",
"main": "lib/index.js",
"scripts": {
"prepublish": "gulp",
"test": "gulp test",
"integration": "gulp test:integration",
"coveralls": "cat ./coverage/lcov.info | coveralls && rm -rf ./coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/jquatier/eureka-js-client.git"
},
"keywords": [
"eureka",
"service",
"registry",
"netflix"
],
"author": "JQ",
"license": "MIT",
"bugs": {
"url": "https://github.com/jquatier/eureka-js-client/issues"
},
"homepage": "https://github.com/jquatier/eureka-js-client",
"dependencies": {
"async": "^2.0.1",
"js-yaml": "^3.3.1",
"lodash": "^4.13.1",
"request": "^2.83.0"
},
"devDependencies": {
"babel-core": "^6.4.5",
"babel-istanbul": "^0.11.0",
"babel-preset-es2015": "^6.3.13",
"babel-preset-es2015-loose": "^7.0.0",
"chai": "^3.2.0",
"coveralls": "^2.11.4",
"eslint": "^2.13.1",
"eslint-config-airbnb-base": "^3.0.1",
"eslint-plugin-import": "^1.10.0",
"gulp": "^3.9.0",
"gulp-babel": "^6.1.1",
"gulp-env": "^0.4.0",
"gulp-eslint": "^2.0.0",
"gulp-istanbul": "^1.0.0",
"gulp-mocha": "^3.0.0",
"sinon": "^1.15.4",
"sinon-chai": "^2.8.0"
},
"greenkeeper": {
"ignore": [
"eslint",
"eslint-config-airbnb-base",
"eslint-plugin-import",
"gulp-eslint",
"gulp-mocha"
]
}
}
================================================
FILE: src/AwsMetadata.js
================================================
import request from 'request';
import async from 'async';
import Logger from './Logger';
/*
Utility class for pulling AWS metadata that Eureka requires when
registering as an Amazon instance (datacenter).
*/
export default class AwsMetadata {
constructor(config = {}) {
this.logger = config.logger || new Logger();
this.host = config.host || '169.254.169.254';
}
fetchMetadata(resultsCallback) {
async.parallel({
'ami-id': callback => {
this.lookupMetadataKey('ami-id', callback);
},
'instance-id': callback => {
this.lookupMetadataKey('instance-id', callback);
},
'instance-type': callback => {
this.lookupMetadataKey('instance-type', callback);
},
'local-ipv4': callback => {
this.lookupMetadataKey('local-ipv4', callback);
},
'local-hostname': callback => {
this.lookupMetadataKey('local-hostname', callback);
},
'availability-zone': callback => {
this.lookupMetadataKey('placement/availability-zone', callback);
},
'public-hostname': callback => {
this.lookupMetadataKey('public-hostname', callback);
},
'public-ipv4': callback => {
this.lookupMetadataKey('public-ipv4', callback);
},
mac: callback => {
this.lookupMetadataKey('mac', callback);
},
accountId: callback => {
// the accountId is in the identity document.
this.lookupInstanceIdentity((error, identity) => {
callback(null, identity ? identity.accountId : null);
});
},
}, (error, results) => {
// we need the mac before we can lookup the vpcId...
this.lookupMetadataKey(`network/interfaces/macs/${results.mac}/vpc-id`, (err, vpcId) => {
results['vpc-id'] = vpcId;
this.logger.debug('Found Instance AWS Metadata', results);
const filteredResults = Object.keys(results).reduce((filtered, prop) => {
if (results[prop]) filtered[prop] = results[prop];
return filtered;
}, {});
resultsCallback(filteredResults);
});
});
}
lookupMetadataKey(key, callback) {
request.get({
url: `http://${this.host}/latest/meta-data/${key}`,
}, (error, response, body) => {
if (error) {
this.logger.error('Error requesting metadata key', error);
}
callback(null, (error || response.statusCode !== 200) ? null : body);
});
}
lookupInstanceIdentity(callback) {
request.get({
url: `http://${this.host}/latest/dynamic/instance-identity/document`,
}, (error, response, body) => {
if (error) {
this.logger.error('Error requesting instance identity document', error);
}
callback(null, (error || response.statusCode !== 200) ? null : JSON.parse(body));
});
}
}
================================================
FILE: src/ConfigClusterResolver.js
================================================
import Logger from './Logger';
/*
Locates a Eureka host using static configuration. Configuration can either be
done using a simple host and port, or a map of serviceUrls.
*/
export default class ConfigClusterResolver {
constructor(config, logger) {
this.logger = logger || new Logger();
this.config = config;
this.serviceUrls = this.buildServiceUrls();
}
resolveEurekaUrl(callback, retryAttempt = 0) {
if (this.serviceUrls.length > 1 && retryAttempt > 0) {
this.serviceUrls.push(this.serviceUrls.shift());
}
callback(null, this.serviceUrls[0]);
}
buildServiceUrls() {
const { host, port, servicePath, ssl,
serviceUrls, preferSameZone } = this.config.eureka;
const { dataCenterInfo } = this.config.instance;
const metadata = dataCenterInfo ? dataCenterInfo.metadata : undefined;
const instanceZone = metadata ? metadata['availability-zone'] : undefined;
const urls = [];
const zones = this.getAvailabilityZones();
if (serviceUrls) {
zones.forEach((zone) => {
if (serviceUrls[zone]) {
if (preferSameZone && instanceZone && instanceZone === zone) {
urls.unshift(...serviceUrls[zone]);
}
urls.push(...serviceUrls[zone]);
}
});
}
if (!urls.length) {
const protocol = ssl ? 'https' : 'http';
urls.push(`${protocol}://${host}:${port}${servicePath}`);
}
return urls;
}
getAvailabilityZones() {
const { ec2Region, availabilityZones } = this.config.eureka;
if (ec2Region && availabilityZones && availabilityZones[ec2Region]) {
return availabilityZones[ec2Region];
}
return ['default'];
}
}
================================================
FILE: src/DnsClusterResolver.js
================================================
import dns from 'dns';
import async from 'async';
import shuffle from 'lodash/shuffle';
import xor from 'lodash/xor';
import Logger from './Logger';
function noop() {}
/*
Locates a Eureka host using DNS lookups. The DNS records are looked up by a naming
convention and TXT records must be created according to the Eureka Wiki here:
https://github.com/Netflix/eureka/wiki/Configuring-Eureka-in-AWS-Cloud
Naming convention: txt.<REGION>.<HOST>
*/
export default class DnsClusterResolver {
constructor(config, logger) {
this.logger = logger || new Logger();
this.serverList = undefined;
this.config = config;
if (!this.config.eureka.ec2Region) {
throw new Error(
'EC2 region was undefined. ' +
'config.eureka.ec2Region must be set to resolve Eureka using DNS records.'
);
}
if (this.config.eureka.clusterRefreshInterval) {
this.startClusterRefresh();
}
}
resolveEurekaUrl(callback, retryAttempt = 0) {
this.getCurrentCluster((err) => {
if (err) return callback(err);
if (retryAttempt > 0) {
this.serverList.push(this.serverList.shift());
}
const { port, servicePath, ssl } = this.config.eureka;
const protocol = ssl ? 'https' : 'http';
callback(null, `${protocol}://${this.serverList[0]}:${port}${servicePath}`);
});
}
getCurrentCluster(callback) {
if (this.serverList) {
return callback(null, this.serverList);
}
this.refreshCurrentCluster((err) => {
if (err) return callback(err);
return callback(null, this.serverList);
});
}
startClusterRefresh() {
const refreshTimer = setInterval(() => {
this.refreshCurrentCluster((err) => {
if (err) this.logger.warn(err.message);
});
}, this.config.eureka.clusterRefreshInterval);
refreshTimer.unref();
}
refreshCurrentCluster(callback = noop) {
this.resolveClusterHosts((err, hosts) => {
if (err) return callback(err);
// if the cluster is the same (aside from order), we want to maintain our order
if (xor(this.serverList, hosts).length) {
this.serverList = hosts;
this.logger.info('Eureka cluster located, hosts will be used in the following order',
this.serverList);
} else {
this.logger.debug('Eureka cluster hosts unchanged, maintaining current server list.');
}
callback();
});
}
resolveClusterHosts(callback = noop) {
const { ec2Region, host, preferSameZone } = this.config.eureka;
const { dataCenterInfo } = this.config.instance;
const metadata = dataCenterInfo ? dataCenterInfo.metadata : undefined;
const availabilityZone = metadata ? metadata['availability-zone'] : undefined;
const dnsHost = `txt.${ec2Region}.${host}`;
dns.resolveTxt(dnsHost, (err, addresses) => {
if (err) {
return callback(new Error(
`Error resolving eureka cluster for region [${ec2Region}] using DNS: [${err}]`
));
}
const zoneRecords = [].concat(...addresses);
const dnsTasks = {};
zoneRecords.forEach((zoneRecord) => {
dnsTasks[zoneRecord] = (cb) => {
this.resolveZoneHosts(`txt.${zoneRecord}`, cb);
};
});
async.parallel(dnsTasks, (error, results) => {
if (error) return callback(error);
const hosts = [];
const myZoneHosts = [];
Object.keys(results).forEach((zone) => {
if (preferSameZone && availabilityZone && zone.lastIndexOf(availabilityZone, 0) === 0) {
myZoneHosts.push(...results[zone]);
} else {
hosts.push(...results[zone]);
}
});
const combinedHosts = [].concat(shuffle(myZoneHosts), shuffle(hosts));
if (!combinedHosts.length) {
return callback(
new Error(`Unable to locate any Eureka hosts in any zone via DNS @ ${dnsHost}`));
}
callback(null, combinedHosts);
});
});
}
resolveZoneHosts(zoneRecord, callback) {
dns.resolveTxt(zoneRecord, (err, results) => {
if (err) {
this.logger.warn(`Failed to resolve cluster zone ${zoneRecord}`, err.message);
return callback(new Error(`Error resolving cluster zone ${zoneRecord}: [${err}]`));
}
this.logger.debug(`Found Eureka Servers @ ${zoneRecord}`, results);
callback(null, ([].concat(...results)).filter((value) => (!!value)));
});
}
}
================================================
FILE: src/EurekaClient.js
================================================
import request from 'request';
import fs from 'fs';
import yaml from 'js-yaml';
import { merge, findIndex } from 'lodash';
import { normalizeDelta, findInstance } from './deltaUtils';
import path from 'path';
import { series, waterfall } from 'async';
import { EventEmitter } from 'events';
import AwsMetadata from './AwsMetadata';
import ConfigClusterResolver from './ConfigClusterResolver';
import DnsClusterResolver from './DnsClusterResolver';
import Logger from './Logger';
import defaultConfig from './defaultConfig';
function noop() {}
/*
Eureka JS client
This module handles registration with a Eureka server, as well as heartbeats
for reporting instance health.
*/
function fileExists(file) {
try {
return fs.statSync(file);
} catch (e) {
return false;
}
}
function getYaml(file) {
let yml = {};
if (!fileExists(file)) {
return yml; // no configuration file
}
try {
yml = yaml.safeLoad(fs.readFileSync(file, 'utf8'));
} catch (e) {
// configuration file exists but was malformed
throw new Error(`Error loading YAML configuration file: ${file} ${e}`);
}
return yml;
}
export default class Eureka extends EventEmitter {
constructor(config = {}) {
super();
// Allow passing in a custom logger:
this.logger = config.logger || new Logger();
this.logger.debug('initializing eureka client');
// Load up the current working directory and the environment:
const cwd = config.cwd || process.cwd();
const env = process.env.EUREKA_ENV || process.env.NODE_ENV || 'development';
const filename = config.filename || 'eureka-client';
// Load in the configuration files:
const defaultYml = getYaml(path.join(cwd, `${filename}.yml`));
const envYml = getYaml(path.join(cwd, `${filename}-${env}.yml`));
// apply config overrides in appropriate order
this.config = merge({}, defaultConfig, defaultYml, envYml, config);
// Validate the provided the values we need:
this.validateConfig(this.config);
this.requestMiddleware = this.config.requestMiddleware;
this.hasFullRegistry = false;
if (this.amazonDataCenter) {
this.metadataClient = new AwsMetadata({
logger: this.logger,
});
}
if (this.config.eureka.useDns) {
this.clusterResolver = new DnsClusterResolver(this.config, this.logger);
} else {
this.clusterResolver = new ConfigClusterResolver(this.config, this.logger);
}
this.cache = {
app: {},
vip: {},
};
}
/*
Helper method to get the instance ID. If the datacenter is AWS, this will be the
instance-id in the metadata. Else, it's the hostName.
*/
get instanceId() {
if (this.config.instance.instanceId) {
return this.config.instance.instanceId;
} else if (this.amazonDataCenter) {
return this.config.instance.dataCenterInfo.metadata['instance-id'];
}
return this.config.instance.hostName;
}
/*
Helper method to determine if this is an AWS datacenter.
*/
get amazonDataCenter() {
const { dataCenterInfo } = this.config.instance;
return (
dataCenterInfo &&
dataCenterInfo.name &&
dataCenterInfo.name.toLowerCase() === 'amazon'
);
}
/*
Registers instance with Eureka, begins heartbeats, and fetches registry.
*/
start(callback = noop) {
series([
done => {
if (this.metadataClient && this.config.eureka.fetchMetadata) {
return this.addInstanceMetadata(done);
}
done();
},
done => {
if (this.config.eureka.registerWithEureka) {
return this.register(done);
}
done();
},
done => {
if (this.config.eureka.registerWithEureka) {
this.startHeartbeats();
}
if (this.config.eureka.fetchRegistry) {
this.startRegistryFetches();
if (this.config.eureka.waitForRegistry) {
const waitForRegistryUpdate = (cb) => {
this.fetchRegistry(() => {
const instances = this.getInstancesByVipAddress(this.config.instance.vipAddress);
if (instances.length === 0) setTimeout(() => waitForRegistryUpdate(cb), 2000);
else cb();
});
};
return waitForRegistryUpdate(done);
}
this.fetchRegistry(done);
} else {
done();
}
},
], (err, ...rest) => {
if (err) {
this.logger.warn('Error starting the Eureka Client', err);
} else {
this.emit('started');
}
callback(err, ...rest);
});
}
/*
De-registers instance with Eureka, stops heartbeats / registry fetches.
*/
stop(callback = noop) {
clearInterval(this.registryFetch);
if (this.config.eureka.registerWithEureka) {
clearInterval(this.heartbeat);
this.deregister(callback);
} else {
callback();
}
}
/*
Validates client configuration.
*/
validateConfig(config) {
function validate(namespace, key) {
if (!config[namespace][key]) {
throw new TypeError(`Missing "${namespace}.${key}" config value.`);
}
}
if (config.eureka.registerWithEureka) {
validate('instance', 'app');
validate('instance', 'vipAddress');
validate('instance', 'port');
validate('instance', 'dataCenterInfo');
}
if (typeof config.requestMiddleware !== 'function') {
throw new TypeError('requestMiddleware must be a function');
}
}
/*
Registers with the Eureka server and initializes heartbeats on registration success.
*/
register(callback = noop) {
this.config.instance.status = 'UP';
const connectionTimeout = setTimeout(() => {
this.logger.warn('It looks like it\'s taking a while to register with ' +
'Eureka. This usually means there is an issue connecting to the host ' +
'specified. Start application with NODE_DEBUG=request for more logging.');
}, 10000);
this.eurekaRequest({
method: 'POST',
uri: this.config.instance.app,
json: true,
body: { instance: this.config.instance },
}, (error, response, body) => {
clearTimeout(connectionTimeout);
if (!error && response.statusCode === 204) {
this.logger.info(
'registered with eureka: ',
`${this.config.instance.app}/${this.instanceId}`
);
this.emit('registered');
return callback(null);
} else if (error) {
this.logger.warn('Error registering with eureka client.', error);
return callback(error);
}
return callback(
new Error(`eureka registration FAILED: status: ${response.statusCode} body: ${body}`)
);
});
}
/*
De-registers with the Eureka server and stops heartbeats.
*/
deregister(callback = noop) {
this.eurekaRequest({
method: 'DELETE',
uri: `${this.config.instance.app}/${this.instanceId}`,
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.info(
`de-registered with eureka: ${this.config.instance.app}/${this.instanceId}`
);
this.emit('deregistered');
return callback(null);
} else if (error) {
this.logger.warn('Error deregistering with eureka', error);
return callback(error);
}
return callback(
new Error(`eureka deregistration FAILED: status: ${response.statusCode} body: ${body}`)
);
});
}
/*
Sets up heartbeats on interval for the life of the application.
Heartbeat interval by setting configuration property: eureka.heartbeatInterval
*/
startHeartbeats() {
this.heartbeat = setInterval(() => {
this.renew();
}, this.config.eureka.heartbeatInterval);
}
renew() {
this.eurekaRequest({
method: 'PUT',
uri: `${this.config.instance.app}/${this.instanceId}`,
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.debug('eureka heartbeat success');
this.emit('heartbeat');
} else if (!error && response.statusCode === 404) {
this.logger.warn('eureka heartbeat FAILED, Re-registering app');
this.register();
} else {
if (error) {
this.logger.error('An error in the request occured.', error);
}
this.logger.warn(
'eureka heartbeat FAILED, will retry.' +
`statusCode: ${response ? response.statusCode : 'unknown'}` +
`body: ${body} ${error | ''} `
);
}
});
}
/*
Sets up registry fetches on interval for the life of the application.
Registry fetch interval setting configuration property: eureka.registryFetchInterval
*/
startRegistryFetches() {
this.registryFetch = setInterval(() => {
this.fetchRegistry(err => {
if (err) this.logger.warn('Error fetching registry', err);
});
}, this.config.eureka.registryFetchInterval);
}
/*
Retrieves a list of instances from Eureka server given an appId
*/
getInstancesByAppId(appId) {
if (!appId) {
throw new RangeError('Unable to query instances with no appId');
}
const instances = this.cache.app[appId.toUpperCase()] || [];
if (instances.length === 0) {
this.logger.warn(`Unable to retrieve instances for appId: ${appId}`);
}
return instances;
}
/*
Retrieves a list of instances from Eureka server given a vipAddress
*/
getInstancesByVipAddress(vipAddress) {
if (!vipAddress) {
throw new RangeError('Unable to query instances with no vipAddress');
}
const instances = this.cache.vip[vipAddress] || [];
if (instances.length === 0) {
this.logger.warn(`Unable to retrieves instances for vipAddress: ${vipAddress}`);
}
return instances;
}
/*
Orchestrates fetching registry
*/
fetchRegistry(callback = noop) {
if (this.config.shouldUseDelta && this.hasFullRegistry) {
this.fetchDelta(callback);
} else {
this.fetchFullRegistry(callback);
}
}
/*
Retrieves all applications registered with the Eureka server
*/
fetchFullRegistry(callback = noop) {
this.eurekaRequest({
uri: '',
headers: {
Accept: 'application/json',
},
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.debug('retrieved full registry successfully');
try {
this.transformRegistry(JSON.parse(body));
} catch (ex) {
return callback(ex);
}
this.emit('registryUpdated');
this.hasFullRegistry = true;
return callback(null);
} else if (error) {
this.logger.warn('Error fetching registry', error);
return callback(error);
}
callback(new Error('Unable to retrieve full registry from Eureka server'));
});
}
/*
Retrieves all applications registered with the Eureka server
*/
fetchDelta(callback = noop) {
this.eurekaRequest({
uri: 'delta',
headers: {
Accept: 'application/json',
},
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
this.logger.debug('retrieved delta successfully');
let applications;
try {
const jsonBody = JSON.parse(body);
applications = jsonBody.applications.application;
this.handleDelta(this.cache, applications);
return callback(null);
} catch (ex) {
return callback(ex);
}
} else if (error) {
this.logger.warn('Error fetching delta registry', error);
return callback(error);
}
callback(new Error('Unable to retrieve delta registry from Eureka server'));
});
}
/*
Transforms the given registry and caches the registry locally
*/
transformRegistry(registry) {
if (!registry) {
this.logger.warn('Unable to transform empty registry');
} else {
if (!registry.applications.application) {
return;
}
const newCache = { app: {}, vip: {} };
if (Array.isArray(registry.applications.application)) {
registry.applications.application.forEach((app) => {
this.transformApp(app, newCache);
});
} else {
this.transformApp(registry.applications.application, newCache);
}
this.cache = newCache;
}
}
/*
Transforms the given application and places in client cache. If an application
has a single instance, the instance is placed into the cache as an array of one
*/
transformApp(app, cache) {
if (app.instance.length) {
app.instance
.filter(this.validateInstance.bind(this))
.forEach((inst) => this.addInstance(cache, inst));
} else if (this.validateInstance(app.instance)) {
this.addInstance(cache, app.instance);
}
}
/*
Returns true if instance filtering is disabled, or if the instance is UP
*/
validateInstance(instance) {
return (!this.config.eureka.filterUpInstances || instance.status === 'UP');
}
/*
Returns an array of vipAddresses from string vipAddress given by eureka
*/
splitVipAddress(vipAddress) { // eslint-disable-line
if (typeof vipAddress !== 'string') {
return [];
}
return vipAddress.split(',');
}
handleDelta(cache, appDelta) {
const delta = normalizeDelta(appDelta);
delta.forEach((app) => {
app.instance.forEach((instance) => {
switch (instance.actionType) {
case 'ADDED': this.addInstance(cache, instance); break;
case 'MODIFIED': this.modifyInstance(cache, instance); break;
case 'DELETED': this.deleteInstance(cache, instance); break;
default: this.logger.warn('Unknown delta actionType', instance.actionType); break;
}
});
});
}
addInstance(cache, instance) {
if (!this.validateInstance(instance)) return;
const vipAddresses = this.splitVipAddress(instance.vipAddress);
const appName = instance.app.toUpperCase();
vipAddresses.forEach((vipAddress) => {
const alreadyContains = findIndex(cache.vip[vipAddress], findInstance(instance)) > -1;
if (alreadyContains) return;
if (!cache.vip[vipAddress]) {
cache.vip[vipAddress] = [];
}
cache.vip[vipAddress].push(instance);
});
if (!cache.app[appName]) cache.app[appName] = [];
const alreadyContains = findIndex(cache.app[appName], findInstance(instance)) > -1;
if (alreadyContains) return;
cache.app[appName].push(instance);
}
modifyInstance(cache, instance) {
const vipAddresses = this.splitVipAddress(instance.vipAddress);
const appName = instance.app.toUpperCase();
vipAddresses.forEach((vipAddress) => {
const index = findIndex(cache.vip[vipAddress], findInstance(instance));
if (index > -1) cache.vip[vipAddress].splice(index, 1, instance);
else this.addInstance(cache, instance);
});
const index = findIndex(cache.app[appName], findInstance(instance));
if (index > -1) cache.app[appName].splice(cache.vip[instance.vipAddress], 1, instance);
else this.addInstance(cache, instance);
}
deleteInstance(cache, instance) {
const vipAddresses = this.splitVipAddress(instance.vipAddress);
const appName = instance.app.toUpperCase();
vipAddresses.forEach((vipAddress) => {
const index = findIndex(cache.vip[vipAddress], findInstance(instance));
if (index > -1) cache.vip[vipAddress].splice(index, 1);
});
const index = findIndex(cache.app[appName], findInstance(instance));
if (index > -1) cache.app[appName].splice(cache.vip[instance.vipAddress], 1);
}
/*
Fetches the metadata using the built-in client and updates the instance
configuration with the hostname and IP address. If the value of the config
option 'eureka.useLocalMetadata' is true, then the local IP address and
hostname is used. Otherwise, the public IP address and hostname is used. If
'eureka.preferIpAddress' is true, the IP address will be used as the hostname.
A string replacement is done on the healthCheckUrl, statusPageUrl and
homePageUrl so that users can define the URLs with a placeholder for the
host ('__HOST__'). This allows flexibility since the host isn't known until
the metadata is fetched. The replaced value respects the config option
'eureka.useLocalMetadata' as described above.
This will only get called when dataCenterInfo.name is Amazon, but you can
set config.eureka.fetchMetadata to false if you want to provide your own
metadata in AWS environments.
*/
addInstanceMetadata(callback = noop) {
this.metadataClient.fetchMetadata(metadataResult => {
this.config.instance.dataCenterInfo.metadata = merge(
this.config.instance.dataCenterInfo.metadata,
metadataResult
);
const useLocal = this.config.eureka.useLocalMetadata;
const preferIpAddress = this.config.eureka.preferIpAddress;
const metadataHostName = metadataResult[useLocal ? 'local-hostname' : 'public-hostname'];
const metadataIpAddress = metadataResult[useLocal ? 'local-ipv4' : 'public-ipv4'];
this.config.instance.hostName = preferIpAddress ? metadataIpAddress : metadataHostName;
this.config.instance.ipAddr = metadataIpAddress;
if (this.config.instance.statusPageUrl) {
const { statusPageUrl } = this.config.instance;
const replacedUrl = statusPageUrl.replace('__HOST__', this.config.instance.hostName);
this.config.instance.statusPageUrl = replacedUrl;
}
if (this.config.instance.healthCheckUrl) {
const { healthCheckUrl } = this.config.instance;
const replacedUrl = healthCheckUrl.replace('__HOST__', this.config.instance.hostName);
this.config.instance.healthCheckUrl = replacedUrl;
}
if (this.config.instance.homePageUrl) {
const { homePageUrl } = this.config.instance;
const replacedUrl = homePageUrl.replace('__HOST__', this.config.instance.hostName);
this.config.instance.homePageUrl = replacedUrl;
}
callback();
});
}
/*
Helper method for making a request to the Eureka server. Handles resolving
the current cluster as well as some default options.
*/
eurekaRequest(opts, callback, retryAttempt = 0) {
waterfall([
/*
Resolve Eureka Clusters
*/
done => {
this.clusterResolver.resolveEurekaUrl((err, eurekaUrl) => {
if (err) return done(err);
const requestOpts = merge({}, opts, {
baseUrl: eurekaUrl,
gzip: true,
});
done(null, requestOpts);
}, retryAttempt);
},
/*
Apply Request Middleware
*/
(requestOpts, done) => {
this.requestMiddleware(requestOpts, (newRequestOpts) => {
if (typeof newRequestOpts !== 'object') {
return done(new Error('requestMiddleware did not return an object'));
}
done(null, newRequestOpts);
});
},
/*
Perform Request
*/
(requestOpts, done) => {
const method = requestOpts.method ? requestOpts.method.toLowerCase() : 'get';
request[method](requestOpts, (error, response, body) => {
done(error, response, body, requestOpts);
});
},
],
/*
Handle Final Output.
*/
(error, response, body, requestOpts) => {
if (error) this.logger.error('Problem making eureka request', error);
// Perform retry if request failed and we have attempts left
const responseInvalid = response
&& response.statusCode
&& String(response.statusCode)[0] === '5';
if ((error || responseInvalid) && retryAttempt < this.config.eureka.maxRetries) {
const nextRetryDelay = this.config.eureka.requestRetryDelay * (retryAttempt + 1);
this.logger.warn(`Eureka request failed to endpoint ${requestOpts.baseUrl}, ` +
`next server retry in ${nextRetryDelay}ms`);
setTimeout(() => this.eurekaRequest(opts, callback, retryAttempt + 1),
nextRetryDelay);
return;
}
callback(error, response, body);
});
}
}
================================================
FILE: src/Logger.js
================================================
/* eslint-disable no-underscore-dangle */
const LEVELS = {
error: 50,
warn: 40,
info: 30,
debug: 20,
};
const DEFAULT_LEVEL = LEVELS.info;
export default class Logger {
constructor() {
this._level = DEFAULT_LEVEL;
}
level(inVal) {
let val = inVal;
if (val) {
if (typeof val === 'string') {
val = LEVELS[val];
}
this._level = val || DEFAULT_LEVEL;
}
return this._level;
}
// Abstract the console call:
_log(method, args) {
if (this._level <= LEVELS[method === 'log' ? 'debug' : method]) {
/* eslint-disable no-console */
console[method](...args);
/* eslint-enable no-console */
}
}
error(...args) {
return this._log('error', args);
}
warn(...args) {
return this._log('warn', args);
}
info(...args) {
return this._log('info', args);
}
debug(...args) {
return this._log('log', args);
}
}
================================================
FILE: src/defaultConfig.js
================================================
// Default configuration values:
export default {
requestMiddleware: (request, done) => done(request),
shouldUseDelta: false,
eureka: {
heartbeatInterval: 30000,
registryFetchInterval: 30000,
maxRetries: 3,
requestRetryDelay: 500,
fetchRegistry: true,
filterUpInstances: true,
servicePath: '/eureka/v2/apps/',
ssl: false,
useDns: false,
preferSameZone: true,
clusterRefreshInterval: 300000,
fetchMetadata: true,
registerWithEureka: true,
useLocalMetadata: false,
preferIpAddress: false,
},
instance: {},
};
================================================
FILE: src/deltaUtils.js
================================================
/*
General utilities for handling processing of delta changes from eureka.
*/
export function arrayOrObj(mysteryValue) {
return Array.isArray(mysteryValue) ? mysteryValue : [mysteryValue];
}
export function findInstance(a) {
return b => a.hostName === b.hostName && a.port.$ === b.port.$;
}
export function normalizeDelta(appDelta) {
return arrayOrObj(appDelta).map((app) => {
app.instance = arrayOrObj(app.instance);
return app;
});
}
================================================
FILE: src/index.js
================================================
import EurekaClient from './EurekaClient';
export const Eureka = EurekaClient;
export default EurekaClient;
================================================
FILE: test/AwsMetadata.test.js
================================================
import sinon from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import request from 'request';
import AwsMetadata from '../src/AwsMetadata';
chai.use(sinonChai);
describe('AWS Metadata client', () => {
describe('fetchMetadata()', () => {
let client;
beforeEach(() => {
client = new AwsMetadata({ host: '127.0.0.1:8888' });
});
afterEach(() => {
request.get.restore();
});
it('should call metadata URIs', () => {
const requestStub = sinon.stub(request, 'get');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/ami-id',
}).yields(null, { statusCode: 200 }, 'ami-123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-id',
}).yields(null, { statusCode: 200 }, 'i123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-type',
}).yields(null, { statusCode: 200 }, 'medium');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-ipv4',
}).yields(null, { statusCode: 200 }, '1.1.1.1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-hostname',
}).yields(null, { statusCode: 200 }, 'ip-127-0-0-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/placement/availability-zone',
}).yields(null, { statusCode: 200 }, 'fake-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-hostname',
}).yields(null, { statusCode: 200 }, 'ec2-127-0-0-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-ipv4',
}).yields(null, { statusCode: 200 }, '2.2.2.2');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/mac',
}).yields(null, { statusCode: 200 }, 'AB:CD:EF:GH:IJ');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/dynamic/instance-identity/document',
}).yields(null, { statusCode: 200 }, '{"accountId":"123456"}');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/network/interfaces/macs/AB:CD:EF:GH:IJ/vpc-id',
}).yields(null, { statusCode: 200 }, 'vpc123');
const fetchCb = sinon.spy();
client.fetchMetadata(fetchCb);
expect(request.get).to.have.been.callCount(11);
expect(fetchCb).to.have.been.calledWithMatch({
accountId: '123456',
'ami-id': 'ami-123',
'availability-zone': 'fake-1',
'instance-id': 'i123',
'instance-type': 'medium',
'local-hostname': 'ip-127-0-0-1',
'local-ipv4': '1.1.1.1',
mac: 'AB:CD:EF:GH:IJ',
'public-hostname': 'ec2-127-0-0-1',
'public-ipv4': '2.2.2.2',
'vpc-id': 'vpc123',
});
});
it('should call metadata URIs and filter out null and undefined values', () => {
const requestStub = sinon.stub(request, 'get');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/ami-id',
}).yields(null, { statusCode: 200 }, 'ami-123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-id',
}).yields(null, { statusCode: 200 }, 'i123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-type',
}).yields(null, { statusCode: 200 }, 'medium');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-ipv4',
}).yields(null, { statusCode: 200 }, '1.1.1.1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-hostname',
}).yields(null, { statusCode: 200 }, 'ip-127-0-0-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/placement/availability-zone',
}).yields(null, { statusCode: 200 }, 'fake-1');
let undef;
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-hostname',
}).yields(null, { statusCode: 200 }, undef);
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-ipv4',
}).yields(null, { statusCode: 200 }, null);
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/mac',
}).yields(null, { statusCode: 200 }, 'AB:CD:EF:GH:IJ');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/dynamic/instance-identity/document',
}).yields(null, { statusCode: 200 }, '{"accountId":"123456"}');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/network/interfaces/macs/AB:CD:EF:GH:IJ/vpc-id',
}).yields(null, { statusCode: 200 }, 'vpc123');
const fetchCb = sinon.spy();
client.fetchMetadata(fetchCb);
expect(request.get).to.have.been.callCount(11);
expect(fetchCb).to.have.been.calledWithMatch({
accountId: '123456',
'ami-id': 'ami-123',
'availability-zone': 'fake-1',
'instance-id': 'i123',
'instance-type': 'medium',
'local-hostname': 'ip-127-0-0-1',
'local-ipv4': '1.1.1.1',
mac: 'AB:CD:EF:GH:IJ',
'vpc-id': 'vpc123',
});
expect(fetchCb.firstCall.args[0]).to.have.all.keys(['ami-id',
'instance-id',
'instance-type',
'local-ipv4',
'local-hostname',
'availability-zone',
'mac',
'accountId',
'vpc-id']);
});
it('should call metadata URIs and filter out errored values', () => {
const requestStub = sinon.stub(request, 'get');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/ami-id',
}).yields(null, { statusCode: 200 }, 'ami-123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-id',
}).yields(null, { statusCode: 200 }, 'i123');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/instance-type',
}).yields(null, { statusCode: 200 }, 'medium');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-ipv4',
}).yields(null, { statusCode: 200 }, '1.1.1.1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/local-hostname',
}).yields(null, { statusCode: 200 }, 'ip-127-0-0-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/placement/availability-zone',
}).yields(null, { statusCode: 200 }, 'fake-1');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-hostname',
}).yields(new Error('fail'));
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/public-ipv4',
}).yields(new Error('fail'));
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/mac',
}).yields(null, { statusCode: 200 }, 'AB:CD:EF:GH:IJ');
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/dynamic/instance-identity/document',
}).yields(new Error('fail'));
requestStub.withArgs({
url: 'http://127.0.0.1:8888/latest/meta-data/network/interfaces/macs/AB:CD:EF:GH:IJ/vpc-id',
}).yields(null, { statusCode: 200 }, 'vpc123');
const fetchCb = sinon.spy();
client.fetchMetadata(fetchCb);
expect(request.get).to.have.been.callCount(11);
expect(fetchCb).to.have.been.calledWithMatch({
'ami-id': 'ami-123',
'availability-zone': 'fake-1',
'instance-id': 'i123',
'instance-type': 'medium',
'local-hostname': 'ip-127-0-0-1',
'local-ipv4': '1.1.1.1',
mac: 'AB:CD:EF:GH:IJ',
'vpc-id': 'vpc123',
});
expect(fetchCb.firstCall.args[0]).to.have.all.keys(['ami-id',
'instance-id',
'instance-type',
'local-ipv4',
'local-hostname',
'availability-zone',
'mac',
'vpc-id']);
});
});
});
================================================
FILE: test/ConfigClusterResolver.test.js
================================================
/* eslint-disable no-unused-expressions */
import { expect } from 'chai';
import merge from 'lodash/merge';
import ConfigClusterResolver from '../src/ConfigClusterResolver';
function makeConfig(overrides = {}) {
const config = {
instance: {
dataCenterInfo: { metadata: { 'availability-zone': '1b' } },
},
eureka: {
maxRetries: 0,
ec2Region: 'my-region',
},
};
return merge({}, config, overrides);
}
describe('Config Cluster Resolver', () => {
describe('resolveEurekaUrl() with host/port config', () => {
let resolver;
beforeEach(() => {
resolver = new ConfigClusterResolver(makeConfig({
eureka: {
host: 'eureka.mydomain.com',
servicePath: '/eureka/v2/apps/',
port: 9999,
},
}));
});
it('should return base Eureka URL using configured host', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://eureka.mydomain.com:9999/eureka/v2/apps/');
});
});
});
describe('resolveEurekaUrl() with default serviceUrls', () => {
let resolver;
beforeEach(() => {
resolver = new ConfigClusterResolver(makeConfig({
eureka: {
serviceUrls: {
default: [
'http://eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://eureka3.mydomain.com:9999/eureka/v2/apps/',
],
},
},
}));
});
it('should return first Eureka URL from configured serviceUrls', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://eureka1.mydomain.com:9999/eureka/v2/apps/');
});
});
it('should return next Eureka URL from configured serviceUrls', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://eureka2.mydomain.com:9999/eureka/v2/apps/');
// next attempt should still be the next server
resolver.resolveEurekaUrl((errTwo, eurekaUrlTwo) => {
expect(eurekaUrlTwo).to.equal('http://eureka2.mydomain.com:9999/eureka/v2/apps/');
});
}, 1);
});
});
describe('resolveEurekaUrl() with zoned serviceUrls', () => {
let resolver;
beforeEach(() => {
resolver = new ConfigClusterResolver(makeConfig({
eureka: {
availabilityZones: {
'my-region': ['1a', '1b', '1c'],
},
serviceUrls: {
'1a': [
'http://1a-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1b': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1c': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
},
},
}));
});
it('should return first Eureka URL from configured serviceUrls', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://1a-eureka1.mydomain.com:9999/eureka/v2/apps/');
});
});
});
describe('resolveEurekaUrl() with zoned serviceUrls and preferSameZone', () => {
let resolver;
beforeEach(() => {
resolver = new ConfigClusterResolver(makeConfig({
eureka: {
preferSameZone: true,
availabilityZones: {
'my-region': ['1a', '1b', '1c'],
},
serviceUrls: {
'1a': [
'http://1a-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1b': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1c': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
},
},
}));
});
it('should return first Eureka URL from configured serviceUrls', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/');
});
});
});
describe('resolveEurekaUrl(), zoned serviceUrls, preferSameZone, missing dataCenterInfo', () => {
let resolver;
const config = {
instance: {},
eureka: {
maxRetries: 0,
ec2Region: 'my-region',
preferSameZone: true,
availabilityZones: {
'my-region': ['1a', '1b', '1c'],
},
serviceUrls: {
'1a': [
'http://1a-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1a-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1b': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
'1c': [
'http://1b-eureka1.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka2.mydomain.com:9999/eureka/v2/apps/',
'http://1b-eureka3.mydomain.com:9999/eureka/v2/apps/',
],
},
},
};
beforeEach(() => {
resolver = new ConfigClusterResolver(config);
});
it('should return first Eureka URL from configured serviceUrls', () => {
resolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://1a-eureka1.mydomain.com:9999/eureka/v2/apps/');
});
});
});
});
================================================
FILE: test/DnsClusterResolver.test.js
================================================
/* eslint-disable no-unused-expressions */
import sinon from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import dns from 'dns';
import merge from 'lodash/merge';
import DnsClusterResolver from '../src/DnsClusterResolver';
chai.use(sinonChai);
function makeConfig(overrides = {}) {
const config = {
instance: {
dataCenterInfo: { metadata: { 'availability-zone': '1b' } },
},
eureka: {
host: 'eureka.mydomain.com',
servicePath: '/eureka/v2/apps/',
port: 9999,
maxRetries: 0,
ec2Region: 'my-region',
},
};
return merge({}, config, overrides);
}
describe('DNS Cluster Resolver', () => {
describe('DnsClusterResolver', () => {
it('should throw error when ec2Region is undefined', () => {
const config = makeConfig();
config.eureka.ec2Region = undefined;
function fn() {
return new DnsClusterResolver(config);
}
expect(fn).to.throw();
});
});
describe('startClusterRefresh()', () => {
let dnsResolver;
let refreshStub;
let clock;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
dnsResolver.refreshCurrentCluster.restore();
clock.restore();
});
it('should start cluster refreshes on interval', () => {
dnsResolver = new DnsClusterResolver(makeConfig({
eureka: { clusterRefreshInterval: 300000 },
}));
refreshStub = sinon.stub(dnsResolver, 'refreshCurrentCluster');
clock.tick(300000);
expect(refreshStub).to.have.been.calledOnce;
clock.tick(300000);
expect(refreshStub).to.have.been.calledTwice;
clock.restore();
});
it('should log warning on refresh failure', () => {
dnsResolver = new DnsClusterResolver(makeConfig({
eureka: { clusterRefreshInterval: 300000 },
}));
refreshStub = sinon.stub(dnsResolver, 'refreshCurrentCluster');
refreshStub.yields(new Error('fail'));
clock.tick(300000);
expect(refreshStub).to.have.been.calledOnce;
clock.tick(300000);
expect(refreshStub).to.have.been.calledTwice;
clock.restore();
});
});
describe('resolveEurekaUrl()', () => {
let dnsResolver;
beforeEach(() => {
dnsResolver = new DnsClusterResolver(makeConfig());
});
afterEach(() => {
dnsResolver.resolveClusterHosts.restore();
});
it('should return base Eureka URL using current cluster host', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.yields(null, ['a.mydomain.com', 'b.mydomain.com', 'c.mydomain.com']);
dnsResolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://a.mydomain.com:9999/eureka/v2/apps/');
});
});
it('should return base Eureka URL using next cluster host on retry', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.yields(null, ['a.mydomain.com', 'b.mydomain.com', 'c.mydomain.com']);
dnsResolver.resolveEurekaUrl((err, eurekaUrl) => {
expect(eurekaUrl).to.equal('http://b.mydomain.com:9999/eureka/v2/apps/');
expect(dnsResolver.serverList).to.eql(['b.mydomain.com', 'c.mydomain.com',
'a.mydomain.com']);
}, 1);
});
it('should return error when resolve fails', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.yields(new Error('fail'));
dnsResolver.resolveEurekaUrl((err) => {
expect(err).to.not.equal(undefined);
expect(err.message).to.equal('fail');
});
});
});
describe('getCurrentCluster()', () => {
let dnsResolver;
beforeEach(() => {
dnsResolver = new DnsClusterResolver(makeConfig());
});
afterEach(() => {
dnsResolver.resolveClusterHosts.restore();
});
it('should call cluster refresh if server list is undefined', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.onCall(0).yields(null, ['a', 'b', 'c']);
resolveHostsStub.onCall(1).yields(null, ['f', 'a', 'c']);
dnsResolver.getCurrentCluster((err, serverList) => {
expect(serverList).to.include.members(['a', 'b', 'c']);
dnsResolver.getCurrentCluster((errTwo, serverListTwo) => {
expect(serverListTwo).to.include.members(['a', 'b', 'c']);
});
});
});
it('should return error when refresh fails', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.yields(new Error('fail'));
dnsResolver.getCurrentCluster((err) => {
expect(err).to.not.equal(undefined);
expect(err.message).to.equal('fail');
});
});
});
describe('refreshCurrentCluster', () => {
let dnsResolver;
beforeEach(() => {
dnsResolver = new DnsClusterResolver(makeConfig());
});
afterEach(() => {
dnsResolver.resolveClusterHosts.restore();
});
it('should refresh server list', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.onCall(0).yields(null, ['a', 'b', 'c']);
resolveHostsStub.onCall(1).yields(null, ['a', 'b', 'c', 'd']);
dnsResolver.refreshCurrentCluster((err) => {
expect(err).to.equal(undefined);
expect(dnsResolver.serverList).to.eql(['a', 'b', 'c']);
dnsResolver.refreshCurrentCluster((errTwo) => {
expect(errTwo).to.equal(undefined);
expect(dnsResolver.serverList).to.eql(['a', 'b', 'c', 'd']);
});
});
});
it('should maintain server list when cluster remains unchanged', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.onCall(0).yields(null, ['a', 'b', 'c']);
resolveHostsStub.onCall(1).yields(null, ['c', 'a', 'b']);
dnsResolver.refreshCurrentCluster((err) => {
expect(err).to.equal(undefined);
expect(dnsResolver.serverList).to.eql(['a', 'b', 'c']);
dnsResolver.refreshCurrentCluster((errTwo) => {
expect(errTwo).to.equal(undefined);
expect(dnsResolver.serverList).to.eql(['a', 'b', 'c']);
});
});
});
it('should return error when resolve fails', () => {
const resolveHostsStub = sinon.stub(dnsResolver, 'resolveClusterHosts');
resolveHostsStub.yields(new Error('fail'));
dnsResolver.refreshCurrentCluster((err) => {
expect(err).to.not.equal(undefined);
expect(err.message).to.equal('fail');
});
});
});
describe('resolveClusterHosts()', () => {
const eurekaHosts = [
'1a.eureka.mydomain.com',
'1b.eureka.mydomain.com',
'1c.eureka.mydomain.com',
];
afterEach(() => {
dns.resolveTxt.restore();
});
it('should resolve hosts using DNS', (done) => {
const dnsResolver = new DnsClusterResolver(makeConfig());
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com').yields(null, [eurekaHosts]);
resolveStub.withArgs('txt.1a.eureka.mydomain.com').yields(null, [['1.2.3.4']]);
resolveStub.withArgs('txt.1b.eureka.mydomain.com').yields(null, [['2.2.3.4']]);
resolveStub.withArgs('txt.1c.eureka.mydomain.com').yields(null, [['3.2.3.4']]);
dnsResolver.resolveClusterHosts((err, hosts) => {
expect(hosts).to.include.members(['1.2.3.4', '2.2.3.4', '3.2.3.4']);
done();
});
});
it('should resolve hosts using DNS and zone affinity', (done) => {
const dnsResolver = new DnsClusterResolver(makeConfig({
eureka: { preferSameZone: true },
}));
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com').yields(null, [eurekaHosts]);
resolveStub.withArgs('txt.1a.eureka.mydomain.com').yields(null, [['1.2.3.4']]);
resolveStub.withArgs('txt.1b.eureka.mydomain.com').yields(null, [['2.2.3.4']]);
resolveStub.withArgs('txt.1c.eureka.mydomain.com').yields(null, [['3.2.3.4']]);
dnsResolver.resolveClusterHosts((err, hosts) => {
expect(hosts[0]).to.equal('2.2.3.4');
expect(hosts).to.include.members(['1.2.3.4', '2.2.3.4', '3.2.3.4']);
dnsResolver.resolveClusterHosts((error, hostsTwo) => {
expect(hostsTwo[0]).to.equal('2.2.3.4');
expect(hostsTwo).to.include.members(['1.2.3.4', '2.2.3.4', '3.2.3.4']);
done();
});
});
});
it('should resolve hosts when dataCenterInfo is undefined', (done) => {
const config = {
instance: {},
eureka: {
preferSameZone: true,
host: 'eureka.mydomain.com',
servicePath: '/eureka/v2/apps/',
port: 9999,
maxRetries: 0,
ec2Region: 'my-region',
},
};
const dnsResolver = new DnsClusterResolver(config);
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com').yields(null, [eurekaHosts]);
resolveStub.withArgs('txt.1a.eureka.mydomain.com').yields(null, [['1.2.3.4']]);
resolveStub.withArgs('txt.1b.eureka.mydomain.com').yields(null, [['2.2.3.4']]);
resolveStub.withArgs('txt.1c.eureka.mydomain.com').yields(null, [['3.2.3.4']]);
dnsResolver.resolveClusterHosts((err, hosts) => {
expect(hosts).to.include.members(['1.2.3.4', '2.2.3.4', '3.2.3.4']);
dnsResolver.resolveClusterHosts((error, hostsTwo) => {
expect(hostsTwo).to.include.members(['1.2.3.4', '2.2.3.4', '3.2.3.4']);
done();
});
});
});
it('should return error when initial DNS lookup fails', () => {
const resolveCb = sinon.spy();
const dnsResolver = new DnsClusterResolver(makeConfig());
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com')
.yields(new Error('dns error'), null);
function shouldNotThrow() {
dnsResolver.resolveClusterHosts(resolveCb);
}
expect(shouldNotThrow).to.not.throw();
expect(dns.resolveTxt).to.have.been.calledWithMatch('txt.my-region.eureka.mydomain.com');
expect(resolveCb).to.have.been.calledWithMatch({
message: 'Error resolving eureka cluster ' +
'for region [my-region] using DNS: [Error: dns error]',
});
});
it('should return error when DNS lookup fails for an individual zone', () => {
const resolveCb = sinon.spy();
const dnsResolver = new DnsClusterResolver(makeConfig({
eureka: { host: 'eureka.mydomain.com', port: 9999, ec2Region: 'my-region' },
}));
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com').yields(null, [eurekaHosts]);
resolveStub.withArgs('txt.1a.eureka.mydomain.com').yields(null, [['1.2.3.4']]);
resolveStub.withArgs('txt.1b.eureka.mydomain.com').yields(new Error('dns error'), null);
resolveStub.withArgs('txt.1c.eureka.mydomain.com').yields(null, [['3.2.3.4']]);
function shouldNotThrow() {
dnsResolver.resolveClusterHosts(resolveCb);
}
expect(shouldNotThrow).to.not.throw();
expect(resolveCb).to.have.been.calledWithMatch({
message: 'Error resolving cluster zone txt.1b.eureka.mydomain.com: [Error: dns error]',
});
});
it('should return error when no hosts were found', (done) => {
const dnsResolver = new DnsClusterResolver(makeConfig());
const resolveStub = sinon.stub(dns, 'resolveTxt');
resolveStub.withArgs('txt.my-region.eureka.mydomain.com').yields(null, [eurekaHosts]);
resolveStub.withArgs('txt.1a.eureka.mydomain.com').yields(null, []);
resolveStub.withArgs('txt.1b.eureka.mydomain.com').yields(null, []);
resolveStub.withArgs('txt.1c.eureka.mydomain.com').yields(null, []);
dnsResolver.resolveClusterHosts((err) => {
expect(err).to.not.equal(undefined);
expect(err.message).to.equal('Unable to locate any Eureka hosts in any ' +
'zone via DNS @ txt.my-region.eureka.mydomain.com');
done();
});
});
});
});
================================================
FILE: test/EurekaClient.test.js
================================================
/* eslint-disable no-unused-expressions, max-len */
import sinon from 'sinon';
import chai, { expect } from 'chai';
import sinonChai from 'sinon-chai';
import request from 'request';
import { EventEmitter } from 'events';
import { join } from 'path';
import merge from 'lodash/merge';
import Eureka from '../src/EurekaClient';
import DnsClusterResolver from '../src/DnsClusterResolver';
chai.use(sinonChai);
function makeConfig(overrides = {}) {
const config = {
instance: {
app: 'app',
vipAddress: '1.2.2.3',
hostName: 'myhost',
port: 9999,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: { host: '127.0.0.1', port: 9999, maxRetries: 0 },
};
return merge({}, config, overrides);
}
describe('Eureka client', () => {
describe('Eureka()', () => {
it('should extend EventEmitter', () => {
expect(new Eureka(makeConfig())).to.be.instanceof(EventEmitter);
});
it('should throw an error if no config is found', () => {
function fn() {
return new Eureka();
}
expect(fn).to.throw();
});
it('should construct with the correct configuration values', () => {
function shouldThrow() {
return new Eureka();
}
function noApp() {
return new Eureka({
instance: {
vipAddress: true,
port: true,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
host: true,
port: true,
},
});
}
function shouldWork() {
return new Eureka({
instance: {
app: true,
vipAddress: true,
port: true,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
host: true,
port: true,
},
});
}
function shouldWorkNoInstance() {
return new Eureka({
eureka: {
registerWithEureka: false,
host: true,
port: true,
},
});
}
expect(shouldThrow).to.throw();
expect(noApp).to.throw(/app/);
expect(shouldWork).to.not.throw();
expect(shouldWorkNoInstance).to.not.throw();
});
it('should use DnsClusterResolver when configured', () => {
const client = new Eureka({
instance: {
app: true,
vipAddress: true,
port: true,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
host: true,
port: true,
useDns: true,
ec2Region: 'my-region',
},
});
expect(client.clusterResolver.constructor).to.equal(DnsClusterResolver);
});
it('should throw when configured to useDns without setting ec2Region', () => {
function shouldThrow() {
return new Eureka({
instance: {
app: true,
vipAddress: true,
port: true,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
host: true,
port: true,
useDns: true,
},
});
}
expect(shouldThrow).to.throw(/ec2Region/);
});
it('should accept requestMiddleware', () => {
const requestMiddleware = (opts) => opts;
const client = new Eureka({
requestMiddleware,
instance: {
app: true,
vipAddress: true,
port: true,
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
host: true,
port: true,
useDns: true,
ec2Region: 'my-region',
},
});
expect(client.requestMiddleware).to.equal(requestMiddleware);
});
});
describe('get instanceId()', () => {
it('should return the configured instance id', () => {
const instanceId = 'test_id';
const config = makeConfig({
instance: {
instanceId,
},
});
const client = new Eureka(config);
expect(client.instanceId).to.equal(instanceId);
});
it('should return hostname for non-AWS datacenters', () => {
const config = makeConfig();
const client = new Eureka(config);
expect(client.instanceId).to.equal('myhost');
});
it('should return instance ID for AWS datacenters', () => {
const config = makeConfig({
instance: { dataCenterInfo: { name: 'Amazon', metadata: { 'instance-id': 'i123' } } },
});
const client = new Eureka(config);
expect(client.instanceId).to.equal('i123');
});
});
describe('start()', () => {
let config;
let client;
let registerSpy;
let fetchRegistrySpy;
let heartbeatsSpy;
let registryFetchSpy;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
afterEach(() => {
registerSpy.restore();
fetchRegistrySpy.restore();
heartbeatsSpy.restore();
registryFetchSpy.restore();
});
it('should call register, fetch registry, startHeartbeat and startRegistryFetches', (done) => {
registerSpy = sinon.stub(client, 'register').callsArg(0);
fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0);
heartbeatsSpy = sinon.stub(client, 'startHeartbeats');
registryFetchSpy = sinon.stub(client, 'startRegistryFetches');
const eventSpy = sinon.spy();
client.on('started', eventSpy);
client.start(() => {
expect(registerSpy).to.have.been.calledOnce;
expect(fetchRegistrySpy).to.have.been.calledOnce;
expect(heartbeatsSpy).to.have.been.calledOnce;
expect(registryFetchSpy).to.have.been.calledOnce;
expect(eventSpy).to.have.been.calledOnce;
done();
});
});
it('should call fetch registry and startRegistryFetches when registration disabled', (done) => {
config = makeConfig({
eureka: {
registerWithEureka: false,
},
});
client = new Eureka(config);
registerSpy = sinon.stub(client, 'register').callsArg(0);
fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0);
heartbeatsSpy = sinon.stub(client, 'startHeartbeats');
registryFetchSpy = sinon.stub(client, 'startRegistryFetches');
const eventSpy = sinon.spy();
client.on('started', eventSpy);
client.start(() => {
expect(registerSpy).to.not.have.been.called;
expect(fetchRegistrySpy).to.have.been.calledOnce;
expect(heartbeatsSpy).to.not.have.been.called;
expect(registryFetchSpy).to.have.been.calledOnce;
expect(eventSpy).to.have.been.calledOnce;
done();
});
});
it('should return error on start failure', (done) => {
registerSpy = sinon.stub(client, 'register').yields(new Error('fail'));
fetchRegistrySpy = sinon.stub(client, 'fetchRegistry').callsArg(0);
heartbeatsSpy = sinon.stub(client, 'startHeartbeats');
registryFetchSpy = sinon.stub(client, 'startRegistryFetches');
const eventSpy = sinon.spy();
client.on('started', eventSpy);
client.start((error) => {
expect(error).to.match(/fail/);
expect(eventSpy).to.not.have.been.called;
done();
});
});
});
describe('startHeartbeats()', () => {
let config;
let client;
let renewSpy;
let clock;
before(() => {
config = makeConfig();
client = new Eureka(config);
renewSpy = sinon.stub(client, 'renew');
clock = sinon.useFakeTimers();
});
after(() => {
renewSpy.restore();
clock.restore();
});
it('should call renew on interval', () => {
client.startHeartbeats();
clock.tick(30000);
expect(renewSpy).to.have.been.calledOnce;
clock.tick(30000);
expect(renewSpy).to.have.been.calledTwice;
});
});
describe('startRegistryFetches()', () => {
let config;
let client;
let fetchRegistrySpy;
let clock;
before(() => {
config = makeConfig();
client = new Eureka(config);
fetchRegistrySpy = sinon.stub(client, 'fetchRegistry');
clock = sinon.useFakeTimers();
});
after(() => {
fetchRegistrySpy.restore();
clock.restore();
});
it('should call renew on interval', () => {
client.startRegistryFetches();
clock.tick(30000);
expect(fetchRegistrySpy).to.have.been.calledOnce;
clock.tick(30000);
expect(fetchRegistrySpy).to.have.been.calledTwice;
});
});
describe('stop()', () => {
let config;
let client;
let deregisterSpy;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
deregisterSpy = sinon.stub(client, 'deregister').callsArg(0);
});
afterEach(() => {
deregisterSpy.restore();
});
it('should call deregister', () => {
const stopCb = sinon.spy();
client.stop(stopCb);
expect(deregisterSpy).to.have.been.calledOnce;
expect(stopCb).to.have.been.calledOnce;
});
it('should skip deregister if registration disabled', () => {
config = makeConfig({
eureka: {
registerWithEureka: false,
},
});
client = new Eureka(config);
const stopCb = sinon.spy();
client.stop(stopCb);
expect(deregisterSpy).to.not.have.been.called;
expect(stopCb).to.have.been.calledOnce;
});
});
describe('register()', () => {
let config;
let client;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
afterEach(() => {
request.post.restore();
});
it('should trigger register event', () => {
sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null);
const eventSpy = sinon.spy();
client.on('registered', eventSpy);
client.register();
expect(eventSpy).to.have.been.calledOnce;
});
it('should call register URI', () => {
sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null);
const registerCb = sinon.spy();
client.register(registerCb);
expect(request.post).to.have.been.calledWithMatch({
body: {
instance: {
app: 'app',
hostName: 'myhost',
dataCenterInfo: { name: 'MyOwn' },
port: 9999,
status: 'UP',
vipAddress: '1.2.2.3',
},
},
json: true,
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'app',
});
expect(registerCb).to.have.been.calledWithMatch(null);
});
it('should throw error for non-204 response', () => {
sinon.stub(request, 'post').yields(null, { statusCode: 500 }, null);
const registerCb = sinon.spy();
client.register(registerCb);
expect(registerCb).to.have.been.calledWithMatch({
message: 'eureka registration FAILED: status: 500 body: null',
});
});
it('should throw error for request error', () => {
sinon.stub(request, 'post').yields(new Error('request error'), null, null);
const registerCb = sinon.spy();
client.register(registerCb);
expect(registerCb).to.have.been.calledWithMatch({ message: 'request error' });
});
});
describe('deregister()', () => {
let config;
let client;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
afterEach(() => {
request.delete.restore();
});
it('should should trigger deregister event', () => {
sinon.stub(request, 'delete').yields(null, { statusCode: 200 }, null);
const eventSpy = sinon.spy();
client.on('deregistered', eventSpy);
client.register();
client.deregister();
});
it('should call deregister URI', () => {
sinon.stub(request, 'delete').yields(null, { statusCode: 200 }, null);
const deregisterCb = sinon.spy();
client.deregister(deregisterCb);
expect(request.delete).to.have.been.calledWithMatch({
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'app/myhost',
});
expect(deregisterCb).to.have.been.calledWithMatch(null);
});
it('should throw error for non-200 response', () => {
sinon.stub(request, 'delete').yields(null, { statusCode: 500 }, null);
const deregisterCb = sinon.spy();
client.deregister(deregisterCb);
expect(deregisterCb).to.have.been.calledWithMatch({
message: 'eureka deregistration FAILED: status: 500 body: null',
});
});
it('should throw error for request error', () => {
sinon.stub(request, 'delete').yields(new Error('request error'), null, null);
const deregisterCb = sinon.spy();
client.deregister(deregisterCb);
expect(deregisterCb).to.have.been.calledWithMatch({ message: 'request error' });
});
});
describe('renew()', () => {
let config;
let client;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
afterEach(() => {
request.put.restore();
});
it('should call heartbeat URI', () => {
sinon.stub(request, 'put').yields(null, { statusCode: 200 }, null);
client.renew();
expect(request.put).to.have.been.calledWithMatch({
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'app/myhost',
});
});
it('should trigger a heartbeat event', () => {
sinon.stub(request, 'put').yields(null, { statusCode: 200 }, null);
const eventSpy = sinon.spy();
client.on('heartbeat', eventSpy);
client.renew();
expect(eventSpy).to.have.been.calledOnce;
});
it('should re-register on 404', () => {
sinon.stub(request, 'put').yields(null, { statusCode: 404 }, null);
sinon.stub(request, 'post').yields(null, { statusCode: 204 }, null);
client.renew();
expect(request.put).to.have.been.calledWithMatch({
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'app/myhost',
});
expect(request.post).to.have.been.calledWithMatch({
body: {
instance: {
app: 'app',
hostName: 'myhost',
dataCenterInfo: { name: 'MyOwn' },
port: 9999,
status: 'UP',
vipAddress: '1.2.2.3',
},
},
json: true,
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'app',
});
});
});
describe('eureka-client.yml', () => {
let stub;
before(() => {
stub = sinon.stub(process, 'cwd').returns(__dirname);
});
after(() => {
stub.restore();
});
it('should load the correct', () => {
const client = new Eureka(makeConfig());
expect(client.config.eureka.custom).to.equal('test');
});
it('should load the environment overrides', () => {
const client = new Eureka(makeConfig());
expect(client.config.eureka.otherCustom).to.equal('test2');
expect(client.config.eureka.overrides).to.equal(2);
});
it('should support a `cwd` and `filename` property', () => {
const client = new Eureka(makeConfig({
cwd: join(__dirname, 'fixtures'),
filename: 'config',
}));
expect(client.config.eureka.fromFixture).to.equal(true);
});
it('should throw error on malformed config file', () => {
function malformed() {
return new Eureka(makeConfig({
cwd: join(__dirname, 'fixtures'),
filename: 'malformed-config',
}));
}
expect(malformed).to.throw(Error);
});
it('should not throw error on malformed config file', () => {
function missingFile() {
return new Eureka(makeConfig({
cwd: join(__dirname, 'fixtures'),
filename: 'missing-config',
}));
}
expect(missingFile).to.not.throw();
});
});
describe('validateConfig()', () => {
let config;
beforeEach(() => {
config = makeConfig({
instance: { dataCenterInfo: { name: 'Amazon' } },
});
});
it('should throw an exception with a missing instance.app', () => {
function badConfig() {
delete config.instance.app;
return new Eureka(config);
}
expect(badConfig).to.throw(TypeError);
});
it('should throw an exception with a missing instance.vipAddress', () => {
function badConfig() {
delete config.instance.vipAddress;
return new Eureka(config);
}
expect(badConfig).to.throw(TypeError);
});
it('should throw an exception with a missing instance.port', () => {
function badConfig() {
delete config.instance.port;
return new Eureka(config);
}
expect(badConfig).to.throw(TypeError);
});
it('should throw an exception with a missing instance.dataCenterInfo', () => {
function badConfig() {
delete config.instance.dataCenterInfo;
return new Eureka(config);
}
expect(badConfig).to.throw(TypeError);
});
it('should throw an exception with an invalid request middleware', () => {
function badConfig() {
config.requestMiddleware = 'invalid middleware';
return new Eureka(config);
}
expect(badConfig).to.throw(TypeError);
});
});
describe('getInstancesByAppId()', () => {
let client;
let config;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
it('should throw an exception if no appId is provided', () => {
function noAppId() {
client.getInstancesByAppId();
}
expect(noAppId).to.throw(Error);
});
it('should return a list of instances if appId is registered', () => {
const appId = 'THESERVICENAME';
const expectedInstances = [{ host: '127.0.0.1' }];
client.cache.app[appId] = expectedInstances;
const actualInstances = client.getInstancesByAppId(appId);
expect(actualInstances).to.equal(expectedInstances);
});
it('should return empty array if no instances were found for given appId', () => {
expect(client.getInstancesByAppId('THESERVICENAME')).to.deep.equal([]);
});
});
describe('getInstancesByVipAddress()', () => {
let client;
let config;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
});
it('should throw an exception if no vipAddress is provided', () => {
function noVipAddress() {
client.getInstancesByVipAddress();
}
expect(noVipAddress).to.throw(Error);
});
it('should return a list of instances if vipAddress is registered', () => {
const vipAddress = 'the.vip.address';
const expectedInstances = [{ host: '127.0.0.1' }];
client.cache.vip[vipAddress] = expectedInstances;
const actualInstances = client.getInstancesByVipAddress(vipAddress);
expect(actualInstances).to.equal(expectedInstances);
});
it('should return empty array if no instances were found for given vipAddress', () => {
expect(client.getInstancesByVipAddress('the.vip.address')).to.deep.equal([]);
});
});
describe('fetchRegistry()', () => {
let config;
let client;
beforeEach(() => {
config = makeConfig();
client = new Eureka(config);
sinon.stub(client, 'transformRegistry');
sinon.stub(client, 'handleDelta');
});
afterEach(() => {
request.get.restore();
client.transformRegistry.restore();
client.handleDelta.restore();
});
it('should should trigger registryUpdated event', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null);
const eventSpy = sinon.spy();
client.on('registryUpdated', eventSpy);
client.fetchRegistry();
expect(eventSpy).to.have.been.calledOnce;
});
it('should call registry URI', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null);
const registryCb = sinon.spy();
client.fetchRegistry(registryCb);
expect(request.get).to.have.been.calledWithMatch({
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: '',
headers: { Accept: 'application/json' },
});
expect(registryCb).to.have.been.calledWithMatch(null);
});
it('should call registry URI for delta', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ "applications": {} }');
const registryCb = sinon.spy();
client.config.shouldUseDelta = true;
client.hasFullRegistry = true;
client.fetchRegistry(registryCb);
expect(request.get).to.have.been.calledWithMatch({
baseUrl: 'http://127.0.0.1:9999/eureka/v2/apps/',
uri: 'delta',
headers: { Accept: 'application/json' },
});
expect(registryCb).to.have.been.calledWithMatch(null);
});
it('should throw error for non-200 response', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 500 }, null);
const registryCb = sinon.spy();
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWithMatch({
message: 'Unable to retrieve full registry from Eureka server',
});
});
it('should throw error for non-200 response for delta', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 500 }, null);
const registryCb = sinon.spy();
client.config.shouldUseDelta = true;
client.hasFullRegistry = true;
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWithMatch({
message: 'Unable to retrieve delta registry from Eureka server',
});
});
it('should throw error for request error', () => {
sinon.stub(request, 'get').yields(new Error('request error'), null, null);
const registryCb = sinon.spy();
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWithMatch({ message: 'request error' });
});
it('should throw error for request error for delta request', () => {
sinon.stub(request, 'get').yields(new Error('request error'), null, null);
const registryCb = sinon.spy();
client.config.shouldUseDelta = true;
client.hasFullRegistry = true;
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWithMatch({ message: 'request error' });
});
it('should throw error on invalid JSON', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ blah');
const registryCb = sinon.spy();
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWith(new SyntaxError());
});
it('should throw error on invalid JSON for delta request', () => {
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, '{ blah');
const registryCb = sinon.spy();
client.config.shouldUseDelta = true;
client.hasFullRegistry = true;
client.fetchRegistry(registryCb);
expect(registryCb).to.have.been.calledWith(new SyntaxError());
});
});
describe('transformRegistry()', () => {
let client;
let config;
let registry;
let instance1;
let instance2;
let instance3;
let instance4;
let instance5;
let app1;
let app2;
let app3;
beforeEach(() => {
config = makeConfig();
registry = {
applications: { application: {} },
};
instance1 = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'theapp', vipAddress: 'vip1', status: 'UP' };
instance2 = { hostName: '127.0.0.2', port: { $: 2000 }, app: 'theapptwo', vipAddress: 'vip2', status: 'UP' };
instance3 = { hostName: '127.0.0.3', port: { $: 2000 }, app: 'theapp', vipAddress: 'vip2', status: 'UP' };
instance4 = { hostName: '127.0.0.4', port: { $: 2000 }, app: 'theappthree', vipAddress: 'vip3', status: 'UP' };
instance5 = { hostName: '127.0.0.5', port: { $: 2000 }, app: 'theappthree', vipAddress: 'vip2', status: 'UP' };
app1 = { name: 'theapp', instance: instance1 };
app2 = { name: 'theapptwo', instance: [instance2, instance3] };
app3 = { name: 'theappthree', instance: [instance5, instance4] };
client = new Eureka(config);
});
it('should noop if empty registry', () => {
client.transformRegistry(undefined);
expect(client.cache.vip).to.be.empty;
expect(client.cache.app).to.be.empty;
});
it('should return clear the cache if no applications exist', () => {
registry.applications.application = null;
client.transformRegistry(registry);
expect(client.cache.vip).to.be.empty;
expect(client.cache.app).to.be.empty;
});
it('should transform a registry with one app', () => {
registry.applications.application = app1;
client.transformRegistry(registry);
expect(client.cache.app[app1.name.toUpperCase()].length).to.equal(1);
expect(client.cache.vip[instance1.vipAddress].length).to.equal(1);
});
it('should transform a registry with two or more apps', () => {
registry.applications.application = [app1, app2];
client.transformRegistry(registry);
expect(client.cache.app[app1.name.toUpperCase()].length).to.equal(2);
expect(client.cache.vip[instance2.vipAddress].length).to.equal(2);
});
it('should transform a registry with a single application with multiple vips', () => {
registry.applications.application = [app3];
client.transformRegistry(registry);
expect(client.cache.app[app3.name.toUpperCase()].length).to.equal(2);
expect(client.cache.vip[instance5.vipAddress].length).to.equal(1);
expect(client.cache.vip[instance4.vipAddress].length).to.equal(1);
});
});
describe('transformApp()', () => {
let client;
let config;
let app;
let instance1;
let instance2;
let instance3;
let instance4;
let downInstance;
let theVip;
let multiVip;
let cache;
beforeEach(() => {
config = makeConfig({
instance: { dataCenterInfo: { name: 'Amazon' } },
});
client = new Eureka(config);
theVip = 'theVip';
multiVip = 'fooVip,barVip';
instance1 = { hostName: '127.0.0.1', port: 1000, vipAddress: theVip, app: 'theapp', status: 'UP' };
instance2 = { hostName: '127.0.0.2', port: 2000, vipAddress: theVip, app: 'theapp', status: 'UP' };
instance3 = { hostName: '127.0.0.5', port: 2000, vipAddress: multiVip, app: 'theapp', status: 'UP' };
instance4 = { hostName: '127.0.0.6', port: 2000, vipAddress: void 0, app: 'theapp', status: 'UP' };
downInstance = { hostName: '127.0.0.7', port: 2000, app: 'theapp', vipAddress: theVip, status: 'DOWN' };
app = { name: 'theapp' };
cache = { app: {}, vip: {} };
});
it('should transform an app with one instance', () => {
app.instance = instance1;
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(1);
expect(cache.vip[theVip].length).to.equal(1);
});
it('should transform an app with one instance that has a comma separated vipAddress', () => {
app.instance = instance3;
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(1);
expect(cache.vip[multiVip.split(',')[0]].length).to.equal(1);
expect(cache.vip[multiVip.split(',')[1]].length).to.equal(1);
});
it('should transform an app with one instance that has no vipAddress', () => {
app.instance = instance4;
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(1);
expect(Object.keys(cache.vip).length).to.equal(0);
});
it('should transform an app with two or more instances', () => {
app.instance = [instance1, instance2, instance3];
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(3);
expect(cache.vip[theVip].length).to.equal(2);
expect(cache.vip[multiVip.split(',')[0]].length).to.equal(1);
expect(cache.vip[multiVip.split(',')[1]].length).to.equal(1);
});
it('should filter UP instances by default', () => {
app.instance = [instance1, instance2, downInstance];
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(2);
expect(cache.vip[theVip].length).to.equal(2);
});
it('should not filter UP instances when filterUpInstances === false', () => {
config = makeConfig({
instance: { dataCenterInfo: { name: 'Amazon' } },
eureka: { filterUpInstances: false },
});
client = new Eureka(config);
app.instance = [instance1, instance2, downInstance];
client.transformApp(app, cache);
expect(cache.app[app.name.toUpperCase()].length).to.equal(3);
expect(cache.vip[theVip].length).to.equal(3);
});
});
describe('addInstanceMetadata()', () => {
let client;
let config;
let instanceConfig;
let awsMetadata;
let metadataSpy;
beforeEach(() => {
instanceConfig = {
app: 'app',
vipAddress: '1.2.3.4',
port: 9999,
dataCenterInfo: { name: 'Amazon' },
statusPageUrl: 'http://__HOST__:8080/info',
healthCheckUrl: 'http://__HOST__:8077/healthcheck',
homePageUrl: 'http://__HOST__:8080/',
};
awsMetadata = {
'public-hostname': 'ec2-127-0-0-1.us-fake-1.mydomain.com',
'public-ipv4': '54.54.54.54',
'local-hostname': 'fake-1',
'local-ipv4': '10.0.1.1',
};
});
afterEach(() => {
client.metadataClient.fetchMetadata.restore();
});
it('should update hosts with AWS metadata public host', () => {
// Setup
config = {
instance: instanceConfig,
eureka: { host: '127.0.0.1', port: 9999 },
};
client = new Eureka(config);
metadataSpy = sinon.spy();
sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata);
// Act
client.addInstanceMetadata(metadataSpy);
expect(client.config.instance.hostName).to.equal('ec2-127-0-0-1.us-fake-1.mydomain.com');
expect(client.config.instance.ipAddr).to.equal('54.54.54.54');
expect(client.config.instance.statusPageUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8080/info');
expect(client.config.instance.healthCheckUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8077/healthcheck');
expect(client.config.instance.homePageUrl).to.equal('http://ec2-127-0-0-1.us-fake-1.mydomain.com:8080/');
});
it('should update hosts with AWS metadata public IP when preferIpAddress === true', () => {
// Setup
config = {
instance: instanceConfig,
eureka: { host: '127.0.0.1', port: 9999, preferIpAddress: true },
};
client = new Eureka(config);
metadataSpy = sinon.spy();
sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata);
// Act
client.addInstanceMetadata(metadataSpy);
expect(client.config.instance.hostName).to.equal('54.54.54.54');
expect(client.config.instance.ipAddr).to.equal('54.54.54.54');
expect(client.config.instance.statusPageUrl).to.equal('http://54.54.54.54:8080/info');
expect(client.config.instance.healthCheckUrl).to.equal('http://54.54.54.54:8077/healthcheck');
expect(client.config.instance.homePageUrl).to.equal('http://54.54.54.54:8080/');
});
it('should update hosts with AWS metadata local host if useLocalMetadata === true', () => {
// Setup
config = {
instance: instanceConfig,
eureka: { host: '127.0.0.1', port: 9999, useLocalMetadata: true },
};
client = new Eureka(config);
metadataSpy = sinon.spy();
sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata);
// Act
client.addInstanceMetadata(metadataSpy);
expect(client.config.instance.hostName).to.equal('fake-1');
expect(client.config.instance.ipAddr).to.equal('10.0.1.1');
expect(client.config.instance.statusPageUrl).to.equal('http://fake-1:8080/info');
expect(client.config.instance.healthCheckUrl).to.equal('http://fake-1:8077/healthcheck');
expect(client.config.instance.homePageUrl).to.equal('http://fake-1:8080/');
});
it('should update hosts with AWS metadata local IP if useLocalMetadata === true' +
' and preferIpAddress === true', () => {
// Setup
config = {
instance: instanceConfig,
eureka: { host: '127.0.0.1', port: 9999, useLocalMetadata: true, preferIpAddress: true },
};
client = new Eureka(config);
metadataSpy = sinon.spy();
sinon.stub(client.metadataClient, 'fetchMetadata').yields(awsMetadata);
// Act
client.addInstanceMetadata(metadataSpy);
expect(client.config.instance.hostName).to.equal('10.0.1.1');
expect(client.config.instance.ipAddr).to.equal('10.0.1.1');
expect(client.config.instance.statusPageUrl).to.equal('http://10.0.1.1:8080/info');
expect(client.config.instance.healthCheckUrl).to.equal('http://10.0.1.1:8077/healthcheck');
expect(client.config.instance.homePageUrl).to.equal('http://10.0.1.1:8080/');
});
});
describe('eurekaRequest()', () => {
beforeEach(() => {});
afterEach(() => {
if (request.get.restore) request.get.restore();
});
it('should call requestMiddleware with request options', () => {
const overrides = {
requestMiddleware: sinon.spy((opts, done) => done(opts)),
};
const config = makeConfig(overrides);
const client = new Eureka(config);
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null);
client.eurekaRequest({}, (error) => {
expect(Boolean(error)).to.equal(false);
expect(overrides.requestMiddleware).to.be.calledOnce;
expect(overrides.requestMiddleware.args[0][0]).to.be.an('object');
});
});
it('should catch an error in requestMiddleware', () => {
const overrides = {
requestMiddleware: sinon.spy((opts, done) => {
done();
}),
};
const config = makeConfig(overrides);
const client = new Eureka(config);
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null);
client.eurekaRequest({}, (error) => {
expect(overrides.requestMiddleware).to.be.calledOnce;
expect(error).to.be.an('error');
});
});
it('should check the returnType of requestMiddleware', () => {
const overrides = {
requestMiddleware: sinon.spy((opts, done) => done('foo')),
};
const config = makeConfig(overrides);
const client = new Eureka(config);
sinon.stub(request, 'get').yields(null, { statusCode: 200 }, null);
client.eurekaRequest({}, (error) => {
expect(error).to.be.an('error');
expect(error.message).to.equal('requestMiddleware did not return an object');
});
});
it('should retry next server on request failure', (done) => {
const overrides = {
eureka: {
serviceUrls: {
default: ['http://serverA', 'http://serverB'],
},
maxRetries: 3,
requestRetryDelay: 0,
},
};
const config = makeConfig(overrides);
const client = new Eureka(config);
const requestStub = sinon.stub(request, 'get');
requestStub.onCall(0).yields(null, { statusCode: 500 }, null);
requestStub.onCall(1).yields(null, { statusCode: 200 }, null);
client.eurekaRequest({ uri: '/path' }, (error) => {
expect(error).to.be.null;
expect(requestStub).to.be.calledTwice;
expect(requestStub.args[0][0]).to.have.property('baseUrl', 'http://serverA');
expect(requestStub.args[1][0]).to.have.property('baseUrl', 'http://serverB');
done();
});
});
});
describe('handleDelta()', () => {
let client;
beforeEach(() => {
const config = makeConfig({ shouldUseDelta: true });
client = new Eureka(config);
});
it('should add instances', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' },
],
},
];
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(1);
expect(client.cache.app.THEAPP).to.have.length(1);
});
it('should handle duplicate instances on add', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' },
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' },
],
},
];
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(1);
expect(client.cache.app.THEAPP).to.have.length(1);
});
it('should modify instances', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED', newProp: 'foo' },
],
},
];
const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED' };
client.cache = {
app: { THEAPP: [original] },
vip: { thevip: [original] },
};
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(1);
expect(client.cache.app.THEAPP).to.have.length(1);
expect(client.cache.vip.thevip[0]).to.have.property('newProp');
expect(client.cache.app.THEAPP[0]).to.have.property('newProp');
});
it('should modify instances even when status is not UP', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'DOWN', actionType: 'MODIFIED', newProp: 'foo' },
],
},
];
const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED' };
client.cache = {
app: { THEAPP: [original] },
vip: { thevip: [original] },
};
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(1);
expect(client.cache.app.THEAPP).to.have.length(1);
expect(client.cache.vip.thevip[0]).to.have.property('newProp');
expect(client.cache.app.THEAPP[0]).to.have.property('newProp');
});
it('should add if instance doesnt exist when modifying', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'MODIFIED', newProp: 'foo' },
],
},
];
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(1);
expect(client.cache.app.THEAPP).to.have.length(1);
expect(client.cache.vip.thevip[0]).to.have.property('newProp');
expect(client.cache.app.THEAPP[0]).to.have.property('newProp');
});
it('should delete instances', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'DELETED', newProp: 'foo' },
],
},
];
const original = { hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'ADDED' };
client.cache = {
app: { THEAPP: [original] },
vip: { thevip: [original] },
};
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(0);
expect(client.cache.app.THEAPP).to.have.length(0);
});
it('should not delete instances if they do not exist', () => {
const appDelta = [
{
instance: [
{ hostName: '127.0.0.1', port: { $: 1000 }, app: 'THEAPP', vipAddress: 'thevip', status: 'UP', actionType: 'DELETED', newProp: 'foo' },
],
},
];
client.cache = {
app: { THEAPP: [] },
vip: { thevip: [] },
};
client.handleDelta(client.cache, appDelta);
expect(client.cache.vip.thevip).to.have.length(0);
expect(client.cache.app.THEAPP).to.have.length(0);
});
});
});
================================================
FILE: test/Logger.test.js
================================================
import sinon from 'sinon';
import { expect } from 'chai';
import Logger from '../src/Logger.js';
const DEFAULT_LEVEL = 30;
describe('Logger', () => {
it('should construct with no args', () => {
expect(() => new Logger()).to.not.throw();
});
describe('Logger Instance', () => {
let logger;
beforeEach(() => {
logger = new Logger();
});
it('should return the current log level from the "level" method', () => {
expect(logger.level()).to.equal(DEFAULT_LEVEL);
});
it('should update the log level if passed a number', () => {
logger.level(100);
expect(logger.level()).to.equal(100);
logger.level(15);
expect(logger.level()).to.equal(15);
});
it('should update the log level if a valid string is passed', () => {
logger.level('warn');
expect(logger.level()).to.equal(40);
logger.level('error');
expect(logger.level()).to.equal(50);
});
it('should use the default log level is an invalid string is passed', () => {
logger.level('invalid');
expect(logger.level()).to.equal(DEFAULT_LEVEL);
});
it('should only log a message if the log level is higher than the level', () => {
logger.level(100);
const stub = sinon.stub(console, 'error');
logger.error('Some Error');
expect(stub.callCount).to.equal(0);
logger.level(50);
logger.error('Other Error');
expect(stub.callCount).to.equal(1);
stub.restore();
});
describe('Log Methods', () => {
beforeEach(() => {
// Log everything:
logger.level(-1);
});
const stubConsole = method => sinon.stub(console, method);
it('should call console.log with debug', () => {
const stub = stubConsole('log');
logger.debug('test');
expect(stub.callCount).to.equal(1);
stub.restore();
});
it('should call console.info with info', () => {
const stub = stubConsole('info');
logger.info('test');
expect(stub.callCount).to.equal(1);
stub.restore();
});
it('should call console.warn with warn', () => {
const stub = stubConsole('warn');
logger.warn('test');
expect(stub.callCount).to.equal(1);
stub.restore();
});
it('should call console.error with error', () => {
const stub = stubConsole('error');
logger.error('test');
expect(stub.callCount).to.equal(1);
stub.restore();
});
});
});
});
================================================
FILE: test/deltaUtil.test.js
================================================
import { expect } from 'chai';
import { arrayOrObj, findInstance, normalizeDelta } from '../src/deltaUtils';
describe('deltaUtils', () => {
describe('arrayOrObj', () => {
it('should return same array if passed an array', () => {
const arr = ['foo'];
expect(arrayOrObj(arr)).to.equal(arr);
});
it('should return an array containing obj', () => {
const obj = {};
expect(arrayOrObj(obj)[0]).to.equal(obj);
});
});
describe('findInstance', () => {
it('should return true if objects match', () => {
const obj1 = { hostName: 'foo', port: { $: '6969' } };
const obj2 = { hostName: 'foo', port: { $: '6969' } };
expect(findInstance(obj1)(obj2)).to.equal(true);
});
it('should return false if objects do not match', () => {
const obj1 = { hostName: 'foo', port: { $: '6969' } };
const obj2 = { hostName: 'bar', port: { $: '1111' } };
expect(findInstance(obj1)(obj2)).to.equal(false);
});
});
describe('normalizeDelta', () => {
it('should normalize nested objs to arrays', () => {
const delta = {
instance: {
hostName: 'foo', port: { $: '6969' },
},
};
const normalized = normalizeDelta(delta);
expect(normalized).to.be.an('array');
expect(normalized[0].instance).to.be.an('array');
});
});
});
================================================
FILE: test/eureka-client-test.yml
================================================
eureka:
overrides: 2
otherCustom: 'test2'
================================================
FILE: test/eureka-client.yml
================================================
eureka:
overrides: 1
custom: 'test'
heartbeatInterval: 999
registryFetchInterval: 999
fetchRegistry: false
servicePath: '/eureka/v2/apps/'
ssl: false
useDns: false
fetchMetadata: false
instance: {}
================================================
FILE: test/fixtures/config.yml
================================================
eureka:
fromFixture: true
================================================
FILE: test/fixtures/malformed-config.yml
================================================
eureka:
fromFixture: true
@something: false
================================================
FILE: test/index.test.js
================================================
import { expect } from 'chai';
import EurekaClient from '../src/EurekaClient';
import EurekaDefault, { Eureka as EurekaNamed } from '../src/index';
// Compatibility with older node versions:
const EurekaCommonjs = require('../src/index').Eureka;
describe('index', () => {
it('should export both a default and a named', () => {
expect(EurekaDefault).to.equal(EurekaClient);
expect(EurekaDefault).to.equal(EurekaNamed);
});
it('should export correctly for ', () => {
expect(EurekaCommonjs).to.equal(EurekaDefault);
});
});
================================================
FILE: test/integration.test.js
================================================
import Eureka from '../src/index';
import { expect } from 'chai';
describe('Integration Test', () => {
const config = {
instance: {
app: 'jqservice',
hostName: 'localhost',
ipAddr: '127.0.0.1',
port: 8080,
vipAddress: 'jq.test.something.com',
dataCenterInfo: {
name: 'MyOwn',
},
},
eureka: {
heartbeatInterval: 30000,
registryFetchInterval: 5000,
fetchRegistry: true,
waitForRegistry: true,
servicePath: '/eureka/v2/apps/',
ssl: false,
useDns: false,
fetchMetadata: true,
host: 'localhost',
port: 8080,
},
};
const client = new Eureka(config);
before((done) => {
client.start(done);
});
it('should be able to get instance by the app id', () => {
const instances = client.getInstancesByAppId(config.instance.app);
expect(instances.length).to.equal(1);
});
it('should be able to get instance by the vipAddress', () => {
const instances = client.getInstancesByVipAddress(config.instance.vipAddress);
expect(instances.length).to.equal(1);
});
after((done) => {
client.stop(done);
});
});
gitextract_gdwjdngz/
├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── LICENSE
├── README.md
├── example.js
├── gulpfile.babel.js
├── package.json
├── src/
│ ├── AwsMetadata.js
│ ├── ConfigClusterResolver.js
│ ├── DnsClusterResolver.js
│ ├── EurekaClient.js
│ ├── Logger.js
│ ├── defaultConfig.js
│ ├── deltaUtils.js
│ └── index.js
└── test/
├── AwsMetadata.test.js
├── ConfigClusterResolver.test.js
├── DnsClusterResolver.test.js
├── EurekaClient.test.js
├── Logger.test.js
├── deltaUtil.test.js
├── eureka-client-test.yml
├── eureka-client.yml
├── fixtures/
│ ├── config.yml
│ └── malformed-config.yml
├── index.test.js
└── integration.test.js
SYMBOL INDEX (91 symbols across 11 files)
FILE: gulpfile.babel.js
constant EUREKA_INIT_TIMEOUT (line 45) | const EUREKA_INIT_TIMEOUT = 60000;
constant EUREKA_IMAGE (line 46) | const EUREKA_IMAGE = 'netflixoss/eureka:1.1.147';
constant DOCKER_PORT (line 47) | const DOCKER_PORT = '8080';
constant DOCKER_NAME (line 48) | const DOCKER_NAME = 'eureka-js-client';
constant DOCKER_RUN_ARGS (line 49) | const DOCKER_RUN_ARGS = [
constant DOCKER_START_ARGS (line 52) | const DOCKER_START_ARGS = [
function waitForEureka (line 57) | function waitForEureka(cb) {
FILE: src/AwsMetadata.js
class AwsMetadata (line 9) | class AwsMetadata {
method constructor (line 11) | constructor(config = {}) {
method fetchMetadata (line 16) | fetchMetadata(resultsCallback) {
method lookupMetadataKey (line 65) | lookupMetadataKey(key, callback) {
method lookupInstanceIdentity (line 76) | lookupInstanceIdentity(callback) {
FILE: src/ConfigClusterResolver.js
class ConfigClusterResolver (line 7) | class ConfigClusterResolver {
method constructor (line 8) | constructor(config, logger) {
method resolveEurekaUrl (line 14) | resolveEurekaUrl(callback, retryAttempt = 0) {
method buildServiceUrls (line 21) | buildServiceUrls() {
method getAvailabilityZones (line 46) | getAvailabilityZones() {
FILE: src/DnsClusterResolver.js
function noop (line 7) | function noop() {}
class DnsClusterResolver (line 16) | class DnsClusterResolver {
method constructor (line 17) | constructor(config, logger) {
method resolveEurekaUrl (line 33) | resolveEurekaUrl(callback, retryAttempt = 0) {
method getCurrentCluster (line 46) | getCurrentCluster(callback) {
method startClusterRefresh (line 56) | startClusterRefresh() {
method refreshCurrentCluster (line 65) | refreshCurrentCluster(callback = noop) {
method resolveClusterHosts (line 80) | resolveClusterHosts(callback = noop) {
method resolveZoneHosts (line 120) | resolveZoneHosts(zoneRecord, callback) {
FILE: src/EurekaClient.js
function noop (line 16) | function noop() {}
function fileExists (line 24) | function fileExists(file) {
function getYaml (line 32) | function getYaml(file) {
class Eureka (line 46) | class Eureka extends EventEmitter {
method constructor (line 48) | constructor(config = {}) {
method instanceId (line 97) | get instanceId() {
method amazonDataCenter (line 109) | get amazonDataCenter() {
method start (line 121) | start(callback = noop) {
method stop (line 169) | stop(callback = noop) {
method validateConfig (line 182) | validateConfig(config) {
method register (line 204) | register(callback = noop) {
method deregister (line 238) | deregister(callback = noop) {
method startHeartbeats (line 263) | startHeartbeats() {
method renew (line 269) | renew() {
method startRegistryFetches (line 297) | startRegistryFetches() {
method getInstancesByAppId (line 308) | getInstancesByAppId(appId) {
method getInstancesByVipAddress (line 322) | getInstancesByVipAddress(vipAddress) {
method fetchRegistry (line 336) | fetchRegistry(callback = noop) {
method fetchFullRegistry (line 347) | fetchFullRegistry(callback = noop) {
method fetchDelta (line 375) | fetchDelta(callback = noop) {
method transformRegistry (line 403) | transformRegistry(registry) {
method transformApp (line 426) | transformApp(app, cache) {
method validateInstance (line 439) | validateInstance(instance) {
method splitVipAddress (line 446) | splitVipAddress(vipAddress) { // eslint-disable-line
method handleDelta (line 454) | handleDelta(cache, appDelta) {
method addInstance (line 468) | addInstance(cache, instance) {
method modifyInstance (line 486) | modifyInstance(cache, instance) {
method deleteInstance (line 499) | deleteInstance(cache, instance) {
method addInstanceMetadata (line 527) | addInstanceMetadata(callback = noop) {
method eurekaRequest (line 564) | eurekaRequest(opts, callback, retryAttempt = 0) {
FILE: src/Logger.js
constant LEVELS (line 2) | const LEVELS = {
constant DEFAULT_LEVEL (line 8) | const DEFAULT_LEVEL = LEVELS.info;
class Logger (line 10) | class Logger {
method constructor (line 11) | constructor() {
method level (line 15) | level(inVal) {
method _log (line 27) | _log(method, args) {
method error (line 35) | error(...args) {
method warn (line 38) | warn(...args) {
method info (line 41) | info(...args) {
method debug (line 44) | debug(...args) {
FILE: src/deltaUtils.js
function arrayOrObj (line 4) | function arrayOrObj(mysteryValue) {
function findInstance (line 8) | function findInstance(a) {
function normalizeDelta (line 12) | function normalizeDelta(appDelta) {
FILE: test/ConfigClusterResolver.test.js
function makeConfig (line 7) | function makeConfig(overrides = {}) {
FILE: test/DnsClusterResolver.test.js
function makeConfig (line 12) | function makeConfig(overrides = {}) {
function fn (line 33) | function fn() {
function shouldNotThrow (line 275) | function shouldNotThrow() {
function shouldNotThrow (line 298) | function shouldNotThrow() {
FILE: test/EurekaClient.test.js
function makeConfig (line 15) | function makeConfig(overrides = {}) {
function fn (line 38) | function fn() {
function shouldThrow (line 45) | function shouldThrow() {
function noApp (line 49) | function noApp() {
function shouldWork (line 65) | function shouldWork() {
function shouldWorkNoInstance (line 82) | function shouldWorkNoInstance() {
function shouldThrow (line 119) | function shouldThrow() {
function malformed (line 560) | function malformed() {
function missingFile (line 569) | function missingFile() {
function badConfig (line 588) | function badConfig() {
function badConfig (line 596) | function badConfig() {
function badConfig (line 604) | function badConfig() {
function badConfig (line 612) | function badConfig() {
function badConfig (line 620) | function badConfig() {
function noAppId (line 637) | function noAppId() {
function noVipAddress (line 665) | function noVipAddress() {
FILE: test/Logger.test.js
constant DEFAULT_LEVEL (line 5) | const DEFAULT_LEVEL = 30;
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (132K chars).
[
{
"path": ".babelrc",
"chars": 34,
"preview": "{\n \"presets\": [\"es2015-loose\"]\n}\n"
},
{
"path": ".eslintrc",
"chars": 158,
"preview": "{\n \"extends\": \"airbnb-base\",\n \"rules\": {\n \"no-param-reassign\": [2, {\"props\": false}],\n \"consistent-return\": 0\n "
},
{
"path": ".gitignore",
"chars": 555,
"preview": "# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nl"
},
{
"path": ".npmignore",
"chars": 5,
"preview": "src/\n"
},
{
"path": ".travis.yml",
"chars": 218,
"preview": "language: node_js\n\nsudo: required\n\nservices:\n - docker\n\nnode_js:\n - \"4\"\n - \"6\"\n - \"8\"\n\nscript:\n - npm run test && n"
},
{
"path": "LICENSE",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015 Jacob Quatier\n\nPermission is hereby granted, free of charge, to any person obt"
},
{
"path": "README.md",
"chars": 14690,
"preview": "# eureka-js-client\n[](http://badge.fury.io/js/eureka-js-cli"
},
{
"path": "example.js",
"chars": 571,
"preview": "// assuming no transpiler here\nconst Eureka = require('eureka-js-client').Eureka;\n\n// example configuration\nconst client"
},
{
"path": "gulpfile.babel.js",
"chars": 2793,
"preview": "import gulp from 'gulp';\nimport babel from 'gulp-babel';\nimport mocha from 'gulp-mocha';\nimport eslint from 'gulp-eslint"
},
{
"path": "package.json",
"chars": 1561,
"preview": "{\n \"name\": \"eureka-js-client\",\n \"version\": \"4.5.0\",\n \"description\": \"A JavaScript implementation the Netflix OSS serv"
},
{
"path": "src/AwsMetadata.js",
"chars": 2832,
"preview": "import request from 'request';\nimport async from 'async';\nimport Logger from './Logger';\n\n/*\n Utility class for pulling"
},
{
"path": "src/ConfigClusterResolver.js",
"chars": 1690,
"preview": "import Logger from './Logger';\n\n/*\n Locates a Eureka host using static configuration. Configuration can either be\n don"
},
{
"path": "src/DnsClusterResolver.js",
"chars": 4450,
"preview": "import dns from 'dns';\nimport async from 'async';\nimport shuffle from 'lodash/shuffle';\nimport xor from 'lodash/xor';\nim"
},
{
"path": "src/EurekaClient.js",
"chars": 20295,
"preview": "import request from 'request';\nimport fs from 'fs';\nimport yaml from 'js-yaml';\nimport { merge, findIndex } from 'lodash"
},
{
"path": "src/Logger.js",
"chars": 915,
"preview": "/* eslint-disable no-underscore-dangle */\nconst LEVELS = {\n error: 50,\n warn: 40,\n info: 30,\n debug: 20,\n};\nconst DE"
},
{
"path": "src/defaultConfig.js",
"chars": 577,
"preview": "// Default configuration values:\nexport default {\n requestMiddleware: (request, done) => done(request),\n shouldUseDelt"
},
{
"path": "src/deltaUtils.js",
"chars": 456,
"preview": "/*\n General utilities for handling processing of delta changes from eureka.\n*/\nexport function arrayOrObj(mysteryValue)"
},
{
"path": "src/index.js",
"chars": 108,
"preview": "import EurekaClient from './EurekaClient';\nexport const Eureka = EurekaClient;\nexport default EurekaClient;\n"
},
{
"path": "test/AwsMetadata.test.js",
"chars": 8033,
"preview": "import sinon from 'sinon';\nimport chai, { expect } from 'chai';\nimport sinonChai from 'sinon-chai';\nimport request from "
},
{
"path": "test/ConfigClusterResolver.test.js",
"chars": 6322,
"preview": "/* eslint-disable no-unused-expressions */\nimport { expect } from 'chai';\nimport merge from 'lodash/merge';\n\nimport Conf"
},
{
"path": "test/DnsClusterResolver.test.js",
"chars": 12350,
"preview": "/* eslint-disable no-unused-expressions */\nimport sinon from 'sinon';\nimport chai, { expect } from 'chai';\nimport sinonC"
},
{
"path": "test/EurekaClient.test.js",
"chars": 40963,
"preview": "/* eslint-disable no-unused-expressions, max-len */\nimport sinon from 'sinon';\nimport chai, { expect } from 'chai';\nimpo"
},
{
"path": "test/Logger.test.js",
"chars": 2520,
"preview": "import sinon from 'sinon';\nimport { expect } from 'chai';\nimport Logger from '../src/Logger.js';\n\nconst DEFAULT_LEVEL = "
},
{
"path": "test/deltaUtil.test.js",
"chars": 1354,
"preview": "import { expect } from 'chai';\nimport { arrayOrObj, findInstance, normalizeDelta } from '../src/deltaUtils';\n\ndescribe('"
},
{
"path": "test/eureka-client-test.yml",
"chars": 46,
"preview": "eureka:\n overrides: 2\n otherCustom: 'test2'\n"
},
{
"path": "test/eureka-client.yml",
"chars": 216,
"preview": "eureka:\n overrides: 1\n custom: 'test'\n heartbeatInterval: 999\n registryFetchInterval: 999\n fetchRegistry: false\n s"
},
{
"path": "test/fixtures/config.yml",
"chars": 28,
"preview": "eureka:\n fromFixture: true\n"
},
{
"path": "test/fixtures/malformed-config.yml",
"chars": 48,
"preview": "eureka:\n fromFixture: true\n @something: false\n"
},
{
"path": "test/index.test.js",
"chars": 545,
"preview": "import { expect } from 'chai';\n\nimport EurekaClient from '../src/EurekaClient';\nimport EurekaDefault, { Eureka as Eureka"
},
{
"path": "test/integration.test.js",
"chars": 1161,
"preview": "import Eureka from '../src/index';\nimport { expect } from 'chai';\n\ndescribe('Integration Test', () => {\n const config ="
}
]
About this extraction
This page contains the full source code of the jquatier/eureka-js-client GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (123.6 KB), approximately 32.3k tokens, and a symbol index with 91 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.