mirror of
https://github.com/caprover/caprover
synced 2026-05-04 02:30:30 +00:00
detached build initial commit
This commit is contained in:
@@ -10,6 +10,8 @@ let apiStatusCode = {
|
||||
|
||||
STATUS_OK: 100,
|
||||
|
||||
STATUS_OK_DEPLOY_STARTED: 101,
|
||||
|
||||
STATUS_ERROR_GENERIC: 1000,
|
||||
|
||||
STATUS_ERROR_CAPTAIN_NOT_INITIALIZED: 1001,
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.');
|
||||
|
||||
Reference in New Issue
Block a user