mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
Merge pull request #545 from Hornwitser/remote-host-config
Remote host config
This commit is contained in:
21 files changed
+540
-180
No files matched your search
@@ -107,9 +107,7 @@ Sent by the controller when the WebSocket connection has been opened.
|
||||
Host handshake for establishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `agent: string` - Human readable string of the software connecting.
|
||||
- `version: string` - The protocol version of the host, e.g. "2.0.0".
|
||||
- `name: string` - Name of the host.
|
||||
- `id: integer` - ID of the host.
|
||||
- `plugins: Object<string, string>` -
|
||||
Object mapping plugin names to plugin version for plugins that are available on the host.
|
||||
@@ -119,7 +117,6 @@ Host handshake for establishing a new connection session.
|
||||
Control handshake for enstablishing a new connection session.
|
||||
|
||||
- `token: string` - Authentication token.
|
||||
- `agent: string` - Human readable string of the software connecting.
|
||||
- `version: string` - The protocol version of the control, e.g. "2.0.0".
|
||||
|
||||
#### `resume`
|
||||
@@ -243,9 +240,7 @@ Host registering a new connection session.
|
||||
"type": "register_host",
|
||||
"data": {
|
||||
"token": "xyzabc123",
|
||||
"agent": "Clusterio Host",
|
||||
"version": "2.0.0",
|
||||
"name": "Foo",
|
||||
"id": 123
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ const queryLogTime = new lib.Summary(
|
||||
* @alias module:controller/src/ControlConnection
|
||||
*/
|
||||
export default class ControlConnection extends BaseConnection {
|
||||
private _agent: string;
|
||||
private _version: string;
|
||||
logTransport: lib.LinkTransport | null = null;
|
||||
logSubscriptions = {
|
||||
@@ -38,7 +37,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
declare connector: WsServerConnector;
|
||||
|
||||
constructor(
|
||||
registerData: { agent:string, version:string },
|
||||
registerData: { version: string },
|
||||
connector: WsServerConnector,
|
||||
controller: Controller,
|
||||
public user: ControllerUser, // The user making this connection.
|
||||
@@ -46,7 +45,6 @@ export default class ControlConnection extends BaseConnection {
|
||||
) {
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._version = registerData.version;
|
||||
|
||||
this.connector.on("connect", () => {
|
||||
|
||||
@@ -16,10 +16,7 @@ import HostInfo from "./HostInfo";
|
||||
* @alias module:controller/src/HostConnection
|
||||
*/
|
||||
export default class HostConnection extends BaseConnection {
|
||||
private _agent: string;
|
||||
private _id: number;
|
||||
private _name: string;
|
||||
private _version: string;
|
||||
private info: HostInfo;
|
||||
plugins: Map<string, string>;
|
||||
|
||||
constructor(
|
||||
@@ -29,28 +26,24 @@ export default class HostConnection extends BaseConnection {
|
||||
) {
|
||||
super(connector, controller);
|
||||
|
||||
this._agent = registerData.agent;
|
||||
this._id = registerData.id;
|
||||
this._name = registerData.name;
|
||||
this._version = registerData.version;
|
||||
this.plugins = new Map(Object.entries(registerData.plugins));
|
||||
this._checkPluginVersions();
|
||||
|
||||
const previousHostInfo = this._controller.hosts.get(this._id);
|
||||
const currentHostInfo = new HostInfo(
|
||||
this._agent,
|
||||
this._id,
|
||||
this._name,
|
||||
this._version,
|
||||
const previousHostInfo = this._controller.hosts.get(registerData.id);
|
||||
this.info = new HostInfo(
|
||||
registerData.id,
|
||||
previousHostInfo?.name ?? "",
|
||||
registerData.version,
|
||||
this.plugins,
|
||||
true,
|
||||
registerData.publicAddress ?? "",
|
||||
previousHostInfo?.publicAddress,
|
||||
previousHostInfo?.tokenValidAfter,
|
||||
Date.now(),
|
||||
0,
|
||||
false,
|
||||
);
|
||||
this._controller.hosts.set(this._id, currentHostInfo);
|
||||
this._controller.hostsUpdated([currentHostInfo]);
|
||||
this._controller.hosts.set(this.id, this.info);
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
|
||||
this._checkPluginVersions();
|
||||
|
||||
for (let event of ["connect", "drop", "resume", "close"] as const) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
@@ -65,7 +58,7 @@ export default class HostConnection extends BaseConnection {
|
||||
// Update status to unknown for instances on this host.
|
||||
const instances: InstanceInfo[] = [];
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_host") !== this._id) {
|
||||
if (instance.config.get("instance.assigned_host") !== this.id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -76,10 +69,11 @@ export default class HostConnection extends BaseConnection {
|
||||
}
|
||||
this._controller.instanceDetailsUpdated(instances);
|
||||
|
||||
currentHostInfo.connected = false;
|
||||
this._controller.hostsUpdated([currentHostInfo]);
|
||||
this.info.connected = false;
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
});
|
||||
|
||||
this.handle(lib.HostInfoUpdateEvent, this.handleHostInfoUpdateEvent.bind(this));
|
||||
this.handle(lib.InstanceStatusChangedEvent, this.handleInstanceStatusChangedEvent.bind(this));
|
||||
this.handle(lib.InstancesUpdateRequest, this.handleInstancesUpdateRequest.bind(this));
|
||||
this.handle(lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsUpdatesEvent.bind(this));
|
||||
@@ -104,7 +98,7 @@ export default class HostConnection extends BaseConnection {
|
||||
|
||||
case lib.Address.instance:
|
||||
let instance = this._controller.instances.get(message.src.id);
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this.id) {
|
||||
throw new lib.InvalidMessage(
|
||||
`Received message with invalid src ${message.src} from ${origin}`
|
||||
);
|
||||
@@ -121,21 +115,24 @@ export default class HostConnection extends BaseConnection {
|
||||
for (let [name, version] of this.plugins) {
|
||||
let info = pluginInfos.get(name);
|
||||
if (!info) {
|
||||
logger.warn(`Host ${this._name} has plugin ${name} ${version} which the controller does not have`);
|
||||
logger.warn(
|
||||
`Host ${this.info.name} (${this.id}) has plugin ${name} ${version} which the ` +
|
||||
"controller does not have",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (info.version !== version) {
|
||||
logger.warn(
|
||||
`Host ${this._name} has plugin ${name} ${version} which does not match the version of this ` +
|
||||
`plugin on the controller (${info.version})`
|
||||
`Host ${this.info.name} (${this.id}) has plugin ${name} ${version} which does not match ` +
|
||||
`the version of this plugin on the controller (${info.version})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let [name, info] of pluginInfos) {
|
||||
if (!this.plugins.has(name)) {
|
||||
logger.warn(`Host ${this._name} is missing plugin ${name} ${info.version}`);
|
||||
logger.warn(`Host ${this.info.name} (${this.id}) is missing plugin ${name} ${info.version}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,7 +148,13 @@ export default class HostConnection extends BaseConnection {
|
||||
* @returns {number} host ID.
|
||||
*/
|
||||
get id() {
|
||||
return this._id;
|
||||
return this.info.id;
|
||||
}
|
||||
|
||||
async handleHostInfoUpdateEvent(event: lib.HostInfoUpdateEvent) {
|
||||
this.info.name = event.update.name;
|
||||
this.info.publicAddress = event.update.publicAddress;
|
||||
this._controller.hostsUpdated([this.info]);
|
||||
}
|
||||
|
||||
async handleInstanceStatusChangedEvent(request: lib.InstanceStatusChangedEvent) {
|
||||
@@ -161,7 +164,7 @@ export default class HostConnection extends BaseConnection {
|
||||
// or is not assigned to the host it originated from if it was
|
||||
// reassigned or deleted while the connection to the host it was
|
||||
// originally on was down at the time.
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this._id) {
|
||||
if (!instance || instance.config.get("instance.assigned_host") !== this.id) {
|
||||
logger.warn(`Got bogus update for instance id ${request.instanceId}`);
|
||||
return;
|
||||
}
|
||||
@@ -187,7 +190,7 @@ export default class HostConnection extends BaseConnection {
|
||||
async handleInstancesUpdateRequest(request: lib.InstancesUpdateRequest) {
|
||||
// Push updated instance configs
|
||||
for (let instance of this._controller.instances.values()) {
|
||||
if (instance.config.get("instance.assigned_host") === this._id) {
|
||||
if (instance.config.get("instance.assigned_host") === this.id) {
|
||||
await this.send(
|
||||
new lib.InstanceAssignInternalRequest(instance.id, instance.config.toRemote("host"))
|
||||
);
|
||||
@@ -203,7 +206,7 @@ export default class HostConnection extends BaseConnection {
|
||||
let controllerInstance = this._controller.instances.get(instanceConfig.get("instance.id"));
|
||||
if (controllerInstance) {
|
||||
// Check if this instance is assigned somewhere else.
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this._id) {
|
||||
if (controllerInstance.config.get("instance.assigned_host") !== this.id) {
|
||||
await this.send(
|
||||
new lib.InstanceUnassignInternalRequest(instanceConfig.get("instance.id"))
|
||||
);
|
||||
@@ -225,7 +228,7 @@ export default class HostConnection extends BaseConnection {
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceConfig.set("instance.assigned_host", this._id);
|
||||
instanceConfig.set("instance.assigned_host", this.id);
|
||||
let newInstance = new InstanceInfo(
|
||||
instanceConfig,
|
||||
instanceData.status,
|
||||
@@ -301,8 +304,8 @@ export default class HostConnection extends BaseConnection {
|
||||
async handleLogMessageEvent(event: lib.LogMessageEvent) {
|
||||
this._controller.clusterLogger.log({
|
||||
...event.info,
|
||||
host_id: this._id,
|
||||
host_name: this._name,
|
||||
host_id: this.id,
|
||||
host_name: this.info.name,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ import { HostDetails } from "@clusterio/lib";
|
||||
*/
|
||||
export default class HostInfo {
|
||||
constructor(
|
||||
/** Agent this host last connected with */
|
||||
public agent: string,
|
||||
/** Id of this host */
|
||||
public id: number,
|
||||
/** Version this host last connected with */
|
||||
@@ -29,7 +27,6 @@ export default class HostInfo {
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"agent": Type.String(),
|
||||
"id": Type.Number(),
|
||||
"name": Type.String(),
|
||||
"version": Type.String(),
|
||||
@@ -43,7 +40,6 @@ export default class HostInfo {
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.agent,
|
||||
json.id,
|
||||
json.name,
|
||||
json.version,
|
||||
@@ -58,7 +54,6 @@ export default class HostInfo {
|
||||
|
||||
toJSON(): Static<typeof HostInfo.jsonSchema> {
|
||||
return {
|
||||
agent: this.agent,
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
@@ -73,7 +68,6 @@ export default class HostInfo {
|
||||
|
||||
toHostDetails() {
|
||||
return new HostDetails(
|
||||
this.agent,
|
||||
this.version,
|
||||
this.name,
|
||||
this.id,
|
||||
|
||||
@@ -261,11 +261,11 @@ ${err.stack}`
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`WsServer | registered host ${data.name} (${data.id}) using agent ${data.agent} ${data.version}`
|
||||
`WsServer | registered host ${data.id} using version ${data.version}`
|
||||
);
|
||||
if (data.agent === "Clusterio Host" && data.version !== packageVersion) {
|
||||
if (data.version !== packageVersion) {
|
||||
logger.warn(
|
||||
`Host ${data.name} (${data.id}) connected using version ${data.version} which does not match the ` +
|
||||
`Host ${data.id} connected using version ${data.version} which does not match the ` +
|
||||
`version of the controller is currently running (${packageVersion}). It may not work as expected.`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ class ControlConnector extends lib.WebSocketClientConnector {
|
||||
new lib.MessageRegisterControl(
|
||||
new lib.RegisterControlData(
|
||||
this._token,
|
||||
"clusterioctl",
|
||||
version,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -58,7 +58,7 @@ async function configToKeyVal(data: string) {
|
||||
|
||||
function serializedConfigToString(
|
||||
serializedConfig: lib.ConfigSchema,
|
||||
configGroup: typeof lib.ControllerConfig | typeof lib.InstanceConfig,
|
||||
configGroup: typeof lib.ControllerConfig | typeof lib.HostConfig | typeof lib.InstanceConfig,
|
||||
disallowedList: Record<string, unknown>,
|
||||
) {
|
||||
let allConfigElements = "";
|
||||
@@ -289,6 +289,149 @@ hostCommands.add(new lib.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
const hostConfigCommands = new lib.CommandTree({
|
||||
name: "config", alias: ["c"], description: "Host config management",
|
||||
});
|
||||
hostConfigCommands.add(new lib.Command({
|
||||
definition: ["list <host>", "List configuration for a host", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to list config for", type: "string" });
|
||||
}],
|
||||
handler: async function(args: { host: string }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
let config = await control.sendTo({ hostId }, new lib.HostConfigGetRequest());
|
||||
|
||||
for (let [name, value] of Object.entries(config)) {
|
||||
print(`${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
hostConfigCommands.add(new lib.Command({
|
||||
definition: ["set <host> <field> [value]", "Set field in host config", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to set config on", type: "string" });
|
||||
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: { host: string, field: string, value?: string, stdin?: boolean },
|
||||
control: Control,
|
||||
) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
if (args.stdin) {
|
||||
args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
||||
} else if (args.value === undefined) {
|
||||
args.value = "";
|
||||
}
|
||||
await control.sendTo({ hostId }, new lib.HostConfigSetFieldRequest(args.field, args.value));
|
||||
},
|
||||
}));
|
||||
|
||||
hostConfigCommands.add(new lib.Command({
|
||||
definition: ["set-prop <host> <field> <prop> [value]", "Set property of field in host config", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host 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" });
|
||||
yargs.options({
|
||||
"stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" },
|
||||
});
|
||||
}],
|
||||
handler: async function(
|
||||
args: { host: string, field: string, prop: string, value?: string, stdin?: boolean },
|
||||
control: Control
|
||||
) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
if (args.stdin) {
|
||||
args.value = (await lib.readStream(process.stdin)).toString().replace(/\r?\n$/, "");
|
||||
}
|
||||
let value;
|
||||
try {
|
||||
if (args.value !== undefined) {
|
||||
value = JSON.parse(args.value);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// See note for the instance version of set-prop
|
||||
if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value!)) {
|
||||
throw new lib.CommandError(`In parsing value '${args.value}': ${err.message}`);
|
||||
}
|
||||
value = args.value;
|
||||
}
|
||||
await control.sendTo({ hostId }, new lib.HostConfigSetPropRequest(args.field, args.prop, value));
|
||||
},
|
||||
}));
|
||||
|
||||
hostConfigCommands.add(new lib.Command({
|
||||
definition: ["edit <host> [editor]", "Edit host configuration", (yargs) => {
|
||||
yargs.positional("host", { describe: "Host to set config on", type: "string" });
|
||||
yargs.positional("editor", {
|
||||
describe: "Editor to use",
|
||||
type: "string",
|
||||
default: "",
|
||||
});
|
||||
}],
|
||||
handler: async function(args: { host: string, editor: string }, control: Control) {
|
||||
let hostId = await lib.resolveHost(control, args.host);
|
||||
let config = await control.sendTo({ hostId }, new lib.HostConfigGetRequest());
|
||||
let tmpFile = await lib.getTempFile("ctl-", "-tmp", os.tmpdir());
|
||||
let editor = await getEditor(args.editor);
|
||||
if (editor === undefined) {
|
||||
throw new lib.CommandError(`No editor avalible. Checked CLI input, EDITOR and VISUAL env vars
|
||||
Try "ctl controller config edit <editor of choice>"`);
|
||||
}
|
||||
let disallowedList = {"host.id": 0};
|
||||
let allConfigElements = serializedConfigToString(
|
||||
config,
|
||||
lib.HostConfig,
|
||||
disallowedList
|
||||
);
|
||||
await fs.writeFile(tmpFile, allConfigElements);
|
||||
let editorSpawn = child_process.spawn(editor, [tmpFile], {
|
||||
stdio: "inherit",
|
||||
detached: false,
|
||||
});
|
||||
editorSpawn.on("data", (data) => {
|
||||
process.stdout.pipe(data);
|
||||
});
|
||||
let doneEmitter = new events.EventEmitter();
|
||||
editorSpawn.on("exit", async (exit) => {
|
||||
const data = await fs.readFile(tmpFile, "utf8");
|
||||
const final = await configToKeyVal(data);
|
||||
for (let index in final) {
|
||||
if (index in final) {
|
||||
try {
|
||||
await control.sendTo({ hostId }, new lib.HostConfigSetFieldRequest(
|
||||
index,
|
||||
final[index],
|
||||
));
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line
|
||||
print(`\n\n\nAttempt to set ${index} to ${final[index] || String(null)} failed; set back to previous value.`);
|
||||
// If the string is empty, it's better to just print "" instead of nothing
|
||||
print("This message shouldn't normally appear; if the below message does not indicate it");
|
||||
print("was a user mistake, please report it to the clustorio devs.");
|
||||
// added this because it could be a missed entry in disallowedList
|
||||
// i've added all the vanilla clustorio configs, but there may be
|
||||
// some modded ones that need to be added.
|
||||
print(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
doneEmitter.emit("dot_on_done");
|
||||
});
|
||||
await events.once(doneEmitter, "dot_on_done");
|
||||
try {
|
||||
await fs.unlink(tmpFile);
|
||||
} catch (err) {
|
||||
print("err: temporary file", tmpFile, "could not be deleted.");
|
||||
print("This is not fatal, but they may build up over time if the issue persists.");
|
||||
}
|
||||
},
|
||||
}));
|
||||
hostCommands.add(hostConfigCommands);
|
||||
|
||||
|
||||
const instanceCommands = new lib.CommandTree({
|
||||
name: "instance", alias: ["i"], description: "Instance management",
|
||||
|
||||
@@ -51,11 +51,8 @@ export class HostConnector extends lib.WebSocketClientConnector {
|
||||
new lib.MessageRegisterHost(
|
||||
new lib.RegisterHostData(
|
||||
this.hostConfig.get("host.controller_token"),
|
||||
"Clusterio Host",
|
||||
version,
|
||||
this.hostConfig.get("host.name"),
|
||||
this.hostConfig.get("host.id"),
|
||||
this.hostConfig.get("host.public_address"),
|
||||
plugins,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -288,6 +288,9 @@ export default class Host extends lib.Link {
|
||||
this.config = hostConfig;
|
||||
|
||||
this.config.on("fieldChanged", (name, curr, prev) => {
|
||||
if (name === "host.name" || name === "host.public_address") {
|
||||
this.sendHostUpdate();
|
||||
}
|
||||
lib.invokeHook(this.plugins, "onHostConfigFieldChanged", name, curr, prev);
|
||||
});
|
||||
|
||||
@@ -301,6 +304,7 @@ export default class Host extends lib.Link {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendHostUpdate();
|
||||
this.updateInstances().catch((err) => {
|
||||
if (err instanceof lib.SessionLost) {
|
||||
return undefined;
|
||||
@@ -341,6 +345,9 @@ export default class Host extends lib.Link {
|
||||
});
|
||||
}
|
||||
|
||||
this.handle(lib.HostConfigGetRequest, this.handleHostConfigGetRequest.bind(this));
|
||||
this.handle(lib.HostConfigSetFieldRequest, this.handleHostConfigSetFieldRequest.bind(this));
|
||||
this.handle(lib.HostConfigSetPropRequest, this.handleHostConfigSetPropRequest.bind(this));
|
||||
this.handle(lib.SyncUserListsEvent, this.handleSyncUserListsEvent.bind(this));
|
||||
this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.handleAdminlistUpdateEvent.bind(this));
|
||||
this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.handleBanlistUpdateEvent.bind(this));
|
||||
@@ -423,6 +430,25 @@ export default class Host extends lib.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async handleHostConfigGetRequest() {
|
||||
return this.config.toRemote("control");
|
||||
}
|
||||
|
||||
async handleHostConfigSetFieldRequest(request: lib.HostConfigSetFieldRequest) {
|
||||
if (request.field === "host.id") {
|
||||
// Changing host.id at runtime is not worth the effort to
|
||||
// support and will break a lot of things if it is allowed.
|
||||
throw new lib.RequestError("Setting 'host.id' while host is running is not supported");
|
||||
}
|
||||
|
||||
this.config.set(request.field as keyof lib.HostConfigFields, request.value, "control");
|
||||
}
|
||||
|
||||
async handleHostConfigSetPropRequest(request: lib.HostConfigSetPropRequest) {
|
||||
let { field, prop, value } = request;
|
||||
this.config.setProp(field as keyof lib.HostConfigFields, prop, value, "control");
|
||||
}
|
||||
|
||||
async handleSyncUserListsEvent(event: lib.SyncUserListsEvent) {
|
||||
let updateList = (list: Set<string>, updatedList: Set<string>, Event: lib.EventClass<unknown>) => {
|
||||
let added = new Set(updatedList);
|
||||
@@ -970,6 +996,17 @@ export default class Host extends lib.Link {
|
||||
await fs.remove(instanceInfo.path);
|
||||
}
|
||||
|
||||
sendHostUpdate() {
|
||||
this.send(
|
||||
new lib.HostInfoUpdateEvent(
|
||||
new lib.HostInfoUpdate(
|
||||
this.config.get("host.name"),
|
||||
this.config.get("host.public_address"),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover available instances
|
||||
*
|
||||
|
||||
@@ -211,26 +211,20 @@ export class MessageHello extends Message {
|
||||
export class RegisterHostData {
|
||||
constructor(
|
||||
public token: string,
|
||||
public agent: string,
|
||||
public version: string,
|
||||
public name: string,
|
||||
public id: number,
|
||||
public publicAddress: string | undefined,
|
||||
public plugins: Record<string, string>,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"token": Type.String(),
|
||||
"agent": Type.String(),
|
||||
"version": Type.String(),
|
||||
"name": Type.String(),
|
||||
"id": Type.Integer(),
|
||||
"publicAddress": Type.Optional(Type.String()),
|
||||
"plugins": Type.Record(Type.String(), Type.String()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.token, json.agent, json.version, json.name, json.id, json.publicAddress, json.plugins);
|
||||
return new this(json.token, json.version, json.id, json.plugins);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,18 +248,16 @@ export class MessageRegisterHost extends Message {
|
||||
export class RegisterControlData {
|
||||
constructor(
|
||||
public token: string,
|
||||
public agent: string,
|
||||
public version: string,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"token": Type.String(),
|
||||
"agent": Type.String(),
|
||||
"version": Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.token, json.agent, json.version);
|
||||
return new this(json.token, json.version);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,65 @@
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import { jsonArray, StringEnum } from "./composites";
|
||||
import { jsonArray, plainJson, StringEnum } from "./composites";
|
||||
import { CollectorResultSerialized } from "../prometheus";
|
||||
import { HostConfig } from "../config/definitions";
|
||||
|
||||
export class HostConfigGetRequest {
|
||||
declare ["constructor"]: typeof HostConfigGetRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "host" as const;
|
||||
static permission = "core.host.get_config" as const;
|
||||
static Response = plainJson(HostConfig.jsonSchema);
|
||||
}
|
||||
|
||||
export class HostConfigSetFieldRequest {
|
||||
declare ["constructor"]: typeof HostConfigSetFieldRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "host" as const;
|
||||
static permission = "core.host.update_config" as const;
|
||||
|
||||
constructor(
|
||||
public field: string,
|
||||
public value: string,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"field": Type.String(),
|
||||
"value": Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.field, json.value);
|
||||
}
|
||||
}
|
||||
|
||||
export class HostConfigSetPropRequest {
|
||||
declare ["constructor"]: typeof HostConfigSetPropRequest;
|
||||
static type = "request" as const;
|
||||
static src = ["control", "controller"] as const;
|
||||
static dst = "host" as const;
|
||||
static permission = "core.host.update_config" as const;
|
||||
|
||||
constructor(
|
||||
public field: string,
|
||||
public prop: string,
|
||||
public value?: unknown,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"field": Type.String(),
|
||||
"prop": Type.String(),
|
||||
"value": Type.Optional(Type.Unknown()),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(json.field, json.prop, json.value);
|
||||
}
|
||||
}
|
||||
|
||||
export class HostDetails {
|
||||
constructor(
|
||||
public agent: string,
|
||||
public version: string,
|
||||
public name: string,
|
||||
public id: number,
|
||||
@@ -17,7 +72,6 @@ export class HostDetails {
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"agent": Type.String(),
|
||||
"version": Type.String(),
|
||||
"name": Type.String(),
|
||||
"id": Type.Integer(),
|
||||
@@ -30,7 +84,6 @@ export class HostDetails {
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.agent,
|
||||
json.version,
|
||||
json.name,
|
||||
json.id,
|
||||
@@ -52,6 +105,44 @@ export class HostListRequest {
|
||||
static Response = jsonArray(HostDetails);
|
||||
}
|
||||
|
||||
export class HostInfoUpdate {
|
||||
constructor(
|
||||
public name: string,
|
||||
public publicAddress: string,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"name": Type.String(),
|
||||
"publicAddress": Type.String(),
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(
|
||||
json.name,
|
||||
json.publicAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class HostInfoUpdateEvent {
|
||||
declare ["constructor"]: typeof HostInfoUpdateEvent;
|
||||
static type = "event" as const;
|
||||
static src = "host" as const;
|
||||
static dst = "controller" as const;
|
||||
|
||||
constructor(
|
||||
public update: HostInfoUpdate,
|
||||
) { }
|
||||
|
||||
static jsonSchema = Type.Object({
|
||||
"update": HostInfoUpdate.jsonSchema,
|
||||
});
|
||||
|
||||
static fromJSON(json: Static<typeof this.jsonSchema>) {
|
||||
return new this(HostInfoUpdate.fromJSON(json.update));
|
||||
}
|
||||
}
|
||||
|
||||
export class HostUpdatesEvent {
|
||||
declare ["constructor"]: typeof HostUpdatesEvent;
|
||||
static type = "event" as const;
|
||||
|
||||
@@ -32,7 +32,11 @@ export const dataClasses: (RequestClass<unknown, unknown> | EventClass<unknown>)
|
||||
|
||||
subscriptions.SubscriptionRequest,
|
||||
|
||||
host.HostConfigGetRequest,
|
||||
host.HostConfigSetFieldRequest,
|
||||
host.HostConfigSetPropRequest,
|
||||
host.HostListRequest,
|
||||
host.HostInfoUpdateEvent,
|
||||
host.HostUpdatesEvent,
|
||||
host.HostMetricsRequest,
|
||||
host.ControllerConnectionEvent,
|
||||
|
||||
@@ -136,6 +136,16 @@ definePermission({
|
||||
"Subscribe to be notified when the system metrics detailing cpu, memory and disk usage for the " +
|
||||
"controller and hosts are updated.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.host.get_config",
|
||||
title: "Get host configs",
|
||||
description: "Get the config of hosts.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.host.update_config",
|
||||
title: "Modify host configs",
|
||||
description: "Modify the host config or entries of the host config.",
|
||||
});
|
||||
definePermission({
|
||||
name: "core.host.list",
|
||||
title: "List hosts",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Card, Checkbox, Form, Input, InputNumber, Space, Spin, Tooltip, Tree, Typography } from "antd";
|
||||
import {
|
||||
Button, Card, Checkbox, Form, FormInstance, Input, InputNumber, Space, Spin, Tooltip, Tree, Typography,
|
||||
} from "antd";
|
||||
import ReloadOutlined from "@ant-design/icons/ReloadOutlined";
|
||||
|
||||
const { Title } = Typography;
|
||||
@@ -25,20 +27,36 @@ function getInitialValues(config: lib.Config<any>, props: BaseConfigTreeProps) {
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
function renderInput(def: lib.FieldDefinition) {
|
||||
if (def.type === "boolean") {
|
||||
return <Checkbox/>;
|
||||
}
|
||||
if (def.type === "string") {
|
||||
return <Input/>;
|
||||
}
|
||||
if (def.type === "number") {
|
||||
return <InputNumber/>;
|
||||
}
|
||||
|
||||
return `Unknown type ${def.type}`;
|
||||
}
|
||||
|
||||
type BaseConfigTreeProps = {
|
||||
ConfigClass: typeof lib.ControllerConfig | typeof lib.InstanceConfig;
|
||||
ConfigClass: typeof lib.ControllerConfig | typeof lib.HostConfig | typeof lib.InstanceConfig;
|
||||
retrieveConfig: () => Promise<lib.ConfigSchema>;
|
||||
setField: (field:string, value:any) => Promise<void>;
|
||||
setProp: (field:string, prop:string, value:any) => Promise<void>;
|
||||
id?: number;
|
||||
onApply?: () => void;
|
||||
available?: boolean;
|
||||
};
|
||||
export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let [config, setConfig] = useState<lib.Config<any>|null>(null);
|
||||
let [form] = Form.useForm();
|
||||
let [changedFields, setChangedFields] = useState<Set<string>>(new Set());
|
||||
let [errorFields, setErrorFields] = useState(new Map());
|
||||
let [errorFields, setErrorFields] = useState(new Map<string, string>());
|
||||
let [applying, setApplying] = useState(false);
|
||||
const available = props.available ?? true;
|
||||
|
||||
async function updateConfig() {
|
||||
let serializedConfig = await props.retrieveConfig();
|
||||
@@ -47,27 +65,29 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
return newConfig;
|
||||
}
|
||||
useEffect(() => {
|
||||
updateConfig().then(updatedConfig => {
|
||||
form.setFieldsValue(getInitialValues(updatedConfig, props));
|
||||
});
|
||||
}, [props.id]);
|
||||
if (available) {
|
||||
updateConfig().then(updatedConfig => {
|
||||
form.setFieldsValue(getInitialValues(updatedConfig, props));
|
||||
});
|
||||
}
|
||||
}, [props.id, available]);
|
||||
|
||||
function renderInput(def: lib.FieldDefinition) {
|
||||
if (def.type === "boolean") {
|
||||
return <Checkbox/>;
|
||||
}
|
||||
if (def.type === "string") {
|
||||
return <Input/>;
|
||||
}
|
||||
if (def.type === "number") {
|
||||
return <InputNumber/>;
|
||||
}
|
||||
|
||||
return `Unknown type ${def.type}`;
|
||||
if (!available) {
|
||||
return <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Config</Title>
|
||||
<Card size="small" className="config-container">
|
||||
Config is currently unavailable.
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
|
||||
if (config === null) {
|
||||
return <Spin/>;
|
||||
return <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>Config</Title>
|
||||
<Card size="small" className="config-container">
|
||||
<Spin size="large" />;
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
|
||||
let initialValues: any = {};
|
||||
@@ -91,12 +111,109 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const [propsMap, treeData] = computeTreeData(
|
||||
form,
|
||||
initialValues,
|
||||
errorFields,
|
||||
config,
|
||||
setConfig,
|
||||
props.ConfigClass,
|
||||
onValuesChange,
|
||||
);
|
||||
|
||||
return <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>
|
||||
Config
|
||||
<Space style={{ float: "right" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={applying}
|
||||
onClick={async () => {
|
||||
setApplying(true);
|
||||
for (let field of changedFields) {
|
||||
let value = form.getFieldValue(field);
|
||||
let request;
|
||||
if (propsMap.has(field)) {
|
||||
let [fieldName, prop] = propsMap.get(field);
|
||||
request = props.setProp(fieldName, prop, value);
|
||||
|
||||
} else {
|
||||
request = props.setField(field, value === null ? "" : String(value));
|
||||
}
|
||||
|
||||
try {
|
||||
await request;
|
||||
setChangedFields(changed => {
|
||||
let newChanged = new Set(changed);
|
||||
newChanged.delete(field);
|
||||
return newChanged;
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
setErrorFields(errors => {
|
||||
let newErrors = new Map(errors);
|
||||
newErrors.set(field, err.message);
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await updateConfig();
|
||||
setApplying(false);
|
||||
|
||||
if (props.onApply) {
|
||||
props.onApply();
|
||||
}
|
||||
}}
|
||||
disabled={changedFields.size === 0}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setChangedFields(new Set());
|
||||
setErrorFields(new Map());
|
||||
}}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
</Space>
|
||||
</Title>
|
||||
<Card size="small" className="config-container">
|
||||
<Form
|
||||
size="small"
|
||||
form={form}
|
||||
initialValues={initialValues}
|
||||
onValuesChange={onValuesChange}
|
||||
>
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
selectable={false}
|
||||
defaultExpandAll={true}
|
||||
filterTreeNode={(node) => changedFields.has(node.key)}
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>;
|
||||
}
|
||||
|
||||
function computeTreeData(
|
||||
form: FormInstance<any>,
|
||||
initialValues: any,
|
||||
errorFields: Map<string, string>,
|
||||
config: lib.Config<any>,
|
||||
setConfig: (newConfig: lib.Config<any>) => void,
|
||||
ConfigClass: BaseConfigTreeProps["ConfigClass"],
|
||||
onValuesChange: (changedValues: object) => void,
|
||||
) {
|
||||
let restartTip = <Tooltip
|
||||
className="ant-form-item-tooltip"
|
||||
title="A restart is required for this setting to take effect"
|
||||
><ReloadOutlined/></Tooltip>;
|
||||
|
||||
|
||||
type ChildNode = {
|
||||
key: string;
|
||||
children: any[];
|
||||
@@ -111,7 +228,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let treeData = [];
|
||||
let propsMap = new Map();
|
||||
const groups = new Map<string, { [name: string]: lib.FieldDefinition }>();
|
||||
for (let [fieldName, def] of Object.entries(props.ConfigClass.fieldDefinitions)) {
|
||||
for (let [fieldName, def] of Object.entries(ConfigClass.fieldDefinitions)) {
|
||||
const [groupName, field] = lib.splitOn(".", fieldName);
|
||||
let group = groups.get(groupName);
|
||||
if (!group) {
|
||||
@@ -203,7 +320,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let propName = form.getFieldValue(`${newPropPath}.name`);
|
||||
let propValue = form.getFieldValue(`${newPropPath}.value`);
|
||||
if (!Object.prototype.hasOwnProperty.call(value, propName)) {
|
||||
let newConfig = props.ConfigClass.fromJSON(config!.toJSON(), "control");
|
||||
let newConfig = ConfigClass.fromJSON(config!.toJSON(), "control");
|
||||
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
|
||||
setConfig(newConfig);
|
||||
}
|
||||
@@ -246,81 +363,5 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
treeData.push(treeNode);
|
||||
}
|
||||
|
||||
return <>
|
||||
<Title level={5} style={{ marginTop: 16 }}>
|
||||
Config
|
||||
<Space style={{ float: "right" }}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={applying}
|
||||
onClick={async () => {
|
||||
setApplying(true);
|
||||
for (let field of changedFields) {
|
||||
let value = form.getFieldValue(field);
|
||||
let request;
|
||||
if (propsMap.has(field)) {
|
||||
let [fieldName, prop] = propsMap.get(field);
|
||||
request = props.setProp(fieldName, prop, value);
|
||||
|
||||
} else {
|
||||
request = props.setField(field, value === null ? "" : String(value));
|
||||
}
|
||||
|
||||
try {
|
||||
await request;
|
||||
setChangedFields(changed => {
|
||||
let newChanged = new Set(changed);
|
||||
newChanged.delete(field);
|
||||
return newChanged;
|
||||
});
|
||||
|
||||
} catch (err: any) {
|
||||
setErrorFields(errors => {
|
||||
let newErrors = new Map(errors);
|
||||
newErrors.set(field, err.message);
|
||||
return newErrors;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await updateConfig();
|
||||
setApplying(false);
|
||||
|
||||
if (props.onApply) {
|
||||
props.onApply();
|
||||
}
|
||||
}}
|
||||
disabled={changedFields.size === 0}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
form.resetFields();
|
||||
setChangedFields(new Set());
|
||||
setErrorFields(new Map());
|
||||
}}
|
||||
>
|
||||
Revert
|
||||
</Button>
|
||||
</Space>
|
||||
</Title>
|
||||
<Card size="small" className="config-container">
|
||||
<Form
|
||||
size="small"
|
||||
form={form}
|
||||
initialValues={initialValues}
|
||||
onValuesChange={onValuesChange}
|
||||
>
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
selectable={false}
|
||||
defaultExpandAll={true}
|
||||
filterTreeNode={(node) => changedFields.has(node.key)}
|
||||
/>
|
||||
</Form>
|
||||
</Card>
|
||||
</>;
|
||||
return [propsMap, treeData] as [typeof propsMap, typeof treeData];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { useContext } from "react";
|
||||
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
import BaseConfigTree from "./BaseConfigTree";
|
||||
import ControlContext from "./ControlContext";
|
||||
|
||||
|
||||
export default function HostConfigTree(props: { id: number, available: boolean }) {
|
||||
let control = useContext(ControlContext);
|
||||
|
||||
async function retrieveConfig() {
|
||||
return await control.sendTo({ hostId: props.id }, new lib.HostConfigGetRequest());
|
||||
}
|
||||
|
||||
async function setField(field: string, value: any) {
|
||||
await control.sendTo({ hostId: props.id }, new lib.HostConfigSetFieldRequest(field, value));
|
||||
}
|
||||
|
||||
async function setProp(field: string, prop: string, value: any) {
|
||||
if (value) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await control.sendTo({ hostId: props.id }, new lib.HostConfigSetPropRequest(field, prop, value));
|
||||
}
|
||||
|
||||
return <BaseConfigTree
|
||||
ConfigClass={lib.HostConfig}
|
||||
retrieveConfig={retrieveConfig}
|
||||
setField={setField}
|
||||
setProp={setProp}
|
||||
id={props.id}
|
||||
available={props.available}
|
||||
/>;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useParams } from "react-router-dom";
|
||||
import * as lib from "@clusterio/lib";
|
||||
import notify, { notifyErrorHandler } from "../util/notify";
|
||||
import ControlContext from "./ControlContext";
|
||||
import HostConfigTree from "./HostConfigTree";
|
||||
import InstanceList from "./InstanceList";
|
||||
import LogConsole from "./LogConsole";
|
||||
import { useAccount } from "../model/account";
|
||||
@@ -83,7 +84,6 @@ export default function HostViewPage() {
|
||||
<Descriptions.Item label="Memory"><MetricMemoryUsed metrics={metrics} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Disk Usage"><MetricDiskRatio metrics={metrics} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Disk"><MetricDiskUsed metrics={metrics} /></Descriptions.Item>
|
||||
<Descriptions.Item label="Agent">{host["agent"]}</Descriptions.Item>
|
||||
<Descriptions.Item label="Version">{host["version"]}</Descriptions.Item>
|
||||
{
|
||||
host.tokenValidAfter
|
||||
@@ -101,6 +101,7 @@ export default function HostViewPage() {
|
||||
<Title level={5} style={{ marginTop: 16 }}>Console</Title>
|
||||
<LogConsole hosts={[hostId]} />
|
||||
</>}
|
||||
{account.hasPermission("core.host.get_config") && <HostConfigTree id={hostId} available={host.connected} />}
|
||||
<PluginExtra component="HostViewPage" host={host} />
|
||||
</PageLayout>;
|
||||
}
|
||||
@@ -251,12 +251,6 @@ export default function HostsPage() {
|
||||
render: (_, host) => <MetricDiskUsed metrics={metrics.get(host.id)} />,
|
||||
hidden: !showNumbers,
|
||||
},
|
||||
{
|
||||
title: "Agent",
|
||||
dataIndex: "agent",
|
||||
sorter: (a, b) => strcmp(a.agent, b.agent),
|
||||
responsive: ["lg"],
|
||||
},
|
||||
{
|
||||
title: "Version",
|
||||
dataIndex: "version",
|
||||
|
||||
@@ -287,7 +287,7 @@ body {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.config-container {
|
||||
.config-container, .config-container .ant-tree {
|
||||
background-color: #111111;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ export class ControlConnector extends lib.WebSocketClientConnector {
|
||||
new lib.MessageRegisterControl(
|
||||
new lib.RegisterControlData(
|
||||
this.token,
|
||||
"Clusterio Web UI",
|
||||
packageJson.version,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -230,6 +230,29 @@ describe("Integration of Clusterio", function() {
|
||||
await execCtl("host list");
|
||||
});
|
||||
});
|
||||
describe("host config", function() {
|
||||
it("changes host config", async function() {
|
||||
await execCtl("host config set 4 host.name My-Host");
|
||||
const result = await execCtl("host config list 4");
|
||||
assert(/My-Host/.test(result.stdout), "New host name not in config output");
|
||||
});
|
||||
it("should not allow setting host.controller_token", async function() {
|
||||
await assert.rejects(
|
||||
execCtl("host config set 4 host.controller_token xyz"),
|
||||
/Field 'host\.controller_token' is not accessible/,
|
||||
);
|
||||
});
|
||||
it("should not allow setting host.id", async function() {
|
||||
await assert.rejects(
|
||||
execCtl("host config set 4 host.id 200"),
|
||||
/Setting 'host\.id' while host is running is not supported/,
|
||||
);
|
||||
});
|
||||
it("should not leak host.controller_token", async function() {
|
||||
let result = await getControl().sendTo({ hostId: 4 }, new lib.HostConfigGetRequest());
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(result, "host.controller_token"), false);
|
||||
});
|
||||
});
|
||||
describe("host generate-token", function() {
|
||||
it("runs", async function() {
|
||||
await execCtl("host generate-token --id 42");
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ describe("lib/command", function() {
|
||||
let testControl = new mock.MockControl(controlConnector);
|
||||
let testController = new lib.Link(controllerConnector);
|
||||
testController.handle(
|
||||
lib.HostListRequest, () => [new lib.HostDetails("test", "0.1", "Test Host", 11, false)]
|
||||
lib.HostListRequest, () => [new lib.HostDetails("0.1", "Test Host", 11, false)]
|
||||
);
|
||||
testController.handle(
|
||||
lib.InstanceDetailsListRequest, () => [new lib.InstanceDetails("Test Instance", 57, 4, undefined, "stopped")]
|
||||
|
||||
Reference in new issue
Block a user