UNPKG

6.92 kBJavaScriptView Raw
1/* @flow */
2'use strict'
3
4/* ::
5import type {
6 BlinkMRCServer,
7 ServerCLIServiceConfig
8} from '../types.js'
9
10type BMServerAuthentication = [
11 Object,
12 string | void
13]
14*/
15
16const fs = require('fs')
17const path = require('path')
18
19const archiver = require('archiver')
20const AWS = require('aws-sdk')
21const chalk = require('chalk')
22const inquirer = require('inquirer')
23const request = require('request')
24const temp = require('temp').track()
25const ora = require('ora')
26
27const pkg = require('../package.json')
28const values = require('./values.js')
29const scope = require('./scope.js')
30const awsRoles = require('./assume-aws-roles.js')
31
32const EXT = 'zip'
33
34function authenticate(
35 config /* : BlinkMRCServer */,
36 blinkMobileIdentity /* : Object */,
37 env /* : string */
38) /* : Promise<BMServerAuthentication> */ {
39 const spinner = ora('Authenticating...').start()
40 return blinkMobileIdentity
41 .getAccessToken()
42 .then(accessToken => {
43 return awsRoles
44 .assumeAWSRoleToDeploy(config, env, accessToken)
45 .then(awsCredentials => [awsCredentials, accessToken])
46 })
47 .then(results => {
48 spinner.succeed('Authentication complete!')
49 return results
50 })
51 .catch(err => {
52 spinner.fail('Authentication failed...')
53 return Promise.reject(err)
54 })
55}
56
57function confirm(
58 logger /* : typeof console */,
59 force /* : boolean */,
60 env /* : string */
61) /* : Promise<boolean> */ {
62 if (force) {
63 return Promise.resolve(true)
64 }
65 logger.log(
66 chalk.yellow(`
67Please check configuration before continuing
68`)
69 )
70 const promptQuestions = [
71 {
72 type: 'confirm',
73 name: 'confirmation',
74 message: `Are you sure you want to deploy to environment "${env}": [Y]`
75 }
76 ]
77 return inquirer.prompt(promptQuestions).then(results => results.confirmation)
78}
79
80function deploy(
81 bundleKey /* : string */,
82 accessToken /* : string | void */,
83 env /* : string */,
84 config /* : BlinkMRCServer */
85) /* : Promise<void> */ {
86 const serverCLIServiceConfig = scope.serverCLIServiceConfig(config)
87 const spinner = ora(
88 'Deploying project - this may take several minutes...'
89 ).start()
90 return new Promise((resolve, reject) => {
91 // Make request to start deployment
92 const baseRequest = request.defaults({
93 auth: {
94 bearer: accessToken
95 },
96 baseUrl: `${
97 serverCLIServiceConfig.origin
98 }/v1/service-instances/${config.project || ''}/environments/${env}/`,
99 json: true
100 })
101 baseRequest.post(
102 '/deployments',
103 {
104 json: {
105 bundleBucket: serverCLIServiceConfig.bucket,
106 bundleKey,
107 env,
108 bmServerVersion: pkg.version,
109 analytics: config.analytics
110 }
111 },
112 (err, deployResponse, deployData) => {
113 // Ensure deployment started successfully
114 if (err) {
115 spinner.fail('Deployment failed...')
116 reject(err)
117 return
118 }
119 if (deployResponse.statusCode !== 202) {
120 spinner.fail(
121 `Deployment failed - ${deployData.statusCode} ${deployData.error}`
122 )
123 reject(new Error(deployData.message))
124 return
125 }
126
127 // Start polling deployment for status updates
128 const intervalId = setInterval(() => {
129 baseRequest.get(
130 `/deployments/${deployData.id}`,
131 (error, response, data) => {
132 // Ensure request for status of deployment is successful
133 if (error) {
134 spinner.fail('Deployment failed...')
135 clearInterval(intervalId)
136 reject(error)
137 return
138 }
139 if (response.statusCode !== 200) {
140 spinner.fail(
141 `Deployment failed - ${data.statusCode} ${data.error}`
142 )
143 clearInterval(intervalId)
144 reject(new Error(data.message))
145 return
146 }
147 if (data.error) {
148 spinner.fail(`Deployment failed - ${data.error.name}`)
149 clearInterval(intervalId)
150 reject(new Error(data.error.message))
151 return
152 }
153
154 // If status has a result on it, it has finished.
155 // We can stop the polling and display origin deployed to.
156 if (data.result) {
157 spinner.succeed(
158 `Deployment complete - Origin: ${data.result.baseUrl}`
159 )
160 clearInterval(intervalId)
161 resolve(data.result)
162 }
163 }
164 )
165 }, 15000)
166 }
167 )
168 })
169}
170
171function upload(
172 zipFilePath /* : string */,
173 awsCredentials /* : Object */,
174 config /* : BlinkMRCServer */
175) /* : Promise<string> */ {
176 const src = fs.createReadStream(zipFilePath)
177 const key = path.basename(zipFilePath)
178 const s3 = new AWS.S3(awsCredentials)
179 const params = {
180 Bucket:
181 (config.service || {}).bucket || values.SERVER_CLI_SERVICE_S3_BUCKET,
182 Key: `bundles/${key}`,
183 Body: src,
184 ACL: 'bucket-owner-read'
185 }
186
187 let progress = 0
188 const manager = s3.upload(params)
189 const spinner = ora('Transferring project...').start()
190 manager.on('httpUploadProgress', uploadProgress => {
191 // Note that total may be undefined until the payload size is known.
192 // http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3/ManagedUpload.html
193 if (uploadProgress.total) {
194 progress = Math.floor(
195 (uploadProgress.loaded / uploadProgress.total) * 100
196 )
197 spinner.text = `Transferring project: ${progress}%`
198 }
199 })
200
201 return new Promise((resolve, reject) => {
202 manager.send((err, data) => {
203 if (err) {
204 reject(err)
205 spinner.fail(`Transfer failed: ${progress}%`)
206 return
207 }
208 spinner.succeed('Transfer complete!')
209 resolve(data.Key)
210 })
211 })
212}
213
214function zip(cwd /* : string */) /* : Promise<string> */ {
215 const archive = archiver.create(EXT, {})
216 const output = temp.createWriteStream({ suffix: `.${EXT}` })
217 archive.pipe(output)
218 archive.glob('**/*', {
219 cwd,
220 nodir: true,
221 dot: true,
222 ignore: [
223 '.git/**'
224 // Still unsure on whether or not this will be needed.
225 // If projects have private packages, we can not ignore node_modules
226 // as the server cli service will not be able to download them.
227 // 'node_modules/**'
228 ]
229 })
230 archive.finalize()
231 const spinner = ora('Compressing project...').start()
232 return new Promise((resolve, reject) => {
233 const fail = err => {
234 spinner.fail('Compression failed...')
235 reject(err)
236 }
237
238 archive.on('error', err => fail(err))
239 output.on('error', err => fail(err))
240 output.on('finish', () => {
241 spinner.succeed('Compression complete!')
242 resolve(output.path)
243 })
244 })
245}
246
247module.exports = {
248 authenticate,
249 confirm,
250 deploy,
251 upload,
252 zip
253}