You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

349 lines
11 KiB

/*
* Testrunner client for simple commit jobs
*
* Node.js based default client intended for Linux, OSX, and Windows.
*/
var fs = require('fs');
var path = require('path');
var yaml = require('yamljs');
var Promise = require('bluebird');
var http = require('http');
var https = require('https');
var tmp = require('tmp');
var child_process = require('child_process');
var crypto = require('crypto');
/*
* Command line and config parsing
*/
var argv = require('minimist')(process.argv.slice(2));
console.log('Command line options: ' + JSON.stringify(argv));
var configFile = argv.config || './config.yaml';
console.log('Load config: ' + configFile);
var clientConfig = yaml.load(configFile);
/*
* Misc utils
*/
function sha1sum(x) {
return crypto.createHash('sha1').update(x).digest('hex');
}
function sha1sumFile(x) {
return sha1sum(fs.readFileSync(x));
}
function assert(x) {
if (x) { return x; }
throw new Error('assertion failed');
}
/*
* HTTP(S) helpers
*/
var serverTrustRoot = fs.readFileSync(clientConfig.serverCertificate);
// Path -> file data
var fileCache = {};
function getBasicAuthHeader() {
return 'Basic ' + new Buffer(assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword)).toString('base64');
}
function getUserAgent() {
return 'testrunner-client ' + clientConfig.clientName;
}
function serverAuthCheck(res) {
var auth = res.headers['x-testrunner-authenticator']; // lowercase intentionally
if (typeof auth !== 'string' ||
auth !== assert(clientConfig.serverAuthPassword)) {
throw new Error('server not authorized');
}
}
function postJson(path, request, cb) {
return new Promise(function (resolve, reject) {
var requestData = new Buffer(JSON.stringify(request), 'utf8');
var headers = {
'User-Agent': getUserAgent(),
'Authorization': getBasicAuthHeader(),
'Content-Type': 'application/json',
'Content-Length': requestData.length
};
var options = {
host: clientConfig.serverHost,
port: clientConfig.serverPort,
ca: serverTrustRoot,
rejectUnauthorized: true,
path: path,
method: 'POST',
auth: assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword),
headers: headers
};
console.log('sending POST ' + path);
var req = https.request(options, function (res) {
//res.setEncoding('utf8'); // we just want binary
var buffers = [];
res.on('data', function (data) {
try {
buffers.push(data);
} catch (e) {
console.log(e);
}
});
res.on('end', function () {
try {
serverAuthCheck(res);
var data = Buffer.concat(buffers);
var rep = JSON.parse(data.toString('utf8'));
} catch (e) {
reject(e);
return;
}
resolve(rep);
});
});
req.on('error', function (err) {
reject(err);
});
req.write(requestData);
req.end();
});
}
function getFile(path, cb) {
return new Promise(function (resolve, reject) {
var headers = {
'User-Agent': getUserAgent(),
'Authorization': getBasicAuthHeader()
};
var options = {
host: clientConfig.serverHost,
port: clientConfig.serverPort,
ca: serverTrustRoot,
rejectUnauthorized: true,
path: path,
method: 'GET',
auth: assert(clientConfig.clientAuthUsername) + ':' + assert(clientConfig.clientAuthPassword),
headers: headers
};
console.log('sending GET ' + path);
var req = https.request(options, function (res) {
//res.setEncoding('utf8'); // we just want binary
var buffers = [];
res.on('data', function (data) {
try {
buffers.push(data);
} catch (e) {
console.log(e);
}
});
res.on('end', function () {
try {
serverAuthCheck(res);
var data = Buffer.concat(buffers);
} catch (e) {
reject(e);
return;
}
resolve(data);
});
});
req.on('error', function (err) {
reject(err);
});
req.end();
});
}
function cachedGetFile(path) {
if (fileCache[path]) {
return new Promise(function (resolve, reject) {
resolve(fileCache[path]);
});
} else {
return getFile(path).then(function (data) {
fileCache[path] = data;
return data;
});
}
}
/*
* Simple commit jobs
*/
function processSimpleScriptJob(rep) {
var context, repo, repoFull, repoCloneUrl, sha, scriptcmd;
var tmpDir;
var tmpDir = tmp.dirSync({
mode: 0750,
prefix: 'testrunner-' + (rep.context || 'undefined') + '-',
unsafeCleanup: true
});
return new Promise(function (resolve, reject) {
var i;
context = assert(rep.context);
repo = assert(rep.repo);
repoFull = assert(rep.repo_full);
repoCloneUrl = assert(rep.repo_clone_url);
sha = assert(rep.sha);
for (i = 0; i < clientConfig.supportedContexts.length; i++) {
if (clientConfig.supportedContexts[i].context === assert(rep.context)) {
scriptcmd = clientConfig.supportedContexts[i].command;
}
}
if (!scriptcmd) {
reject(new Error('unsupported context: ' + context));
return;
}
console.log('start simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context);
var args = [].concat(scriptcmd);
args = args.concat([
'--repo-full-name', assert(repoFull),
'--repo-clone-url', assert(repoCloneUrl),
'--commit-name', assert(sha),
'--context', assert(context),
'--temp-dir', assert(tmpDir.name),
'--repo-snapshot-dir', assert(clientConfig.repoSnapshotDir)
]);
// XXX: child timeout and recovery
var cld = child_process.spawn(args[0], args.slice(1), { cwd: '/tmp' });
var buffers = [];
cld.stdout.on('data', function (data) {
buffers.push(data);
});
cld.stderr.on('data', function (data) {
buffers.push(data); // just interleave stdout/stderr for now
});
cld.on('close', function (code) {
resolve({ code: code, data: Buffer.concat(buffers) });
});
}).then(function (res) {
// Test execution finished, test case may have succeeded or failed.
console.log('finished simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context + ', code ' + res.code);
//console.log(res.data.toString());
// Initial heuristic for providing descriptions from test script,
// could also use an explicit output file. Match last occurrence
// on purpose so that the script can always override a previous
// status if necessary.
var desc = null;
try {
desc = (function () {
var txt = res.data.toString('utf8'); // XXX: want something lenient
var re = /^TESTRUNNER_DESCRIPTION: (.*?)$/gm;
var m;
var out = null;
while ((m = re.exec(txt)) !== null) {
out = m[1];
}
return out;
})();
} catch (e) {
console.log(e);
}
postJson('/finish-commit-simple', {
repo: assert(repo),
repo_full: assert(repoFull),
repo_clone_url: assert(repoCloneUrl),
sha: assert(sha),
context: assert(context),
state: res.code === 0 ? 'success' : 'failure',
description: desc || (res.code === 0 ? 'Success' : 'Failure'),
text: res.data.toString('base64')
}).then(function (rep) {
//console.log(rep);
}).catch(function (err) {
console.log(err); // XXX: chain error
});
}).catch(function (err) {
// Test execution attempt failed (which is different from a test case
// failing).
console.log('FAILED simple commit test, repo ' + repoCloneUrl + ', sha ' + sha + ', context ' + context);
console.log(err);
var data = new Buffer(String(err.stack || err));
// XXX: indicate possibly transient nature of error
postJson('/finish-commit-simple', {
client_name: clientConfig.clientName,
repo: assert(repo),
repo_full: assert(repoFull),
repo_clone_url: assert(repoCloneUrl),
sha: assert(sha),
context: assert(context),
state: 'failure',
description: 'Failed: ' + String(err),
text: data.toString('base64')
}).then(function (rep) {
//console.log(rep);
}).catch(function (err) {
console.log(err); // XXX: chain error
});
}).finally(function () {
if (tmpDir) {
tmpDir.removeCallback();
console.log('cleaned up ' + tmpDir.name);
}
});
}
function requestAndExecute() {
var contexts = [];
var i;
for (i = 0; i < clientConfig.supportedContexts.length; i++) {
contexts.push(clientConfig.supportedContexts[i].context);
}
postJson('/get-commit-simple', {
client_name: clientConfig.clientName,
contexts: contexts
}).then(function (rep) {
console.log(rep);
if (rep.error_code) {
throw new Error('job failed; error code ' + rep.error_code + ': ' + rep.error_description);
}
return processSimpleScriptJob(rep);
}).then(function () {
setTimeout(function () { requestAndExecute(); }, clientConfig.sleepJobSuccess);
}).catch(function (err) {
// XXX: differentiate between TIMEOUT and other errors
console.log('job failed; sleep a bit and retry:', err);
console.log(err.stack);
setTimeout(function () { requestAndExecute(); }, clientConfig.sleepJobFailure);
});
setTimeout(function () {}, 10000000); // avoid exit
}
function main() {
requestAndExecute();
}
main();