detached build initial commit

This commit is contained in:
Kasra Bigdeli
2018-01-06 17:46:13 -08:00
parent d0fa23265f
commit cfbb386987
5 changed files with 317 additions and 57 deletions
+2
View File
@@ -10,6 +10,8 @@ let apiStatusCode = {
STATUS_OK: 100,
STATUS_OK_DEPLOY_STARTED: 101,
STATUS_ERROR_GENERIC: 1000,
STATUS_ERROR_CAPTAIN_NOT_INITIALIZED: 1001,
+56 -18
View File
@@ -6,6 +6,17 @@ const CaptainConstants = require('../utils/CaptainConstants');
const Logger = require('../utils/Logger');
const EnvVars = require('../utils/EnvVars');
function safeParseChuck(chunk) {
try {
return JSON.parse(chunk);
}
catch (ignore) {
return {
stream: 'Cannot parse ' + chunk
}
}
}
class DockerApi {
constructor(connectionParams) {
@@ -174,7 +185,7 @@ class DockerApi {
});
}
buildImageFromDockerFile(imageName, newVersion, tarballFilePath) {
buildImageFromDockerFile(imageName, newVersion, tarballFilePath, buildLogs) {
const self = this;
@@ -191,10 +202,6 @@ class DockerApi {
return new Promise(function (resolve, reject) {
let errorMessage = '';
let logsBeforeError = [];
for (let i = 0; i < 20; i++) {
logsBeforeError.push('');
}
stream.setEncoding('utf8');
@@ -202,21 +209,22 @@ class DockerApi {
stream.on('data', function (chunk) {
Logger.dev('stream data ' + chunk);
chunk = JSON.parse(chunk);
chunk = safeParseChuck(chunk);
let chuckStream = chunk.stream;
if (chuckStream) {
// Logger.dev('stream data ' + chuckStream);
logsBeforeError.shift();
logsBeforeError.push(chuckStream);
buildLogs.log(chuckStream);
}
if (chunk.error) {
Logger.e(chunk.error);
Logger.e(JSON.stringify(chunk.errorDetail));
errorMessage += '\n [truncated] \n';
errorMessage += logsBeforeError.join('');
let errorDetails = JSON.stringify(chunk.errorDetail);
Logger.e(errorDetails);
buildLogs.log(errorDetails);
buildLogs.log(chunk.error);
errorMessage += '\n';
errorMessage += errorDetails;
errorMessage += chunk.error;
}
});
@@ -279,7 +287,7 @@ class DockerApi {
stream.on('data', function (chunk) {
Logger.dev('stream data ' + chunk);
chunk = JSON.parse(chunk);
chunk = safeParseChuck(chunk);
let chuckStream = chunk.stream;
if (chuckStream) {
@@ -433,7 +441,7 @@ class DockerApi {
});
}
pushImage(imageName, newVersion, authObj) {
pushImage(imageName, newVersion, authObj, buildLogs) {
const self = this;
@@ -454,23 +462,53 @@ class DockerApi {
})
.then(function (stream) {
return new Promise(function (resolve) {
return new Promise(function (resolve, reject) {
let errorMessage = '';
stream.setEncoding('utf8');
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
stream.on('data', function (chunk) {
// THIS BLOCK HAS TO BE HERE. "end" EVENT WON'T GET CALLED OTHERWISE.
// ('stream data ' + chunk);
});
Logger.dev('stream data ' + chunk);
chunk = safeParseChuck(chunk);
let chuckStream = chunk.stream;
if (chuckStream) {
// Logger.dev('stream data ' + chuckStream);
buildLogs.log(chuckStream);
}
if (chunk.error) {
Logger.e(chunk.error);
let errorDetails = JSON.stringify(chunk.errorDetail);
Logger.e(errorDetails);
buildLogs.log(errorDetails);
buildLogs.log(chunk.error);
errorMessage += '\n';
errorMessage += errorDetails;
errorMessage += chunk.error;
}
});
// stream.pipe(process.stdout, {end: true});
// IncomingMessage
// https://nodejs.org/api/stream.html#stream_event_end
stream.on('end', function () {
if (errorMessage) {
reject(errorMessage);
return;
}
resolve();
});
});
stream.on('error', function (chunk) {
errorMessage += chunk;
});
});
});
}
+88 -29
View File
@@ -8,6 +8,39 @@ const fs = require('fs-extra');
const TEMP_UPLOAD = 'temp_upload/';
const upload = multer({dest: TEMP_UPLOAD});
router.get('/:appName/', function (req, res, next) {
let appName = req.params.appName;
let serviceManager = res.locals.user.serviceManager;
return Promise.resolve()
.then(function () {
return serviceManager.getBuildStatus(appName);
})
.then(function (data) {
let baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, 'App build status retrieved');
baseApi.data = data;
res.send(baseApi);
})
.catch(function (error) {
Logger.e(error);
if (error && error.captainErrorType) {
res.send(new BaseApi(error.captainErrorType, error.apiMessage));
return;
}
res.sendStatus(500);
});
});
router.post('/:appName/', function (req, res, next) {
let dataStore = res.locals.user.dataStore;
@@ -39,6 +72,8 @@ router.post('/:appName/', upload.single('sourceFile'), function (req, res, next)
let dataStore = res.locals.user.dataStore;
let serviceManager = res.locals.user.serviceManager;
let isDetachedBuild = !!req.query.detached;
console.log('---------------' + JSON.stringify(req.query));
let appName = req.params.appName;
@@ -86,34 +121,19 @@ router.post('/:appName/', upload.single('sourceFile'), function (req, res, next)
})
.then(function () {
return serviceManager.createImage(appName, {
pathToSrcTarballFile: tarballSourceFilePath
}, gitHash);
})
.then(function (version) {
fs.removeSync(tarballSourceFilePath);
return version;
})
.catch(function (error) {
return new Promise(function (resolve, reject) {
fs.removeSync(tarballSourceFilePath);
reject(error);
})
})
.then(function (version) {
return serviceManager.ensureServiceInitedAndUpdated(appName, version);
})
.then(function () {
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, 'App Data Saved'));
if (isDetachedBuild) {
res.send(new BaseApi(ApiStatusCodes.STATUS_OK_DEPLOY_STARTED, 'Deploy is started'));
startBuildProcess()
.catch(function (error) {
Logger.e(error);
});
}
else {
return startBuildProcess()
.then(function () {
res.send(new BaseApi(ApiStatusCodes.STATUS_OK, 'Deploy is done'));
});
}
})
.catch(function (error) {
@@ -130,8 +150,47 @@ router.post('/:appName/', upload.single('sourceFile'), function (req, res, next)
res.send(new BaseApi(ApiStatusCodes.STATUS_ERROR_GENERIC, error.stack + ''));
try {
fs.removeSync(tarballSourceFilePath);
} catch (ignore) {
}
});
function startBuildProcess() {
return serviceManager
.createImage(appName, {
pathToSrcTarballFile: tarballSourceFilePath
}, gitHash)
.then(function (version) {
fs.removeSync(tarballSourceFilePath);
return version;
})
.catch(function (error) {
return new Promise(function (resolve, reject) {
fs.removeSync(tarballSourceFilePath);
reject(error);
})
})
.then(function (version) {
return serviceManager.ensureServiceInitedAndUpdated(appName, version);
})
.catch(function (error) {
return new Promise(function (resolve, reject) {
serviceManager.logBuildFailed(appName, error);
reject(error);
})
});
}
});
module.exports = router;
+67 -2
View File
@@ -10,6 +10,7 @@ const Authenticator = require('./Authenticator');
const GitHelper = require('../utils/GitHelper');
const uuid = require('uuid/v4');
const BUILD_LOG_SIZE = 50;
const SOURCE_FOLDER_NAME = 'src';
const DOCKER_FILE = 'Dockerfile';
const CAPTAIN_DEFINITION_FILE = 'captain-definition';
@@ -32,6 +33,47 @@ function getCaptainDefinitionTempFolder(serviceName, randomSuffix) {
return CaptainConstants.captainDefinitionTempDir + '/' + serviceName + '/' + randomSuffix;
}
class BuildLog {
constructor(size) {
this.size = size;
this.clear();
}
onBuildFailed(error) {
this.log('Deploy failed!');
this.log(error);
this.isBuildFailed = true;
}
clear() {
this.isBuildFailed = false;
this.firstLineNumber = -this.size;
this.lines = [];
for (let i = 0; i < this.size; i++) {
this.lines.push('');
}
}
log(msg) {
msg = (msg || '') + '';
this.lines.shift();
this.lines.push(msg);
this.firstLineNumber++;
}
getLogs() {
const self = this;
// if we don't copy the object, "lines" can get changed but firstLineNumber stay as is, causing bug!
return JSON.parse(JSON.stringify({
lines: self.lines,
firstLineNumber: self.firstLineNumber
}));
}
}
class ServiceManager {
constructor(user, dockerApi, loadBalancerManager) {
@@ -40,6 +82,7 @@ class ServiceManager {
this.dockerApi = dockerApi;
this.loadBalancerManager = loadBalancerManager;
this.activeBuilds = {};
this.buildLogs = {};
this.isReady = true;
@@ -115,6 +158,11 @@ class ServiceManager {
let dockerFilePath = null;
this.activeBuilds[appName] = true;
this.buildLogs[appName] = this.buildLogs[appName] || new BuildLog(BUILD_LOG_SIZE);
this.buildLogs[appName].clear();
this.buildLogs[appName].log('------------------------- ' + (new Date()));
this.buildLogs[appName].log('Build started for ' + appName);
return Promise.resolve()
.then(function () {
@@ -298,7 +346,7 @@ class ServiceManager {
.then(function () {
return dockerApi
.buildImageFromDockerFile(imageName, newVersion, tarballFilePath)
.buildImageFromDockerFile(imageName, newVersion, tarballFilePath, self.buildLogs[appName])
.catch(function (error) {
throw ApiStatusCodes.createError(ApiStatusCodes.BUILD_ERROR, ('' + error).trim());
})
@@ -328,7 +376,7 @@ class ServiceManager {
Logger.d('Docker Auth is found. Pushing the image...');
return dockerApi
.pushImage(imageName, newVersion, authObj);
.pushImage(imageName, newVersion, authObj, self.buildLogs[appName]);
})
.then(function () {
@@ -770,6 +818,23 @@ class ServiceManager {
return !!this.activeBuilds[appName];
}
getBuildStatus(appName) {
const self = this;
this.buildLogs[appName] = this.buildLogs[appName] || new BuildLog(BUILD_LOG_SIZE);
return {
isAppBuilding: self.isAppBuilding(appName),
logs: self.buildLogs[appName].getLogs(),
isBuildFailed: self.buildLogs[appName].isBuildFailed
}
}
logBuildFailed(appName, error) {
error = (error || '') + '';
this.buildLogs[appName] = this.buildLogs[appName] || new BuildLog(BUILD_LOG_SIZE);
this.buildLogs[appName].onBuildFailed(error);
}
updateServiceOnDefinitionUpdate(appName) {
let serviceName = this.dataStore.getServiceName(appName);
+104 -8
View File
@@ -131,7 +131,6 @@ function savePropForDirectory(propType, propValue) {
function getDefaultMachine() {
let machine = getPropForDirectory(MACHINE_TO_DEPLOY);
console.log(machine);
if (machine) {
return machine.name;
}
@@ -227,15 +226,15 @@ if (!program.default || defaultInvalid) {
function deployTo(machineToDeploy, branchToPush, appName) {
if (!commandExistsSync('git')) {
console.log(chalk.red('"git" command not found...'));
console.log(chalk.red('Captain needs "git" to create zip file of your source files...'));
console.log(chalk.red('Captain needs "git" to create tar file of your source files...'));
console.log(' ');
process.exit(1);
}
let zipFileNameToDeploy = 'temporary-captain-to-deploy.zip';
let zipFileNameToDeploy = 'temporary-captain-to-deploy.tar';
let zipFileFullPath = path.join(process.cwd(), zipFileNameToDeploy);
console.log('Saving zip file to:');
console.log('Saving tar file to:');
console.log(zipFileFullPath);
console.log(' ');
@@ -298,7 +297,7 @@ function sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, b
let options = {
url: machineToDeploy.baseUrl + '/api/v1/user/appData/' + appName,
url: machineToDeploy.baseUrl + '/api/v1/user/appData/' + appName + '/?detached=1',
headers: {
'x-namespace': 'captain',
'x-captain-auth': machineToDeploy.authToken
@@ -337,7 +336,7 @@ function sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, b
return;
}
if (data.status !== 100) {
if (data.status !== 100 && data.status !== 101) {
throw new Error(JSON.stringify(data, null, 2));
}
@@ -345,8 +344,14 @@ function sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, b
savePropForDirectory(BRANCH_TO_PUSH, branchToPush);
savePropForDirectory(MACHINE_TO_DEPLOY, machineToDeploy);
console.log(chalk.green('Deployed successful: ') + appName);
console.log(' ');
if (data.status === 100) {
console.log(chalk.green('Deployed successful: ') + appName);
console.log(' ');
} else if (data.status === 101) {
console.log(chalk.green('Building started: ') + appName);
console.log(' ');
startFetchingBuildLogs(machineToDeploy, appName);
}
return;
}
@@ -384,6 +389,97 @@ function sendFileToCaptain(machineToDeploy, zipFileFullPath, appName, gitHash, b
}
var lastLineNumberPrinted = -10000; // we want to show all lines to begin with!
function startFetchingBuildLogs(machineToDeploy, appName) {
let options = {
url: machineToDeploy.baseUrl + '/api/v1/user/appData/' + appName,
headers: {
'x-namespace': 'captain',
'x-captain-auth': machineToDeploy.authToken
},
method: 'GET'
};
function onLogRetrieved(data) {
if (data) {
var lines = data.logs.lines;
var firstLineNumberOfLogs = data.logs.firstLineNumber;
var firstLinesToPrint = 0;
if (firstLineNumberOfLogs > lastLineNumberPrinted) {
if (firstLineNumberOfLogs < 0) {
// This is the very first fetch, probably firstLineNumberOfLogs is around -50
firstLinesToPrint = -firstLineNumberOfLogs;
} else {
console.log('[[ TRUNCATED ]]');
}
} else {
firstLinesToPrint = lastLineNumberPrinted - firstLineNumberOfLogs;
}
lastLineNumberPrinted = firstLineNumberOfLogs + lines.length;
for (var i = firstLinesToPrint; i < lines.length; i++) {
console.log((lines[i] || '').trim());
}
}
if (data && !data.isAppBuilding) {
console.log(' ');
if (!data.isBuildFailed) {
console.log(chalk.green('Deployed successful: ') + appName);
console.log(chalk.magenta('App is available at ') + (machineToDeploy.baseUrl.replace('//captain.', '//' + appName + '.')));
} else {
console.error(chalk.red('\nSomething bad happened. Cannot deploy "' + appName + '"\n'));
}
console.log(' ');
return;
}
setTimeout(function () {
startFetchingBuildLogs(machineToDeploy, appName);
}, 2000);
}
function callback(error, response, body) {
try {
if (!error && response.statusCode === 200) {
let data = JSON.parse(body);
if (data.status !== 100) {
throw new Error(JSON.stringify(data, null, 2));
}
onLogRetrieved(data.data);
return;
}
if (error) {
throw new Error(error)
}
throw new Error(response ? JSON.stringify(response, null, 2) : 'Response NULL');
} catch (error) {
console.error(chalk.red('\nSomething while retrieving app build logs.. "' + error + '"\n'));
onLogRetrieved(null);
}
}
request(options, callback);
}
function requestLogin(serverName, serverAddress, loginCallback) {
console.log('Your auth token is not valid anymore. Try to login again.');