blob: f03a698275d26afd401bb9b359d726be54ecd003 [file] [log] [blame]
var uuid = require('uuid'),
openwhisk = require('openwhisk'),
invokerPackageNamespace = 'nickm@us.ibm.com_canary-advisor', // this is currently housed in one of nick's namespace
invokerPackageName = 'owdbg',
invokerActionName = 'invoker',
invoker = invokerPackageName + '/' + invokerActionName,
api = {
host: 'https://openwhisk.ng.bluemix.net',
path: '/api/v1'
};
/** the dictionary of live attachments to actions */
var attached = {}, chainAttached = {};
var created = {};
function echoContinuation(entity, entityNamespace) {
return {
annotations: [{ key: 'debug', value: '/' + entityNamespace + '/' + entity }],
exec: {
kind: 'nodejs',
code: 'function main(params) { return params; }'
}
};
}
/**
* Initialize a connection mediator to openwhisk
*
*/
function setupOpenWhisk(wskprops) {
var key = wskprops['AUTH'];
var namespace = wskprops['NAMESPACE'];
var ow = openwhisk({
api: api.host + api.path,
api_key: key,
namespace: namespace
});
return ow;
}
/**
* Log an error, and continue
*
*/
function errorWhile(inOperation, callback) {
return function(err) {
console.error('Error ' + inOperation);
console.error(err);
callback && callback();
};
}
function ok(next) {
return function() {
console.log('ok');
next();
};
}
function ok_(next) {
ok(next)();
}
/**
*
* @return a new unique name for an entity
*/
var Namer = {
prefix: '___debug___',
name: function name(extra) {
return Namer.prefix + (extra ? extra + '-' : '') + uuid.v4();
},
isDebugArtifact: function(name) {
return name.indexOf(Namer.prefix) == 0;
}
};
exports.list = function list(wskprops, callback, type) {
var ow = setupOpenWhisk(wskprops);
_list(ow, callback, type);
};
function _list(ow, callback, type) {
ow[type || 'actions']
.list({ limit: 200 })
.then(function onList(L) { callback(L, ow); },
errorWhile('fetching actions', callback));
}
exports.listToConsole = function listToConsole(wskprops, options, next) {
if (options.help) return next();
console.log('Available actions:'.blue);
function print(actions) {
actions
.filter(action => options && options.full || !Namer.isDebugArtifact(action.name))
.forEach(action => console.log(' ', action.name[created[action.name] ? 'green' : 'reset']));
ok_(next);
}
exports.list(wskprops, print);
};
/**
* Create an action
*
*/
exports.create = function create(wskprops, next, name) {
var questions = [];
if (!name) {
questions.push({ name: 'name', message: 'Choose a name for your new action' });
}
questions.push({ name: 'kind', type: 'list',
message: 'Which runtime do you want to use?',
choices: ['nodejs', 'swift', 'python' ]
});
questions.push({ name: 'code', type: 'editor',
message: 'Please provide the function body for your new action',
default: function(response) {
if (response.kind == 'nodejs') return 'function main(params) {\n return { message: \'hello\' };\n}\n'
else if (response.kind == 'swift') return 'func main(args: [String:Any]) -> [String:Any] {\n return [ "message" : "Hello world" ]\n}\n'
else return 'import sys\n\ndef main(dict):\n return { \'message\': \'Hello world\' }\n'
}
});
require('inquirer')
.prompt(questions)
.then(response => {
return setupOpenWhisk(wskprops).actions.create({
actionName: name || response.name,
action: {
exec: {
kind: response.kind,
code: response.code
}
}
});
})
.then((action) => created[action.name] = true)
.then(ok(next), errorWhile('creating action', next));
};
/**
* Delete an action
*
*/
exports.deleteAction = function deleteAction(wskprops, next, name) {
var ow = setupOpenWhisk(wskprops);
function doDelete(name) {
ow.actions.delete({ actionName: name })
.then((action) => delete created[action.name])
.then(ok(next), errorWhile('deleting action', next));
}
if (!name) {
_list(ow, function(L) {
require('inquirer')
.prompt([{ name: 'name', type: 'list',
message: 'Which action do you wish to delete',
choices: L.map(function(action) { return action.name; })
}])
.then(function(response) { doDelete(response.name); });
});
} else {
doDelete(name);
}
};
/**
* Clean up any residual debugging artifacts
*
*/
exports.clean = function clean(wskprops, next) {
function cleanType(type) {
var types = type + 's';
// console.log('Cleaning ' + types);
return new Promise(function(resolve, reject) {
exports.list(wskprops, function onList(entities, ow) {
var toClean = entities.filter(function(entity) {
return Namer.isDebugArtifact(entity.name);
});
var counter = toClean.length;
if (counter == 0) {
return resolve(toClean.length);
}
function countDown() {
if (--counter == 0) {
resolve(toClean.length);
}
}
toClean.forEach(function(entity) {
var params = {};
params[type + 'Name'] = entity.name;
ow[types].delete(params).then(countDown,
errorWhile('cleaning ' + types, countDown));
});
}, types);
});
}
Promise.all([cleanType('action'),
cleanType('trigger'),
cleanType('package')
])
.then(function() {
cleanType('rule')
.then(ok(next),
errorWhile('cleaning rules', next))
}, errorWhile('cleaning actions and triggers', next));
};
function createUpstreamAdapterNames(continuationName) {
return {
ruleName: Namer.name('continuation-rule'),
triggerName: Namer.name('continuation-trigger'),
continuationName: continuationName || Namer.name('continuation-action'),
createContinuationPlease: !continuationName,
debugStubName: Namer.name('stub')
};
}
function createUpstreamAdapter(ow, actionBeingDebugged, actionBeingDebuggedNamespace, names) {
try {
var work = [
ow.triggers.create(names),
ow.packages.create({ packageName: names.debugStubName,
package: {
binding: {
namespace: invokerPackageNamespace,
name: invokerPackageName
},
parameters: [{ key: 'action', value: actionBeingDebugged },
{ key: 'namespace', value: actionBeingDebuggedNamespace },
{ key: 'onDone_trigger', value: names.triggerName }
]
}
})
];
if (names.createContinuationPlease) {
work.push(ow.actions.create({ actionName: names.continuationName, action: echoContinuation(actionBeingDebugged,
actionBeingDebuggedNamespace) }));
}
return Promise.all(work)
.then(() => ow.rules.create({ ruleName: names.ruleName, trigger: names.triggerName, action: names.continuationName }),
errorWhile('creating upstream adapter part 1'))
.then(() => names, errorWhile('creating upstream adapter part 2'));
} catch (e) {
console.error(e);
}
}
/**
* Create a rule splice
*/
function splice(ow, entity, entityNamespace, next) {
try {
var names = attached[entity] = {
debugStubName: Namer.name('stub'),
triggerName: Namer.name('continuation-trigger'),
continuationName: Namer.name('continuation-action'),
ruleName: Namer.name('continuation-rule')
};
Promise.all([ow.triggers.create(names),
ow.actions.create({ actionName: names.continuationName, action: echoContinuation(entity, entityNamespace) }),
ow.packages.create({ packageName: names.debugStubName,
package: {
binding: {
namespace: invokerPackageNamespace,
name: invokerPackageName
},
parameters: [{ key: 'action', value: entity },
{ key: 'namespace', value: entityNamespace },
{ key: 'onDone_trigger', value: names.triggerName }
]
}
})
])
.then(function onSuccessOfPart1() {
ow.rules
.create({ ruleName: names.ruleName, trigger: names.triggerName, action: names.continuationName })
.then(function() { next(names); },
errorWhile('attaching to action', next));
}, errorWhile('attaching to action', next));
} catch (e) {
console.error(e);
next();
}
}
function sequenceUses(maybeUsingEntity, entity, entityNamespace) {
var fqn = '/' + entityNamespace + '/' + entity;
return maybeUsingEntity.name !== entity
&& maybeUsingEntity.exec && maybeUsingEntity.exec.kind == 'sequence'
&& maybeUsingEntity.exec.components && maybeUsingEntity.exec.components.find(function(c) {
return c === fqn;
});
}
function beforeSpliceSplitter(element, replacement, A) { A = A.slice(0, A.indexOf(element)); A.push(replacement); return A; }
function afterSpliceSplitter(element, tackOnTheEnd, A) { A = A.slice(A.indexOf(element) + 1); return A; }
function makeSequenceSplicePart(ow, name, sequence, splitter) {
var opts = {
actionName: name,
action: {
exec: {
kind: sequence.exec.kind,
code: '',
components: splitter(sequence.exec.components)
}
}
};
return ow.actions.create(opts);
}
function spliceSequence(ow, sequence, entity, entityNamespace, names) {
try {
var finalBit = undefined/*{
actionName: Namer.name('action'),
action: echoContinuation(entity, entityNamespace, spliceNames.onDone_trigger)
};*/
var fqn = '/' + entityNamespace + '/' + entity;
var afterSpliceContinuation = Namer.name('sequence-splice-after');
var upstreamAdapterNames = createUpstreamAdapterNames(afterSpliceContinuation);
var beforeSpliceUpstream = '/' + entityNamespace + '/' + upstreamAdapterNames.debugStubName + '/' + invokerActionName;
//var afterSpliceContinuation = '/' + entityNamespace + '/' + upstreamAdapterNames.continuationName;
return Promise.all([
makeSequenceSplicePart(ow,
Namer.name('sequence-splice-before'),
sequence,
beforeSpliceSplitter.bind(undefined, fqn, beforeSpliceUpstream)), // before: _/--upstream
makeSequenceSplicePart(ow,
afterSpliceContinuation,
sequence,
afterSpliceSplitter.bind(undefined, fqn, finalBit)) // after: -\__continuation
]).then((beforeAndAfter) => { // a destructuring bind would clean this up
// after the breakpoint, continue with the afterSplice
return createUpstreamAdapter(ow, entity, entityNamespace, upstreamAdapterNames)
.then(() => {
//
// this sequence splice uses its own downstream trigger, not the generic one from the action splice
//
return chainAttached[sequence.name] = {
before: beforeAndAfter[0].name,
after: beforeAndAfter[1].name,
triggerName: upstreamAdapterNames.triggerName
};
}, errorWhile('creating upstream adapter'));
}, errorWhile('splicing sequence'));
} catch (e) {
console.error(e);
}
}
/**
* Attach to the given entity, allowing for debugging its invocations
*
*/
exports.attach = function attach(wskprops, options, next, entity) {
if (options.help) return next();
console.log('Attaching'.blue + ' to ' + entity);
try {
var entityNamespace = wskprops['NAMESPACE'];
var ow = setupOpenWhisk(wskprops);
console.log(' Creating action trampoline'.green);
splice(ow, entity, entityNamespace, function afterSplice(names) {
if (options && options['action-only']) {
//
// user asked not to instrument any rules or sequences
//
return next();
}
_list(ow, function onList(entities) {
var counter = entities.length;
function countDown(names) {
if (--counter <= 0) {
ok_(next);
}
}
entities.forEach(function(otherEntity) {
if (otherEntity.name === entity) {
countDown();
} else {
ow.actions.get({ actionName: otherEntity.name, namespace: otherEntity.namespace })
.then(function(sequenceWithDetails) {
if (sequenceUses(sequenceWithDetails, entity, entityNamespace)) {
console.log(' Creating sequence splice'.green, otherEntity.name);
spliceSequence(ow, sequenceWithDetails, entity, entityNamespace, names)
.then(countDown, errorWhile('creating sequence splice', countDown));
} else {
countDown();
}
}).catch(function() { countDown(); });
}
});
});
});
} catch (e) {
console.error(e);
}
};
exports.detachAll = function detachAll(wskprops, next) {
var ow = setupOpenWhisk(wskprops);
var count = 0;
function done() {
if (--count <= 0) {
next && next();
}
}
for (var entity in attached) {
count++;
}
if (count == 0) {
done();
} else {
for (var entity in attached) {
exports.detach(wskprops, done, entity);
}
}
};
exports.detach = function detach(wskprops, next, entity) {
if (!entity) {
var L = [];
for (var x in attached) { L.push(x); }
if (L.length === 0) {
console.error("No attached actions detected");
next();
} else {
require('inquirer')
.prompt([{ name: 'name', type: 'list',
message: 'From which action do you wish to detach',
choices: L
}])
.then(function(response) { doDetach(wskprops, next, response.name); });
}
} else {
doDetach(wskprops, next, entity);
}
}
function doDetach(wskprops, next, entity) {
console.log('Detaching'.blue + ' from ' + entity);
function errlog(idx, noNext) {
return function(err) {
if (err.indexOf && err.indexOf('HTTP 404') < 0) {
console.error('Error ' + idx, err);
}
if (!noNext) next();
};
}
var names = attached[entity];
if (names) {
try {
var ow = setupOpenWhisk(wskprops);
ow.rules.disable(names).then(function() {
try {
// first delete the action and rule and debug package
Promise.all([ow.triggers.delete(names),
ow.actions.delete({ actionName: names.continuationName }),
ow.packages.delete({ packageName: names.debugStubName })
])
.then(function(values) {
// then we can delete the rule
ow.rules.delete(names).then(function() {
try {
delete attached[entity];
ok_(next);
} catch (err) {
errlog(5, true)(err);
}
}, errlog(4));
}, errlog(3));
} catch (err) { errlog(2, true)(err); }
}, errlog(1));
} catch (e) {
console.error(e);
}
}
};
/**
* Invoke an action
*
*/
exports.invoke = function invoke() {
try {
exports._invoke.apply(undefined, arguments);
} catch (e) {
console.error(e);
}
};
exports._invoke = function invoke() {
var args = Array.prototype.slice.call(arguments);
var wskprops = args.shift();
var namespace = wskprops['NAMESPACE'];
var next = args.shift();
var action = args.shift();
var params = {};
for (var i = 0; i < args.length; i++) {
if (args[i] == '-p') {
params[args[++i]] = args[++i];
}
}
var invokeThisAction, waitForThisAction;
var attachedTo = attached[action];
if (!attachedTo) {
var seq = chainAttached[action];
if (seq) {
invokeThisAction = seq.before;
waitForThisAction = seq.after;
} else {
invokeThisAction = action;
waitForThisAction = action;
}
} else {
invokeThisAction = attachedTo.debugStubName + '/' + invokerActionName;
// these are now part of the debug stub binding
// params.action = action;
// params.namespace = namespace;
// params.onDone_trigger = attachedTo.triggerName;
waitForThisAction = attachedTo.continuationName;
}
console.log('Invoking', action);
if (!action) {
console.error('Please provide an action to invoke'.red);
return next();
}
var key = wskprops['AUTH'];
var ow = setupOpenWhisk(wskprops);
var owForActivations = openwhisk({
api: api.host + api.path,
api_key: key,
namespace: '_'
});
ow.actions.invoke({
actionName: invokeThisAction,
params: params
}).then(function onSuccess(activation) {
if (activation && activation.activationId) {
// successfully invoked
if (!attachedTo) {
console.log('Successfully invoked with activationId', activation.activationId);
} else {
// we'll wait for the result...
}
//
// wait for activation completion
//
var timer = setInterval(function waitForResponse() {
owForActivations.activations.list({ limit: 10 }).then(function(list) {
for (var i = 0; i < list.length; i++) {
var activation = list[i];
if (activation.name == waitForThisAction) {
clearInterval(timer);
owForActivations.activations.get({ activation: activation.activationId }).then(function(activation) {
console.log(JSON.stringify(activation, undefined, 4));
next();
});
break;
}
}
});
}, 1000);
}
}, function onError(err) {
console.error('Unable to invoke your specified action');
next();
});
}