Add remote editing of master config

Add commands and web UI for editing the master configuration remotely.
This commit is contained in:
Hornwitser committed 2021-02-24 22:16:03 +01:00
1 parent 38b7d0f600
commit bec30c9927
9 files changed
+225 -4

No files matched your search

+74
View File
@@ -31,6 +31,79 @@ function print(...content) {
console.log(...content);
}
const masterCommands = new libCommand.CommandTree({ name: "master", description: "Master management" });
const masterConfigCommands = new libCommand.CommandTree({
name: "config", alias: ["c"], description: "master config management",
});
masterConfigCommands.add(new libCommand.Command({
definition: ["list", "List master configuration"],
handler: async function(args, control) {
let response = await libLink.messages.getMasterConfig.send(control);
for (let group of response.serialized_config.groups) {
for (let [name, value] of Object.entries(group.fields)) {
print(`${group.name}.${name} ${JSON.stringify(value)}`);
}
}
},
}));
masterConfigCommands.add(new libCommand.Command({
definition: ["set <field> [value]", "Set field in master config", (yargs) => {
yargs.positional("field", { describe: "Field to set", type: "string" });
yargs.positional("value", { describe: "Value to set", type: "string" });
yargs.options({
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
});
}],
handler: async function(args, control) {
if (args.stdin) {
args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
} else if (args.value === undefined) {
args.value = "";
}
await libLink.messages.setMasterConfigField.send(control, {
field: args.field,
value: args.value,
});
},
}));
masterConfigCommands.add(new libCommand.Command({
definition: ["set-prop <field> <prop> [value]", "Set property of field in master config", (yargs) => {
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" });
yargs.options({
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
});
}],
handler: async function(args, control) {
if (args.stdin) {
args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
}
let request = {
field: args.field,
prop: args.prop,
};
try {
if (args.value !== undefined) {
request.value = JSON.parse(args.value);
}
} catch (err) {
// See note for the instance version of set-prop
if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) {
throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`);
}
request.value = args.value;
}
await libLink.messages.setMasterConfigProp.send(control, request);
},
}));
masterCommands.add(masterConfigCommands);
const slaveCommands = new libCommand.CommandTree({ name: "slave", description: "Slave management" });
slaveCommands.add(new libCommand.Command({
definition: [["list", "l"], "List slaves connected to the master"],
@@ -701,6 +774,7 @@ async function loadPlugins(pluginList) {
async function registerCommands(controlPlugins, yargs) {
const rootCommands = new libCommand.CommandTree({ name: "clusterioctl", description: "Manage cluster" });
rootCommands.add(masterCommands);
rootCommands.add(slaveCommands);
rootCommands.add(instanceCommands);
rootCommands.add(permissionCommands);
+31
View File
@@ -300,6 +300,37 @@ messages.debugDumpWs = new Request({
// Management requests
messages.getMasterConfig = new Request({
type: "get_master_config",
links: ["control-master"],
permission: "core.master.get_config",
responseProperties: {
"serialized_config": { type: "object" },
},
});
messages.setMasterConfigField = new Request({
type: "set_master_config_field",
links: ["control-master"],
permission: "core.master.update_config",
requestProperties: {
"field": { type: "string" },
"value": { type: "string" },
},
});
messages.setMasterConfigProp = new Request({
type: "set_master_config_prop",
links: ["control-master"],
permission: "core.master.update_config",
requestRequired: ["field", "prop"],
requestProperties: {
"field": { type: "string" },
"prop": { type: "string" },
"value": {},
},
});
messages.listSlaves = new Request({
type: "list_slaves",
links: ["control-master"],
+10
View File
@@ -302,6 +302,16 @@ definePermission({
grantByDefault: true,
});
definePermission({
name: "core.master.get_config",
title: "Get master config",
description: "Get the config of master.",
});
definePermission({
name: "core.master.update_config",
title: "Modify master config",
description: "Modify the master config or entries of the master config.",
});
definePermission({
name: "core.slave.list",
title: "List slaves",
+13
View File
@@ -684,6 +684,19 @@ class ControlConnection extends BaseConnection {
});
}
async getMasterConfigRequestHandler() {
return { serialized_config: masterConfig.serialize("control") };
}
async setMasterConfigFieldRequestHandler(message) {
masterConfig.set(message.data.field, message.data.value, "control");
}
async setMasterConfigPropRequestHandler(message) {
let { field, prop, value } = message.data;
masterConfig.setProp(field, prop, value, "control");
}
async listSlavesRequestHandler(message) {
let list = [];
for (let slave of db.slaves.values()) {
+3 -4
View File
@@ -2,15 +2,14 @@
// Routes used in the web interface and served by the master server
module.exports = [
"/",
"/instances",
"/instances/:id/view",
"/master",
"/slaves",
"/slaves/:id/view",
"/instances",
"/instances/:id/view",
"/users",
"/users/:id/view",
"/roles",
"/roles/:id/view",
"/permissions",
"/permissions/:id/view",
"/settings",
];
@@ -0,0 +1,44 @@
import React, { useContext } from "react";
import { Typography } from "antd";
import libLink from "@clusterio/lib/link";
import libConfig from "@clusterio/lib/config";
import BaseConfigTree from "./BaseConfigTree";
import ControlContext from "./ControlContext";
export default function MasterConfigTree(props) {
let control = useContext(ControlContext);
async function retrieveConfig() {
let result = await libLink.messages.getMasterConfig.send(control);
return result.serialized_config;
}
async function setField(field, value) {
await libLink.messages.setMasterConfigField.send(control, {
field,
value: String(value),
});
}
async function setProp(field, prop, value) {
let data = { field, prop };
if (value) {
try {
data.value = JSON.parse(value);
} catch (err) {
return;
}
}
await libLink.messages.setMasterConfigProp.send(control, data);
}
return <BaseConfigTree
ConfigClass={libConfig.MasterConfig}
retrieveConfig={retrieveConfig}
setField={setField}
setProp={setProp}
/>;
}
@@ -0,0 +1,12 @@
import React, { useContext } from "react";
import MasterConfigTree from "./MasterConfigTree";
import PageLayout from "./PageLayout";
export default function SlavesPage() {
return <PageLayout nav={[{ name: "Master" }]}>
<h2>Master</h2>
<MasterConfigTree />
</PageLayout>;
};
@@ -9,6 +9,7 @@ import ErrorPage from "./ErrorPage";
const { Header, Sider } = Layout;
import MasterPage from "./MasterPage";
import SlavesPage from "./SlavesPage";
import InstancesPage from "./InstancesPage";
import InstanceViewPage from "./InstanceViewPage";
@@ -24,6 +25,7 @@ const pages = [
];
const sidebar = [
{ name: "Master", path: "/master", content: <MasterPage /> },
{ name: "Slaves", path: "/slaves", content: <SlavesPage /> },
{ name: "Instances", path: "/instances", content: <InstancesPage />},
{ name: "Users", path: "/users", content: <UsersPage /> },
+36
View File
@@ -35,6 +35,42 @@ describe("Integration of Clusterio", function() {
});
describe("clusterioctl", function() {
describe("master config list", function() {
it("runs", async function() {
await execCtl("master config list");
});
it("should not leak auth_secret", async function() {
let result = await libLink.messages.getMasterConfig.send(getControl());
let done = false;
for (let group of result.serialized_config.groups) {
if (group.name === "master") {
assert.equal(Object.prototype.hasOwnProperty.call(group.fields, "auth_secret"), false);
done = true;
break;
}
}
assert(done, "master group not found");
});
});
describe("master config set", function() {
it("sets given config option", async function() {
await execCtl('master config set master.name "Test Cluster"');
let result = await libLink.messages.getMasterConfig.send(getControl());
let done = false;
for (let group of result.serialized_config.groups) {
if (group.name === "master") {
assert.equal(group.fields.name, "Test Cluster");
done = true;
break;
}
}
assert(done, "master group not found");
});
it("should not allow setting auth_secret", async function() {
await assert.rejects(execCtl("master config set master.auth_secret root"));
});
});
describe("slave list", function() {
it("runs", async function() {
await execCtl("slave list");