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

+4 -4
View File
@@ -223,7 +223,7 @@ master server. In order for slaves to connect to the master server they
need a valid authentication token, you can create a slave config with
a valid token with the following command.
node clusterctl create-slave-config --name Local --generate-token
node clusterctl slave create-config --name Local --generate-token
This will write a new `config-slave.json` file in the current directory
(you can change the location with the `--output` option) with the name,
@@ -253,9 +253,9 @@ clusterctl control-config set` command:
The basic operations to start a new instance is the following
node clusterctl create-instance --name "My instance"
node clusterctl assign-instance --instance "My instance" --slave "Local"
node clusterctl start-instance --instance "My instance"
node clusterctl instance create "My instance"
node clusterctl instance assign "My instance" "Local"
node clusterctl instance start "My instance"
The first line creates the instance configuration on the master server.
The second assigns the instance to a slave which creates the instance
+189 -306
View File
@@ -14,6 +14,8 @@ const events = require("events");
const link = require("lib/link");
const errors = require("lib/errors");
const config = require("lib/config");
const plugin = require("lib/plugin");
const command = require("lib/command");
/**
@@ -54,141 +56,17 @@ function formatOutputColored(output) {
return time + info + output.message;
}
/**
* Represents a command that can be runned by clusterctl
*/
class Command {
constructor({ definition, handler }) {
this.name = definition[0];
this._definition = definition;
this._handler = handler;
}
register(yargs) {
yargs.command(...this._definition);
}
async run(args, control) {
await this._handler(args, control);
}
}
/**
* 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.
* @private
*/
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;
}
/**
* 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.
* @private
*/
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 into a role object
*
* 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.
* @private
*/
async function resolveRole(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;
}
let commands = [];
commands.push(new Command({
definition: ["list-slaves", "List slaves connected to the master"],
const slaveCommands = new command.CommandTree({ name: "slave", description: "Slave management" });
slaveCommands.add(new command.Command({
definition: [["list", "l"], "List slaves connected to the master"],
handler: async function(args, control) {
let response = await link.messages.listSlaves.send(control);
console.log(asTable(response.list));
},
}));
commands.push(new Command({
definition: ["generate-slave-token", "Generate token for a slave", (yargs) => {
slaveCommands.add(new command.Command({
definition: ["generate-token", "Generate token for a slave", (yargs) => {
yargs.option("id", { type: "number", nargs: 1, describe: "Slave id", demandOption: true });
}],
handler: async function(args, control) {
@@ -197,8 +75,8 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["create-slave-config", "Create slave config", (yargs) => {
slaveCommands.add(new command.Command({
definition: ["create-config", "Create slave config", (yargs) => {
yargs.option("id", { type: "number", nargs: 1, describe: "Slave id", default: null });
yargs.option("name", { type: "string", nargs: 1, describe: "Slave name", default: null });
yargs.option("generate-token", {
@@ -231,34 +109,23 @@ commands.push(new Command({
}));
commands.push(new Command({
definition: ["list-instances", "List instances known to the master"],
const instanceCommands = new command.CommandTree({
name: "instance", alias: ["i"], description: "Instance management",
});
instanceCommands.add(new command.Command({
definition: [["list", "l"], "List instances known to the master"],
handler: async function(args, control) {
let response = await link.messages.listInstances.send(control);
console.log(asTable(response.list));
},
}));
commands.push(new Command({
definition: ["create-instance", "Create an instance", (yargs) => {
instanceCommands.add(new command.Command({
definition: ["create <name>", "Create an instance", (yargs) => {
// XXX TODO: set any specific options?
yargs.option("name", {
type: "string",
nargs: 1,
describe: "Instance name",
group: "Instance Config:",
});
yargs.option("id", {
type: "number",
nargs: 1,
describe: "Instance ID",
group: "Instance Config:",
});
yargs.option("base", {
type: "boolean",
nargs: 0,
describe: "Specify this is a base config",
group: "Instance Config:",
yargs.positional("name", { describe: "Instance name", type: "string" });
yargs.options({
"id": { type: "number", nargs: 1, describe: "Instance id" },
});
}],
handler: async function(args, control) {
@@ -267,22 +134,21 @@ commands.push(new Command({
if (args.id) {
instanceConfig.set("instance.id", args.id);
}
if (args.name) {
instanceConfig.set("instance.name", args.name);
}
instanceConfig.set("instance.name", args.name);
let serialized_config = instanceConfig.serialize();
let response = await link.messages.createInstance.send(control, { serialized_config });
},
}));
commands.push(new Command({
definition: ["list-instance-config", "List configuration for an instance", (yargs) => {
yargs.option({
"instance": { describe: "Instance to list config for", nargs: 1, type: "string", demandOption: true },
});
const instanceConfigCommands = new command.CommandTree({
name: "config", alias: ["c"], description: "Instance config management",
});
instanceConfigCommands.add(new command.Command({
definition: ["list <instance>", "List configuration for an instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to list config for", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
let response = await link.messages.getInstanceConfig.send(control, { instance_id: instanceId });
for (let group of response.serialized_config.groups) {
@@ -293,16 +159,14 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["set-instance-config", "Set field in instance config", (yargs) => {
yargs.option({
"instance": { describe: "Instance to set config on", nargs: 1, type: "string", demandOption: true },
"field": { describe: "Field to set", nargs: 1, type: "string", demandOption: true },
"value": { describe: "Value to set", nargs: 1, type: "string", demandOption: true },
});
instanceConfigCommands.add(new command.Command({
definition: ["set <instance> <field> <value>", "Set field in instance config", (yargs) => {
yargs.positional("instance", { describe: "Instance to set config on", type: "string" });
yargs.positional("field", { describe: "Field to set", type: "string" });
yargs.positional("value", { describe: "Value to set", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceConfigField.send(control, {
instance_id: instanceId,
field: args.field,
@@ -311,17 +175,15 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["set-instance-config-prop", "Set property of field in instance config", (yargs) => {
yargs.option({
"instance": { describe: "Instance to set config on", nargs: 1, type: "string", demandOption: true },
"field": { describe: "Field to set", nargs: 1, type: "string", demandOption: true },
"prop": { describe: "Property to set", nargs: 1, type: "string", demandOption: true },
"value": { describe: "JSON parsed value to set", nargs: 1, type: "string", demandOption: true },
});
instanceConfigCommands.add(new command.Command({
definition: ["set-prop <instance> <field> <prop> <value>", "Set property of field in instance config", (yargs) => {
yargs.positional("instance", { describe: "Instance to set config on", type: "string" });
yargs.positional("field", { describe: "Field to set", type: "string" });
yargs.positional("prop", { describe: "Property to set", type: "string" });
yargs.positional("value", { describe: "JSON parsed value to set", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceConfigProp.send(control, {
instance_id: instanceId,
field: args.field,
@@ -330,17 +192,16 @@ commands.push(new Command({
});
},
}));
instanceCommands.add(instanceConfigCommands);
commands.push(new Command({
definition: ["assign-instance", "Assign instance to a slave", (yargs) => {
yargs.options({
"instance": { describe: "Instance to assign", nargs: 1, type: "string", demandOption: true },
"slave": { describe: "Slave to assign to", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["assign <instance> <slave>", "Assign instance to a slave", (yargs) => {
yargs.positional("instance", { describe: "Instance to assign", type: "string" });
yargs.positional("slave", { describe: "Slave to assign to", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let slaveId = await resolveSlave(control, args.slave);
let instanceId = await command.resolveInstance(control, args.instance);
let slaveId = await command.resolveSlave(control, args.slave);
await link.messages.assignInstanceCommand.send(control, {
instance_id: instanceId,
slave_id: slaveId,
@@ -348,14 +209,12 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["create-save", "Create a new save on an instance", (yargs) => {
yargs.options({
"instance": { describe: "Instance to create on", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["create-save <instance>", "Create a new save on an instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to create on", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instanceId] });
let response = await link.messages.createSave.send(control, {
instance_id: instanceId,
@@ -363,14 +222,12 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["export-data", "Export item icons and locale from instance", (yargs) => {
yargs.options({
"instance": { describe: "Instance to export from", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["export-data <instance>", "Export item icons and locale from instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to export from", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instanceId] });
let response = await link.messages.exportData.send(control, {
instance_id: instanceId,
@@ -378,16 +235,16 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["start-instance", "Start instance", (yargs) => {
instanceCommands.add(new command.Command({
definition: ["start <instance>", "Start instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to start", type: "string" });
yargs.options({
"instance": { describe: "Instance to start", nargs: 1, type: "string", demandOption: true },
"save": { describe: "Save load, defaults to latest", nargs: 1, type: "string" },
"keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false },
});
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instanceId] });
let response = await link.messages.startInstance.send(control, {
instance_id: instanceId,
@@ -396,16 +253,16 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["load-scenario", "Start instance by loading a scenario", (yargs) => {
instanceCommands.add(new command.Command({
definition: ["load-scenario <instance> <scenario>", "Start instance by loading a scenario", (yargs) => {
yargs.positional("instance", { describe: "Instance to start", type: "string" });
yargs.positional("scenario", { describe: "Scenario to load", type: "string" });
yargs.options({
"instance": { describe: "Instance to start", nargs: 1, type: "string", demandOption: true },
"scenario": { describe: "Scenario to load", nargs: 1, type: "string" },
"keep-open": { describe: "Keep console open", nargs: 0, type: "boolean", default: false },
});
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instanceId] });
let response = await link.messages.loadScenario.send(control, {
instance_id: instanceId,
@@ -414,14 +271,12 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["stop-instance", "Stop instance", (yargs) => {
yargs.options({
"instance": { describe: "Instance to stop", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["stop <instance>", "Stop instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to stop", type: "string" });
}],
handler: async function(args, control) {
let instanceId = await resolveInstance(control, args.instance);
let instanceId = await command.resolveInstance(control, args.instance);
await link.messages.setInstanceOutputSubscriptions.send(control, { instance_ids: [instanceId] });
let response = await link.messages.stopInstance.send(control, {
instance_id: instanceId,
@@ -429,29 +284,25 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["delete-instance", "Delete instance", (yargs) => {
yargs.options({
"instance": { describe: "Instance to delete", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["delete <instance>", "Delete instance", (yargs) => {
yargs.positional("instance", { describe: "Instance to delete", type: "string" });
}],
handler: async function(args, control) {
let response = await link.messages.deleteInstance.send(control, {
instance_id: await resolveInstance(control, args.instance),
instance_id: await command.resolveInstance(control, args.instance),
});
},
}));
commands.push(new Command({
definition: ["send-rcon", "Send RCON command", (yargs) => {
yargs.options({
"instance": { describe: "Instance to sent to", nargs: 1, type: "string", demandOption: true },
"command": { describe: "command to send", nargs: 1, type: "string", demandOption: true },
});
instanceCommands.add(new command.Command({
definition: ["send-rcon <instance> <command>", "Send RCON command", (yargs) => {
yargs.positional("instance", { describe: "Instance to send to", type: "string" });
yargs.positional("command", { describe: "command to send", type: "string" });
}],
handler: async function(args, control) {
let response = await link.messages.sendRcon.send(control, {
instance_id: await resolveInstance(control, args.instance),
instance_id: await command.resolveInstance(control, args.instance),
command: args.command,
});
@@ -460,26 +311,29 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["list-permissions", "List permissions in the cluster"],
const permissionCommands = new command.CommandTree({ name: "permission", description: "Permission inspection" });
permissionCommands.add(new command.Command({
definition: [["list", "l"], "List permissions in the cluster"],
handler: async function(args, control) {
let response = await link.messages.listPermissions.send(control);
console.log(asTable(response.list));
},
}));
commands.push(new Command({
definition: ["list-roles", "List roles in the cluster"],
const roleCommands = new command.CommandTree({ name: "role", description: "Role management" });
roleCommands.add(new command.Command({
definition: [["list", "l"], "List roles in the cluster"],
handler: async function(args, control) {
let response = await link.messages.listRoles.send(control);
console.log(asTable(response.list));
},
}));
commands.push(new Command({
definition: ["create-role", "Create a new role", (yargs) => {
roleCommands.add(new command.Command({
definition: ["create <name>", "Create a new role", (yargs) => {
yargs.positional("name", { describe: "Name of role to create", type: "string" });
yargs.options({
"name": { describe: "Name of role to create", nargs: 1, type: "string", demandOption: true },
"description": { describe: "Description for role", nargs: 1, type: "string", default: "" },
"permissions": { describe: "Permissions role grants", nargs: 1, array: true, type: "string", default: [] },
});
@@ -494,10 +348,10 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["edit-role", "Edit existing role", (yargs) => {
roleCommands.add(new command.Command({
definition: ["edit <role>", "Edit existing role", (yargs) => {
yargs.positional("role", { describe: "Role to edit", type: "string" });
yargs.options({
"role": { describe: "Role to edit", nargs: 1, type: "string", demandOption: true },
"name": { describe: "New name for role", nargs: 1, type: "string" },
"description": { describe: "New description for role", nargs: 1, type: "string" },
"permissions": { describe: "New permissions for role", array: true, type: "string" },
@@ -505,7 +359,7 @@ commands.push(new Command({
});
}],
handler: async function(args, control) {
let role = await resolveRole(control, args.role);
let role = await command.retrieveRole(control, args.role);
if (args.name !== undefined) {
role.name = args.name;
@@ -524,44 +378,39 @@ commands.push(new Command({
},
}));
commands.push(new Command({
definition: ["delete-role", "Delete role", (yargs) => {
yargs.options({
"role": { describe: "Role to delete", nargs: 1, type: "string", demandOption: true },
});
roleCommands.add(new command.Command({
definition: ["delete <role>", "Delete role", (yargs) => {
yargs.positional("role", { describe: "Role to delete", type: "string" });
}],
handler: async function(args, control) {
let role = await resolveRole(control, args.role);
let role = await command.retrieveRole(control, args.role);
await link.messages.deleteRole.send(control, { id: role.id });
},
}));
commands.push(new Command({
definition: ["list-users", "List user in the cluster"],
const userCommands = new command.CommandTree({ name: "user", alias: ["u"], description: "User management" });
userCommands.add(new command.Command({
definition: [["list", "l"], "List user in the cluster"],
handler: async function(args, control) {
let response = await link.messages.listUsers.send(control);
console.log(asTable(response.list));
},
}));
commands.push(new Command({
definition: ["create-user", "Create a user", (yargs) => {
yargs.options({
"name": { describe: "Name of user to create", nargs: 1, type: "string", demandOption: true },
});
userCommands.add(new command.Command({
definition: ["create <name>", "Create a user", (yargs) => {
yargs.positional("name", { describe: "Name of user to create", type: "string" });
}],
handler: async function(args, control) {
await link.messages.createUser.send(control, { name: args.name });
},
}));
commands.push(new Command({
definition: ["edit-user-roles", "Edit user roles", (yargs) => {
yargs.options({
"name": { describe: "Name of user to change roles for", nargs: 1, type: "string", demandOption: true },
"roles": { describe: "roles to assign", array: true, type: "string", demandOption: true },
});
userCommands.add(new command.Command({
definition: ["set-roles <user> [roles...]", "Replace user roles", (yargs) => {
yargs.positional("user", { describe: "Name of user to change roles for", type: "string" });
yargs.positional("roles", { describe: "roles to assign", type: "string" });
}],
handler: async function(args, control) {
let response = await link.messages.listRoles.send(control);
@@ -588,32 +437,28 @@ commands.push(new Command({
}
}
await link.messages.updateUserRoles.send(control, { name: args.name, roles: resolvedRoles });
await link.messages.updateUserRoles.send(control, { name: args.user, roles: resolvedRoles });
},
}));
commands.push(new Command({
definition: ["delete-user", "Delete user", (yargs) => {
yargs.options({
"name": { describe: "Name of user to delete", nargs: 1, type: "string", demandOption: true },
});
userCommands.add(new command.Command({
definition: ["delete <user>", "Delete user", (yargs) => {
yargs.positional("user", { describe: "Name of user to delete", type: "string" });
}],
handler: async function(args, control) {
await link.messages.deleteUser.send(control, { name: args.name });
await link.messages.deleteUser.send(control, { name: args.user });
},
}));
commands.push(new Command({
definition: ["debug-dump-ws", "Dump WebSocket messages sent and received by master", (yargs) => { }],
const debugCommands = new command.CommandTree({ name: "debug", description: "Debugging utilities" });
debugCommands.add(new command.Command({
definition: ["dump-ws", "Dump WebSocket messages sent and received by master", (yargs) => { }],
handler: async function(args, control) {
await link.messages.debugDumpWs.send(control);
return new Promise(() => {});
},
}));
// Convert to mapping from name to command instance
commands = new Map([...commands.map(command => [command.name, command])]);
/**
* Connector for control connection to master server
@@ -639,14 +484,22 @@ class ControlConnector extends link.WebSocketClientConnector {
* Handles running the control
*
* Connects to the master server over WebSocket and sends commands to it.
* @static
*/
class Control extends link.Link {
// I don't like God classes, but the alternative of putting all this state
// into global variables is not much better.
constructor(connector) {
constructor(connector, controlPlugins) {
super("control", "master", connector);
link.attachAllMessages(this);
/**
* Mapping of plugin names to their instance for loaded plugins.
* @type {Map<string, module:lib/plugin.BaseControlPlugin>}
*/
this.plugins = controlPlugins;
for (let controlPlugin of controlPlugins.values()) {
plugin.attachPluginMessages(this, controlPlugin.info, controlPlugin);
}
}
async instanceOutputEventHandler(message) {
@@ -687,18 +540,42 @@ async function startControl() {
})
.command("control-config", "Manage Control config", config.configCommand)
.wrap(yargs.terminalWidth())
.strict()
;
for (let command of commands.values()) {
command.register(yargs);
const rootCommands = new command.CommandTree({ name: "clusterctl", description: "Manage cluster" });
rootCommands.add(slaveCommands);
rootCommands.add(instanceCommands);
rootCommands.add(permissionCommands);
rootCommands.add(roleCommands);
rootCommands.add(userCommands);
rootCommands.add(debugCommands);
console.log("Loading Plugin info");
let pluginInfos = await plugin.loadPluginInfos("plugins");
config.registerPluginConfigGroups(pluginInfos);
config.finalizeConfigs();
let controlPlugins = new Map();
for (let pluginInfo of pluginInfos) {
if (!pluginInfo.controlEntrypoint) {
continue;
}
let { ControlPlugin } = require(`./plugins/${pluginInfo.name}/${pluginInfo.controlEntrypoint}`);
let controlPlugin = new ControlPlugin(pluginInfo);
controlPlugins.set(pluginInfo.name, controlPlugin);
await controlPlugin.init();
await controlPlugin.addCommands(rootCommands);
}
const args = yargs.demandCommand(1, "You need to specify a command to run")
.strict()
.argv
;
for (let [name, command] of rootCommands.subCommands) {
if (name === command.name) {
command.register(yargs);
}
}
config.finalizeConfigs();
const args = yargs.argv;
console.log(`Loading config from ${args.config}`);
let controlConfig = new config.ControlConfig();
@@ -715,12 +592,24 @@ async function startControl() {
}
}
let commandName = args._[0];
if (commandName === "control-config") {
if (args._.length === 0) {
yargs.showHelp();
yargs.exit();
}
// Handle the control-config command before trying to connect.
if (args._[0] === "control-config") {
await config.handleConfigCommand(args, controlConfig, args.config);
return;
}
// Determine which command is being executed.
let commandPath = [...args._];
let targetCommand = rootCommands;
while (commandPath.length && targetCommand instanceof command.CommandTree) {
targetCommand = targetCommand.get(commandPath.shift());
}
// The remaining commands require connecting to the master server.
if (!controlConfig.get("control.master_url") || !controlConfig.get("control.master_token")) {
console.error("Missing URL and/or token to connect with. See README.md for setting up access.");
@@ -733,7 +622,7 @@ async function startControl() {
controlConfig.get("control.reconnect_delay"),
controlConfig.get("control.master_token")
);
let control = new Control(controlConnector);
let control = new Control(controlConnector, controlPlugins);
try {
await controlConnector.connect();
} catch(err) {
@@ -751,42 +640,36 @@ async function startControl() {
});
});
if (commands.has(commandName)) {
let command = commands.get(commandName);
let keepOpen = Boolean(args.keepOpen);
try {
await targetCommand.run(args, control);
let keepOpen = Boolean(args.keepOpen);
try {
await command.run(args, control);
} catch (err) {
keepOpen = false;
if (err instanceof errors.CommandError) {
console.error(`Error running command: ${err.message}`);
process.exitCode = 1;
} catch (err) {
keepOpen = false;
if (err instanceof errors.CommandError) {
console.error(`Error running command: ${err.message}`);
process.exitCode = 1;
} else if (err instanceof errors.RequestError) {
console.error(`Error sending request: ${err.message}`);
process.exitCode = 1;
} else if (err instanceof errors.RequestError) {
console.error(`Error sending request: ${err.message}`);
process.exitCode = 1;
} else {
throw err;
}
} else {
throw err;
}
} finally {
if (!keepOpen) {
await control.shutdown();
}
} finally {
if (!keepOpen) {
await control.shutdown();
}
}
}
module.exports = {
Control,
// for testing only
_formatOutputColored: formatOutputColored,
_resolveInstance: resolveInstance,
_Control: Control,
_commands: commands,
};
+162 -64
View File
@@ -19,6 +19,7 @@ Contents
- [Sending Link Messages](#sending-link-messages)
- [Handling connection events](#handling-connection-events)
- [Collecting Statistics](#collecting-statistics)
- [Adding Custom Commands to clusterctl](#adding-custom-commands-to-clusterctl)
Plugin Structure
@@ -31,6 +32,7 @@ The basic file structure of a plugin is the following.
+- package.json
+- master.js
+- instance.js
+- control.js
+- test/
| +- plugin.js
+- module/
@@ -94,6 +96,12 @@ The following properties are recognized:
configuration fields for this plugin. See [Plugin
Configuration](#plugin-configuration)
**controlEntrypoint**:
Path to a Node.js module relative to the plugin directory which
contains the ControlPlugin class definition for this plugin. This
is an optional paramater. A plugin can be made that only runs on
the clusterctl side.
**messages**:
Object with link messages definitions for this plugin. See guide
for [defining link messages](#defining-link-messages) below.
@@ -137,8 +145,9 @@ code can be used:
}
For the instance plugin it's exactly the same except "Master" is
replaced with "Instance". The available hooks that you can override are
documented in the base class [in lib/plugin.js](../lib/plugin.js).
replaced with "Instance", and for the clusterctl plugin "Control" is
used. The available hooks that you can override are documented in the
base class [in lib/plugin.js](../lib/plugin.js).
It's best to avoid defining a constructor, but if you insist on defining
one, forward all arguments to the base class. E.g.:
@@ -318,46 +327,60 @@ the event, for example the following could be defined in `info.js`:
},
This specifies an event that can be sent from the master to a slave,
and from a slave to an instance. It also specifies that the event must contain the
property `frobnication_type`, with a string value in the data payload.
It will also be forwarded by slaves to a specific instance.
and from a slave to an instance. It also specifies that the event must
contain the property `frobnication_type`, with a string value in the
data payload. It will also be forwarded by slaves to a specific
instance.
The following properties are recognized by the Event constructor:
**type**:
The message type sent over the wire. This must start with the name
of the plugin followed by colon and and be unique for the plugin.
The type of the message sent over the socket will have the suffix
`_event` appended to it.
#### type
**links**:
An array of strings describing which links this event can be sent
over. Direction matters, `'master-slave'` means the event can be
sent from the master to the slave, but can't be sent back the other way,
unless `'slave-master'` is also present in the links array.
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_event`
appended to it.
**forwardTo**:
Target to forward an event to. Can either be `'master'`, to indicate a
slave should forward it to the master server, or `'instance'`, to
indicate it should be forwarded to the instances specified by the
`instance_id` event property. This works by using a default handler
for the event at the links that forward it.
#### links
**broadcastTo**:
Target to broadcast this message towards. A value of "instance" means
the event will be broadcast to all instances downstream of the target
it's sent to, but not back from where it came from. Currently, only
"instance" is supported. This means that sending an event to a slave
that slave except for the instance it came from.
from an instance will cause it to be broadcast to all instances of
An array of strings describing which links this event can be sent over.
Direction matters, `'master-slave'` means the event can be sent from the
master to the slave, but can't be sent back the other way, unless
`'slave-master'` is also present in the links array.
**eventProperties**:
Object with properties mapping to a JSON schema of that property
that specifies what's valid to send in the event. This is
equivalent to using the `properties` keyword in JSON schema, except
that the properties specified are implicitly required and additional
properties are not allowed. See [this guide][guide] for an
introduction to writing JSON schemas.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `'master-slave'` and
`'slave-instance'` in the links array). See `forwardTo` and
`broadcastTo` for ways to forward an event to the next link in a chain.
#### forwardTo
Target to forward an event to. Can either be `'master'`, to indicate a
slave should forward it to the master server, or `'instance'`, to
indicate it should be forwarded to the instances specified by the
`instance_id` event property. This works by using a default handler for
the event at the links that forward it.
#### broadcastTo
Target to broadcast this message towards. A value of "instance" means
the event will be broadcast to all instances downstream of the target
it's sent to, but not back from where it came from. Currently, only
"instance" is supported. This means that sending an event to a slave
that slave except for the instance it came from. from an instance will
cause it to be broadcast to all instances of
#### eventProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the event. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are implicitly required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas.
The forwardTo and broadcastTo can be combined such that specifying
`'master'` as the forwardTo value and `'instance'` as the broadcastTo
@@ -404,39 +427,51 @@ instance specified by `instance_id`.
The following properties are recognized by the Request constructor:
**type**:
The message type sent over the wire. This must start with the name
of the plugin followed by colon and and be unique for the plugin.
The type of the message sent over the socket will have the suffix
`_request` appended to it for the request and `_response` appended
to it for the response.
#### type
**links**:
An array of strings describing which links this request can be sent
over. Direction matters; `'master-slave'` means the request can be
sent from the master to the slave and the slave can reply to the master,
but the slave can't send a request to the master unless
`'slave-master'` is also present in the links array.
The message type sent over the wire. This must start with the name of
the plugin followed by colon and and be unique for the plugin. The type
of the message sent over the socket will have the suffix `_request`
appended to it for the request and `_response` appended to it for the
response.
**forwardTo**:
Target to forward the request to. Can either be `'master'` to indicate
a slave should forward it to the master server when receiving it
from an instance, or `'instance'` to indicate it should be forwarded
to the instances specified by the `instance_id` request property.
This works by using a default handler for the request by the links
that forward it.
#### links
**requestProperties**:
Object with properties mapping to a JSON schema of that property
that specifies what's valid to send in the request. This is
equivalent to using the `properties` keyword in JSON schema, except
that the properties specified are implicitly required and additional
properties are not allowed. See [this guide][guide] for an
introduction to writing JSON schemas
An array of strings describing which links this request can be sent
over. Direction matters; `'master-slave'` means the request can be sent
from the master to the slave and the slave can reply to the master, but
the slave can't send a request to the master unless `'slave-master'` is
also present in the links array.
**responseProperties**:
Same as the requestProperties only for the response sent back by the
target.
The available endpoints are `master`, `slave`, `instance`, and
`control`. Master talks with slave and control, and slave talks to
instance. The full chain must be specified as the individual links in
order for a message to travers multiple hops, (i.e., for a message to go
from master to instance it must have both `'master-slave'` and
`'slave-instance'` in the links array). See `forwardTo` for ways to
forward a request to the next link in a chain.
#### forwardTo
Target to forward the request to. Can either be `'master'` to indicate
a slave should forward it to the master server when receiving it from an
instance, or `'instance'` to indicate it should be forwarded to the
instances specified by the `instance_id` request property. This works
by using a default handler for the request by the links that forward it.
#### requestProperties
Object with properties mapping to a JSON schema of that property that
specifies what's valid to send in the request. This is equivalent to
using the `properties` keyword in JSON schema, except that the
properties specified are implicitly required and additional properties
are not allowed. See [this guide][guide] for an introduction to writing
JSON schemas
#### responseProperties
Same as the requestProperties only for the response sent back by the
target.
Sending Link Messages
@@ -554,3 +589,66 @@ label matches the id of the instance shut down.
For statistics you need to update on collection there's an `onMetrics`
hook on both master and instance plugins that is run before the
metrics in the default registry are collected.
Adding Custom Commands to clusterctl
------------------------------------
The control entrypoint for plugins allows you to extend clustectl with
your own commands. The creation of custom commands typically starts
with defining a command tree for the plugin:
const { Command, CommandTree } = require("lib/command");
const fooFrobberCommands = new CommandTree({
name: "foo-frobber", description: "Foo Frobber Plugin commands"
});
Then commands are added to the the plugin's command tree:
const info = require("./info");
fooFrobberCommands.add(new Command({
definition: ["frobnicate <type>", "Do frobnications", (yargs) => {
yargs.positional("level", {
describe: "type of frobnication", type: "string"
});
}],
handler: async function(args, control) {
await info.messages.frobnicate.send(control, {
instance_name: "Console",
content: args.message,
});
},
}));
For a command the `definition` is the arguments to pass to
[yargs.command](http://yargs.js.org/docs/#api-reference-commandcmd-desc-builder-handler)
(see also
[yargs.positional](http://yargs.js.org/docs/#api-reference-positionalkey-opt)
and
[yargs.options](http://yargs.js.org/docs/#api-reference-optionskey-opt)
for setting up positional and optional arguments to commands). The
`handler` is an async function that's invoked when the command is
executed and it's passed the parsed command line arguments and a
reference to the `Control` class of clusterctl. It's possible to optain
a reference to the plugin class with `control.plugins.get(info.name)`.
Note that messages sent from clusterctl needs to have
`"control-master"` as a part of the links array for it to be accepted by
the master server, see [Defining Link Messages](#defining-link-messages)
for how to define the messages that can be sent to the master.
To have the command tree become part of clusterctl it needs to be added
to the rootCommand tree in `addCommands` callback of the Control plugin:
const plugin = require("lib/plugin");
class ControlPlugin extends plugin.BaseControlPlugin {
async addCommands(rootCommand) {
rootCommand.add(fooFrobberCommands);
}
}
module.exports = {
ControlPlugin,
}
+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,
-31
View File
@@ -2,10 +2,8 @@
const assert = require("assert").strict;
const chalk = require("chalk");
const errors = require("lib/errors");
const { testLines } = require("./lib/factorio/lines");
const clusterctl = require("../clusterctl.js");
const mock = require("./mock");
describe("clusterctl", function() {
@@ -23,33 +21,4 @@ describe("clusterctl", function() {
chalk.level = old;
});
});
let mockConnector = new mock.MockConnector();
mockConnector.on("send", function(message) {
if (message.type === "list_instances_request") {
this.emit("message", {
seq: 1, type: "list_instances_response",
data: {
seq: message.seq,
list: [{ id: 57, assigned_slave: 4, name: "Test Instance" }],
},
});
}
});
let testControl = new clusterctl._Control(mockConnector);
describe("resolveInstance", function() {
it("should pass an integer like string back", async function() {
assert.equal(await clusterctl._resolveInstance(null, "123"), 123);
});
it("should resolve an instance name with the master server", async function() {
assert.equal(await clusterctl._resolveInstance(testControl, "Test Instance"), 57);
});
it("should throw if instance is not found", async function() {
await assert.rejects(
clusterctl._resolveInstance(testControl, "invalid"),
new errors.CommandError("No instance named invalid")
);
});
});
});
+23 -21
View File
@@ -32,24 +32,24 @@ describe("Integration of Clusterio", function() {
describe("clusterctl", function() {
describe("list-slaves", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-slaves`);
await exec(`node clusterctl --config ${controlConfigPath} slave list`);
});
});
describe("list-instances", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-instances`);
await exec(`node clusterctl --config ${controlConfigPath} instance list`);
});
});
describe("create-instances", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} create-instance --id 44 --name test`);
await exec(`node clusterctl --config ${controlConfigPath} instance create test --id 44`);
});
});
describe("assign-instance", function() {
it("creates the instance files", async function() {
await exec(`node clusterctl --config ${controlConfigPath} assign-instance --instance test --slave 4`);
await exec(`node clusterctl --config ${controlConfigPath} instance assign test 4`);
assert(await fs.exists(path.join(instancesDir, "test")), "Instance was not created");
});
});
@@ -57,14 +57,14 @@ describe("Integration of Clusterio", function() {
describe("create-save", function() {
it("creates a save", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} create-save --instance test`);
await exec(`node clusterctl --config ${controlConfigPath} instance create-save test`);
});
});
describe("export-data", function() {
it("exports the data", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} export-data --instance test`);
await exec(`node clusterctl --config ${controlConfigPath} instance export-data test`);
assert(await fs.exists(path.join("static", "export", "locale.json")), "Export was not created");
});
});
@@ -72,7 +72,7 @@ describe("Integration of Clusterio", function() {
describe("start-instance", function() {
it("starts the instance", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} start-instance --instance test`);
await exec(`node clusterctl --config ${controlConfigPath} instance start test`);
// TODO check that the instance actually started
});
});
@@ -80,7 +80,7 @@ describe("Integration of Clusterio", function() {
describe("send-rcon", function() {
it("sends the command", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} send-rcon --instance test --command test`);
await exec(`node clusterctl --config ${controlConfigPath} instance send-rcon test test`);
// TODO check that the command was received
});
});
@@ -158,8 +158,8 @@ describe("Integration of Clusterio", function() {
for (let [prop, value, ,] of testConfigs) {
value = `"'${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}'"`;
let args = `--instance test --field factorio.settings --prop ${prop} --value ${value}`;
await exec(`node clusterctl --config ${controlConfigPath} set-instance-config-prop ${args}`);
let args = `test factorio.settings ${prop} ${value}`;
await exec(`node clusterctl --config ${controlConfigPath} instance config set-prop ${args}`);
}
// Do this afterwards to leave enough to time for the
@@ -173,7 +173,7 @@ describe("Integration of Clusterio", function() {
describe("stop-instance", function() {
it("stops the instance", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} stop-instance --instance test`);
await exec(`node clusterctl --config ${controlConfigPath} instance stop test`);
// TODO check that the instance actually stopped
});
});
@@ -181,26 +181,27 @@ describe("Integration of Clusterio", function() {
describe("delete-instance", function() {
it("deletes the instance", async function() {
slowTest(this);
await exec(`node clusterctl --config ${controlConfigPath} delete-instance --instance test`);
await exec(`node clusterctl --config ${controlConfigPath} instance delete test`);
assert(!await fs.exists(path.join(instancesDir, "test")), "Instance was not deleted");
});
});
describe("list-permissions", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-permissions`);
await exec(`node clusterctl --config ${controlConfigPath} permission list`);
});
});
describe("list-roles", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-roles`);
await exec(`node clusterctl --config ${controlConfigPath} role list`);
});
});
describe("create-role", function() {
it("should create the given role", async function() {
await exec(`node clusterctl --config ${controlConfigPath} create-role --name temp --description "A temp role" --permissions core.control.connect`);
let args = "--description \"A temp role\" --permissions core.control.connect";
await exec(`node clusterctl --config ${controlConfigPath} role create temp ${args}`);
let result = await link.messages.listRoles.send(getControl());
let role = result.list.find(role => role.name === "temp");
assert.deepEqual(role, { id: 5, name: "temp", description: "A temp role", permissions: ["core.control.connect"] });
@@ -209,7 +210,8 @@ describe("Integration of Clusterio", function() {
describe("edit-role", function() {
it("should modify the given role", async function() {
await exec(`node clusterctl --config ${controlConfigPath} edit-role --role temp --name new --description "A new role" --permissions`);
let args = "--name new --description \"A new role\" --permissions";
await exec(`node clusterctl --config ${controlConfigPath} role edit temp ${args}`);
let result = await link.messages.listRoles.send(getControl());
let role = result.list.find(role => role.name === "new");
assert.deepEqual(role, { id: 5, name: "new", description: "A new role", permissions: [] });
@@ -218,7 +220,7 @@ describe("Integration of Clusterio", function() {
describe("delete-role", function() {
it("should modify the given role", async function() {
await exec(`node clusterctl --config ${controlConfigPath} delete-role --role new`);
await exec(`node clusterctl --config ${controlConfigPath} role delete new`);
let result = await link.messages.listRoles.send(getControl());
let role = result.list.find(role => role.name === "new");
assert(!role, "Role was not deleted");
@@ -227,13 +229,13 @@ describe("Integration of Clusterio", function() {
describe("list-users", function() {
it("runs", async function() {
await exec(`node clusterctl --config ${controlConfigPath} list-users`);
await exec(`node clusterctl --config ${controlConfigPath} user list`);
});
});
describe("create-user", function() {
it("should create the given user", async function() {
await exec(`node clusterctl --config ${controlConfigPath} create-user --name temp`);
await exec(`node clusterctl --config ${controlConfigPath} user create temp`);
let result = await link.messages.listUsers.send(getControl());
let user = result.list.find(user => user.name === "temp");
assert(user, "user was not created");
@@ -242,7 +244,7 @@ describe("Integration of Clusterio", function() {
describe("edit-user-roles", function() {
it("should set the roles on the user", async function() {
await exec(`node clusterctl --config ${controlConfigPath} edit-user-roles --name temp --roles Admin`);
await exec(`node clusterctl --config ${controlConfigPath} user set-roles temp Admin`);
let result = await link.messages.listUsers.send(getControl());
let user = result.list.find(user => user.name === "temp");
assert.deepEqual(user.roles, [0]);
@@ -251,7 +253,7 @@ describe("Integration of Clusterio", function() {
describe("delete-user", function() {
it("should delete the user", async function() {
await exec(`node clusterctl --config ${controlConfigPath} delete-user --name temp`);
await exec(`node clusterctl --config ${controlConfigPath} user delete temp`);
let result = await link.messages.listUsers.send(getControl());
let user = result.list.find(user => user.name === "temp");
assert(!user, "user was note deleted");
+2 -1
View File
@@ -123,7 +123,8 @@ before(async function() {
masterProcess = await spawn("master:", `node master --config ${masterConfigPath} run`, "All plugins loaded");
await exec(`node clusterctl --config ${controlConfigPath} create-slave-config --id 4 --name slave --generate-token --output ${slaveConfigPath}`);
let createArgs = `--id 4 --name slave --generate-token --output ${slaveConfigPath}`;
await exec(`node clusterctl --config ${controlConfigPath} slave create-config ${createArgs}`);
await exec(`node slave --config ${slaveConfigPath} config set slave.instances_directory ${instancesDir}`);
slaveProcess = await spawn("slave:", `node slave --config ${slaveConfigPath} run`, "SOCKET | received ready from master");
+88
View File
@@ -0,0 +1,88 @@
"use strict";
const assert = require("assert").strict;
const command = require("lib/command");
const errors = require("lib/errors");
const link = require("lib/link");
const mock = require("../mock");
describe("lib/command", function() {
let testRole = { id: 28, name: "Test Role", description: "Test", permissions: [] };
let mockConnector = new mock.MockConnector();
mockConnector.on("send", function(message) {
if (message.type === "list_slaves_request") {
this.emit("message", {
seq: 1, type: "list_slaves_response",
data: {
seq: message.seq,
list: [{ agent: "test", version: "0.1", id: 11, name: "Test Slave", connected: false }],
},
});
} else if (message.type === "list_instances_request") {
this.emit("message", {
seq: 1, type: "list_instances_response",
data: {
seq: message.seq,
list: [{ id: 57, assigned_slave: 4, name: "Test Instance" }],
},
});
} else if (message.type === "list_roles_request") {
this.emit("message", {
seq: 1, type: "list_roles_response",
data: {
seq: message.seq,
list: [testRole],
},
});
}
});
let testControl = new mock.MockControl(mockConnector);
link.messages.listSlaves.attach(testControl);
link.messages.listInstances.attach(testControl);
link.messages.listRoles.attach(testControl);
describe("resolveSlave", function() {
it("should pass an integer like string back", async function() {
assert.equal(await command.resolveSlave(null, "123"), 123);
});
it("should resolve a slave name with the master server", async function() {
assert.equal(await command.resolveSlave(testControl, "Test Slave"), 11);
});
it("should throw if slave is not found", async function() {
await assert.rejects(
command.resolveSlave(testControl, "invalid"),
new errors.CommandError("No slave named invalid")
);
});
});
describe("resolveInstance", function() {
it("should pass an integer like string back", async function() {
assert.equal(await command.resolveInstance(null, "123"), 123);
});
it("should resolve an instance name with the master server", async function() {
assert.equal(await command.resolveInstance(testControl, "Test Instance"), 57);
});
it("should throw if instance is not found", async function() {
await assert.rejects(
command.resolveInstance(testControl, "invalid"),
new errors.CommandError("No instance named invalid")
);
});
});
describe("retrieveRole", function() {
it("should retrieve a role by id from an integer like string", async function() {
assert.deepEqual(await command.retrieveRole(testControl, "28"), testRole);
});
it("should retrieve a role by name from a string", async function() {
assert.deepEqual(await command.retrieveRole(testControl, "Test Role"), testRole);
});
it("should throw if role is not found", async function() {
await assert.rejects(
command.retrieveRole(testControl, "invalid"),
new errors.CommandError("No role named invalid")
);
});
});
});
+7
View File
@@ -81,10 +81,17 @@ class MockSlave extends link.Link {
}
}
class MockControl extends link.Link {
constructor(connector) {
super("control", "master", connector);
}
}
module.exports = {
MockSocket,
MockConnector,
MockInstance,
MockSlave,
MockControl,
};