├── .gitignore
├── LICENSE
├── README.md
├── bin
├── deployer
└── deployer.js
├── index.js
├── lib
├── Deployer.js
├── reporters
│ ├── SpinnerReporter.js
│ ├── StdReporter.js
│ └── VoidReporter.js
├── strategies
│ └── GitStrategy.js
└── utils
│ ├── ConnUtils.js
│ ├── DeployUtils.js
│ ├── HookUtils.js
│ └── config.js
├── package.json
└── test
├── fixtures
└── ssh_server.js
├── integrations
└── setup.js
└── units
└── config.js
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | /node_modules
3 | *.log
4 | *.log
5 | *.pid
6 | test/child
7 | *.iml
8 | .idea/**
9 | *.heapsnapshot
10 | *.cpuprofile
11 | .cache-require-paths.json
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 vmarchaud
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DeployerJS
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | #### Why Another Deployement Tool ?
11 |
12 | Pure javascript, no binary needed on the local machine, forget about under-the-hood `shellscript` or `rsync`/`ssh` binary spawn. I just want to run this tool everywhere (even under windows, yes, windows) i can run NodeJS.
13 |
14 | # How to use it ?
15 |
16 | ##### CLI Overview
17 | ```bash
18 | deployer update [file] # update remote to latest
19 | deployer rollback [file] [n] # revert to [n]th last
20 | deployer currrent [file] # output current release commit
21 | deployer previous [file] # output previous release commit
22 | deployer execute [file] # execute the given
23 | deployer list [file] # list previous deployements
24 | ```
25 |
26 | ### CLI options
27 |
28 | You can specify some options when using the CLI :
29 |
30 | - `-d` or `--details` when set to true, a file containing detailed logs of the deployement will be created in the current directory
31 | - `-s [strategy]` or `--strategy [strategy]` choose the strategy used to deploy, [see below for explanation](https://github.com/vmarchaud/deployerjs#deployement-strategy)
32 | - `-r [reporter]` or `--reporter [reporter]` choose the reporter used to show deployement info, [see below for explanation](https://github.com/vmarchaud/deployerjs#deployment-reporter)
33 |
34 |
35 | # Configuration
36 |
37 | DeployerJS is based on the configuration architecture of [PM2](https://github.com/Unitech/pm2), so we represent a remote environnement like this (where `production` is the environnement name) :
38 |
39 | ```javascript
40 | {
41 | "production": {
42 | "user": "totallynotroot", // ssh user
43 | "host": "127.0.0.1:22" // host adress + ssh port
44 | "ref": "origin/master", // remote branch
45 | "repo": "git@github.com:vmarchaud/deployerjs.git", // git repo
46 | "path": "/home/totallynotroot/myprod/", // path where the deployement will be done
47 | "passphrase" : true, // if my rsa key have a passphrase
48 | "post-deploy": "npm install", // will be executed after update on remote system
49 | "post-deploy-local": "touch deploy.done", // will be executed after update on local system
50 | "env" : { // all remote hooks will be executed with this env
51 | "NODE_ENV": "production"
52 | }
53 | }
54 | }
55 | ```
56 | To list all environnements you have two choices, put the list at the root of the JSON document (like above) or specify `deploy` object that will list them (example here with a `package.json` file) :
57 | ```javascript
58 | {
59 | "name": "myproject",
60 | "version": "0.0.1",
61 | "scripts": {
62 | ...
63 | },
64 | "dependencies": {
65 | ...
66 | },
67 | "deploy" : { // here you can define the environnement entries
68 | "staging" : { ... }, // define here your staging environnement
69 | "production" : { ... } // define here you production environnement
70 | }
71 | }
72 | ```
73 |
74 | Here are the **principal options** you can set in a environnement :
75 |
76 | | Option key | Value Type | Default | Example | Description |
77 | | ---------: | -----------:| -------:| --------:| -----------:|
78 | | user | String | | root | username used to authenticate to the ssh server |
79 | | host | String or Array | | `['localhost:22']` or `localhost` | can contains port of the remote host, array may contains multiple hosts |
80 | | port | String or Number | 22 | `22` or `"22"` | port of the remote ssh server |
81 | | password | Boolean | false | `true` or `false` | set a password to connect |
82 | | privateKey | String | ~/.ssh/id_rsa | a valid path on local system | use a rsa key to connect |
83 | | passphrase | Boolean | false | `true` or `false` | set a passphrase to use the rsa key |
84 | | path | String ||| remote path where the deployement will be done |
85 | | group | String | | `production` | set the group of the environnement (usefull to deploy on a group of servers) |
86 | | path | String ||| remote path where the deployement will be done |
87 | | env | Object || `{ "NODE_ENV": "production" }`| environnement used to execute remote hooks |
88 | | ssh_options | Object | | `{ "agent": "myagent_path" }` | options for ssh see [ssh2 connect doc](https://github.com/mscdex/ssh2#client-methods) |
89 |
90 | ## Hooks
91 |
92 | You can tell deployerjs to execute commands for you, we call them **hooks**, they can be run either on **remote** or **local** system, you just need an entry in the configuration (like the above example), here are the current hooks :
93 | - pre-setup & pre-setup-local
94 | - post-setup & post-setup-local
95 | - post-rollback & post-rollback-local
96 | - post-deploy & post-deploy-local
97 |
98 |
99 | ## Deployement Strategy
100 |
101 | Deployement action (like updating/rollbacking) are done via **Strategy**, the default is the`GitStrategy` that will simply use **git** on the remote server to execute all commands necessary to deploy the code, they use **their own key/value configuration**, so i split their configuration.
102 |
103 | Available strategies (and the corresponding CLI option to use) :
104 | - GitStrategy (`--strategy git`)
105 |
106 |
107 | ### GitStrategy
108 |
109 | #### Configuration
110 |
111 | | Option key | Value Type | Default | Example | Description |
112 | | ---------: | -----------:| -------:| --------:| -----------:|
113 | | ref | String | `origin/master`| any origin and branch separed by slash | Which remote and branch should be used |
114 | | branch | String | `master` | | branch used (override `ref`) |
115 | | origin | String | `origin` | | origin used (override `ref`) |
116 | | repo | String | | `github.com/vmarchaud/deployer` | Git repo URI |
117 |
118 | ## Deployment Reporter
119 |
120 | To display information relative to deployement, we use a `reporter` (just a class with some methods) that will handle display of information.
121 |
122 | By default the `StdReporter` is used, it will just print all data to STDOUT.
123 |
124 | Available reporters (and the corresponding CLI option to use) :
125 | - StdReporter (`--reporter std`)
126 | - SpinnerReporter (`--reporter spinner`)
127 |
128 | # Architecture
129 |
130 | Because you want to know how to fork this and start hacking it :
131 |
132 | **soon™**
133 |
134 | # Relevant documentation
135 |
136 | - [ssh2](https://github.com/mscdex/ssh2) - the module used to connect over a SSH connection to the servers
137 | - [ssh2-streams](https://github.com/mscdex/ssh2-streams/blob/master/SFTPStream.md) - the module used by `ssh2` to implement some protocols (sftp mainly used here).
138 |
139 | ## TODO:
140 |
141 | - More reporters
142 | - More strategies
143 | - Things to remember to fix :
144 | - [ ] git clone without host verified
145 |
--------------------------------------------------------------------------------
/bin/deployer:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | require('./deployer.js');
--------------------------------------------------------------------------------
/bin/deployer.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict'
4 |
5 | const Deployer = require('../lib/Deployer')
6 | const deployUtls = require('../lib/utils/DeployUtils')
7 | const pkg = require('../package.json')
8 | const commander = require('commander')
9 | const fs = require('fs')
10 | const path = require('path')
11 | const vm = require('vm')
12 |
13 | commander
14 | .version(pkg.version)
15 | .option('-s, --strategy [git]', 'set the strategy used to balance connection (default to git)', 'git')
16 | .option('-r, --reporter [std]', 'change reporter used to print informations (default to stdout)', 'std')
17 | .option('-d, --details', 'create a detailed report on the filesystem (default to false)', false)
18 | .on('--help', () => {
19 | console.log('')
20 | console.log('-----> DeployerJs CLI Help')
21 | console.log('')
22 | console.log(' Commands:')
23 | console.log(' update [file] update remote to the latest release')
24 | console.log(' rollback [file] [n] revert to [n]th last deployment or 1')
25 | console.log(' curr[ent] [file] output current release commit')
26 | console.log(' prev[ious] [file] output previous release commit')
27 | console.log(' exec [file] execute the given ')
28 | console.log(' list [file] list previous deploy commits')
29 | console.log('')
30 | })
31 | commander.command('deploy [file]')
32 | .description('deploy the code in your remote environement')
33 | .alias('update')
34 | .action((environement, file) => {
35 | // instanciate api
36 | var deployer = new Deployer(resolveConf(file), {
37 | strategy: commander.strategy,
38 | reporter: commander.reporter
39 | })
40 |
41 | // select and deploy
42 | deployer.select(environement, (err, servers) => {
43 | if (err) return commander.details ? generateDetails(servers, exit, err) : exit(err)
44 | deployer.deploy(servers, (err) => {
45 | return commander.details ? generateDetails(servers, exit, err) : exit(err)
46 | })
47 | })
48 | })
49 | commander.command('rollback [file] [n]')
50 | .description('rollback the code of your remote environement')
51 | .alias('revert')
52 | .action((environement, file, nbr) => {
53 | if (!nbr && !isNaN(file)) nbr = file
54 |
55 | // instanciate api
56 | var deployer = new Deployer(resolveConf(file), {
57 | strategy: commander.strategy,
58 | reporter: commander.reporter
59 | })
60 |
61 | // select and deploy
62 | deployer.select(environement, (err, servers) => {
63 | if (err) return commander.details ? generateDetails(servers, exit, err) : exit(err)
64 | deployer.rollback(servers, { rollback: nbr }, (err) => {
65 | return commander.details ? generateDetails(servers, exit, err) : exit(err)
66 | })
67 | })
68 | })
69 | commander.command('clean [file]')
70 | .description('clean the remote enviromment and get the latest version of the code')
71 | .alias('clear')
72 | .action((environement, file) => {
73 | deployUtls.askUser('[CLI] Are you sure you want to proceed a clean install of remote system ? y/n ', (res) => {
74 | if (!res.match(/yes|y/gi)) return exit(new Error('Aborting clean install'))
75 | // instanciate api
76 | var deployer = new Deployer(resolveConf(file), {
77 | strategy: commander.strategy,
78 | reporter: commander.reporter
79 | })
80 |
81 | // select and deploy
82 | deployer.select(environement, (err, servers) => {
83 | if (err) return commander.details ? generateDetails(servers, exit, err) : exit(err)
84 | deployer.clean(servers, (err) => {
85 | return commander.details ? generateDetails(servers, exit, err) : exit(err)
86 | })
87 | })
88 | })
89 | })
90 |
91 | commander.parse(process.argv)
92 |
93 | function trimUnicode (string) {
94 | return string.replace(/\\u[a-z0-9]{4}\[([^\\]{0,3})|\\u[a-z0-9]{4}/gmi, '').replace('\b', '\n')
95 | }
96 |
97 | function generateDetails (servers, next, err) {
98 | let reports = { '_CLI_GOT_': err ? err.message || err : 'SUCCESS' }
99 | // only get the details
100 | for (let server in servers) {
101 | reports[server] = servers[server].reporter.details(servers[server]) || []
102 | reports[server].forEach((report) => {
103 | if (report.stdout) { report.stdout = JSON.parse(trimUnicode(JSON.stringify(report.stdout)).split('\n')) }
104 | if (report.stderr) {
105 | report.stderr = JSON.parse(trimUnicode(JSON.stringify(report.stderr)).split('\n'))
106 | }
107 | })
108 | }
109 | // try to write it on filesystem
110 | let pwd = path.join(process.env.PWD || process.cwd(), `deployer_details.json`)
111 | fs.writeFile(pwd, JSON.stringify(reports, null, 2), next)
112 | }
113 |
114 | function resolveConf (confPath) {
115 | let file
116 | for (var tmpPath of [confPath, 'ecosystem.json', 'package.json']) {
117 | if (!tmpPath) continue
118 |
119 | tmpPath = path.resolve(tmpPath)
120 | try {
121 | let str = fs.readFileSync(tmpPath)
122 | // run the file if its js or json
123 | if (tmpPath.match(/\.js|\.json/)) {
124 | file = vm.runInThisContext('(' + str + ')')
125 | } else {
126 | file = JSON.parse(str)
127 | }
128 | break
129 | } catch (err) {
130 | console.log(err)
131 | continue
132 | }
133 | }
134 | return !file ? exit(new Error('Cant find any valid file configuration')) : (file.deploy ? file.deploy : file)
135 | }
136 |
137 | function exit (err) {
138 | if (err) { console.log('[CLI] ERROR : %s', err.message || err) } else { console.log('[CLI] SUCCESS') }
139 | process.exit(err ? 1 : 0)
140 | }
141 |
142 | commander.command('*')
143 | .action(commander.outputHelp)
144 |
145 | if (process.argv.length === 2) {
146 | commander.parse(process.argv)
147 | commander.outputHelp()
148 | process.exit(0)
149 | }
150 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.export = require('./lib/Deployer')
2 |
--------------------------------------------------------------------------------
/lib/Deployer.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | var async = require('async')
5 | var util = require('util')
6 |
7 | const DeployUtils = require('./utils/DeployUtils')
8 | const ConnUtils = require('./utils/ConnUtils')
9 | const HookUtils = require('./utils/HookUtils')
10 | const config = require('./utils/config')
11 |
12 | module.exports = class Deployer {
13 |
14 | constructor (conf, opts) {
15 | this.envs = typeof (conf) === 'string' ? JSON.parse(conf) : conf
16 | this.groups = {}
17 | opts = opts || {}
18 | if (typeof (this.envs) !== 'object') {
19 | throw new Error('Invalid configuration object')
20 | }
21 |
22 | for (let key in this.envs) {
23 | this.envs[key].name = key
24 | }
25 |
26 | // compute group of each env if defined
27 | for (let key in this.envs) {
28 | let env = this.envs[key]
29 | if (!env.group) continue
30 |
31 | if (!this.groups[env.group]) { this.groups[env.group] = [] }
32 |
33 | this.groups[env.group].push(env)
34 | }
35 |
36 | // resolve global reporter from opts
37 | if (typeof (opts.reporter) === 'string') {
38 | let resolved = config.reporters[opts.reporter.toUpperCase()]
39 | this.Reporter = resolved || config.reporters.STD
40 | } else if (typeof (opts.reporter) === 'function') {
41 | this.Reporter = opts.reporter
42 | } else {
43 | this.Reporter = config.reporters.STD
44 | }
45 |
46 | // resolve global strategy from opts
47 | if (typeof (opts.strategy) === 'string') {
48 | let resolved = config.strategies[opts.strategy.toUpperCase()]
49 | this.Strategy = resolved || config.strategies.GIT
50 | } else if (typeof (opts.strategy) === 'function') {
51 | this.Strategy = opts.strategy
52 | } else {
53 | this.Strategy = config.strategies.GIT
54 | }
55 | }
56 |
57 | /**
58 | * Function called to any remote action, for example ask for credentials like ssh password or private key passphrase
59 | * Must be called before any action since its resolve password / passphrase
60 | *
61 | * @param {String} target A declared environement OR a group of declared environements
62 | * @param {Function} cb(err, servers) Callback called after preparation completed
63 | */
64 | select (target, opts, cb) {
65 | if (!cb) cb = opts
66 | var servers = DeployUtils.resolveServers(this.envs, this.groups, target)
67 | // an error is returned (malformed config)
68 | if (servers instanceof Error) return cb(servers)
69 |
70 | async.eachLimit(Object.keys(servers), 1, (serverName, next) => {
71 | var server = servers[serverName]
72 |
73 | // if password or passphrase is a boolean (and true), the user must enter credentials for each server
74 | if ((typeof (server.password) === 'boolean' && server.password) || (typeof (server.passphrase) === 'boolean' && server.passphrase)) {
75 | var type = server.password ? 'password' : 'passphrase'
76 |
77 | DeployUtils.askUser(util.format('[DEPLOYER] You need to provide a %s for server %s : ', type, server.name), true, (value) => {
78 | server[type] = value
79 | return next(null)
80 | })
81 | } else {
82 | return next(null)
83 | }
84 | }, function (err) {
85 | return cb(err, servers)
86 | })
87 | }
88 |
89 | /**
90 | * Function called to make remote deployement on each remove servers
91 | *
92 | * @param {Object} servers Map with host for key and configuration in value
93 | */
94 | deploy (servers, opts, cb) {
95 | if (!cb) cb = opts
96 | // instanciate reporter that has been configured
97 | this.reporter = new this.Reporter(servers)
98 | async.each(servers, (server, next) => {
99 | // we will push all data in this field to make a detailed report of all data we got
100 | server.details = []
101 | server.reporter = this.reporter
102 |
103 | // create ssh connection to the hostname
104 | ConnUtils.create(server.name, server, (err, conn) => {
105 | if (err) return this.reporter.error(server, err, next)
106 |
107 | this.reporter.info(server, 'Verification of remote system')
108 | server.conn = conn
109 | server.strategy = new this.Strategy(server)
110 |
111 | // ensure that the remote system is already setup
112 | DeployUtils.ensureSetup(server, (err) => {
113 | if (err) return this.reporter.error(server, err, next)
114 |
115 | this.reporter.info(server, 'Remote system is ready !')
116 | server.strategy.update((err) => {
117 | ConnUtils.logRemote('update', server, err)
118 | if (err) return this.reporter.error(server, err, next)
119 |
120 | HookUtils.call('post-deploy', server, (err) => {
121 | return err ? this.reporter.error(server, err)
122 | : this.reporter.success(server, 'Remote system updated', next)
123 | })
124 | })
125 | })
126 | })
127 | }, function (err, results) {
128 | // clean all connections
129 | for (let server in servers) {
130 | if (servers[server].conn) {
131 | servers[server].conn.end()
132 | }
133 | }
134 | return cb(err, results)
135 | })
136 | }
137 |
138 | /**
139 | * Function called to make rollback on remote environements
140 | *
141 | * @param {Object} servers Map with host for key and configuration in value
142 | */
143 | rollback (servers, opts, cb) {
144 | if (!cb) cb = opts
145 |
146 | // instanciate reporter that has been configured
147 | this.reporter = new this.Reporter(servers)
148 |
149 | // resolve rollback number
150 | opts.rollback = opts.rollback ? typeof (opts.rollback) !== 'number'
151 | ? parseInt(opts.rollback) || config.rollback : opts.rollback : config.rollback
152 |
153 | async.each(servers, (server, next) => {
154 | // we will push all data in this field to make a detailed report of all data we got
155 | server.details = []
156 | server.reporter = this.reporter
157 |
158 | // create ssh connection to the hostname
159 | ConnUtils.create(server.name, server, (err, conn) => {
160 | if (err) return this.reporter.error(server, err, next)
161 |
162 | this.reporter.info(server, 'Verification of remote system')
163 | server.conn = conn
164 | server.strategy = new this.Strategy(server)
165 |
166 | // ensure that the remote system is already setup
167 | DeployUtils.ensureSetup(server, (err) => {
168 | ConnUtils.logRemote('rollback', server, err)
169 | if (err) return this.reporter.error(server, err, next)
170 |
171 | this.reporter.info(server, 'Remote system is ready !')
172 | server.strategy.rollback(opts.rollback, (err) => {
173 | if (err) return this.reporter.error(server, err, next)
174 |
175 | HookUtils.call('post-rollback', server, (err) => {
176 | return err ? this.reporter.error(server, err)
177 | : this.reporter.success(server, 'Remote system has been rollback of ' + opts.rollback + ' releases', next)
178 | })
179 | })
180 | })
181 | })
182 | }, function (err, results) {
183 | // clean all connections
184 | for (let server in servers) {
185 | if (servers[server].conn) { servers[server].conn.end() }
186 | }
187 | return cb(err, results)
188 | })
189 | }
190 |
191 | /**
192 | * Function called to make remote deployement on each remove servers
193 | *
194 | * @param {Object} servers Map with host for key and configuration in value
195 | */
196 | clean (servers, opts, cb) {
197 | if (!cb) cb = opts
198 | // instanciate reporter that has been configured
199 | this.reporter = new this.Reporter(servers)
200 | async.each(servers, (server, next) => {
201 | // we will push all data in this field to make a detailed report of all data we got
202 | server.details = []
203 | server.reporter = this.reporter
204 |
205 | // create ssh connection to the hostname
206 | ConnUtils.create(server.name, server, (err, conn) => {
207 | if (err) return this.reporter.error(server, err, next)
208 |
209 | this.reporter.info(server, 'Cleaning of the remote system')
210 | server.conn = conn
211 | server.strategy = new this.Strategy(server)
212 |
213 | // ensure that the remote system is already setup
214 | DeployUtils.ensureSetup(server, {
215 | force: true
216 | }, (err) => {
217 | if (err) return this.reporter.error(server, err, next)
218 |
219 | this.reporter.info(server, 'Remote system is ready !')
220 | server.strategy.update((err) => {
221 | ConnUtils.logRemote('update', server, err)
222 | if (err) return this.reporter.error(server, err, next)
223 |
224 | HookUtils.call('post-deploy', server, (err) => {
225 | return err ? this.reporter.error(server, err)
226 | : this.reporter.success(server, 'Remote system updated', next)
227 | })
228 | })
229 | })
230 | })
231 | }, function (err, results) {
232 | // clean all connections
233 | for (let server in servers) {
234 | if (servers[server].conn) { servers[server].conn.end() }
235 | }
236 | return cb(err, results)
237 | })
238 | }
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/lib/reporters/SpinnerReporter.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | var Spinner = require('multispinner')
5 |
6 | module.exports = class SpinnerReporter {
7 |
8 | constructor (entries) {
9 | this._entries = entries
10 | this.spinners = new Spinner(Object.keys(entries), {
11 | postSpace: ' : ',
12 | color: {
13 | 'incomplete': 'gray',
14 | 'success': 'green',
15 | 'error': 'red'
16 | }
17 | })
18 |
19 | this._details = {}
20 | for (let entry in entries) {
21 | this._details[entry] = []
22 | }
23 | }
24 |
25 | /**
26 | * Info log, will be printed and added to details
27 | *
28 | * @param {string} entry the entry name
29 | * @param {string} msg the message that will be displayed
30 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
31 | */
32 | info (entry, msg, cb) {
33 | if (!this._entries[entry]) { return }
34 | this.spinners.updateText(entry, { postText: msg })
35 | this._details[entry].push(msg)
36 |
37 | return cb ? cb(null, msg) : true
38 | }
39 |
40 | /**
41 | * Error log, will be printed and added to details
42 | *
43 | * @param {string} entry the entry name
44 | * @param {string} msg the error instance that has caused the problem
45 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
46 | */
47 | error (entry, err, cb) {
48 | if (!this._entries[entry]) {
49 | return
50 | }
51 | this.spinners.updateText(entry, { postText: err.message || err })
52 | this.spinners.error(entry)
53 | this.spinners.loop()
54 | this._details[entry].push(err.message || err)
55 |
56 | return cb ? cb(err) : true
57 | }
58 |
59 | /**
60 | * Success log, will be printed and added to details
61 | *
62 | * @param {string} entry the entry name
63 | * @param {string} msg the message that will be displayed
64 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
65 | */
66 | success (entry, msg, cb) {
67 | if (!this._entries[entry]) {
68 | return
69 | }
70 | this.spinners.updateText(entry, { postText: msg || 'Success' })
71 | this.spinners.success(entry)
72 | this.spinners.loop()
73 | this._details[entry].push(msg)
74 |
75 | return cb ? cb(null, msg) : true
76 | }
77 |
78 | /**
79 | * Fine log, will not be printed but will be added in details
80 | */
81 | fine (entry, msg, cb) {
82 | if (!this._entries[entry]) {
83 | return
84 | }
85 | this._details[entry].push(msg)
86 |
87 | return cb ? cb(null, msg) : true
88 | }
89 |
90 | /**
91 | * Will return detailed data for entry provided
92 | *
93 | * @param {string} entry the entry name
94 | */
95 | details (entry) {
96 | return this._details[entry]
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/reporters/StdReporter.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | module.exports = class StdReporter {
5 |
6 | constructor (entries) {
7 | this._entries = entries
8 | this._details = {}
9 | for (let key in entries) {
10 | this._details[entries[key].host] = []
11 | }
12 | }
13 |
14 | /**
15 | * Info log, will be printed and added to details
16 | *
17 | * @param {string} entry the entry
18 | * @param {string} msg the message that will be displayed
19 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
20 | */
21 | info (entry, msg, cb) {
22 | if (!this._entries[entry.host]) { return console.log(entry) }
23 | console.log('[INFO] (%s) %s - %s', entry.name, entry.host, msg)
24 | this._details[entry.host].push(msg)
25 |
26 | return cb ? cb(null, msg) : true
27 | }
28 |
29 | /**
30 | * Error log, will be printed and added to details
31 | *
32 | * @param {string} entry the entry
33 | * @param {string} msg the error instance that has caused the problem
34 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
35 | */
36 | error (entry, err, cb) {
37 | if (!this._entries[entry.host]) {
38 | return
39 | }
40 | console.log('[ERROR] (%s) %s - %s', entry.name, entry.host, err.message || err)
41 | this._details[entry.host].push(err.message || err)
42 |
43 | return cb ? cb(err) : true
44 | }
45 |
46 | /**
47 | * Success log, will be printed and added to details
48 | *
49 | * @param {string} entry the entry
50 | * @param {string} msg the message that will be displayed
51 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
52 | */
53 | success (entry, msg, cb) {
54 | if (!this._entries[entry.host]) {
55 | return
56 | }
57 | console.log('[SUCCESS] (%s) %s - %s', entry.name, entry.host, msg)
58 | this._details[entry.host].push(msg)
59 |
60 | return cb ? cb(null, msg) : true
61 | }
62 |
63 | /**
64 | * Fine log, will not be printed but will be added in details
65 | */
66 | fine (entry, msg, cb) {
67 | if (!this._entries[entry.host]) {
68 | return
69 | }
70 | this._details[entry.host].push(msg)
71 |
72 | return cb ? cb(null, msg) : true
73 | }
74 |
75 | /**
76 | * Will return detailed data for entry provided
77 | *
78 | * @param {string} entry the entry
79 | */
80 | details (entry) {
81 | return this._details[entry.host]
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/reporters/VoidReporter.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | module.exports = class VoidReporter {
5 |
6 | constructor (entries) {
7 | this._entries = entries
8 |
9 | this._details = {}
10 | for (let entry in entries) {
11 | this._details[entry] = []
12 | }
13 | }
14 |
15 | /**
16 | * Info log, will be printed and added to details
17 | *
18 | * @param {string} entry the entry name
19 | * @param {string} msg the message that will be displayed
20 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
21 | */
22 | info (entry, msg, cb) {
23 | if (!this._entries[entry]) { return }
24 | this._details[entry].push(msg)
25 |
26 | return cb ? cb(null, msg) : true
27 | }
28 |
29 | /**
30 | * Error log, will be printed and added to details
31 | *
32 | * @param {string} entry the entry name
33 | * @param {string} msg the error instance that has caused the problem
34 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
35 | */
36 | error (entry, err, cb) {
37 | if (!this._entries[entry]) {
38 | return
39 | }
40 | this._details[entry].push(err.message || err)
41 |
42 | return cb ? cb(err) : true
43 | }
44 |
45 | /**
46 | * Success log, will be printed and added to details
47 | *
48 | * @param {string} entry the entry name
49 | * @param {string} msg the message that will be displayed
50 | * @param {function} cb(err, msg) callback that will be called after (usefull for chaining)
51 | */
52 | success (entry, msg, cb) {
53 | if (!this._entries[entry]) {
54 | return
55 | }
56 | this._details[entry].push(msg)
57 |
58 | return cb ? cb(null, msg) : true
59 | }
60 |
61 | /**
62 | * Fine log, will not be printed but will be added in details
63 | */
64 | fine (entry, msg, cb) {
65 | if (!this._entries[entry]) {
66 | return
67 | }
68 | this._details[entry].push(msg)
69 |
70 | return cb ? cb(null, msg) : true
71 | }
72 |
73 | /**
74 | * Will return detailed data for entry provided
75 | *
76 | * @param {string} entry the entry name
77 | */
78 | details (entry) {
79 | return this._details[entry]
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/strategies/GitStrategy.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | const util = require('util')
5 |
6 | module.exports = class GitStrategy {
7 |
8 | constructor (server) {
9 | this.reporter = server.reporter
10 | this.server = server
11 | this.conn = server.conn
12 |
13 | let ref = this.server.ref.split('/')
14 | this.server.origin = this.server.origin || ref[0] || 'origin'
15 | this.server.branch = this.server.branch || ref[1] || 'master'
16 | }
17 |
18 | retrieve (cb) {
19 | // try git clone the repo into the remote source folder
20 | this.server.reporter.info(this.server, 'Retrieving repo in source folder (GIT)')
21 |
22 | let command = util.format('git clone %s -o %s -b %s %s',
23 | this.server.repo, this.server.origin, this.server.branch, this.server.path + '/current')
24 | let stderr = ''
25 | let stdout = ''
26 |
27 | this.conn.shell((err, stream) => {
28 | if (err) return cb(err)
29 |
30 | stream.on('close', (code, signal) => {
31 | this.server.reporter.fine(this.server, { code: code, stdout: stdout || 'empty', stderr: stderr || 'empty' })
32 | return code !== 0 ? cb(new Error(util.format('Git pull failed with code %d', code))) : cb(null)
33 | }).on('data', (data) => {
34 | console.log(data.toString())
35 | stdout += data instanceof Buffer ? data.toString() : data
36 | }).stderr.on('data', (data) => {
37 | stderr += data instanceof Buffer ? data.toString() : data
38 | })
39 | stream.end(command + '\nexit\n')
40 | })
41 | }
42 |
43 | update (cb) {
44 | // try git reset to head in the repo
45 | this.server.reporter.info(this.server, 'Updating repository from remote (GIT)')
46 |
47 | let command = util.format('cd %s ; git reset --hard %s', this.server.path + '/current', this.server.ref)
48 | let stderr = ''
49 | let stdout = ''
50 |
51 | this.conn.shell((err, stream) => {
52 | if (err) return cb(err)
53 |
54 | stream.on('close', (code, signal) => {
55 | this.server.reporter.fine(this.server, { code: code, stdout: stdout || 'empty', stderr: stderr || 'empty' })
56 | return code !== 0 ? cb(new Error(util.format('Git reset to HEAD failed with code %d', code))) : cb(null)
57 | }).on('data', (data) => {
58 | stdout += data instanceof Buffer ? data.toString() : data
59 | }).stderr.on('data', (data) => {
60 | stderr += data instanceof Buffer ? data.toString() : data
61 | })
62 | stream.end(command + '\nexit\n')
63 | })
64 | }
65 |
66 | rollback (nbr, cb) {
67 | // try git reset to head in the repo
68 | this.server.reporter.info(this.server, 'Updating repository from remote (GIT)')
69 |
70 | let command = util.format('cd %s ; git reset --hard HEAD~%d', this.server.path + '/current', nbr)
71 | let stderr = ''
72 | let stdout = ''
73 |
74 | this.conn.shell((err, stream) => {
75 | if (err) return cb(err)
76 |
77 | stream.on('close', (code, signal) => {
78 | this.server.reporter.fine(this.server, { code: code, stdout: stdout || 'empty', stderr: stderr || 'empty' })
79 | return code !== 0 ? cb(new Error(util.format('Git reset to HEAD~%d failed with code %d', nbr, code))) : cb(null)
80 | }).on('data', (data) => {
81 | stdout += data instanceof Buffer ? data.toString() : data
82 | }).stderr.on('data', (data) => {
83 | stderr += data instanceof Buffer ? data.toString() : data
84 | })
85 | stream.end(command + '\nexit\n')
86 | })
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/utils/ConnUtils.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | const fs = require('fs')
5 | const SSH = require('ssh2').Client
6 | const os = require('os')
7 | const util = require('util')
8 | const VOID = () => {}
9 |
10 | module.exports = class ConnUtils {
11 |
12 | static create (host, server, cb) {
13 | let connection = new SSH()
14 |
15 | connection.once('ready', function () {
16 | server.reporter.fine(server, 'SSH connection is ready')
17 | return cb(null, connection)
18 | })
19 | connection.once('error', cb)
20 |
21 | // normalize configuration
22 | server.username = server.user || server.username
23 |
24 | // remove end slash if present
25 | if (server.path[server.path.length - 1] === '/') {
26 | server.path = server.path.substr(0, server.path.length - 1)
27 | }
28 |
29 | // set port if contained in hostname
30 | if (server.host.indexOf(':') !== -1) {
31 | let tmp = server.host.split(':')
32 | server.host = tmp[0]
33 | server.port = tmp[1]
34 | }
35 |
36 | // resolve path of private key if given or if there isn't any password configured
37 | if (server.key || server.privateKey || !server.password) {
38 | server.reporter.fine(server, 'Resolving SSH key')
39 | try {
40 | let path = server.key || server.privateKey || '~/.ssh/id_rsa'
41 | path = path.indexOf('~') !== -1 ? path.replace(/~/g, os.homedir()) : path
42 | server.privateKey = fs.readFileSync(path)
43 | server.reporter.fine('SSH key has been succesfully fetched from fs')
44 | } catch (err) {
45 | return cb(err)
46 | }
47 | }
48 |
49 | // build env if provided in configuration
50 | if (server.env) {
51 | let builder = ''
52 | for (let key in server.env) {
53 | builder = util.format('%s%s=%s ', builder, key, server.env[key])
54 | }
55 | server.env = builder
56 | } else {
57 | server.env = ''
58 | }
59 |
60 | try {
61 | server.reporter.fine(server, 'Initialization of the ssh connection')
62 | if (typeof (server.ssh_options) !== 'object' || server.ssh_options instanceof Array) {
63 | server.ssh_options = {}
64 | }
65 | connection.connect(Object.assign(server, server.ssh_options))
66 | } catch (err) {
67 | // the error is encoding too long mean that the passphrase is incorrect
68 | return cb(new Error('Bad passphrase provided for ssh private key'))
69 | }
70 | }
71 |
72 | static logRemote (action, server, err) {
73 | server.reporter.info(server, 'Logging action on remote host')
74 |
75 | let command = util.format('cd %s ; echo "[%s] %s run and returned %s" >> .deploys',
76 | server.path, new Date().toISOString(), action, err ? err.message || err : 'success')
77 |
78 | server.conn.shell((err, stream) => {
79 | if (err) return
80 |
81 | stream.on('close', (code, signal) => {
82 | // we don't care if it fail
83 | return
84 | }).on('data', VOID).stderr.on('data', VOID)
85 | stream.end(command + '\nexit\n')
86 | })
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/lib/utils/DeployUtils.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | const async = require('async')
5 | const readline = require('readline')
6 | const HookUtils = require('./HookUtils')
7 |
8 | const FileState = {
9 | VALID: 0,
10 | INVALID: -1,
11 | NOT_FOUND: -2
12 | }
13 |
14 | module.exports = class DeployUtils {
15 |
16 | /**
17 | * Function used to verify that the remote path is correctly setup (good folder etc)
18 | *
19 | */
20 | static ensureSetup (server, options, cb) {
21 | if (!cb) {
22 | cb = options
23 | options = {}
24 | }
25 | server.conn.sftp((err, sftp) => {
26 | if (err) return cb(err)
27 |
28 | sftp.readdir(server.path, (err, list) => {
29 | if (err) return cb(err)
30 |
31 | // retrieve both entry based on the name
32 | let current, source
33 | for (let entry of list) {
34 | if (entry.filename === 'current') { current = entry } else if (entry.filename === 'source') {
35 | source = entry
36 | }
37 | }
38 |
39 | let opts = {
40 | force: options ? options.force : false,
41 | current: current ? (current.longname[0] === 'l' ? FileState.VALID : FileState.INVALID) : FileState.NOT_FOUND,
42 | source: source ? (source.longname[0] === 'd' ? FileState.VALID : FileState.INVALID) : FileState.NOT_FOUND
43 | }
44 |
45 | // if any of both folder is invalid or inexistant, setup the remote system
46 | if ((opts.current !== FileState.VALID || opts.source !== FileState.VALID) || options.force) {
47 | return DeployUtils.setup(server, opts, cb)
48 | } else {
49 | return cb(null)
50 | }
51 | })
52 | })
53 | }
54 |
55 | /**
56 | * Function used to setup remote system
57 | *
58 | */
59 | static setup (server, opts, cb) {
60 | async.series([
61 | function (next) {
62 | if (opts.force !== true) return next(null)
63 |
64 | server.conn.sftp((err, sftp) => {
65 | if (err) return next(err)
66 |
67 | server.reporter.info(server, 'Removing old folders')
68 | sftp.unlink(server.path + '/current_old', (err1) => {
69 | sftp.rmdir(server.path + '/source_old', (err2) => {
70 | return next()
71 | })
72 | })
73 | })
74 | },
75 | // if the remote source folder already exist, move him
76 | // or we are forced to make a backup and the file exist
77 | function (next) {
78 | if (!(opts.source === FileState.INVALID || (opts.force && opts.source === FileState.VALID))) return next(null)
79 |
80 | server.conn.sftp((err, sftp) => {
81 | if (err) return next(err)
82 |
83 | server.reporter.info(server, 'Making a backup of remote source folder')
84 | sftp.rename(server.path + '/source', server.path + '/source_old', next)
85 | })
86 | },
87 | // if the remote current link isn't a link, move him too
88 | // or we are forced to move it and the file exist
89 | function (next) {
90 | if (opts.current !== FileState.INVALID &&
91 | !(opts.force && opts.source === FileState.VALID)) return next(null)
92 |
93 | server.conn.sftp((err, sftp) => {
94 | if (err) return next(err)
95 |
96 | server.reporter.info(server, 'Making a backup of remote current symlink')
97 | sftp.rename(server.path + '/current', server.path + '/current_old', next)
98 | })
99 | },
100 | // at this point we either valid or inexistant folder, so create it if needed
101 | function (next) {
102 | if (opts.source === FileState.VALID && !opts.force) return next(null)
103 |
104 | server.conn.sftp((err, sftp) => {
105 | if (err) return next(err)
106 |
107 | server.reporter.info(server, 'Creating source folder')
108 | sftp.mkdir(server.path + '/source', next)
109 | })
110 | },
111 | // create symlink if needed
112 | function (next) {
113 | if (opts.current === FileState.VALID && !opts.force) return next(null)
114 |
115 | server.conn.sftp((err, sftp) => {
116 | if (err) return next(err)
117 |
118 | server.reporter.info(server, 'Create current symlink to current')
119 | sftp.symlink(server.path + '/source', server.path + '/current', next)
120 | })
121 | },
122 | // now we have a valid remote setup, so if the source folder wasn't already here, call hooks
123 | function (next) {
124 | if (opts.source === FileState.VALID && !opts.force) return next(null)
125 |
126 | // execute pre-hook
127 | HookUtils.call('pre-setup', server, (err) => {
128 | if (err) return next(err)
129 | server.strategy.retrieve((err) => {
130 | if (err) return next(err)
131 | HookUtils.call('post-setup', server, next)
132 | })
133 | })
134 | }
135 | ], cb)
136 | }
137 |
138 | static resolveServers (envs, groups, name) {
139 | var hosts = {}
140 | let env = envs[name]
141 |
142 | if (env && env.host) {
143 | if (typeof (env.host) === 'string') {
144 | hosts[env.host] = env
145 | } else if (env.host instanceof Array) {
146 | env.host.forEach((host) => {
147 | hosts[host] = env
148 | })
149 | }
150 | return hosts
151 | } else if (groups[name]) {
152 | for (let env of groups[name]) {
153 | if (typeof (env.host) === 'string') {
154 | hosts[env.host] = env
155 | } else if (env.host instanceof Array) {
156 | env.host.forEach((host) => {
157 | hosts[host] = env
158 | })
159 | }
160 | }
161 | return hosts
162 | } else {
163 | return new Error('Environement/Group provided not found')
164 | }
165 | }
166 |
167 | static askUser (query, hide, cb) {
168 | if (!cb) {
169 | cb = hide
170 | hide = undefined
171 | }
172 |
173 | var rl = readline.createInterface({
174 | input: process.stdin,
175 | output: process.stdout
176 | })
177 |
178 | if (hide === true) {
179 | process.stdin.on('data', function (char) {
180 | char = char + ''
181 | switch (char) {
182 | case '\n':
183 | case '\r':
184 | case '\u0004':
185 | process.stdin.pause()
186 | break
187 | default:
188 | process.stdout.write('\x1B[2K\x1B[200D' + query + Array(rl.line.length + 1).join('*'))
189 | break
190 | }
191 | })
192 | }
193 |
194 | rl.question(query, (value) => {
195 | rl.close()
196 | return cb(value)
197 | })
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/lib/utils/HookUtils.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict'
3 |
4 | const util = require('util')
5 | const async = require('async')
6 | const exec = require('child_process').exec
7 |
8 | module.exports = class HookUtils {
9 |
10 | /**
11 | * Function used to call a configured hook (if exist) on the remote system
12 | */
13 | static callRemote (hook, server, cb) {
14 | if (!server[hook]) { return cb(null) }
15 | if (typeof (server[hook]) !== 'string') {
16 | return cb(new Error(`Remote '${hook}' hook configured is not a string`))
17 | }
18 |
19 | server.reporter.info(server, `Executing ${hook} on remote system`)
20 | let stderr = ''
21 | let stdout = ''
22 |
23 | server.conn.shell((err, stream) => {
24 | if (err) return cb(err)
25 |
26 | stream.on('close', (code, signal) => {
27 | server.reporter.fine(server, { hook: hook, code: code, stdout: stdout || 'empty', stderr: stderr || 'empty' })
28 | return server.reporter.info(server, `${hook} hook returned code ${code}`, cb)
29 | }).on('data', (data) => {
30 | stdout += data instanceof Buffer ? data.toString() : data
31 | }).stderr.on('data', (data) => {
32 | stderr += data instanceof Buffer ? data.toString() : data
33 | })
34 |
35 | stream.end(util.format('cd %s ; %s %s \nexit\n', server.path + '/current', server.env, server[hook]))
36 | })
37 | }
38 |
39 | /**
40 | * Function used to call a configured hook (if exist) on the local system
41 | */
42 | static callLocal (hook, server, cb) {
43 | if (!server[hook]) {
44 | return cb(null)
45 | }
46 | if (typeof (server[hook]) !== 'string') {
47 | return cb(new Error(`Local '${hook}' hook configured is not a string`))
48 | }
49 |
50 | server.reporter.info(server, `Executing ${hook} on local system`)
51 |
52 | exec(server[hook], (err, stdout, stderr) => {
53 | if (err) return cb(err)
54 |
55 | server.reporter.fine(server, { hook: hook, code: 0, stdout: stdout || '', stderr: stderr || '' })
56 | return server.reporter.info(server, `${hook} hook returned code 0`, cb)
57 | })
58 | }
59 |
60 | /**
61 | * Function used to call both hook (remote and local) asynchronously
62 | */
63 | static call (hook, server, cb) {
64 | async.parallel([
65 | function (next) {
66 | HookUtils.callLocal(hook + '-local', server, next)
67 | },
68 | function (next) {
69 | HookUtils.callRemote(hook, server, next)
70 | }
71 | ], cb)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/utils/config.js:
--------------------------------------------------------------------------------
1 |
2 | // declared internal reporters and strategies
3 | module.exports = {
4 | reporters: {
5 | STD: require('../reporters/StdReporter'),
6 | SPINNER: require('../reporters/SpinnerReporter'),
7 | VOID: require('../reporters/VoidReporter')
8 | },
9 | strategies: {
10 | GIT: require('../strategies/GitStrategy')
11 | },
12 | rollback: 1
13 | }
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "deployerjs",
3 | "version": "0.2.2",
4 | "description": "",
5 | "main": "index.js",
6 | "directories": {
7 | "bin": "./bin",
8 | "lib": "./lib",
9 | "example": "./examples"
10 | },
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1"
13 | },
14 | "bin": {
15 | "deployer": "./bin/deployer"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/vmarchaud/deployerjs.git"
20 | },
21 | "author": "vmarchaud",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/vmarchaud/deployerjs/issues"
25 | },
26 | "homepage": "https://github.com/vmarchaud/deployerjs#readme",
27 | "dependencies": {
28 | "async": "^2.1.2",
29 | "commander": "^2.9.0",
30 | "multispinner": "github:vmarchaud/node-multispinner",
31 | "ssh2": "^0.5.2"
32 | },
33 | "devDependencies": {
34 | "chai": "^3.5.0",
35 | "mocha": "^3.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/test/fixtures/ssh_server.js:
--------------------------------------------------------------------------------
1 |
2 | 'use scrict'
3 |
4 | const crypto = require('crypto')
5 | const ssh2 = require('ssh2')
6 | const utils = ssh2.utils
7 | const EventEmitter = require('events')
8 |
9 | class EventBus extends EventEmitter {}
10 |
11 | export class ServerWrapper {
12 |
13 | static buildServer (opts) {
14 | if (opts.privateKey && !opts.publicKey) {
15 | opts.publicKey = utils.genPublicKey(opts.privateKey)
16 | }
17 | const eventBus = new EventBus()
18 | const server = new ssh2.Server({
19 | hostKeys: [ opts.privateKey ]
20 | }, function (client) {
21 | client.on('authentication', (ctx) => {
22 | if (this.methods.indexOf(ctx.method) < 0) {
23 | return ctx.reject()
24 | }
25 | // auth using password
26 | if (ctx.method === 'password' &&
27 | ctx.username === opts.userrname &&
28 | ctx.password === opts.password) {
29 | return ctx.accept()
30 | }
31 | // auth using public key
32 | if (ctx.method === 'publickey' && ctx.key.algo === opts.publicKey.fulltype &&
33 | ctx.key.data.equals(opts.publicKey.public)) {
34 | if (!ctx.signature) return ctx.accept()
35 |
36 | var verifier = crypto.createVerify(ctx.sigAlgo)
37 | verifier.update(ctx.blob)
38 | return verifier.verify(opts.pubKey.publicOrig, ctx.signature)
39 | ? ctx.accept() : ctx.reject()
40 | }
41 | }).on('ready', () => {
42 | client.on('session', (accept, reject) => {
43 | eventBus.emit('session', accept())
44 | })
45 | }).on('end', () => {
46 | client.emit('disconnect')
47 | })
48 | }).listen(opts.port, opts.host, () => {})
49 | return { bus: eventBus, server: server }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/integrations/setup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vmarchaud/deployerjs/91a9f46b1c5c312b9c9eb9f4434f0969f5d149b6/test/integrations/setup.js
--------------------------------------------------------------------------------
/test/units/config.js:
--------------------------------------------------------------------------------
1 | /* eslint-env mocha */
2 | 'use strict'
3 |
4 | const chai = require('chai')
5 | const expect = chai.expect
6 | const Deployer = require('../../lib/Deployer')
7 | const config = require('../../lib/utils/config')
8 |
9 | describe('Configuration', () => {
10 | describe('Deployer constructor', () => {
11 | const sample = {
12 | test: {
13 | group: 'sample',
14 | username: 'notroot',
15 | password: 'notadmin',
16 | host: '127.0.0.1'
17 | },
18 | production: {
19 | group: 'sample',
20 | username: 'notroot',
21 | password: 'notadmin',
22 | host: '127.0.0.1'
23 | }
24 | }
25 |
26 | it('should create a Deployer instance', () => {
27 | let deployer = new Deployer(sample)
28 | expect(deployer).to.be.an.instanceof(Deployer)
29 | })
30 |
31 | it('should throw an error when no args', () => {
32 | let fn = function () {
33 | let deployer = new Deployer()
34 | deployer.lol()
35 | }
36 | expect(fn).to.throw(/Invalid/)
37 | })
38 |
39 | it('should have put both envs in same group', () => {
40 | let deployer = new Deployer(sample)
41 | expect(deployer.groups).to.exist
42 | expect(deployer.groups).to.have.all.keys(['sample'])
43 | expect(deployer.groups.sample).to.have.deep.property('[0].name', 'test')
44 | expect(deployer.groups.sample).to.have.deep.property('[1].name', 'production')
45 | })
46 |
47 | it('should set default options', () => {
48 | let deployer = new Deployer(sample)
49 | expect(deployer.strategy).to.equal(config.strategies.GIT)
50 | expect(deployer.reporter).to.equal(config.reporters.STD)
51 | })
52 |
53 | it('should use options when provided as string', () => {
54 | let deployer = new Deployer(sample, {
55 | reporter: 'spinner',
56 | strategy: 'git'
57 | })
58 | expect(deployer.strategy).to.equal(config.strategies.GIT)
59 | expect(deployer.reporter).to.equal(config.reporters.SPINNER)
60 | })
61 |
62 | it('should fallback to default option when invalid provided', () => {
63 | let deployer = new Deployer(sample, {
64 | reporter: 'toto',
65 | strategy: 'tata'
66 | })
67 | expect(deployer.strategy).to.equal(config.strategies.GIT)
68 | expect(deployer.reporter).to.equal(config.reporters.STD)
69 | })
70 |
71 | it('should use options when provided as function', () => {
72 | let deployer = new Deployer(sample, {
73 | reporter: config.reporters.VOID,
74 | strategy: config.strategies.GIT
75 | })
76 | expect(deployer.strategy).to.equal(config.strategies.GIT)
77 | expect(deployer.reporter).to.equal(config.reporters.VOID)
78 | })
79 | })
80 |
81 | describe('Selecting environnements', () => {
82 | let sample = {
83 | staging1: {
84 | group: 'dev',
85 | username: 'notroot',
86 | password: 'notadmin',
87 | host: '127.0.0.1'
88 | },
89 | prod1: {
90 | group: 'prod',
91 | username: 'notroot',
92 | password: true,
93 | host: '127.0.0.1'
94 | },
95 | staging2: {
96 | group: 'dev',
97 | username: 'notroot',
98 | password: 'notadmin',
99 | host: 'localhost'
100 | }
101 | }
102 |
103 | it('should select a group w/ one host per env', (done) => {
104 | let deployer = new Deployer(sample, { reporter: config.reporters.VOID })
105 | deployer.select('dev', (err, servers) => {
106 | expect(err).to.be.null
107 | expect(Object.keys(servers).length).to.be.equal(2)
108 | expect(servers).to.have.all.keys([sample.staging1.host, sample.staging2.host])
109 | return done()
110 | })
111 | })
112 |
113 | it('should select a group w/ multiple host per env', (done) => {
114 | var conf = JSON.parse(JSON.stringify(sample))
115 | conf.staging2.host = ['localhost:3030', 'localhost:4000']
116 | conf.staging1.host = ['127.0.0.1:3984', '127.0.0.1:3000']
117 | let deployer = new Deployer(conf, { reporter: config.reporters.VOID })
118 | deployer.select('dev', (err, servers) => {
119 | expect(err).to.be.null
120 | expect(Object.keys(servers).length).to.be.equal(4)
121 | expect(servers[conf.staging2.host[0]].name).to.equal('staging2')
122 | expect(servers[conf.staging2.host[1]].name).to.equal('staging2')
123 | expect(servers[conf.staging1.host[0]].name).to.equal('staging1')
124 | expect(servers[conf.staging1.host[1]].name).to.equal('staging1')
125 | return done()
126 | })
127 | })
128 |
129 | it.skip('should select env and ask for password', (done) => {
130 | var conf = JSON.parse(JSON.stringify(sample))
131 | let deployer = new Deployer(conf, { reporter: config.reporters.VOID })
132 | deployer.select('prod1', (err, servers) => {
133 | expect(err).to.be.null
134 | // TODO
135 | return done()
136 | })
137 | setTimeout(() => {
138 | process.stdin.write('toto\n')
139 | process.stdin.flush
140 | }, 1000)
141 | })
142 | })
143 | })
144 |
--------------------------------------------------------------------------------