2015-07-20 00:09:47 -07:00
/* jslint node:true */
'use strict' ;
exports = module . exports = {
AppsError : AppsError ,
get : get ,
getBySubdomain : getBySubdomain ,
getAll : getAll ,
purchase : purchase ,
install : install ,
configure : configure ,
uninstall : uninstall ,
restore : restore ,
restoreApp : restoreApp ,
update : update ,
backup : backup ,
backupApp : backupApp ,
getLogStream : getLogStream ,
getLogs : getLogs ,
start : start ,
stop : stop ,
exec : exec ,
checkManifestConstraints : checkManifestConstraints ,
setRestorePoint : setRestorePoint ,
autoupdateApps : autoupdateApps ,
// exported for testing
_validateHostname : validateHostname ,
_validatePortBindings : validatePortBindings
} ;
var addons = require ( './addons.js' ) ,
appdb = require ( './appdb.js' ) ,
assert = require ( 'assert' ) ,
async = require ( 'async' ) ,
backups = require ( './backups.js' ) ,
BackupsError = require ( './backups.js' ) . BackupsError ,
config = require ( './config.js' ) ,
constants = require ( './constants.js' ) ,
DatabaseError = require ( './databaseerror.js' ) ,
debug = require ( 'debug' ) ( 'box:apps' ) ,
docker = require ( './docker.js' ) ,
fs = require ( 'fs' ) ,
manifestFormat = require ( 'cloudron-manifestformat' ) ,
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
semver = require ( 'semver' ) ,
shell = require ( './shell.js' ) ,
split = require ( 'split' ) ,
superagent = require ( 'superagent' ) ,
taskmanager = require ( './taskmanager.js' ) ,
util = require ( 'util' ) ,
validator = require ( 'validator' ) ;
var BACKUP _APP _CMD = path . join ( _ _dirname , 'scripts/backupapp.sh' ) ,
RESTORE _APP _CMD = path . join ( _ _dirname , 'scripts/restoreapp.sh' ) ,
BACKUP _SWAP _CMD = path . join ( _ _dirname , 'scripts/backupswap.sh' ) ;
function debugApp ( app , args ) {
assert ( ! app || typeof app === 'object' ) ;
var prefix = app ? app . location : '(no app)' ;
debug ( prefix + ' ' + util . format . apply ( util , Array . prototype . slice . call ( arguments , 1 ) ) ) ;
}
function ignoreError ( func ) {
return function ( callback ) {
func ( function ( error ) {
if ( error ) console . error ( 'Ignored error:' , error ) ;
callback ( ) ;
} ) ;
} ;
}
// http://dustinsenos.com/articles/customErrorsInNode
// http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
function AppsError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( AppsError , Error ) ;
AppsError . INTERNAL _ERROR = 'Internal Error' ;
AppsError . EXTERNAL _ERROR = 'External Error' ;
AppsError . ALREADY _EXISTS = 'Already Exists' ;
AppsError . NOT _FOUND = 'Not Found' ;
AppsError . BAD _FIELD = 'Bad Field' ;
AppsError . BAD _STATE = 'Bad State' ;
AppsError . PORT _RESERVED = 'Port Reserved' ;
AppsError . PORT _CONFLICT = 'Port Conflict' ;
AppsError . BILLING _REQUIRED = 'Billing Required' ;
// Hostname validation comes from RFC 1123 (section 2.1)
// Domain name validation comes from RFC 2181 (Name syntax)
// https://en.wikipedia.org/wiki/Hostname#Restrictions_on_valid_host_names
// We are validating the validity of the location-fqdn as host name
function validateHostname ( location , fqdn ) {
var RESERVED _LOCATIONS = [ constants . ADMIN _LOCATION , constants . API _LOCATION ] ;
if ( RESERVED _LOCATIONS . indexOf ( location ) !== - 1 ) return new Error ( location + ' is reserved' ) ;
if ( location === '' ) return null ; // bare location
if ( ( location . length + 1 /*+ hyphen */ + fqdn . indexOf ( '.' ) ) > 63 ) return new Error ( 'Hostname length cannot be greater than 63' ) ;
if ( location . match ( /^[A-Za-z0-9-]+$/ ) === null ) return new Error ( 'Hostname can only contain alphanumerics and hyphen' ) ;
if ( location [ 0 ] === '-' || location [ location . length - 1 ] === '-' ) return new Error ( 'Hostname cannot start or end with hyphen' ) ;
if ( location . length + 1 /* hyphen */ + fqdn . length > 253 ) return new Error ( 'FQDN length exceeds 253 characters' ) ;
return null ;
}
// validate the port bindings
function validatePortBindings ( portBindings , tcpPorts ) {
// keep the public ports in sync with firewall rules in scripts/initializeBaseUbuntuImage.sh
// these ports are reserved even if we listen only on 127.0.0.1 because we setup HostIp to be 127.0.0.1
// for custom tcp ports
var RESERVED _PORTS = [
25 , /* smtp */
53 , /* dns */
80 , /* http */
443 , /* https */
2015-09-10 10:08:31 -07:00
919 , /* ssh */
2015-07-20 00:09:47 -07:00
2003 , /* graphite (lo) */
2004 , /* graphite (lo) */
2020 , /* install server */
config . get ( 'port' ) , /* app server (lo) */
config . get ( 'internalPort' ) , /* internal app server (lo) */
2015-09-16 10:12:59 -07:00
config . get ( 'ldapPort' ) , /* ldap server (lo) */
config . get ( 'oauthProxyPort' ) , /* oauth proxy server (lo) */
2015-07-20 00:09:47 -07:00
3306 , /* mysql (lo) */
8000 /* graphite (lo) */
] ;
if ( ! portBindings ) return null ;
var env ;
for ( env in portBindings ) {
if ( ! /^[a-zA-Z0-9_]+$/ . test ( env ) ) return new AppsError ( AppsError . BAD _FIELD , env + ' is not valid environment variable' ) ;
if ( ! Number . isInteger ( portBindings [ env ] ) ) return new Error ( portBindings [ env ] + ' is not an integer' ) ;
if ( portBindings [ env ] <= 0 || portBindings [ env ] > 65535 ) return new Error ( portBindings [ env ] + ' is out of range' ) ;
if ( RESERVED _PORTS . indexOf ( portBindings [ env ] ) !== - 1 ) return new AppsError ( AppsError . PORT _RESERVED , + portBindings [ env ] ) ;
}
// it is OK if there is no 1-1 mapping between values in manifest.tcpPorts and portBindings. missing values implies
// that the user wants the service disabled
tcpPorts = tcpPorts || { } ;
for ( env in portBindings ) {
if ( ! ( env in tcpPorts ) ) return new AppsError ( AppsError . BAD _FIELD , 'Invalid portBindings ' + env ) ;
}
return null ;
}
function getDuplicateErrorDetails ( location , portBindings , error ) {
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( error . reason , DatabaseError . ALREADY _EXISTS ) ;
var match = error . message . match ( /ER_DUP_ENTRY: Duplicate entry '(.*)' for key/ ) ;
if ( ! match ) {
console . error ( 'Unexpected SQL error message.' , error ) ;
return new AppsError ( AppsError . INTERNAL _ERROR ) ;
}
// check if the location conflicts
if ( match [ 1 ] === location ) return new AppsError ( AppsError . ALREADY _EXISTS ) ;
// check if any of the port bindings conflict
for ( var env in portBindings ) {
if ( portBindings [ env ] === parseInt ( match [ 1 ] ) ) return new AppsError ( AppsError . PORT _CONFLICT , match [ 1 ] ) ;
}
return new AppsError ( AppsError . ALREADY _EXISTS ) ;
}
function getIconUrlSync ( app ) {
var iconPath = paths . APPICONS _DIR + '/' + app . id + '.png' ;
return fs . existsSync ( iconPath ) ? '/api/v1/apps/' + app . id + '/icon' : null ;
}
function get ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
app . iconUrl = getIconUrlSync ( app ) ;
app . fqdn = config . appFqdn ( app . location ) ;
callback ( null , app ) ;
} ) ;
}
function getBySubdomain ( subdomain , callback ) {
assert . strictEqual ( typeof subdomain , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . getBySubdomain ( subdomain , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
app . iconUrl = getIconUrlSync ( app ) ;
app . fqdn = config . appFqdn ( app . location ) ;
callback ( null , app ) ;
} ) ;
}
function getAll ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . getAll ( function ( error , apps ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
apps . forEach ( function ( app ) {
app . iconUrl = getIconUrlSync ( app ) ;
app . fqdn = config . appFqdn ( app . location ) ;
} ) ;
callback ( null , apps ) ;
} ) ;
}
function validateAccessRestriction ( accessRestriction ) {
// TODO: make the values below enumerations in the oauth code
switch ( accessRestriction ) {
case '' :
case 'roleUser' :
case 'roleAdmin' :
return null ;
default :
return new Error ( 'Invalid accessRestriction' ) ;
}
}
function purchase ( appStoreId , callback ) {
assert . strictEqual ( typeof appStoreId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
// Skip purchase if appStoreId is empty
if ( appStoreId === '' ) return callback ( null ) ;
var url = config . apiServerOrigin ( ) + '/api/v1/apps/' + appStoreId + '/purchase' ;
superagent . post ( url ) . query ( { token : config . token ( ) } ) . end ( function ( error , res ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( res . status === 402 ) return callback ( new AppsError ( AppsError . BILLING _REQUIRED ) ) ;
if ( res . status !== 201 && res . status !== 200 ) return callback ( new Error ( util . format ( 'App purchase failed. %s %j' , res . status , res . body ) ) ) ;
callback ( null ) ;
} ) ;
}
function install ( appId , appStoreId , manifest , location , portBindings , accessRestriction , icon , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof appStoreId , 'string' ) ;
assert ( manifest && typeof manifest === 'object' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( typeof accessRestriction , 'string' ) ;
assert ( ! icon || typeof icon === 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest error: ' + error . message ) ) ;
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest cannot be installed: ' + error . message ) ) ;
error = validateHostname ( location , config . fqdn ( ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
error = validatePortBindings ( portBindings , manifest . tcpPorts ) ;
if ( error ) return callback ( error ) ;
error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
if ( icon ) {
if ( ! validator . isBase64 ( icon ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'icon is not base64' ) ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPICONS _DIR , appId + '.png' ) , new Buffer ( icon , 'base64' ) ) ) {
return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving icon:' + safe . error . message ) ) ;
}
}
debug ( 'Will install app with id : ' + appId ) ;
purchase ( appStoreId , function ( error ) {
if ( error ) return callback ( error ) ;
appdb . add ( appId , appStoreId , manifest , location . toLowerCase ( ) , portBindings , accessRestriction , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( location . toLowerCase ( ) , portBindings , error ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function configure ( appId , location , portBindings , accessRestriction , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof location , 'string' ) ;
assert . strictEqual ( typeof portBindings , 'object' ) ;
assert . strictEqual ( typeof accessRestriction , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var error = validateHostname ( location , config . fqdn ( ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
error = validateAccessRestriction ( accessRestriction ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
error = validatePortBindings ( portBindings , app . manifest . tcpPorts ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
var values = {
location : location . toLowerCase ( ) ,
accessRestriction : accessRestriction ,
portBindings : portBindings ,
oldConfig : {
location : app . location ,
accessRestriction : app . accessRestriction ,
portBindings : app . portBindings
}
} ;
debug ( 'Will configure app with id:%s values:%j' , appId , values ) ;
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _CONFIGURE , values , function ( error ) {
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( location . toLowerCase ( ) , portBindings , error ) ) ;
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function update ( appId , force , manifest , portBindings , icon , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof force , 'boolean' ) ;
assert ( manifest && typeof manifest === 'object' ) ;
assert ( ! portBindings || typeof portBindings === 'object' ) ;
assert ( ! icon || typeof icon === 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will update app with id:%s' , appId ) ;
var error = manifestFormat . parse ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest error:' + error . message ) ) ;
error = checkManifestConstraints ( manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest cannot be installed:' + error . message ) ) ;
error = validatePortBindings ( portBindings , manifest . tcpPorts ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , error . message ) ) ;
if ( icon ) {
if ( ! validator . isBase64 ( icon ) ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'icon is not base64' ) ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . APPICONS _DIR , appId + '.png' ) , new Buffer ( icon , 'base64' ) ) ) {
return callback ( new AppsError ( AppsError . INTERNAL _ERROR , 'Error saving icon:' + safe . error . message ) ) ;
}
}
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
var values = {
manifest : manifest ,
portBindings : portBindings ,
oldConfig : {
manifest : app . manifest ,
portBindings : app . portBindings
}
} ;
appdb . setInstallationCommand ( appId , force ? appdb . ISTATE _PENDING _FORCE _UPDATE : appdb . ISTATE _PENDING _UPDATE , values , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error && error . reason === DatabaseError . ALREADY _EXISTS ) return callback ( getDuplicateErrorDetails ( '' /* location cannot conflict */ , portBindings , error ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function getLogStream ( appId , fromLine , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof fromLine , 'number' ) ; // behaves like tail -n
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Getting logs for %s' , appId ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( app . installationState !== appdb . ISTATE _INSTALLED ) return callback ( new AppsError ( AppsError . BAD _STATE , util . format ( 'App is in %s state.' , app . installationState ) ) ) ;
var container = docker . getContainer ( app . containerId ) ;
var tail = fromLine < 0 ? - fromLine : 'all' ;
// note: cannot access docker file directly because it needs root access
container . logs ( { stdout : true , stderr : true , follow : true , timestamps : true , tail : tail } , function ( error , logStream ) {
if ( error && error . statusCode === 404 ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
var lineCount = 0 ;
var skipLinesStream = split ( function mapper ( line ) {
if ( ++ lineCount < fromLine ) return undefined ;
var timestamp = line . substr ( 0 , line . indexOf ( ' ' ) ) ; // sometimes this has square brackets around it
return JSON . stringify ( { lineNumber : lineCount , timestamp : timestamp . replace ( /[[\]]/g , '' ) , log : line . substr ( timestamp . length + 1 ) } ) ;
} ) ;
skipLinesStream . close = logStream . req . abort ;
logStream . pipe ( skipLinesStream ) ;
return callback ( null , skipLinesStream ) ;
} ) ;
} ) ;
}
function getLogs ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Getting logs for %s' , appId ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( app . installationState !== appdb . ISTATE _INSTALLED ) return callback ( new AppsError ( AppsError . BAD _STATE , util . format ( 'App is in %s state.' , app . installationState ) ) ) ;
var container = docker . getContainer ( app . containerId ) ;
// note: cannot access docker file directly because it needs root access
container . logs ( { stdout : true , stderr : true , follow : false , timestamps : true , tail : 'all' } , function ( error , logStream ) {
if ( error && error . statusCode === 404 ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
return callback ( null , logStream ) ;
} ) ;
} ) ;
}
function restore ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will restore app with id:%s' , appId ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-08-19 10:54:39 -07:00
// restore without a backup is the same as re-install
var restoreConfig = app . lastBackupConfig , values = { } ;
if ( restoreConfig ) {
// re-validate because this new box version may not accept old configs.
// if we restore location, it should be validated here as well
error = checkManifestConstraints ( restoreConfig . manifest ) ;
if ( error ) return callback ( new AppsError ( AppsError . BAD _FIELD , 'Manifest cannot be installed: ' + error . message ) ) ;
error = validatePortBindings ( restoreConfig . portBindings , restoreConfig . manifest . tcpPorts ) ; // maybe new ports got reserved now
if ( error ) return callback ( error ) ;
// ## should probably query new location, access restriction from user
values = {
manifest : restoreConfig . manifest ,
portBindings : restoreConfig . portBindings ,
oldConfig : {
location : app . location ,
accessRestriction : app . accessRestriction ,
portBindings : app . portBindings ,
manifest : app . manifest
}
} ;
}
2015-07-20 00:09:47 -07:00
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _RESTORE , values , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
function uninstall ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will uninstall app with id:%s' , appId ) ;
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _UNINSTALL , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ; // since uninstall is allowed from any state, kill current task
callback ( null ) ;
} ) ;
}
function start ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will start app with id:%s' , appId ) ;
appdb . setRunCommand ( appId , appdb . RSTATE _PENDING _START , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
}
function stop ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( 'Will stop app with id:%s' , appId ) ;
appdb . setRunCommand ( appId , appdb . RSTATE _PENDING _STOP , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
}
function checkManifestConstraints ( manifest ) {
2015-08-19 11:08:45 -07:00
if ( ! manifest . dockerImage ) return new Error ( 'Missing dockerImage' ) ; // dockerImage is optional in manifest
2015-07-20 00:09:47 -07:00
if ( semver . valid ( manifest . maxBoxVersion ) && semver . gt ( config . version ( ) , manifest . maxBoxVersion ) ) {
return new Error ( 'Box version exceeds Apps maxBoxVersion' ) ;
}
if ( semver . valid ( manifest . minBoxVersion ) && semver . gt ( manifest . minBoxVersion , config . version ( ) ) ) {
return new Error ( 'minBoxVersion exceeds Box version' ) ;
}
return null ;
}
function exec ( appId , options , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
var cmd = options . cmd || [ '/bin/bash' ] ;
assert ( util . isArray ( cmd ) && cmd . length > 0 ) ;
appdb . get ( appId , function ( error , app ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
var container = docker . getContainer ( app . containerId ) ;
var execOptions = {
AttachStdin : true ,
AttachStdout : true ,
AttachStderr : true ,
Tty : true ,
Cmd : cmd
} ;
container . exec ( execOptions , function ( error , exec ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
var startOptions = {
Detach : false ,
Tty : true ,
stdin : true // this is a dockerode option that enabled openStdin in the modem
} ;
exec . start ( startOptions , function ( error , stream ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
if ( options . rows && options . columns ) {
exec . resize ( { h : options . rows , w : options . columns } , function ( error ) { if ( error ) debug ( 'Error resizing console' , error ) ; } ) ;
}
return callback ( null , stream ) ;
} ) ;
} ) ;
} ) ;
}
function setRestorePoint ( appId , lastBackupId , lastBackupConfig , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof lastBackupId , 'string' ) ;
assert . strictEqual ( typeof lastBackupConfig , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
appdb . update ( appId , { lastBackupId : lastBackupId , lastBackupConfig : lastBackupConfig } , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND , 'No such app' ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
return callback ( null ) ;
} ) ;
}
function autoupdateApps ( updateInfo , callback ) { // updateInfo is { appId -> { manifest } }
assert . strictEqual ( typeof updateInfo , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
function canAutoupdateApp ( app , newManifest ) {
2015-09-10 11:39:03 -07:00
var tcpPorts = newManifest . tcpPorts || { } ;
var portBindings = app . portBindings ; // this is never null
2015-07-20 00:09:47 -07:00
2015-09-10 11:39:03 -07:00
if ( Object . keys ( tcpPorts ) . length === 0 && Object . keys ( portBindings ) . length === 0 ) return null ;
if ( Object . keys ( tcpPorts ) . length === 0 ) return new Error ( 'tcpPorts is now empty but portBindings is not' ) ;
if ( Object . keys ( portBindings ) . length === 0 ) return new Error ( 'portBindings is now empty but tcpPorts is not' ) ;
2015-07-20 00:09:47 -07:00
2015-09-10 11:39:03 -07:00
for ( var env in tcpPorts ) {
if ( ! ( env in portBindings ) ) return new Error ( env + ' is required from user' ) ;
}
// it's fine if one or more keys got removed
return null ;
2015-07-20 00:09:47 -07:00
}
if ( ! updateInfo ) return callback ( null ) ;
async . eachSeries ( Object . keys ( updateInfo ) , function iterator ( appId , iteratorDone ) {
get ( appId , function ( error , app ) {
2015-09-10 11:39:03 -07:00
if ( error ) {
debug ( 'Cannot autoupdate app %s : %s' , appId , error . message ) ;
return iteratorDone ( ) ;
}
error = canAutoupdateApp ( app , updateInfo [ appId ] . manifest ) ;
if ( error ) {
debug ( 'app %s requires manual update. %s' , appId , error . message ) ;
2015-07-20 00:09:47 -07:00
return iteratorDone ( ) ;
}
2015-08-25 10:01:18 -07:00
update ( appId , false /* force */ , updateInfo [ appId ] . manifest , app . portBindings , null /* icon */ , function ( error ) {
2015-09-10 11:39:03 -07:00
if ( error ) debug ( 'Error initiating autoupdate of %s. %s' , appId , error . message ) ;
2015-07-20 00:09:47 -07:00
iteratorDone ( null ) ;
} ) ;
} ) ;
} , callback ) ;
}
2015-09-21 14:34:25 -07:00
function canBackupApp ( app ) {
// only backup apps that are installed or pending configure. Rest of them are in some
// state not good for consistent backup (i.e addons may not have been setup completely)
return ( app . installationState === appdb . ISTATE _INSTALLED && app . health === appdb . HEALTH _HEALTHY ) ||
app . installationState === appdb . ISTATE _PENDING _CONFIGURE ||
app . installationState === appdb . ISTATE _PENDING _BACKUP ||
app . installationState === appdb . ISTATE _PENDING _UPDATE ; // called from apptask
}
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
function reuseOldBackup ( app , callback ) {
assert . strictEqual ( typeof app . lastBackupId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
backups . copyLastBackup ( app , function ( error , newBackupId ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:09:47 -07:00
2015-09-21 16:02:58 -07:00
debugApp ( app , 'reuseOldBackup: reused old backup %s as %s' , app . lastBackupId , newBackupId ) ;
2015-09-21 14:34:25 -07:00
callback ( null , newBackupId ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
function createNewBackup ( app , addonsToBackup , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( ! addonsToBackup || typeof addonsToBackup , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-07-20 00:09:47 -07:00
2015-08-26 09:06:45 -07:00
backups . getBackupUrl ( app , function ( error , result ) {
2015-07-20 00:09:47 -07:00
if ( error && error . reason === BackupsError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
debugApp ( app , 'backupApp: backup url:%s backup id:%s' , result . url , result . id ) ;
async . series ( [
ignoreError ( shell . sudo . bind ( null , 'mountSwap' , [ BACKUP _SWAP _CMD , '--on' ] ) ) ,
2015-07-20 00:50:36 -07:00
addons . backupAddons . bind ( null , app , addonsToBackup ) ,
2015-08-25 12:33:51 -07:00
shell . sudo . bind ( null , 'backupApp' , [ BACKUP _APP _CMD , app . id , result . url , result . backupKey , result . sessionToken ] ) ,
2015-07-20 00:09:47 -07:00
ignoreError ( shell . sudo . bind ( null , 'unmountSwap' , [ BACKUP _SWAP _CMD , '--off' ] ) ) ,
] , function ( error ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-09-21 14:34:25 -07:00
callback ( null , result . id ) ;
} ) ;
} ) ;
}
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
function backupApp ( app , addonsToBackup , callback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert ( ! addonsToBackup || typeof addonsToBackup , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2015-07-20 00:09:47 -07:00
2015-09-21 14:34:25 -07:00
var appConfig = null , backupFunction ;
if ( ! canBackupApp ( app ) ) {
2015-09-21 16:02:58 -07:00
if ( ! app . lastBackupId ) {
debugApp ( app , 'backupApp: cannot backup app' ) ;
return callback ( new AppsError ( AppsError . BAD _STATE , 'App not healthy and never backed up previously' ) ) ;
}
2015-09-21 14:34:25 -07:00
appConfig = app . lastBackupConfig ;
backupFunction = reuseOldBackup . bind ( null , app ) ;
} else {
appConfig = {
manifest : app . manifest ,
location : app . location ,
portBindings : app . portBindings ,
accessRestriction : app . accessRestriction
} ;
backupFunction = createNewBackup . bind ( null , app , addonsToBackup ) ;
if ( ! safe . fs . writeFileSync ( path . join ( paths . DATA _DIR , app . id + '/config.json' ) , JSON . stringify ( appConfig ) , 'utf8' ) ) {
return callback ( safe . error ) ;
}
}
backupFunction ( function ( error , backupId ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
debugApp ( app , 'backupApp: successful id:%s' , backupId ) ;
setRestorePoint ( app . id , backupId , appConfig , function ( error ) {
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
return callback ( null , backupId ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}
function backup ( appId , callback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
get ( appId , function ( error , app ) {
if ( error && error . reason === AppsError . NOT _FOUND ) return callback ( new AppsError ( AppsError . NOT _FOUND ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
appdb . setInstallationCommand ( appId , appdb . ISTATE _PENDING _BACKUP , function ( error ) {
if ( error && error . reason === DatabaseError . NOT _FOUND ) return callback ( new AppsError ( AppsError . BAD _STATE ) ) ; // might be a bad guess
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
taskmanager . restartAppTask ( appId ) ;
callback ( null ) ;
} ) ;
} ) ;
}
2015-07-20 00:50:36 -07:00
function restoreApp ( app , addonsToRestore , callback ) {
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2015-07-20 00:50:36 -07:00
assert . strictEqual ( typeof addonsToRestore , 'object' ) ;
2015-07-20 00:09:47 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
assert ( app . lastBackupId ) ;
backups . getRestoreUrl ( app . lastBackupId , function ( error , result ) {
if ( error && error . reason == BackupsError . EXTERNAL _ERROR ) return callback ( new AppsError ( AppsError . EXTERNAL _ERROR , error . message ) ) ;
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
debugApp ( app , 'restoreApp: restoreUrl:%s' , result . url ) ;
2015-08-26 09:14:15 -07:00
shell . sudo ( 'restoreApp' , [ RESTORE _APP _CMD , app . id , result . url , result . backupKey , result . sessionToken ] , function ( error ) {
2015-07-20 00:09:47 -07:00
if ( error ) return callback ( new AppsError ( AppsError . INTERNAL _ERROR , error ) ) ;
2015-07-20 00:50:36 -07:00
addons . restoreAddons ( app , addonsToRestore , callback ) ;
2015-07-20 00:09:47 -07:00
} ) ;
} ) ;
}