2017-03-29 11:03:40 +00:00
|
|
|
Promise = require('bluebird')
|
2017-04-24 19:05:18 +00:00
|
|
|
dockerUtils = require('../utils/docker')
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
getBuilderPushEndpoint = (baseUrl, owner, app) ->
|
2017-04-24 10:06:53 +00:00
|
|
|
querystring = require('querystring')
|
|
|
|
args = querystring.stringify({ owner, app })
|
|
|
|
"https://builder.#{baseUrl}/v1/push?#{args}"
|
|
|
|
|
|
|
|
getBuilderLogPushEndpoint = (baseUrl, buildId, owner, app) ->
|
|
|
|
querystring = require('querystring')
|
|
|
|
args = querystring.stringify({ owner, app, buildId })
|
|
|
|
"https://builder.#{baseUrl}/v1/pushLogs?#{args}"
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
formatImageName = (image) ->
|
|
|
|
image.split('/').pop()
|
|
|
|
|
|
|
|
parseInput = Promise.method (params, options) ->
|
|
|
|
if not params.appName?
|
|
|
|
throw new Error('Need an application to deploy to!')
|
|
|
|
appName = params.appName
|
|
|
|
image = undefined
|
|
|
|
if params.image?
|
|
|
|
if options.build or options.source?
|
|
|
|
throw new Error('Build and source parameters are not applicable when specifying an image')
|
|
|
|
options.build = false
|
|
|
|
image = params.image
|
|
|
|
else if options.build
|
2017-04-24 14:55:54 +00:00
|
|
|
source = options.source || '.'
|
2017-03-29 11:03:40 +00:00
|
|
|
else
|
|
|
|
throw new Error('Need either an image or a build flag!')
|
|
|
|
|
2017-04-24 14:55:54 +00:00
|
|
|
return [appName, options.build, source, image]
|
2017-03-29 11:03:40 +00:00
|
|
|
|
2017-05-10 19:00:51 +00:00
|
|
|
# Builds and returns a Docker-like progress bar like this:
|
|
|
|
# [==================================> ] 64%
|
|
|
|
renderProgress = (percentage, stepCount = 50) ->
|
|
|
|
_ = require('lodash')
|
|
|
|
percentage = Math.max(0, Math.min(percentage, 100))
|
|
|
|
barCount = stepCount * percentage // 100
|
|
|
|
spaceCount = stepCount - barCount
|
|
|
|
bar = "[#{_.repeat('=', barCount)}>#{_.repeat(' ', spaceCount)}]"
|
|
|
|
return "#{bar} #{percentage.toFixed(1)}%"
|
|
|
|
|
2017-06-13 17:28:37 +00:00
|
|
|
showPushProgress = (imageSize, request, logStreams, timeout = 250) ->
|
2017-04-24 12:48:43 +00:00
|
|
|
logging = require('../utils/logging')
|
|
|
|
ansiEscapes = require('ansi-escapes')
|
|
|
|
|
2017-05-10 19:00:51 +00:00
|
|
|
logging.logInfo(logStreams, 'Initializing...')
|
2017-03-29 11:03:40 +00:00
|
|
|
progressReporter = setInterval ->
|
|
|
|
sent = request.req.connection._bytesDispatched
|
|
|
|
percent = (sent / imageSize) * 100
|
|
|
|
if percent >= 100
|
|
|
|
clearInterval(progressReporter)
|
|
|
|
percent = 100
|
2017-04-24 12:48:43 +00:00
|
|
|
process.stdout.write(ansiEscapes.cursorUp(1))
|
2017-03-29 11:03:40 +00:00
|
|
|
process.stdout.clearLine()
|
|
|
|
process.stdout.cursorTo(0)
|
2017-05-10 19:00:51 +00:00
|
|
|
logging.logInfo(logStreams, renderProgress(percent))
|
2017-03-29 11:03:40 +00:00
|
|
|
, timeout
|
|
|
|
|
|
|
|
getBundleInfo = (options) ->
|
|
|
|
helpers = require('../utils/helpers')
|
|
|
|
|
|
|
|
helpers.getAppInfo(options.appName)
|
|
|
|
.then (app) ->
|
|
|
|
[app.arch, app.device_type]
|
|
|
|
|
2017-06-13 17:28:37 +00:00
|
|
|
performUpload = (gzippedImage, token, username, url, appName, logStreams) ->
|
2017-03-29 11:03:40 +00:00
|
|
|
request = require('request')
|
2017-06-13 17:28:37 +00:00
|
|
|
uploadRequest = request.post
|
2017-03-29 11:03:40 +00:00
|
|
|
url: getBuilderPushEndpoint(url, username, appName)
|
2017-06-13 17:28:37 +00:00
|
|
|
headers:
|
|
|
|
'Content-Encoding': 'gzip'
|
2017-03-29 11:03:40 +00:00
|
|
|
auth:
|
|
|
|
bearer: token
|
2017-06-13 17:28:37 +00:00
|
|
|
body: gzippedImage.stream
|
2017-03-29 11:03:40 +00:00
|
|
|
|
2017-06-13 17:28:37 +00:00
|
|
|
uploadToPromise(uploadRequest, gzippedImage.size, logStreams)
|
2017-03-29 11:03:40 +00:00
|
|
|
|
2017-04-24 10:06:53 +00:00
|
|
|
uploadLogs = (logs, token, url, buildId, username, appName) ->
|
|
|
|
request = require('request')
|
|
|
|
request.post
|
|
|
|
json: true
|
|
|
|
url: getBuilderLogPushEndpoint(url, buildId, username, appName)
|
|
|
|
auth:
|
|
|
|
bearer: token
|
|
|
|
body: Buffer.from(logs)
|
|
|
|
|
2017-06-13 17:28:37 +00:00
|
|
|
uploadToPromise = (uploadRequest, size, logStreams) ->
|
2017-04-24 12:48:43 +00:00
|
|
|
logging = require('../utils/logging')
|
2017-04-24 10:06:53 +00:00
|
|
|
|
2017-03-29 11:03:40 +00:00
|
|
|
new Promise (resolve, reject) ->
|
|
|
|
|
|
|
|
handleMessage = (data) ->
|
|
|
|
data = data.toString()
|
2017-04-24 12:48:43 +00:00
|
|
|
logging.logDebug(logStreams, "Received data: #{data}")
|
2017-03-29 11:03:40 +00:00
|
|
|
|
2017-04-24 10:06:53 +00:00
|
|
|
try
|
|
|
|
obj = JSON.parse(data)
|
|
|
|
catch e
|
|
|
|
logging.logError(logStreams, 'Error parsing reply from remote side')
|
|
|
|
reject(e)
|
|
|
|
return
|
|
|
|
|
2017-03-29 11:03:40 +00:00
|
|
|
if obj.type?
|
|
|
|
switch obj.type
|
|
|
|
when 'error' then reject(new Error("Remote error: #{obj.error}"))
|
2017-04-24 10:06:53 +00:00
|
|
|
when 'success' then resolve(obj)
|
2017-04-24 12:48:43 +00:00
|
|
|
when 'status' then logging.logInfo(logStreams, "Remote: #{obj.message}")
|
2017-03-29 11:03:40 +00:00
|
|
|
else reject(new Error("Received unexpected reply from remote: #{data}"))
|
|
|
|
else
|
|
|
|
reject(new Error("Received unexpected reply from remote: #{data}"))
|
|
|
|
|
2017-06-13 17:28:37 +00:00
|
|
|
uploadRequest
|
2017-03-29 11:03:40 +00:00
|
|
|
.on('error', reject)
|
|
|
|
.on('data', handleMessage)
|
|
|
|
|
|
|
|
# Set up upload reporting
|
2017-06-13 17:28:37 +00:00
|
|
|
showPushProgress(size, uploadRequest, logStreams)
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
|
|
|
|
module.exports =
|
|
|
|
signature: 'deploy <appName> [image]'
|
|
|
|
description: 'Deploy a container to a resin.io application'
|
|
|
|
help: '''
|
|
|
|
Use this command to deploy and optionally build an image to an application.
|
|
|
|
|
|
|
|
Usage: deploy <appName> ([image] | --build [--source build-dir])
|
|
|
|
|
|
|
|
Note: If building with this command, all options supported by `resin build`
|
2017-04-26 12:34:40 +00:00
|
|
|
are also supported with this command.
|
2017-03-29 11:03:40 +00:00
|
|
|
|
|
|
|
Examples:
|
2017-04-26 12:34:40 +00:00
|
|
|
$ resin deploy myApp --build --source myBuildDir/
|
|
|
|
$ resin deploy myApp myApp/myImage
|
2017-03-29 11:03:40 +00:00
|
|
|
'''
|
|
|
|
permission: 'user'
|
2017-04-24 19:05:18 +00:00
|
|
|
options: dockerUtils.appendOptions [
|
2017-03-29 11:03:40 +00:00
|
|
|
{
|
|
|
|
signature: 'build'
|
|
|
|
boolean: true
|
|
|
|
description: 'Build image then deploy'
|
|
|
|
alias: 'b'
|
|
|
|
},
|
|
|
|
{
|
|
|
|
signature: 'source'
|
|
|
|
parameter: 'source'
|
|
|
|
description: 'The source directory to use when building the image'
|
|
|
|
alias: 's'
|
2017-04-24 10:06:53 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
signature: 'nologupload'
|
|
|
|
description: "Don't upload build logs to the dashboard with image (if building)"
|
|
|
|
boolean: true
|
2017-03-29 11:03:40 +00:00
|
|
|
}
|
|
|
|
]
|
|
|
|
action: (params, options, done) ->
|
|
|
|
_ = require('lodash')
|
|
|
|
tmp = require('tmp')
|
|
|
|
tmpNameAsync = Promise.promisify(tmp.tmpName)
|
|
|
|
resin = require('resin-sdk-preconfigured')
|
|
|
|
|
2017-04-24 12:48:43 +00:00
|
|
|
logging = require('../utils/logging')
|
|
|
|
|
|
|
|
logStreams = logging.getLogStreams()
|
|
|
|
|
2017-03-29 11:03:40 +00:00
|
|
|
# Ensure the tmp files gets deleted
|
|
|
|
tmp.setGracefulCleanup()
|
|
|
|
|
2017-04-24 10:06:53 +00:00
|
|
|
logs = ''
|
|
|
|
|
|
|
|
upload = (token, username, url) ->
|
|
|
|
docker = dockerUtils.getDocker(options)
|
|
|
|
# Check input parameters
|
|
|
|
parseInput(params, options)
|
|
|
|
.then ([appName, build, source, imageName]) ->
|
|
|
|
tmpNameAsync()
|
2017-06-13 17:28:37 +00:00
|
|
|
.then (bufferFile) ->
|
2017-04-24 10:06:53 +00:00
|
|
|
|
|
|
|
# Setup the build args for how the build routine expects them
|
|
|
|
options = _.assign({}, options, { appName })
|
|
|
|
params = _.assign({}, params, { source })
|
|
|
|
|
|
|
|
Promise.try ->
|
|
|
|
if build
|
|
|
|
dockerUtils.runBuild(params, options, getBundleInfo, logStreams)
|
|
|
|
else
|
|
|
|
{ image: imageName, log: '' }
|
|
|
|
.then ({ image: imageName, log: buildLogs }) ->
|
|
|
|
logs = buildLogs
|
2017-06-13 17:28:37 +00:00
|
|
|
Promise.all [
|
|
|
|
dockerUtils.gzipAndBufferImage(docker, imageName, bufferFile)
|
2017-04-24 10:06:53 +00:00
|
|
|
token
|
|
|
|
username
|
|
|
|
url
|
|
|
|
params.appName
|
|
|
|
logStreams
|
2017-06-13 17:28:37 +00:00
|
|
|
]
|
|
|
|
.spread(performUpload)
|
2017-04-24 10:06:53 +00:00
|
|
|
.finally ->
|
|
|
|
# If the file was never written to (for instance because an error
|
|
|
|
# has occured before any data was written) this call will throw an
|
|
|
|
# ugly error, just suppress it
|
2017-06-13 17:28:37 +00:00
|
|
|
require('mz/fs').unlink(bufferFile)
|
2017-04-24 10:06:53 +00:00
|
|
|
.catch(_.noop)
|
|
|
|
.tap ({ image: imageName, buildId }) ->
|
|
|
|
logging.logSuccess(logStreams, "Successfully deployed image: #{formatImageName(imageName)}")
|
|
|
|
return buildId
|
|
|
|
.then ({ image: imageName, buildId }) ->
|
|
|
|
if logs is '' or options.nologupload?
|
|
|
|
return ''
|
|
|
|
|
|
|
|
logging.logInfo(logStreams, 'Uploading logs to dashboard...')
|
|
|
|
|
|
|
|
Promise.join(
|
|
|
|
logs
|
|
|
|
token
|
|
|
|
url
|
|
|
|
buildId
|
|
|
|
username
|
|
|
|
params.appName
|
|
|
|
uploadLogs
|
|
|
|
)
|
|
|
|
.return('Successfully uploaded logs')
|
|
|
|
.then (msg) ->
|
|
|
|
logging.logSuccess(logStreams, msg) if msg isnt ''
|
|
|
|
.asCallback(done)
|
|
|
|
|
|
|
|
Promise.join(
|
|
|
|
resin.auth.getToken()
|
|
|
|
resin.auth.whoami()
|
|
|
|
resin.settings.get('resinUrl')
|
|
|
|
upload
|
|
|
|
)
|