diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e4b7b1a4..ef4cb4b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY! This project adheres to [Semantic Versioning](http://semver.org/). +## v7.1.0 - 2018-03-22 + +* Warn early if deploying a multicontainer project to an incompatible app #818 [Akis Kesoglou] +* Add legacy deploy method back #818 [Akis Kesoglou] + ## v7.0.7 - 2018-03-20 * Update resin-preload to 6.1.2 #821 [Alexis Svinartchouk] diff --git a/lib/actions/build.coffee b/lib/actions/build.coffee index a43548407..f57f3834e 100644 --- a/lib/actions/build.coffee +++ b/lib/actions/build.coffee @@ -7,10 +7,9 @@ compose = require('../utils/compose') ### Opts must be an object with the following keys: - appName: the name of the app this build is for; optional + app: the app this build is for arch: the architecture to build for deviceType: the device type to build for - projectPath: the project root directory; must be absolute buildEmulated buildOpts: arguments to forward to docker build command ### @@ -21,6 +20,12 @@ buildProject = (docker, logger, composeOpts, opts) -> composeOpts.projectName ) .then (project) -> + if project.descriptors.length > 1 and not opts.app.application_type?[0]?.supports_multicontainer + logger.logWarn( + 'Target application does not support multiple containers.\n' + + 'Continuing with build, but you will not be able to deploy.' + ) + compose.buildProject( docker logger @@ -111,18 +116,25 @@ module.exports = if arch? and deviceType? [ undefined, arch, deviceType ] else - helpers.getArchAndDeviceType(application) + Promise.join( + helpers.getApplication(application) + helpers.getArchAndDeviceType(application) + (app, { arch, device_type }) -> + app.arch = arch + app.device_type = device_type + return app + ) .then (app) -> - [ application, app.arch, app.device_type ] + [ app, app.arch, app.device_type ] - .then ([ appName, arch, deviceType ]) -> + .then ([ app, arch, deviceType ]) -> Promise.join( dockerUtils.getDocker(options) dockerUtils.generateBuildOpts(options) compose.generateOpts(options) (docker, buildOpts, composeOpts) -> buildProject(docker, logger, composeOpts, { - appName + app arch deviceType buildEmulated: !!options.emulated diff --git a/lib/actions/deploy.coffee b/lib/actions/deploy.coffee index 377ecdd67..86bee0da4 100644 --- a/lib/actions/deploy.coffee +++ b/lib/actions/deploy.coffee @@ -26,6 +26,9 @@ deployProject = (docker, logger, composeOpts, opts) -> opts.image ) .then (project) -> + if project.descriptors.length > 1 and !opts.app.application_type?[0]?.supports_multicontainer + throw new Error('Target application does not support multiple containers. Aborting!') + # find which services use images that already exist locally Promise.map project.descriptors, (d) -> # unconditionally build (or pull) if explicitly requested @@ -66,6 +69,29 @@ deployProject = (docker, logger, composeOpts, opts) -> props: {} } .then (images) -> + if opts.app.application_type?[0]?.is_legacy + chalk = require('chalk') + legacyDeploy = require('../utils/deploy-legacy') + + msg = chalk.yellow('Target application requires legacy deploy method.') + logger.logWarn(msg) + + return Promise.join( + docker + logger + sdk.auth.getToken() + sdk.auth.whoami() + sdk.settings.get('resinUrl') + { + appName: opts.app.app_name + imageName: images[0].name + buildLogs: images[0].logs + shouldUploadLogs: opts.shouldUploadLogs + } + legacyDeploy + ) + .then (releaseId) -> + sdk.models.release.get(releaseId, $select: [ 'commit' ]) Promise.join( sdk.auth.getUserId() sdk.auth.getToken() diff --git a/lib/utils/deploy-legacy.coffee b/lib/utils/deploy-legacy.coffee new file mode 100644 index 000000000..28d530192 --- /dev/null +++ b/lib/utils/deploy-legacy.coffee @@ -0,0 +1,136 @@ +Promise = require('bluebird') + +getBuilderPushEndpoint = (baseUrl, owner, app) -> + 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}" + +bufferImage = (docker, imageId, bufferFile) -> + Promise = require('bluebird') + streamUtils = require('./streams') + + image = docker.getImage(imageId) + imageMetadata = image.inspect() + + Promise.join image.get(), imageMetadata.get('Size'), (imageStream, imageSize) -> + streamUtils.buffer(imageStream, bufferFile) + .tap (bufferedStream) -> + bufferedStream.length = imageSize + +showPushProgress = (message) -> + visuals = require('resin-cli-visuals') + progressBar = new visuals.Progress(message) + progressBar.update({ percentage: 0 }) + return progressBar + +uploadToPromise = (uploadRequest, logger) -> + new Promise (resolve, reject) -> + + handleMessage = (data) -> + data = data.toString() + logger.logDebug("Received data: #{data}") + + try + obj = JSON.parse(data) + catch e + logger.logError('Error parsing reply from remote side') + reject(e) + return + + switch obj.type + when 'error' then reject(new Error("Remote error: #{obj.error}")) + when 'success' then resolve(obj) + when 'status' then logger.logInfo(obj.message) + else reject(new Error("Received unexpected reply from remote: #{data}")) + + uploadRequest + .on('error', reject) + .on('data', handleMessage) + +uploadImage = (imageStream, token, username, url, appName, logger) -> + request = require('request') + progressStream = require('progress-stream') + zlib = require('zlib') + + # Need to strip off the newline + progressMessage = logger.formatMessage('info', 'Uploading').slice(0, -1) + progressBar = showPushProgress(progressMessage) + streamWithProgress = imageStream.pipe progressStream + time: 500, + length: imageStream.length + , ({ percentage, eta }) -> + progressBar.update + percentage: Math.min(percentage, 100) + eta: eta + + uploadRequest = request.post + url: getBuilderPushEndpoint(url, username, appName) + headers: + 'Content-Encoding': 'gzip' + auth: + bearer: token + body: streamWithProgress.pipe(zlib.createGzip({ + level: 6 + })) + + uploadToPromise(uploadRequest, logger) + +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) + +### +opts must be a hash with the following keys: + +- appName: the name of the app to deploy to +- imageName: the name of the image to deploy +- buildLogs: a string with build output +- shouldUploadLogs +### +module.exports = (docker, logger, token, username, url, opts) -> + tmp = require('tmp') + tmpNameAsync = Promise.promisify(tmp.tmpName) + + # Ensure the tmp files gets deleted + tmp.setGracefulCleanup() + + { appName, imageName, buildLogs, shouldUploadLogs } = opts + logs = buildLogs + + tmpNameAsync() + .then (bufferFile) -> + logger.logInfo('Initializing deploy...') + bufferImage(docker, imageName, bufferFile) + .then (stream) -> + uploadImage(stream, token, username, url, appName, logger) + .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 + Promise.try -> + require('mz/fs').unlink(bufferFile) + .catchReturn() + .tap ({ buildId }) -> + return if not shouldUploadLogs + + logger.logInfo('Uploading logs...') + Promise.join( + logs + token + url + buildId + username + appName + uploadLogs + ) + .get('buildId') diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index 486050072..88bbfcf98 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -138,11 +138,23 @@ export function getApplication(applicationName: string) { // that off to a special handler (before importing any modules) const match = /(\w+)\/(\w+)/.exec(applicationName); + const extraOptions = { + $expand: { + application_type: { + $select: ['name', 'slug', 'supports_multicontainer', 'is_legacy'], + }, + }, + }; + if (match) { - return resin.models.application.getAppByOwner(match[2], match[1]); + return resin.models.application.getAppByOwner( + match[2], + match[1], + extraOptions, + ); } - return resin.models.application.get(applicationName); + return resin.models.application.get(applicationName, extraOptions); } // A function to reliably execute a command diff --git a/package.json b/package.json index 8cb2c668b..3cd80e1b0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resin-cli", - "version": "7.0.7", + "version": "7.1.0", "description": "The official resin.io CLI tool", "main": "./build/actions/index.js", "homepage": "https://github.com/resin-io/resin-cli", @@ -83,6 +83,7 @@ }, "dependencies": { "@resin.io/valid-email": "^0.1.0", + "@types/stream-to-promise": "^2.2.0", "ansi-escapes": "^2.0.0", "any-promise": "^1.3.0", "archiver": "^2.1.0", @@ -129,7 +130,7 @@ "resin-cli-errors": "^1.2.0", "resin-cli-form": "^1.4.1", "resin-cli-visuals": "^1.4.0", - "resin-compose-parse": "^1.5.2", + "resin-compose-parse": "^1.8.0", "resin-config-json": "^1.0.0", "resin-device-config": "^4.0.0", "resin-device-init": "^4.0.0", @@ -139,8 +140,8 @@ "resin-image-manager": "^5.0.0", "resin-multibuild": "^0.5.1", "resin-preload": "^6.1.2", - "resin-release": "^1.1.1", - "resin-sdk": "^9.0.0-beta7", + "resin-release": "^1.2.0", + "resin-sdk": "9.0.0-beta14", "resin-sdk-preconfigured": "^6.9.0", "resin-settings-client": "^3.6.1", "resin-stream-logger": "^0.1.0",