mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
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:
1 parent
9b870fde64
commit
da1cbc16f7
10 files changed
+783
-428
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
Reference in new issue
Block a user