Restructre clusterctl cli interface

Turn the flat list of commands into a hierarchy of commands, converting
most required parameters into positional parameters and add plugin
loading support for clusterctl.
This commit is contained in:
Hornwitser committed 2020-08-18 18:52:11 +02:00
1 parent 9b870fde64
commit da1cbc16f7
10 files changed
+783 -428

No files matched your search

+248
View File
@@ -0,0 +1,248 @@
/**
* Command library
*
* Data types and utilites for commands to clusterctl's CLI.
*
* @author Hornwitser
* @module
*/
"use strict";
const link = require("lib/link");
const errors = require("lib/errors");
/**
* Represents a command that can be runned by clusterctl
* @static
*/
class Command {
/**
* Define an executable command.
*
* @param {Object} cmd - Command definiton.
* @param {Array} cmd.definiton -
* Arguments to pass to yargs .command method to define this
* command.
* @param {function(args, control)} handler -
* Async function invoked when the command is executed. Is given
* the parsed args and a reference to the {@link
* module:clusterctl.Control} instance.
*/
constructor({ definition, handler }) {
if (definition[0] instanceof Array) {
this.name = definition[0][0].split(" ")[0];
this.alias = definition[0].slice(1);
} else {
this.name = definition[0].split(" ")[0];
this.alias = [];
}
this._definition = definition;
this._handler = handler;
}
register(yargs) {
yargs.command(...this._definition);
}
async run(args, control) {
await this._handler(args, control);
}
}
/**
* A node in the command tree that can hold commands and sub trees
*
* Container which can contain {@link module:lib/command.Command}s and other
* CommandTrees. This is used by clusterctl to hold the full tree of
* available commands, you may extend this tree by using a control plugin,
* see {@link module:lib/plugin.BaseControlPlugin#addCommands}
*
* @static
*/
class CommandTree {
/**
* Define a command containing sub commands.
*
* @param {Object} cmd - Command definition
* @param {string} cmd.name - Name of the command.
* @param {Array<string>} cmd.alias - Aliases for this tree.
* @param {description} cmd.description -
* Descripton to provide for this command tree node.
*/
constructor({ name, alias, description }) {
if (typeof name !== "string") {
throw new Error("name must be a string");
}
this.name = name;
this.alias = alias || [];
this.description = description;
this.subCommands = new Map();
}
/**
* Add a command or command tree to this tree
*
* @param {module:lib/command.Command|module:lib/command.CommandTree} command -
* The command to add to this command tree.
*/
add(command) {
if (this.subCommands.has(command.name)) {
throw new Error(`Command ${command.name} already exists`);
}
this.subCommands.set(command.name, command);
for (let alias of command.alias) {
if (this.subCommands.has(alias)) {
throw new Error(`Alias ${alias} for command ${command.name} already exists`);
}
this.subCommands.set(alias, command);
}
}
/**
* Get a command or command tree from this tree
*
* @param {string} name -
* The name of the command or command tree to retrieve.
* @return {?module:lib/command.Command|module:lib/command.CommandTree}
* The command to add to this command tree.
*/
get(name) {
return this.subCommands.get(name) || null;
}
/**
* Insert the registered commands into the given yargs parser
*
* Traverses the the tree of sub-commands recursively and adds all of
* them to the given yargs parser.
*
* @param {Object} yargs - yargs parser to register with.
*/
register(yargs) {
yargs.command([this.name].concat(this.alias), this.description, (yargs) => {
for (let [name, command] of this.subCommands) {
// Check if the entry is not an alias.
if (name === command.name) {
command.register(yargs);
}
}
}, () => {
yargs.showHelp();
yargs.exit();
});
}
}
/**
* Resolve a string into a slave ID
*
* Resolves a string with either an slave name or an id into an integer with
* the slave ID.
*
* @param {module:lib/link.Link} client - link to master server to query slave on.
* @param {string} slaveName - string with name or id of slave.
* @returns {number} slave ID.
* @static
*/
async function resolveSlave(client, slaveName) {
let slaveId;
if (/^-?\d+$/.test(slaveName)) {
slaveId = parseInt(slaveName, 10);
} else {
let response = await link.messages.listSlaves.send(client);
for (let slave of response.list) {
if (slave.name === slaveName) {
slaveId = slave.id;
break;
}
}
if (slaveId === undefined) {
throw new errors.CommandError(`No slave named ${slaveName}`);
}
}
return slaveId;
}
/**
* Resolve a string to an instance ID
*
* Resolves a string with either an instance name or an id into an integer
* with the instance ID.
*
* @param {module:lib/link.Link} client - link to master server to query instance on.
* @param {string} instanceName - string with name or id of instance.
* @returns {number} instance ID.
* @static
*/
async function resolveInstance(client, instanceName) {
let instanceId;
if (/^-?\d+$/.test(instanceName)) {
instanceId = parseInt(instanceName, 10);
} else {
let response = await link.messages.listInstances.send(client);
for (let instance of response.list) {
if (instance.name === instanceName) {
instanceId = instance.id;
break;
}
}
if (instanceId === undefined) {
throw new errors.CommandError(`No instance named ${instanceName}`);
}
}
return instanceId;
}
/**
* Retrieve role object from string
*
* Resolves a string with either a role name or an id into an object
* representing the role.
*
* @param {module:lib/link.Link} client - link to master server to query role on.
* @param {string} roleName - string with name or id of role.
* @returns {Object} Role info.
* @static
*/
async function retrieveRole(client, roleName) {
let response = await link.messages.listRoles.send(client);
let resolvedRole;
if (/^-?\d+$/.test(roleName)) {
let roleId = parseInt(roleName, 10);
for (let role of response.list) {
if (role.id === roleId) {
resolvedRole = role;
break;
}
}
} else {
for (let role of response.list) {
if (role.name === roleName) {
resolvedRole = role;
break;
}
}
}
if (!resolvedRole) {
throw new errors.CommandError(`No role named ${roleName}`);
}
return resolvedRole;
}
module.exports = {
Command,
CommandTree,
resolveSlave,
resolveInstance,
retrieveRole,
};
+60 -1
View File
@@ -11,11 +11,26 @@ const errors = require("lib/errors");
/**
* Conceptual base for master and instance plugins.
* @typedef {module:lib/plugin.BaseMasterPlugin|module:lib/plugin.BaseInstancePlugin} module:lib/plugin.BasePlugin
* @typedef {
* module:lib/plugin.BaseMasterPlugin
* |module:lib/plugin.BaseInstancePlugin
* |module:lib/plugin.BaseControlPlugin
* } module:lib/plugin.BasePlugin
*/
/**
* Base class for instance plugins
*
* Instance plugins are subclasses of this class which get instantiated by
* the slave when it brings up an instance with the plugin enabled in the
* config. To be discovered the class must be exported under the name
* `InstancePlugin` in the module specified by the `instanceEntrypoint` in
* the plugin's info.js file.
*
* Instances may be started and stopped many times, and many instances may
* be running at the same time, each of which will have their own instance
* of the InstancePlugin class.
*
* @static
*/
class BaseInstancePlugin {
@@ -185,6 +200,13 @@ class BaseInstancePlugin {
/**
* Base class for master plugins
*
* Master plugins are subclasses of this class which get instantiated by
* the master server on startup when the plugin is enabled in the config.
* To be discovered the class must be exported under the name `MasterPlugin`
* in the module specified by the `masterEntrypoint` in the plugin's info.js
* file.
*
* @static
*/
class BaseMasterPlugin {
@@ -316,6 +338,42 @@ class BaseMasterPlugin {
}
}
/**
* Base class for clusterctl plugins
*
* Control plugins are subclasses of this class which get instantiated by
* clusterctl in order to extend its functionallity. To be discovered the
* class must be exported under the name `ControlPlugin` in the module
* specified by the `controlEntrypoint` in the plugin's info.js file.
*
* @static
*/
class BaseControlPlugin {
constructor(info) {
/**
* The plugin's own info module
*/
this.info = info;
}
/**
* Called immediately after the class is instantiated
*/
async init() { }
/**
* Called to add commands to the command line interface.
*
* Invoked by clusterctl to let plugins add commands. `rootCommand` is
* the top level command node which the plugin should add its own {@link
* module:lib/command.CommandTree} to.
*
* @param {module:lib/command.CommandTree} rootCommand -
* Root of the clusterctl command tree.
*/
async addCommands(rootCommand) { }
}
/**
* Load plugin information
*
@@ -413,6 +471,7 @@ async function invokeHook(plugins, hook, ...args) {
module.exports = {
BaseInstancePlugin,
BaseMasterPlugin,
BaseControlPlugin,
loadPluginInfos,
attachPluginMessages,