├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── .nvmrc ├── README.md ├── hydra-cli.js ├── license.txt ├── message.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["mocha"], 3 | "extends": ["eslint:recommended", "google"], 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "valid-jsdoc": [ 2, { 13 | "requireReturn": true, 14 | "requireReturnDescription": true, 15 | "requireParamDescription": true, 16 | "prefer": { 17 | "return": "return" 18 | } 19 | }], 20 | "comma-dangle": 0, 21 | "curly": 2, 22 | "semi": [2, "always"], 23 | "no-console": 0, 24 | "no-debugger": 2, 25 | "no-extra-semi": 2, 26 | "no-constant-condition": 2, 27 | "no-alert": 2, 28 | "one-var-declaration-per-line": [2, "always"], 29 | "operator-linebreak": [ 30 | 2, 31 | "after" 32 | ], 33 | "max-len": [ 34 | 2, 35 | 240 36 | ], 37 | "indent": [ 38 | 2, 39 | 2, 40 | { 41 | "SwitchCase": 1 42 | } 43 | ], 44 | "quotes": [ 45 | 2, 46 | "single", 47 | { 48 | "avoidEscape": true 49 | } 50 | ], 51 | "no-multi-str": 2, 52 | "no-mixed-spaces-and-tabs": 2, 53 | "no-trailing-spaces": 2, 54 | "space-unary-ops": [ 55 | 2, 56 | { 57 | "nonwords": false, 58 | "overrides": {} 59 | } 60 | ], 61 | "one-var": [ 62 | 2, 63 | { 64 | "uninitialized": "always", 65 | "initialized": "never" 66 | } 67 | ], 68 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 69 | "keyword-spacing": [ 70 | 2, 71 | {} 72 | ], 73 | "space-infix-ops": 2, 74 | "space-before-blocks": [ 75 | 2, 76 | "always" 77 | ], 78 | "eol-last": 2, 79 | "space-in-parens": [ 80 | 2, 81 | "never" 82 | ], 83 | "no-multiple-empty-lines": 2, 84 | "no-multi-spaces": 2, 85 | "key-spacing": [ 86 | 2, 87 | { 88 | "beforeColon": false, 89 | "afterColon": true 90 | } 91 | ] 92 | }, 93 | "env": { 94 | "browser": true, 95 | "node": true, 96 | "es6": true, 97 | "mocha": true 98 | }, 99 | "globals": { 100 | "-": 0, 101 | "define": true, 102 | "expect": true, 103 | "it": true, 104 | "require": true 105 | } 106 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .bak 3 | .log 4 | scratch.* 5 | config/properties.js 6 | node_modules 7 | public 8 | 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.2.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hydra-cli 2 | 3 | [![npm version](https://badge.fury.io/js/hydra-cli.svg)](https://badge.fury.io/js/hydra-cli) NPM downloads [![npm](https://img.shields.io/npm/l/hydra-cli.svg)]() 4 | 5 | Hydra command line interface for use with [Hydra](https://github.com/flywheelsports/hydra) enabled microservices. 6 | 7 | ## install 8 | 9 | ```shell 10 | $ [sudo] npm install -g hydra-cli 11 | ``` 12 | 13 | ## Command overview 14 | 15 | ``` 16 | hydra-cli version 1.8.0 17 | Usage: hydra-cli command [parameters] 18 | See docs at: https://github.com/flywheelsports/hydra-cli 19 | 20 | A command line interface for Hydra services 21 | 22 | Commands: 23 | help - this help list 24 | cfg list serviceName - display a list of config versions 25 | cfg pull label - download configuration file 26 | cfg push label filename - update configuration file 27 | cfg remove label - remove a configuration version 28 | config instanceName - configure connection to redis 29 | config list - display current configuration 30 | use instanceName - name of redis instance to use 31 | health [serviceName] - display service health 32 | healthlog serviceName - display service health log 33 | message create - create a message object 34 | message send message.json - send a message 35 | message queue message.json - queue a message 36 | nodes [serviceName] - display service instance nodes 37 | redis info - display redis info 38 | refresh node list - refresh list of nodes 39 | rest path [payload.json] - make an HTTP RESTful call to a service 40 | routes [serviceName] - display service API routes 41 | services [serviceName] - display list of services 42 | shell - display command to open redis shell 43 | ``` 44 | 45 | ## help 46 | Lists the help screen above. 47 | 48 | > syntax: hydra-cli help 49 | 50 | ```shell 51 | $ hydra-cli help 52 | ``` 53 | 54 | ## cfg 55 | 56 | Hydra-cli allows you to push and pull configurations for your microservices. 57 | 58 | In this example we're pushing the config.json file from the local directory and storing it in Redis under the myservice:0.0.1 key. This allows the `myservice` service to pull its 0.0.1 configuration during startup. 59 | 60 | ```shell 61 | $ hydra-cli cfg push myservice:0.0.1 config.json 62 | ``` 63 | 64 | We can download the configuration file for the myservice using: 65 | 66 | ```shell 67 | $ hydra-cli cfg pull myservice:0.0.1 > config.json 68 | ``` 69 | 70 | Because the `cfg pull` command outputs the contents to screen you'll need to use the standard out redirection to copy the output to a file. 71 | 72 | You can retrieve a list of config versions for a given service using: 73 | 74 | ```shell 75 | $ hydra-cli cfg list myservice 76 | ``` 77 | 78 | To remove an entry, use: 79 | 80 | ```shell 81 | $ hydra-cli cfg remove myservice:0.0.1 82 | ``` 83 | 84 | ## config 85 | 86 | Hydra-cli requires that you first point it to the instance of Redis which your microservices are using. 87 | You must name the instance you're configuring. 88 | 89 | > syntax: hydra-cli config instanceName 90 | 91 | ```shell 92 | $ hydra-cli config local 93 | redisUrl: 127.0.0.1 94 | redisPort: 6379 95 | redisDb: 15 96 | ``` 97 | 98 | ## config list 99 | Lists your config settings. 100 | 101 | > syntax: hydra-cli config list 102 | 103 | ```javascript 104 | $ hydra-cli config list 105 | { 106 | "version": 2, 107 | "local": { 108 | "redisUrl": "127.0.0.1", 109 | "redisPort": "6379", 110 | "redisDb": "15" 111 | } 112 | } 113 | ``` 114 | 115 | ## use 116 | Specify which redis instance to use. 117 | 118 | ```javascript 119 | $ hydra-cli use local 120 | ``` 121 | 122 | ## health 123 | The health command displays the health of services which are currently running. 124 | If you specify the name of a service than only that service is displayed. 125 | 126 | > syntax: hydra-cli health [serviceName] 127 | 128 | > serviceName is optional 129 | 130 | ```javascript 131 | $ hydra-cli health 132 | [ 133 | [ 134 | { 135 | "updatedOn": "2016-11-22T18:01:49.637Z", 136 | "serviceName": "hello-service", 137 | "instanceID": "2c87057963121e1d7983bc952951ff3f", 138 | "sampledOn": "2016-11-22T18:01:49.637Z", 139 | "processID": 54906, 140 | "architecture": "x64", 141 | "platform": "darwin", 142 | "nodeVersion": "v6.8.1", 143 | "memory": { 144 | "rss": 41324544, 145 | "heapTotal": 24154112, 146 | "heapUsed": 20650296 147 | }, 148 | "uptime": "2 hours, 58 minutes, 27.68 seconds", 149 | "uptimeSeconds": 10707.68, 150 | "usedDiskSpace": "53%" 151 | } 152 | ], 153 | [ 154 | { 155 | "updatedOn": "2016-11-22T18:01:50.323Z", 156 | "serviceName": "red-service", 157 | "instanceID": "ce62591552a8b304d7236c820d0a4859", 158 | "sampledOn": "2016-11-22T18:01:50.323Z", 159 | "processID": 13431, 160 | "architecture": "x64", 161 | "platform": "darwin", 162 | "nodeVersion": "v6.8.1", 163 | "memory": { 164 | "rss": 45961216, 165 | "heapTotal": 26251264, 166 | "heapUsed": 20627944 167 | }, 168 | "uptime": "1 hour, 9 minutes, 19.038999999999536 seconds", 169 | "uptimeSeconds": 4159.039, 170 | "usedDiskSpace": "53%" 171 | } 172 | ] 173 | ] 174 | ``` 175 | 176 | ## healthlog 177 | Displays internal log for a service. The serviceName is required. 178 | 179 | > syntax: hydra-cli healthlog serviceName 180 | 181 | > serviceName is required 182 | 183 | ```shell 184 | $ hydra-cli healthlog red-service 185 | fatal | 2016-11-22T16:51:58.609Z PID:12664: Port 6000 is already in use 186 | ``` 187 | 188 | ## message create 189 | The `message create` command will create a [UMF](https://github.com/cjus/umf) message which you can customize for use with the `message send` command. 190 | 191 | > syntax: hydra-cli message create 192 | 193 | ```shell 194 | $ hydra-cli message create 195 | { 196 | "to": "{serviceName here}:/", 197 | "from": "hydra-cli:/", 198 | "mid": "378572ab-3cd3-414b-b56f-3d2bbf089c19", 199 | "timestamp": "2016-11-22T18:14:49.441Z", 200 | "version": "UMF/1.4.3", 201 | "body": {} 202 | } 203 | ``` 204 | 205 | Just edit the to field with the name of the service you'd like to send a message to. 206 | 207 | ## message send 208 | 209 | > syntax: hydra-cli send message.json 210 | 211 | > message.json is the name of a file containing JSON which you wish to send. This field is required. 212 | 213 | The `message send` command sends a [UMF](https://github.com/cjus/umf) fomatted message to a service. Use the `message create` command to create a message and place it in a file, such as message.json. 214 | 215 | ```shell 216 | $ hydra-cli message send message.json 217 | ``` 218 | 219 | ## message queue 220 | 221 | ```shell 222 | $ hydra-cli message queue message.json 223 | ``` 224 | 225 | Like send message but the message is pushed on the a service's queue rather then sent directly to a service. 226 | 227 | ## nodes 228 | 229 | Displays a list of services instances (called nodes). If you specify a serviceName then only service instances with that name will be displayed. 230 | 231 | > syntax: hydra-cli nodes [serviceName] 232 | 233 | > serviceName is optional 234 | 235 | ```javascript 236 | $ hydra-cli nodes 237 | [ 238 | { 239 | "serviceName": "red-service", 240 | "serviceDescription": "red stuff", 241 | "version": "0.0.1", 242 | "instanceID": "6b8eacc1ead3e1f904647110ce8c092f", 243 | "updatedOn": "2016-11-22T18:19:04.377Z", 244 | "processID": 3801, 245 | "ip": "10.1.1.163", 246 | "port": 6000, 247 | "elapsed": 3 248 | }, 249 | { 250 | "serviceName": "hello-service", 251 | "serviceDescription": "Hello service demo", 252 | "version": "not specified", 253 | "instanceID": "2c87057963121e1d7983bc952951ff3f", 254 | "updatedOn": "2016-11-22T18:19:02.943Z", 255 | "processID": 54906, 256 | "ip": "192.168.1.186", 257 | "port": 3000, 258 | "elapsed": 4 259 | } 260 | ] 261 | ``` 262 | 263 | ## redis 264 | You can pull Redis runtime info using: 265 | 266 | ```shell 267 | $ hydra-cli redis info 268 | # Server 269 | redis_version:3.0.7 270 | redis_git_sha1:00000000 271 | redis_git_dirty:0 272 | redis_build_id:9608eaf6bab769c5 273 | redis_mode:standalone 274 | os:Linux 4.9.49-moby x86_64 275 | arch_bits:64 276 | multiplexing_api:epoll 277 | gcc_version:4.9.2 278 | ... 279 | ``` 280 | 281 | ## refresh 282 | Refresh clears dead services from the nodes list. Use this when `hydra-cli nodes` returns nodes which have expired. 283 | 284 | ```javascript 285 | $ hydra-cli refresh node list 286 | ``` 287 | 288 | ## rest 289 | The `rest` command allows you to make a RESTful API call to a service which exposes HTTP endpoints. 290 | 291 | > syntax: hydra-cli rest path [payload.json] 292 | 293 | > payload is a file containing JSON which you wish to send with POST and PUT calls. 294 | 295 | Note the use of the path `hello-service:[get]/` below. This format is required. 296 | The full format is: `{serviceID}@{serviceName}:{HTTP method get/post/put/delete etc...}{API path} 297 | 298 | This is an example of how you would call an API endpoint on a specific service instance: 299 | 300 | ``` 301 | $ hydra-cli rest a921a00de7caf9103a0d96346b3a61f8@hello-service:[get]/v1/hello/greeting 302 | ``` 303 | 304 | You may supply a file to upload when using `post` and `put` HTTP calls. 305 | 306 | > You can locate a service's instance using the `hydra-cli nodes` command. 307 | 308 | ```javascript 309 | $ hydra-cli rest hello-service:[get]/ 310 | { 311 | "headers": { 312 | "access-control-allow-origin": "*", 313 | "x-process-id": "44032", 314 | "x-dns-prefetch-control": "off", 315 | "x-frame-options": "SAMEORIGIN", 316 | "x-download-options": "noopen", 317 | "x-content-type-options": "nosniff", 318 | "x-xss-protection": "1; mode=block", 319 | "x-powered-by": "hello-service/0.11.4", 320 | "content-type": "text/html; charset=utf-8", 321 | "content-length": "59", 322 | "etag": "W/\"3b-Kwmf5A3/0YsqP+L3cGK3eg\"", 323 | "x-response-time": "17.750ms", 324 | "date": "Tue, 22 Nov 2016 19:09:51 GMT", 325 | "connection": "close" 326 | }, 327 | "body": "hello from hello-service - a921a00de7caf9103a0d96346b3a61f8", 328 | "statusCode": 200 329 | } 330 | ``` 331 | 332 | If you forget to specify an HTTP type then you'll see a response like this one: 333 | 334 | ```javascript 335 | $ hydra-cli rest hello-service:/ 336 | { 337 | "statusCode": 400, 338 | "statusMessage": "Bad Request", 339 | "statusDescription": "Request is invalid, missing parameters?", 340 | "result": { 341 | "reason": "HTTP method not specified in `to` field" 342 | } 343 | } 344 | ``` 345 | 346 | ## routes 347 | The routes command will display routes which services register via hydra-express or via the use of the hydra.registerRoute call. 348 | 349 | > syntax: hydra-cli routes 350 | 351 | ```javascript 352 | $ hydra-cli routes 353 | { 354 | "hello-service": [ 355 | "[GET]/_config/hello-service", 356 | "[get]/" 357 | ] 358 | } 359 | ``` 360 | 361 | ## services 362 | 363 | Display a list of registered services. 364 | 365 | > syntax: hydra-cli services 366 | 367 | ```javascript 368 | $ hydra-cli services 369 | [ 370 | { 371 | "serviceName": "hello-service", 372 | "type": "demo", 373 | "registeredOn": "2016-11-22T19:09:47.772Z" 374 | }, 375 | { 376 | "serviceName": "red-service", 377 | "type": "red", 378 | "registeredOn": "2016-11-22T19:31:31.061Z" 379 | }, 380 | { 381 | "serviceName": "blue-service", 382 | "type": "blue", 383 | "registeredOn": "2016-11-22T19:31:27.853Z" 384 | } 385 | ] 386 | ``` 387 | 388 | ## shell 389 | 390 | Display command used to open a redis shell using redis-cli. On *nix machines you can use the following to quickly open a redis shell: 391 | 392 | ```shell 393 | $ $(hydra-cli shell) 394 | 52.3.229.252:6379[15]> 395 | ``` 396 | -------------------------------------------------------------------------------- /hydra-cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const version = require('./package.json').version; 5 | 6 | const fs = require('fs'); 7 | const rl = require('readline'); 8 | const hydra = require('hydra'); 9 | const Utils = hydra.getUtilsHelper(); 10 | const config = hydra.getConfigHelper(); 11 | const UMFMessage = hydra.getUMFMessageHelper(); 12 | 13 | const CONFIG_FILE_VERSION = 2; 14 | 15 | /** 16 | * @name Program 17 | */ 18 | class Program { 19 | /** 20 | * @name constructor 21 | */ 22 | constructor() { 23 | this.configData = null; 24 | this.configName = ''; 25 | this.hydraConfig = null; 26 | } 27 | 28 | /** 29 | * @name displayHelp 30 | * @description Display program help info 31 | * @return {undefined} 32 | */ 33 | displayHelp() { 34 | console.log(`hydra-cli version ${version}`); 35 | console.log('Usage: hydra-cli command [parameters]'); 36 | console.log('See docs at: https://github.com/flywheelsports/hydra-cli'); 37 | console.log(''); 38 | console.log('A command line interface for Hydra services'); 39 | console.log(''); 40 | console.log('Commands:'); 41 | console.log(' help - this help list'); 42 | console.log(' cfg list serviceName - display a list of config versions'); 43 | console.log(' cfg pull label - download configuration file'); 44 | console.log(' cfg push label filename - update configuration file'); 45 | console.log(' cfg remove label - remove a configuration version'); 46 | console.log(' config instanceName - configure connection to redis'); 47 | console.log(' config list - display current configuration'); 48 | console.log(' use instanceName - name of redis instance to use'); 49 | console.log(' health [serviceName] - display service health'); 50 | console.log(' healthlog serviceName - display service health log'); 51 | console.log(' message create - create a message object'); 52 | console.log(' message send message.json - send a message'); 53 | console.log(' message queue message.json - queue a message'); 54 | console.log(' nodes [serviceName] - display service instance nodes'); 55 | console.log(' redis info - display redis info'); 56 | console.log(' refresh node list - refresh list of nodes'); 57 | console.log(' rest path [payload.json] - make an HTTP RESTful call to a service'); 58 | console.log(' routes [serviceName] - display service API routes'); 59 | console.log(' services [serviceName] - display list of services'); 60 | console.log(' shell - display command to open redis shell'); 61 | console.log(''); 62 | } 63 | 64 | /** 65 | * @name getUserHome 66 | * @description Retrieve user's home directory 67 | * @return {string} - user's home directory path 68 | */ 69 | getUserHome() { 70 | return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME']; 71 | } 72 | 73 | /** 74 | * @name main 75 | * @description entry point for command dispatch processing 76 | * @return {undefined} 77 | */ 78 | main() { 79 | if (process.argv.length < 3) { 80 | this.displayHelp(); 81 | process.exit(); 82 | return; 83 | } 84 | 85 | let command = process.argv[2]; 86 | let args = process.argv.slice(3); 87 | 88 | this.hydraConfig = `${this.getUserHome()}/.hydra-cli`; 89 | fs.readFile(this.hydraConfig, 'utf8', (err, data) => { 90 | if (!err) { 91 | try { 92 | this.configData = JSON.parse(data); 93 | if (!this.configData.version) { 94 | this.configData = { 95 | version: CONFIG_FILE_VERSION 96 | }; 97 | } 98 | if (command === 'use' || command === 'config') { 99 | this.processCommand(command, args); 100 | return; 101 | } 102 | let conf = { 103 | 'hydra': { 104 | 'serviceName': 'hydra-cli', 105 | 'serviceDescription': 'Not a service', 106 | 'serviceIP': '', 107 | 'servicePort': 0, 108 | 'serviceType': 'non', 109 | 'redis': { 110 | 'url': this.configData.redisUrl || '', 111 | 'port': this.configData.redisPort || 0, 112 | 'db': this.configData.redisDb || 0 113 | } 114 | } 115 | }; 116 | if (this.configData.redisPassword && this.configData.redisPassword !== '') { 117 | conf.hydra.redis.password = this.configData.redisPassword; 118 | } 119 | 120 | let tid = setTimeout(() => { 121 | console.log('Unable to connect to Redis. Use "hydra-cli config list" or "hydra-cli use instanceName" to switch to another instance.'); 122 | clearTimeout(tid); 123 | process.exit(); 124 | return; 125 | }, 5000); 126 | 127 | hydra.init(conf) 128 | .then(() => { 129 | this.processCommand(command, args); 130 | return 0; 131 | }) 132 | .catch((err) => { 133 | console.log('err', err.message); 134 | this.configData = null; 135 | }); 136 | } catch (err) { 137 | console.log('err', err.message); 138 | this.configData = null; 139 | this.processCommand(command, args); 140 | } 141 | } else { 142 | this.configData = null; 143 | this.processCommand(command, args); 144 | } 145 | }); 146 | } 147 | 148 | /** 149 | * @name processCommand 150 | * @description process hydra-cli command 151 | * @param {string} command - command string 152 | * @param {array} args - array of command params 153 | * @return {undefined} 154 | */ 155 | processCommand(command, args) { 156 | if (!this.configData && command !== 'use' && command !== 'config') { 157 | console.log('Warning, hydra-cli is not configured.'); 158 | console.log('Use the hydra-cli config command.'); 159 | return; 160 | } 161 | 162 | switch (command) { 163 | case 'cfg': 164 | this.handleCfgCommand(args); 165 | break; 166 | case 'config': 167 | this.handleConfigCommand(args); 168 | break; 169 | case 'use': 170 | this.handleUseCommand(args); 171 | break; 172 | case 'health': 173 | this.handleHealth(args); 174 | break; 175 | case 'healthlog': 176 | this.handleHealthLog(args); 177 | break; 178 | case 'help': 179 | this.displayHelp(); 180 | process.exit(); 181 | break; 182 | case 'message': 183 | switch (args[0]) { 184 | case 'create': 185 | this.handleMessageCreate(args); 186 | break; 187 | case 'send': 188 | this.handleMessageSend(args); 189 | break; 190 | case 'queue': 191 | this.handleMessageQueue(args); 192 | break; 193 | default: 194 | console.log(`Unknown message options: ${args[0]}`); 195 | this.exitApp(); 196 | break; 197 | } 198 | break; 199 | case 'nodes': 200 | this.handleNodesList(args); 201 | break; 202 | case 'refresh': 203 | this.handleRefresh(args); 204 | break; 205 | case 'redis': 206 | this.handleRedis(args); 207 | break; 208 | case 'rest': 209 | this.handleRest(args); 210 | break; 211 | case 'routes': 212 | this.handleRoutes(args); 213 | break; 214 | case 'services': 215 | this.handleServices(args); 216 | break; 217 | case 'shell': 218 | this.handleShell(); 219 | break; 220 | default: 221 | console.log(`Unknown command: ${command}`); 222 | this.exitApp(); 223 | break; 224 | } 225 | } 226 | 227 | /** 228 | * @name exitApp 229 | * @description properly exit this app 230 | * @return {undefined} 231 | */ 232 | exitApp() { 233 | setTimeout(() => { 234 | hydra.shutdown(); 235 | process.exit(); 236 | }, 250); 237 | } 238 | 239 | /** 240 | * @name displayJSON 241 | * @description pretty print json 242 | * @param {string} json - stringified json 243 | * @return {undefined} 244 | */ 245 | displayJSON(json) { 246 | if (typeof json === 'string') { 247 | let js = Utils.safeJSONParse(json); 248 | if (!js) { 249 | console.log(json); 250 | } else { 251 | console.log(JSON.stringify(js, null, 2)); 252 | } 253 | } else { 254 | console.log(JSON.stringify(json, null, 2)); 255 | } 256 | } 257 | 258 | /* ************************************************************************* */ 259 | /* ************************************************************************* */ 260 | /* ************************************************************************* */ 261 | /* ************************************************************************* */ 262 | 263 | /** 264 | * @name handleCfgCommand 265 | * @description handle service config files 266 | * @param {array} args - program arguments 267 | * @return {undefined} 268 | */ 269 | handleCfgCommand(args) { 270 | if (!args[0]) { 271 | console.log('requires "push" or "pull" options'); 272 | this.exitApp(); 273 | } 274 | 275 | if (args[0] === 'push') { 276 | if (args.length != 3) { 277 | console.log('cfg push requires a label and config filename'); 278 | this.exitApp(); 279 | } 280 | fs.readFile(args[2], 'utf8', (err, data) => { 281 | if (!err) { 282 | let strMessage = Utils.safeJSONParse(data); 283 | hydra.putConfig(args[1], strMessage) 284 | .then(() => { 285 | this.exitApp(); 286 | }) 287 | .catch((err) => { 288 | console.log(err.message); 289 | this.exitApp(); 290 | }); 291 | } else { 292 | console.log(`cfg push can't open file ${args[2]}`); 293 | this.exitApp(); 294 | } 295 | }); 296 | } 297 | 298 | if (args[0] === 'pull') { 299 | hydra.getConfig(args[1]) 300 | .then((result) => { 301 | this.displayJSON(result); 302 | this.exitApp(); 303 | }) 304 | .catch((err) => { 305 | console.log(err.message); 306 | this.exitApp(); 307 | }); 308 | } 309 | 310 | if (args[0] === 'list') { 311 | hydra.listConfig(args[1]) 312 | .then((result) => { 313 | result.forEach((item) => { 314 | console.log(item); 315 | }); 316 | this.exitApp(); 317 | }) 318 | .catch((err) => { 319 | console.log(err.message); 320 | this.exitApp(); 321 | }); 322 | } 323 | 324 | if (args[0] === 'remove') { 325 | if (!args[1]) { 326 | console.log('Label is required.'); 327 | this.exitApp(); 328 | return; 329 | } 330 | let segs = args[1].split(':'); 331 | if (segs.length !== 2) { 332 | console.log('Label format must be in serviceName:version format.'); 333 | this.exitApp(); 334 | return; 335 | } 336 | let redisClient = hydra.getClonedRedisClient(); 337 | redisClient.hdel(`hydra:service:${segs[0]}:configs`, segs[1], (err, result) => { 338 | if (result > 0) { 339 | console.log(`Removed entry ${args[1]}`); 340 | } else { 341 | console.log(`Unable to locate entry for ${args[1]}`); 342 | } 343 | redisClient.quit(); 344 | this.exitApp(); 345 | }); 346 | } 347 | } 348 | 349 | /** 350 | * @name handleConfigCommand 351 | * @description handle the creation of the app config DOT file. 352 | * @param {array} args - program arguments 353 | * @return {undefined} 354 | */ 355 | handleConfigCommand(args) { 356 | if (args.length === 1 && args[0] === 'list') { 357 | console.log(JSON.stringify(this.configData, null, 2)); 358 | process.exit(); 359 | return; 360 | } else if (args.length !== 1) { 361 | console.log('An instance name is required.'); 362 | process.exit(); 363 | return; 364 | } 365 | 366 | this.configName = args[0]; 367 | 368 | let prompts = rl.createInterface(process.stdin, process.stdout); 369 | prompts.question('redisUrl: ', (redisUrl) => { 370 | prompts.question('redisPort: ', (redisPort) => { 371 | prompts.question('redisDb: ', (redisDb) => { 372 | prompts.question('redisPassword (blank for null): ', (redisPassword)=>{ 373 | let data = this.configData || { 374 | version: CONFIG_FILE_VERSION 375 | }; 376 | Object.assign(data, { 377 | redisUrl, 378 | redisPort, 379 | redisDb, 380 | [this.configName]: { 381 | redisUrl, 382 | redisPort, 383 | redisDb 384 | } 385 | }); 386 | if (redisPassword && redisPassword !== '') { 387 | data.redisPassword = redisPassword; 388 | data[this.configName].redisPassword = redisPassword; 389 | } 390 | fs.writeFile(this.hydraConfig, JSON.stringify(data), (err) => { 391 | if (err) { 392 | console.log(err.message); 393 | } 394 | process.exit(); 395 | }); 396 | }); 397 | }); 398 | }); 399 | }); 400 | } 401 | 402 | /** 403 | * @name handleUseCommand 404 | * @description handle the switching of configs 405 | * @param {array} args - program arguments 406 | * @return {undefined} 407 | */ 408 | handleUseCommand(args) { 409 | if (args.length !== 1) { 410 | console.log('An instance name is required.'); 411 | process.exit(); 412 | return; 413 | } 414 | this.configName = args[0]; 415 | if (!this.configData[this.configName]) { 416 | console.log('Instance name not found, create with hydra-cli config instanceName command.'); 417 | process.exit(); 418 | return; 419 | } 420 | 421 | this.configData = Object.assign(this.configData, this.configData[this.configName]); 422 | fs.writeFile(this.hydraConfig, JSON.stringify(this.configData), (err) => { 423 | if (err) { 424 | console.log(err.message); 425 | } 426 | process.exit(); 427 | }); 428 | } 429 | 430 | /** 431 | * @name handleHealth 432 | * @description display service health 433 | * @param {array} args - program arguments 434 | * @return {undefined} 435 | */ 436 | handleHealth(args) { 437 | let serviceName = args[0]; 438 | let entries = []; 439 | hydra.getServiceHealthAll() 440 | .then((services) => { 441 | services.forEach((service) => { 442 | if (serviceName && service.health.length > 0 && service.health[0].serviceName === serviceName) { 443 | entries.push(service.health); 444 | } 445 | if (!serviceName) { 446 | entries.push(service.health); 447 | } 448 | }); 449 | this.displayJSON(entries); 450 | this.exitApp(); 451 | }); 452 | } 453 | 454 | /** 455 | * @name handleHealthLog 456 | * @description display service health log 457 | * @param {array} args - program arguments 458 | * @return {undefined} 459 | */ 460 | handleHealthLog(args) { 461 | let serviceName = args[0]; 462 | 463 | if (!serviceName) { 464 | console.log('Missing serviceName'); 465 | this.exitApp(); 466 | return; 467 | } 468 | 469 | hydra.getServiceHealthLog(serviceName) 470 | .then((logs) => { 471 | logs.forEach((entry) => { 472 | console.error(`${entry.type} | ${entry.ts} PID:${entry.processID}: ${entry.message}`); 473 | }); 474 | this.exitApp(); 475 | }); 476 | } 477 | 478 | /** 479 | * @name handleMessageCreate 480 | * @description Display a new message 481 | * @param {array} _args - program arguments 482 | * @return {undefined} 483 | */ 484 | handleMessageCreate(_args) { 485 | let msg = UMFMessage.createMessage({ 486 | to: '{serviceName here}:/', 487 | from: 'hydra-cli:/', 488 | body: {} 489 | }); 490 | this.displayJSON(msg); 491 | this.exitApp(); 492 | } 493 | 494 | /** 495 | * @name handleMessageSend 496 | * @description Send message 497 | * @param {array} args - program arguments 498 | * @return {undefined} 499 | */ 500 | handleMessageSend(args) { 501 | if (args.length !== 2) { 502 | console.log('Invalid number of parameters'); 503 | this.exitApp(); 504 | return; 505 | } 506 | config.init(args[1]) 507 | .then(() => { 508 | hydra.sendMessage(config.getObject()); 509 | this.exitApp(); 510 | return null; 511 | }) 512 | .catch((err) => { 513 | console.log(err.message); 514 | this.exitApp(); 515 | }); 516 | } 517 | 518 | /** 519 | * @name handleMessageQueue 520 | * @description Queue message 521 | * @param {array} args - program arguments 522 | * @return {undefined} 523 | */ 524 | handleMessageQueue(args) { 525 | if (args.length !== 2) { 526 | console.log('Invalid number of parameters'); 527 | this.exitApp(); 528 | return; 529 | } 530 | config.init(args[1]) 531 | .then(() => { 532 | hydra.queueMessage(config.getObject()); 533 | this.exitApp(); 534 | return null; 535 | }) 536 | .catch((err) => { 537 | console.log(err.message); 538 | this.exitApp(); 539 | }); 540 | } 541 | 542 | /** 543 | * @name handleNodesList 544 | * @description handle the display of service nodes. 545 | * @param {array} args - program arguments 546 | * @return {undefined} 547 | */ 548 | handleNodesList(args) { 549 | hydra.getServiceNodes() 550 | .then((nodes) => { 551 | let serviceList = []; 552 | let serviceName = args[0]; 553 | if (serviceName) { 554 | nodes.forEach((service) => { 555 | if (serviceName === service.serviceName) { 556 | serviceList.push(service); 557 | } 558 | }); 559 | } else { 560 | serviceList = nodes; 561 | } 562 | this.displayJSON(serviceList); 563 | this.exitApp(); 564 | }) 565 | .catch((err) => console.log(err)); 566 | } 567 | 568 | /** 569 | * @name handleRedis 570 | * @description handle redis calls 571 | * @param {array} args - program arguments 572 | * @return {undefined} 573 | */ 574 | handleRedis(args) { 575 | if (!args[0]) { 576 | console.log('requires "info" option'); 577 | this.exitApp(); 578 | return; 579 | } 580 | 581 | if (args[0] === 'info') { 582 | let redisClient = hydra.getClonedRedisClient(); 583 | redisClient.info((err, result) => { 584 | if (!err) { 585 | console.log(`${result}`); 586 | } else { 587 | console.log(err); 588 | } 589 | redisClient.quit(); 590 | this.exitApp(); 591 | }); 592 | } 593 | 594 | this.exitApp(); 595 | } 596 | 597 | /** 598 | * @name handleRest 599 | * @description handle RESTful calls 600 | * @param {array} args - program arguments 601 | * @return {undefined} 602 | */ 603 | handleRest(args) { 604 | let route = UMFMessage.parseRoute(args[0]); 605 | if (route.error) { 606 | console.log(`${route.error}`); 607 | this.exitApp(); 608 | return; 609 | } 610 | let method = route.httpMethod || 'get'; 611 | if ((method === 'get' || method === 'delete') && args.length > 1) { 612 | console.log(`Payload not allowed for HTTP '${method}' method`); 613 | this.exitApp(); 614 | return; 615 | } 616 | if (args.length > 1) { 617 | config.init(args[1]) 618 | .then(() => { 619 | let msg = UMFMessage.createMessage({ 620 | to: `${args[0]}`, 621 | from: 'hydra-cli:/', 622 | headers: { 623 | 'content-type': 'application/json' 624 | }, 625 | body: config.getObject() || {} 626 | }); 627 | hydra.makeAPIRequest(msg) 628 | .then((res) => { 629 | if ((res.payLoad ?? res) == undefined) { 630 | console.trace(`Error parsing response from service.`); 631 | this.exitApp(); 632 | return; 633 | } 634 | const consoleOutput = {statusCode: res.statusCode, statusMessage: res.statusMessage, statusDescription: res.statusDescription, headers: res.headers} 635 | consoleOutput.result = res.payLoad != null ? res.payLoad.toString(): (() => { 636 | const internalKeys = Object.keys(consoleOutput) 637 | const resultObj = {}; 638 | Object.entries(res).forEach(([key, val]) => { 639 | if (!internalKeys.includes(key) && !(key == "result" && Object.entries(val).length == 0)) { 640 | resultObj[key] = val; 641 | } 642 | }) 643 | return resultObj; 644 | })() 645 | if (res.payLoad != undefined) { 646 | delete res.payLoad; 647 | } 648 | this.displayJSON(consoleOutput); 649 | this.exitApp(); 650 | }) 651 | .catch((err) => { 652 | console.log(err.message); 653 | this.exitApp(); 654 | }); 655 | return null; 656 | }) 657 | .catch((_err) => { 658 | console.log(`Unable to open ${args[1]}`); 659 | this.exitApp(); 660 | }); 661 | } else { 662 | let msg = UMFMessage.createMessage({ 663 | to: `${args[0]}`, 664 | from: 'hydra-cli:/', 665 | body: {} 666 | }); 667 | hydra.makeAPIRequest(msg) 668 | .then((res) => { 669 | if ((res.payLoad ?? res) == undefined) { 670 | console.trace(`Error parsing response from service.`); 671 | this.exitApp(); 672 | return; 673 | } 674 | const consoleOutput = {statusCode: res.statusCode, statusMessage: res.statusMessage, statusDescription: res.statusDescription, headers: res.headers} 675 | consoleOutput.result = res.payLoad != null ? res.payLoad.toString(): (() => { 676 | const internalKeys = Object.keys(consoleOutput) 677 | const resultObj = {}; 678 | Object.entries(res).forEach(([key, val]) => { 679 | if (!internalKeys.includes(key) && !(key == "result" && Object.entries(val).length == 0)) { 680 | resultObj[key] = val; 681 | } 682 | }) 683 | return resultObj; 684 | })() 685 | if (res.payLoad != undefined) { 686 | delete res.payLoad; 687 | } 688 | this.displayJSON(consoleOutput); 689 | this.exitApp(); 690 | }) 691 | .catch((err) => { 692 | console.log(err.message); 693 | this.exitApp(); 694 | }); 695 | return null; 696 | } 697 | } 698 | 699 | /** 700 | * @name handleRoutes 701 | * @description handle the display of service routes 702 | * @param {array} args - program arguments 703 | * @return {undefined} 704 | */ 705 | handleRoutes(args) { 706 | hydra.getAllServiceRoutes() 707 | .then((routes) => { 708 | let serviceName = args[0]; 709 | if (serviceName) { 710 | routes = { 711 | serviceName: routes[serviceName] 712 | }; 713 | } 714 | this.displayJSON(routes); 715 | this.exitApp(); 716 | }) 717 | .catch((err) => console.log(err.message)); 718 | } 719 | 720 | /** 721 | * @name handleServices 722 | * @description display list of services 723 | * @param {array} args - program arguments 724 | * @return {undefined} 725 | */ 726 | handleServices(args) { 727 | hydra.getServices() 728 | .then((services) => { 729 | let serviceList = []; 730 | let serviceName = args[0]; 731 | if (serviceName) { 732 | services.forEach((service) => { 733 | if (serviceName === service.serviceName) { 734 | serviceList.push(service); 735 | } 736 | }); 737 | } else { 738 | serviceList = services; 739 | } 740 | this.displayJSON(serviceList); 741 | this.exitApp(); 742 | }) 743 | .catch((err) => console.log(err.message)); 744 | } 745 | 746 | /** 747 | * @name handleRefresh 748 | * @description refresh list of nodes 749 | * @param {array} _args - program arguments 750 | * @return {undefined} 751 | */ 752 | handleRefresh(_args) { 753 | hydra.getServiceNodes() 754 | .then((nodes) => { 755 | let ids = []; 756 | nodes.forEach((node) => { 757 | if (node.elapsed > 60) { 758 | ids.push(node.instanceID); 759 | } 760 | }); 761 | if (ids.length) { 762 | let redisClient = hydra.getClonedRedisClient(); 763 | redisClient.hdel('hydra:service:nodes', ids); 764 | redisClient.quit(); 765 | } 766 | console.log(`${ids.length} entries removed`); 767 | this.exitApp(); 768 | }) 769 | .catch((err) => { 770 | console.log(err); 771 | this.exitApp(); 772 | }); 773 | } 774 | 775 | /** 776 | * @name handleShell 777 | * @summary displays the command used to open a redis shell for the currently selected instance 778 | * @return {undefined} 779 | */ 780 | handleShell() { 781 | console.log(`redis-cli -h ${this.configData.redisUrl} -p ${this.configData.redisPort} -n ${this.configData.redisDb}`); 782 | this.exitApp(); 783 | } 784 | } 785 | 786 | new Program().main(); 787 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Flywheel Sports Inc., and Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /message.json: -------------------------------------------------------------------------------- 1 | { 2 | "to": "hello-service:/", 3 | "from": "hydra-cli:/", 4 | "mid": "d22dc384-6bf7-4f73-9cb6-867faab15311", 5 | "timestamp": "2016-11-21T21:16:37.626Z", 6 | "version": "UMF/1.4.3", 7 | "body": { 8 | "message": "Hello from the hydra-cli app" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra-cli", 3 | "version": "1.8.0", 4 | "description": "Hydra Commandline Interface", 5 | "author": "Carlos Justiniano ", 6 | "keywords": [ 7 | "hydra", 8 | "command line", 9 | "microservice" 10 | ], 11 | "analyze": false, 12 | "preferGlobal": true, 13 | "license": "MIT", 14 | "engines": { 15 | "node": ">=6.2.1" 16 | }, 17 | "repository": "pnxtech/hydra-cli", 18 | "dependencies": { 19 | "bluebird": "3.5.1", 20 | "fwsp-logger": "0.4.0", 21 | "hydra": "1.8.0" 22 | }, 23 | "devDependencies": { 24 | "chai": "3.5.0", 25 | "eslint": "5.15.3", 26 | "eslint-config-google": "0.7.1", 27 | "eslint-plugin-mocha": "4.9.0", 28 | "mocha": "8.1.3", 29 | "redis-mock": "0.17.0", 30 | "superagent": "^6.1.0" 31 | }, 32 | "bin": { 33 | "hydra-cli": "hydra-cli.js" 34 | } 35 | } 36 | --------------------------------------------------------------------------------