mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
Remove config groups
Refactor away the config group concept and simplyfy the way config fields are defined. Fields are defined directly as properties of the static field fieldDefinitions on the class itself with a separate interface to declare the types of fields. This makes it possible to easily make accessing the config fields type checked in a way that lets plugins extend the interfaces with their own config options by adding additional fields to the interface via declation merging.
This commit is contained in:
1 parent
96210373f3
commit
e74065a493
35 files changed
+1259
-2090
No files matched your search
+18
-20
@@ -2,36 +2,34 @@
|
||||
|
||||
<sub>This document describes the implementation and inner working of the configuration system in Clusterio, and unless you're developing Clusterio it's probably not going to be very useful for you.</sub>
|
||||
|
||||
Clusterio uses a group based configuration system to manage settings for the controller, instances, hosts, and the clusterioctl utility.
|
||||
Plugins get their own config group for the controller and instances which they can add their own fields to.
|
||||
The fields are defined early in the startup, and the groups and their fields for disabled plugins will still be created.
|
||||
Clusterio uses a field based configuration system to manage settings for the controller, instances, hosts, and the clusterioctl utility.
|
||||
Plugins can add their own config fields for the controller and instances configs.
|
||||
The fields are defined early in the startup, and fields for disabled plugins will still be created.
|
||||
|
||||
The built-in config groups and their fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config groups to the `ControllerConfigGroup` and `InstanceConfigGroup` properties of the `info.js` export using subclasses of `PluginConfigGroup`.
|
||||
The plugin group has the enabled field pre-defined on it, and it's used to enable/disable plugins.
|
||||
The built-in config fields are defined in [packages/lib/config/definitions.js](/packages/lib/config/definitions.js) while plugins can add their own config field definitions to the `controllerConfigFields` and `instanceConfigFields` properties of the `info.js` export.
|
||||
A `<plugin_name>.load_plugin` field is automatically created for each plugin and it's used to enable/disable the loading plugins on startup.
|
||||
|
||||
|
||||
## Defing Fields
|
||||
|
||||
Fields are defined using the `define` class method on group classes after the groupName have been set and before the class has been finalized.
|
||||
The `define` method takes an object as argument with the following properties:
|
||||
Fields are defined as POJS object properties where the name of the property correspond to the name of the configuration field.
|
||||
The property name is the primary way the field is accessed both in code and on the command line, as such it should follow the lower\_case\_underscore style and consist of a group name and a field name separated by a dot.
|
||||
For plugins the group name is the name of the plugin, e.g., the `level` field of the `foo_frobber` plugin would be named `foo_frobber.lavel`.
|
||||
|
||||
**access**:
|
||||
The locations this config field can be read and modified from.
|
||||
This is an array of strings which can contain the values `"controller"`, `"host"` and `"control"`.
|
||||
If a party is missing in this array then that party will not receive the value of this field when the config is shared, and will be unable to modify the field.
|
||||
Takes the value of `defaultAccess` set on the config group class if not passed.
|
||||
Note that instance config groups should have both `"controller"` and `"host"` for all of its fields or unexpected behaviour may occur.
|
||||
If not set any party can read and modify this config entry.
|
||||
Note that instance config fields should have both `"controller"` and `"host"` or unexpected behaviour may occur.
|
||||
|
||||
**type**:
|
||||
The type of config value this field will support.
|
||||
The supported values are boolean, string, number, and object, and if an attept is made to set the field to a value that is not of the right type it will be reject unless it's a string that can be converted to the right type.
|
||||
|
||||
**name**:
|
||||
Name of the configuration field, this is the primary way the field is accessed, both in code and on the command line.
|
||||
As such it should follow the lower\_case\_underscore style and not contain any spaces or dots.
|
||||
|
||||
**title** (optional):
|
||||
Text used to identify the config field in user interfaces.
|
||||
This should not contain the grouping used in `name`.
|
||||
|
||||
**description** (optional):
|
||||
Text used to describe the config field in user interfaces.
|
||||
@@ -44,7 +42,7 @@ The `define` method takes an object as argument with the following properties:
|
||||
True if this field can be set to null.
|
||||
Defaults to false.
|
||||
|
||||
**initial_value** (optional if optional is true):
|
||||
**initialValue** (optional if optional is true):
|
||||
Value this config field should take in a newly initialized config.
|
||||
This can also be an async function returning the value to use.
|
||||
|
||||
@@ -53,24 +51,24 @@ If the value for a field stored in on disk somehow ends up being invalid the fie
|
||||
|
||||
## Accessing Fields
|
||||
|
||||
On the Config instance there's two ways to access fields: through the set and get methods using dot notation.
|
||||
On the Config instance there's two ways to access fields: through the set and get methods.
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.get("group.field");
|
||||
|
||||
Or by getting the group first and then calling set or get on that.
|
||||
Or by through the raw fields object (which is best avoided as it doesn't check types or whether the accessed field exists).
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.group("group").get("field")
|
||||
let value = configInstance.fields["group.field"]
|
||||
|
||||
The set method takes an extra argument, which is the value to set the field to.
|
||||
If the value is not valid this will throw an InvalidValue exception, if the group or field doesn't exist it will throw an InvalidField exception.
|
||||
The set method also takes an extra argument, which is the value to set the field to.
|
||||
If the value is not valid this will throw an InvalidValue exception, if the field doesn't exist it will throw an InvalidField exception.
|
||||
|
||||
|
||||
## Config Lifecycle
|
||||
|
||||
Values for all config fields are created when the config is initialized, as well as for any fields with missing values when a config is loaded.
|
||||
Once a field has a value it is never automatically removed, even if the field or group it was part of no longer exists.
|
||||
Once a field has a value it is never automatically removed, even if the field it was part of no longer exists.
|
||||
This ensures that plugin configuration is not lost should the server be started up without the plugins that defined the configuration installed.
|
||||
|
||||
For instance configs, they are kept on the controller and serialized and sent over to the relevant hosts on startup.
|
||||
|
||||
+23
-20
@@ -82,8 +82,8 @@ The following properties are recognized:
|
||||
This is an optional paramater.
|
||||
A plugin may have code only for instances but it must still be loaded on the controller in order for it to be possible to load it on an instance.
|
||||
|
||||
**InstanceConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the per instance configuration fields for this plugin.
|
||||
**instanceConfigFields**:
|
||||
Object mapping instance configuration fields to `FieldDefinitions`for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**controllerEntrypoint**:
|
||||
@@ -92,7 +92,7 @@ The following properties are recognized:
|
||||
A plugin can be made that only runs on the controller.
|
||||
|
||||
**ControllerConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` for defining the controller configuration fields for this plugin.
|
||||
Object mapping controller configuration fields to `FieldDefinitions`for this plugin.
|
||||
See [Plugin Configuration](#plugin-configuration)
|
||||
|
||||
**ctlEntrypoint**:
|
||||
@@ -202,32 +202,35 @@ For plugin hooks expections thrown are automatically catched and logged, but for
|
||||
## Plugin Configuration
|
||||
|
||||
Clusterio provides a configuration system that handles storing, distributing, editing and validating config fields for you.
|
||||
You can take advantage of it by subclassing `PluginConfigGroup`, setting `defaultAccess` to where config entries can be accessed from, setting the `groupName` to your plugin name, defining fields on it, finalizing it, and passing it as either `ControllerConfigGroup` or `InstanceConfigGroup` in the `info.js` export.
|
||||
You can take advantage of it by declaring config fields under either `controllerConfigFields` or `instanceConfigFields` in the `info.js` export.
|
||||
For example in info.js:
|
||||
|
||||
```js
|
||||
const lib = require("@clusterio/lib");
|
||||
```ts
|
||||
import type * as lib from "@clusterio/lib";
|
||||
|
||||
class ControllerConfigGroup extends lib.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = "foo_frobber";
|
||||
ControllerConfigGroup.define({
|
||||
name: "level",
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initial_value: 2,
|
||||
});
|
||||
ControllerConfigGroup.finalize();
|
||||
declare module "@clusterio/lib" {
|
||||
// Extend the interface defining available fields so that instance.config.get(...)
|
||||
// will have our own options.
|
||||
export interface ControllerConfigFields {
|
||||
"foo_frobber.level": number, // If optional is true then this should have | null in addition.
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
...
|
||||
ControllerConfigGroup: ControllerConfigGroup,
|
||||
};
|
||||
controllerConfigFields: {
|
||||
"foo_frobber.level": {
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initialValue: 2,
|
||||
},
|
||||
}
|
||||
} satisfies lib.PluginDeclaration;
|
||||
```
|
||||
|
||||
Code inside the `ControllerPlugin` class will then be able to access the level config field through the `Config` object at `this.controller.config`, for example in the ControllerPluginClass:
|
||||
|
||||
```js
|
||||
```ts
|
||||
async init() {
|
||||
let level = this.controller.config.get("foo_frobber.level");
|
||||
this.logger.info(`I got a frobnication level of ${level}`);
|
||||
|
||||
@@ -316,8 +316,7 @@ async function initialize(): Promise<InitializeParameters> {
|
||||
logger.info("Loading Plugin info");
|
||||
const pluginInfos = await lib.loadPluginInfos(pluginList);
|
||||
lib.registerPluginMessages(pluginInfos);
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
lib.addPluginConfigFields(pluginInfos);
|
||||
|
||||
const controllerConfigPath = args.config;
|
||||
logger.info(`Loading config from ${controllerConfigPath}`);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
CollectorResult, ConfigGroup, Event, InstanceStatus, Logger,
|
||||
CollectorResult, Event, InstanceStatus, Logger,
|
||||
ModPack, ModInfo, PlayerEvent, PluginNodeEnvInfo,
|
||||
} from "@clusterio/lib";
|
||||
import type Controller from "./Controller";
|
||||
@@ -99,12 +99,11 @@ export default class BaseControllerPlugin {
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on the controller.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param curr - The current value of the field.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onControllerConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
async onControllerConfigFieldChanged(field: string, curr: unknown, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called when the value of an instance config field changed.
|
||||
@@ -114,12 +113,11 @@ export default class BaseControllerPlugin {
|
||||
*
|
||||
* @param instance -
|
||||
* The instance the config changed on.
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param curr - The current value of the field.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(instance: InstanceInfo, group: ConfigGroup, field: string, prev: unknown) { }
|
||||
async onInstanceConfigFieldChanged(instance: InstanceInfo, field: string, curr: unknown, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
|
||||
@@ -174,12 +174,12 @@ export default class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async handleControllerConfigSetFieldRequest(request: lib.ControllerConfigSetFieldRequest) {
|
||||
this._controller.config.set(request.field, request.value, "control");
|
||||
this._controller.config.set(request.field as keyof lib.ControllerConfigFields, request.value, "control");
|
||||
}
|
||||
|
||||
async handleControllerConfigSetPropRequest(request: lib.ControllerConfigSetPropRequest) {
|
||||
let { field, prop, value } = request;
|
||||
this._controller.config.setProp(field, prop, value, "control");
|
||||
this._controller.config.setProp(field as keyof lib.ControllerConfigFields, prop, value, "control");
|
||||
}
|
||||
|
||||
async handleHostRevokeTokensRequest(request: lib.HostRevokeTokensRequest) {
|
||||
@@ -261,14 +261,14 @@ export default class ControlConnection extends BaseConnection {
|
||||
throw new lib.RequestError("Setting instance.id is not supported");
|
||||
}
|
||||
|
||||
instance.config.set(request.field, request.value, "control");
|
||||
instance.config.set(request.field as keyof lib.InstanceConfigFields, request.value, "control");
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
async handleInstanceConfigSetPropRequest(request: lib.InstanceConfigSetPropRequest) {
|
||||
let instance = this._controller.getRequestInstance(request.instanceId);
|
||||
let { field, prop, value } = request;
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
instance.config.setProp(field as keyof lib.InstanceConfigFields, prop, value, "control");
|
||||
await this._controller.instanceConfigUpdated(instance);
|
||||
}
|
||||
|
||||
|
||||
@@ -216,11 +216,11 @@ export default class Controller {
|
||||
this._startDevServer(args);
|
||||
}
|
||||
|
||||
this.config.on("fieldChanged", (group, field, prev) => {
|
||||
if (group.name === "controller" && field === "autosave_interval") {
|
||||
this.config.on("fieldChanged", (field, curr, prev) => {
|
||||
if (field === "controller.autosave_interval") {
|
||||
this.onAutosaveIntervalChanged();
|
||||
}
|
||||
lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", group, field, prev);
|
||||
lib.invokeHook(this.plugins, "onControllerConfigFieldChanged", field, curr, prev);
|
||||
});
|
||||
for (let instance of this.instances.values()) {
|
||||
this.addInstanceHooks(instance);
|
||||
@@ -799,7 +799,7 @@ export default class Controller {
|
||||
this.clearSavesOfInstance(instanceId);
|
||||
|
||||
// Assign to target
|
||||
instance.config.set("instance.assigned_host", hostId);
|
||||
instance.config.set("instance.assigned_host", hostId ?? null);
|
||||
// "fieldChanged" event handler will set this.instancesDirty
|
||||
if (hostId !== undefined && newHostConnection) {
|
||||
await newHostConnection.send(
|
||||
@@ -859,14 +859,14 @@ export default class Controller {
|
||||
}
|
||||
|
||||
addInstanceHooks(instance: InstanceInfo) {
|
||||
instance.config.on("fieldChanged", (group: lib.ConfigGroup, field: string, prev: any) => {
|
||||
instance.config.on("fieldChanged", (field: string, curr: any, prev: any) => {
|
||||
instance.updatedAt = Date.now();
|
||||
if (group.name === "instance" && field === "name") {
|
||||
if (field === "instance.name") {
|
||||
this.instanceDetailsUpdated([instance]);
|
||||
}
|
||||
|
||||
this.instancesDirty = true;
|
||||
lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", instance, group, field, prev);
|
||||
lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", instance, field, curr, prev);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -988,7 +988,7 @@ export default class Controller {
|
||||
logger.warn(`Unable to load dist/web/manifest.json for plugin ${pluginInfo.name}`);
|
||||
}
|
||||
|
||||
if (!this.config.group(pluginInfo.name).get("load_plugin")) {
|
||||
if (!this.config.get(`${pluginInfo.name}.load_plugin` as keyof lib.ControllerConfigFields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,9 @@ function getPlugins(req: Request, res: Response) {
|
||||
for (let pluginInfo of req.app.locals.controller.pluginInfos) {
|
||||
let name = pluginInfo.name;
|
||||
let loaded = req.app.locals.controller.plugins.has(name);
|
||||
let enabled = loaded && req.app.locals.controller.config.group(pluginInfo.name).get("load_plugin") as boolean;
|
||||
let enabled = loaded && req.app.locals.controller.config.get(
|
||||
`${pluginInfo.name}.load_plugin` as keyof lib.ControllerConfigFields,
|
||||
) as unknown as boolean;
|
||||
let web: { main:any, error:string|undefined } = { main: undefined, error: undefined};
|
||||
let devPlugins = req.app.locals.devPlugins;
|
||||
if (devPlugins && devPlugins.has(name)) {
|
||||
|
||||
+5
-6
@@ -114,8 +114,7 @@ export class Control extends lib.Link {
|
||||
async function loadPlugins(pluginList: Map<string, string>) {
|
||||
let pluginInfos = await lib.loadPluginInfos(pluginList);
|
||||
lib.registerPluginMessages(pluginInfos);
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
lib.addPluginConfigFields(pluginInfos);
|
||||
|
||||
let ctlPlugins = new Map<string, BaseCtlPlugin>();
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
@@ -261,16 +260,16 @@ async function startControl() {
|
||||
}
|
||||
|
||||
let tlsCa: string | undefined;
|
||||
let tlsCaPath = controlConfig.get("control.tls_ca") as string | null;
|
||||
let tlsCaPath = controlConfig.get("control.tls_ca");
|
||||
if (tlsCaPath) {
|
||||
tlsCa = await fs.readFile(tlsCaPath, "utf8");
|
||||
}
|
||||
|
||||
let controlConnector = new ControlConnector(
|
||||
controlConfig.get("control.controller_url") as string,
|
||||
controlConfig.get("control.max_reconnect_delay") as number,
|
||||
controlConfig.get("control.controller_url")!,
|
||||
controlConfig.get("control.max_reconnect_delay"),
|
||||
tlsCa,
|
||||
controlConfig.get("control.controller_token") as string,
|
||||
controlConfig.get("control.controller_token")!,
|
||||
);
|
||||
let control = new Control(controlConnector, controlConfig, tlsCa, ctlPlugins);
|
||||
try {
|
||||
|
||||
@@ -57,28 +57,26 @@ async function configToKeyVal(data: string) {
|
||||
}
|
||||
|
||||
function serializedConfigToString(
|
||||
serializedConfig: lib.SerializedConfig,
|
||||
configGroup: typeof lib.Config,
|
||||
serializedConfig: lib.ConfigSchema,
|
||||
configGroup: typeof lib.ControllerConfig | typeof lib.InstanceConfig,
|
||||
disallowedList: Record<string, unknown>,
|
||||
) {
|
||||
let allConfigElements = "";
|
||||
for (let group of serializedConfig.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
if (`${group.name}.${name}` in disallowedList) {
|
||||
continue;
|
||||
}
|
||||
let desc = "";
|
||||
try {
|
||||
desc += configGroup.groups.get(group.name)!._definitions.get(name)!.description;
|
||||
} catch (err) {
|
||||
desc += "No description found";
|
||||
}
|
||||
// split onto two lines for readability and es-lint
|
||||
if (String(value) === "null") {
|
||||
value = "";
|
||||
}
|
||||
allConfigElements += `${group.name}.${name} = ${value}\n\n`;
|
||||
for (let [name, value] of Object.entries(serializedConfig)) {
|
||||
if (name in disallowedList) {
|
||||
continue;
|
||||
}
|
||||
let desc = "";
|
||||
try {
|
||||
desc += (configGroup.fieldDefinitions as any)[name]!.description;
|
||||
} catch (err) {
|
||||
desc += "No description found";
|
||||
}
|
||||
// split onto two lines for readability and es-lint
|
||||
if (String(value) === "null") {
|
||||
value = "";
|
||||
}
|
||||
allConfigElements += `${name} = ${value}\n\n`;
|
||||
}
|
||||
return allConfigElements;
|
||||
}
|
||||
@@ -93,10 +91,8 @@ controllerConfigCommands.add(new lib.Command({
|
||||
handler: async function(args: object, control: Control) {
|
||||
let config = await control.send(new lib.ControllerConfigGetRequest());
|
||||
|
||||
for (let group of config.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
print(`${group.name}.${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
for (let [name, value] of Object.entries(config)) {
|
||||
print(`${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -115,7 +111,7 @@ controllerConfigCommands.add(new lib.Command({
|
||||
} else if (args.value === undefined) {
|
||||
args.value = "";
|
||||
}
|
||||
await control.send(new lib.ControllerConfigSetFieldRequest(args.field as string, args.value as string));
|
||||
await control.send(new lib.ControllerConfigSetFieldRequest(args.field, args.value));
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -214,7 +210,7 @@ const controllerPluginCommands = new lib.CommandTree({
|
||||
controllerPluginCommands.add(new lib.Command({
|
||||
definition: ["list", "List plugins on controller"],
|
||||
handler: async function(args: object, control: Control) {
|
||||
let url = new URL(control.config.get("control.controller_url") as string);
|
||||
let url = new URL(control.config.get("control.controller_url")!);
|
||||
url.pathname += "api/plugins";
|
||||
let response = await phin<[]>({
|
||||
url,
|
||||
@@ -335,10 +331,8 @@ instanceConfigCommands.add(new lib.Command({
|
||||
let instanceId = await lib.resolveInstance(control, args.instance);
|
||||
let config = await control.send(new lib.InstanceConfigGetRequest(instanceId));
|
||||
|
||||
for (let group of config.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
print(`${group.name}.${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
for (let [name, value] of Object.entries(config)) {
|
||||
print(`${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
@@ -612,7 +606,7 @@ instanceSaveCommands.add(new lib.Command({
|
||||
let content = await fs.readFile(args.filepath);
|
||||
|
||||
let instanceId = await lib.resolveInstance(control, args.instance);
|
||||
let url = new URL(control.config.get("control.controller_url") as string);
|
||||
let url = new URL(control.config.get("control.controller_url")!);
|
||||
url.pathname += "api/upload-save";
|
||||
url.searchParams.append("instance_id", String(instanceId));
|
||||
url.searchParams.append("filename", filename);
|
||||
@@ -694,7 +688,7 @@ instanceSaveCommands.add(new lib.Command({
|
||||
let instanceId = await lib.resolveInstance(control, args.instance);
|
||||
let streamId = await control.send(new lib.InstanceDownloadSaveRequest(instanceId, args.save));
|
||||
|
||||
let url = new URL(control.config.get("control.controller_url") as string);
|
||||
let url = new URL(control.config.get("control.controller_url")!);
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
@@ -1280,7 +1274,7 @@ modCommands.add(new lib.Command({
|
||||
// phin doesn't support streaming requests :(
|
||||
let content = await fs.readFile(args.file);
|
||||
|
||||
let url = new URL(control.config.get("control.controller_url") as string);
|
||||
let url = new URL(control.config.get("control.controller_url")!);
|
||||
url.pathname += "api/upload-mod";
|
||||
url.searchParams.append("filename", filename);
|
||||
|
||||
@@ -1324,7 +1318,7 @@ modCommands.add(new lib.Command({
|
||||
handler: async function(args: { name: string, modVersion: string }, control: Control) {
|
||||
let streamId = await control.send(new lib.ModDownloadRequest(args.name, args.modVersion));
|
||||
|
||||
let url = new URL(control.config.get("control.controller_url") as string);
|
||||
let url = new URL(control.config.get("control.controller_url")!);
|
||||
url.pathname += `api/stream/${streamId}`;
|
||||
let response = await phin({
|
||||
url, method: "GET",
|
||||
|
||||
@@ -142,8 +142,7 @@ async function startHost() {
|
||||
logger.info("Loading Plugin info");
|
||||
let pluginInfos = await lib.loadPluginInfos(pluginList);
|
||||
lib.registerPluginMessages(pluginInfos);
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
lib.addPluginConfigFields(pluginInfos);
|
||||
|
||||
logger.info(`Loading config from ${args.config}`);
|
||||
let hostConfig;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ConfigGroup, CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo,
|
||||
CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo,
|
||||
} from "@clusterio/lib";
|
||||
import type Host from "./Host";
|
||||
|
||||
@@ -46,12 +46,11 @@ export default class BaseHostPlugin {
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed on this host.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param curr - The current value of the field.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onHostConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
async onHostConfigFieldChanged(field: string, curr: unknown, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ConfigGroup, CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo,
|
||||
CollectorResult, Logger, ParsedFactorioOutput, PlayerEvent, PluginNodeEnvInfo,
|
||||
} from "@clusterio/lib";
|
||||
import type Instance from "./Instance";
|
||||
import type Host from "./Host";
|
||||
@@ -70,12 +70,11 @@ export default class BaseInstancePlugin {
|
||||
* Invoked after the value of the config field given by `field` has
|
||||
* changed.
|
||||
*
|
||||
* @param group -
|
||||
* The group who's field got changed.
|
||||
* @param field - Name of the field that changed.
|
||||
* @param curr - The current value of the field.
|
||||
* @param prev - The previous value of the field.
|
||||
*/
|
||||
async onInstanceConfigFieldChanged(group: ConfigGroup, field: string, prev: unknown) { }
|
||||
async onInstanceConfigFieldChanged(field: string, curr: unknown, prev: unknown) { }
|
||||
|
||||
/**
|
||||
* Called before collecting Prometheus metrics
|
||||
|
||||
@@ -287,8 +287,8 @@ export default class Host extends lib.Link {
|
||||
this.configPath = hostConfigPath;
|
||||
this.config = hostConfig;
|
||||
|
||||
this.config.on("fieldChanged", (group, field, prev) => {
|
||||
lib.invokeHook(this.plugins, "onHostConfigFieldChanged", group, field, prev);
|
||||
this.config.on("fieldChanged", (name, curr, prev) => {
|
||||
lib.invokeHook(this.plugins, "onHostConfigFieldChanged", name, curr, prev);
|
||||
});
|
||||
|
||||
this.connector.on("hello", data => {
|
||||
@@ -362,7 +362,7 @@ export default class Host extends lib.Link {
|
||||
|
||||
async loadPlugins() {
|
||||
for (let pluginInfo of this.pluginInfos) {
|
||||
if (!this.config.group(pluginInfo.name).get("load_plugin")) {
|
||||
if (!this.config.get(`${pluginInfo.name}.load_plugin` as keyof lib.HostConfigFields)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ export default class Instance extends lib.Link {
|
||||
|
||||
_host: Host;
|
||||
_dir: string;
|
||||
_configFieldChanged: (group: lib.ConfigGroup, field: string, prev: unknown) => void;
|
||||
_configFieldChanged: (field: string, curr: unknown, prev: unknown) => void;
|
||||
_status: "stopped" | "starting" | "running" | "stopping" | "creating_save" | "exporting_data" = "stopped";
|
||||
_loadedSave: string | null = null;
|
||||
_playerCheckInterval: ReturnType<typeof setInterval> | undefined;
|
||||
@@ -150,16 +150,16 @@ export default class Instance extends lib.Link {
|
||||
instance_name: this.name,
|
||||
});
|
||||
|
||||
this._configFieldChanged = (group: lib.ConfigGroup, field: string, prev: unknown) => {
|
||||
let hook = () => lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", group, field, prev);
|
||||
this._configFieldChanged = (field: string, curr: unknown, prev: unknown) => {
|
||||
let hook = () => lib.invokeHook(this.plugins, "onInstanceConfigFieldChanged", field, curr, prev);
|
||||
|
||||
if (group.name === "factorio" && field === "settings") {
|
||||
this.updateFactorioSettings(group.get(field) as any, prev as any).finally(hook);
|
||||
} else if (group.name === "factorio" && field === "enable_whitelist") {
|
||||
this.updateFactorioWhitelist(group.get(field) as any).finally(hook);
|
||||
if (field === "factorio.settings") {
|
||||
this.updateFactorioSettings(curr as any, prev as any).finally(hook);
|
||||
} else if (field === "factorio.enable_whitelist") {
|
||||
this.updateFactorioWhitelist(curr as any).finally(hook);
|
||||
} else {
|
||||
if (group.name === "factorio" && field === "max_concurrent_commands") {
|
||||
this.server.maxConcurrentCommands = group.get(field) as number;
|
||||
if (field === "factorio.max_concurrent_commands") {
|
||||
this.server.maxConcurrentCommands = curr as number;
|
||||
}
|
||||
hook();
|
||||
}
|
||||
@@ -546,7 +546,7 @@ rcon.print(game.table_to_json(players))`.replace(/\r?\n/g, " ");
|
||||
if (
|
||||
!pluginInfo.instanceEntrypoint
|
||||
|| !this._host.serverPlugins.has(pluginInfo.name)
|
||||
|| !this.config.group(pluginInfo.name).get("load_plugin")
|
||||
|| !this.config.get(`${pluginInfo.name}.load_plugin` as keyof lib.InstanceConfigFields)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+225
-653
File diff suppressed because it is too large.
Load diff
@@ -3,569 +3,469 @@ import * as classes from "./classes";
|
||||
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "../plugin";
|
||||
|
||||
|
||||
/**
|
||||
* Controller config group for {@link ControllerConfig}
|
||||
* @extends classes.ConfigGroup
|
||||
*/
|
||||
export class ControllerGroup extends classes.ConfigGroup { }
|
||||
ControllerGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerGroup.groupName = "controller";
|
||||
ControllerGroup.define({
|
||||
name: "name",
|
||||
title: "Name",
|
||||
description: "Name of the cluster.",
|
||||
type: "string",
|
||||
initial_value: "Your Cluster",
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "mods_directory",
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods shared with the cluster are stored.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "mods",
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "database_directory",
|
||||
title: "Database directory",
|
||||
description: "Directory where item and configuration data is stored.",
|
||||
type: "string",
|
||||
initial_value: "database",
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "autosave_interval",
|
||||
title: "Autosave Interval",
|
||||
description: "Interval in seconds to autosave data in memory to disk.",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "http_port",
|
||||
title: "HTTP Port",
|
||||
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
initial_value: 8080,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "https_port",
|
||||
title: "HTTPS Port",
|
||||
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "bind_address",
|
||||
title: "Bind Address",
|
||||
description: "IP address to bind the HTTP and HTTPS ports on.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "external_address",
|
||||
title: "External Address",
|
||||
description: "Public facing address the controller is hosted on.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "tls_certificate",
|
||||
title: "TLS Certificate",
|
||||
description: "Path to the certificate to use for HTTPS.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "tls_private_key",
|
||||
title: "TLS Private Key",
|
||||
description: "Path to the private key to use for HTTPS.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
access: ["controller"],
|
||||
name: "auth_secret",
|
||||
title: "Controller Authentication Secret",
|
||||
description:
|
||||
"Secret used to generate and verify authentication tokens. " +
|
||||
"Should be a long string of random letters and numbers. " +
|
||||
"Do not share this.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "heartbeat_interval",
|
||||
title: "Heartbeat Interval",
|
||||
description: "Interval heartbeats are sent out on WebSocket connections.",
|
||||
type: "number",
|
||||
initial_value: 15,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "session_timeout",
|
||||
title: "Session Timeout",
|
||||
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "metrics_timeout",
|
||||
title: "Metrics Timeout",
|
||||
description: "Timeout in seconds for metrics gathering from hosts.",
|
||||
type: "number",
|
||||
initial_value: 8,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "proxy_stream_timeout",
|
||||
title: "Proxy Stream Timeout",
|
||||
description: "Timeout in seconds for proxy streams to start flowing.",
|
||||
type: "number",
|
||||
initial_value: 15,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "default_mod_pack_id",
|
||||
title: "Default Mod Pack",
|
||||
description: "Mod pack used by default for instances.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
ControllerGroup.define({
|
||||
name: "default_role_id",
|
||||
title: "Default role",
|
||||
description: "ID of role assigned by default to new users.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
initial_value: 1,
|
||||
});
|
||||
ControllerGroup.finalize();
|
||||
export interface ControllerConfigFields {
|
||||
"controller.name": string;
|
||||
"controller.mods_directory": string;
|
||||
"controller.database_directory": string;
|
||||
"controller.http_port": number | null;
|
||||
"controller.https_port": number | null;
|
||||
"controller.bind_address": string | null;
|
||||
"controller.external_address": string | null;
|
||||
"controller.tls_certificate": string | null;
|
||||
"controller.tls_private_key": string | null;
|
||||
"controller.auth_secret": string;
|
||||
"controller.heartbeat_interval": number;
|
||||
"controller.session_timeout": number;
|
||||
"controller.metrics_timeout": number;
|
||||
"controller.proxy_stream_timeout": number;
|
||||
"controller.default_mod_pack_id": number | null;
|
||||
"controller.default_role_id": number | null;
|
||||
"controller.autosave_interval": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller Config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class ControllerConfig extends classes.Config {
|
||||
declare static fromJSON: (json: classes.SerializedConfig, location: classes.ConfigLocation) => ControllerConfig;
|
||||
get(name: "controller.name"): string;
|
||||
get(name: "controller.mods_directory"): string;
|
||||
get(name: "controller.database_directory"): string;
|
||||
get(name: "controller.http_port"): number | null;
|
||||
get(name: "controller.https_port"): number | null;
|
||||
get(name: "controller.bind_address"): string | null;
|
||||
get(name: "controller.external_address"): string | null;
|
||||
get(name: "controller.tls_certificate"): string | null;
|
||||
get(name: "controller.tls_private_key"): string | null;
|
||||
get(name: "controller.auth_secret"): string;
|
||||
get(name: "controller.heartbeat_interval"): number;
|
||||
get(name: "controller.session_timeout"): number;
|
||||
get(name: "controller.metrics_timeout"): number;
|
||||
get(name: "controller.proxy_stream_timeout"): number;
|
||||
get(name: "controller.default_mod_pack_id"): number | null;
|
||||
get(name: "controller.default_role_id"): number | null;
|
||||
get(name: "controller.autosave_interval"): number;
|
||||
get(name: string): unknown;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
export class ControllerConfig extends classes.Config<ControllerConfigFields> {
|
||||
declare static fromJSON: (json: classes.ConfigSchema, location: classes.ConfigLocation) => ControllerConfig;
|
||||
static fieldDefinitions: classes.ConfigDefs<ControllerConfigFields> = {
|
||||
"controller.name": {
|
||||
title: "Name",
|
||||
description: "Name of the cluster.",
|
||||
type: "string",
|
||||
initialValue: "Your Cluster",
|
||||
},
|
||||
"controller.mods_directory": {
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods shared with the cluster are stored.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "mods",
|
||||
},
|
||||
"controller.database_directory": {
|
||||
title: "Database directory",
|
||||
description: "Directory where item and configuration data is stored.",
|
||||
type: "string",
|
||||
initialValue: "database",
|
||||
},
|
||||
"controller.autosave_interval": {
|
||||
title: "Autosave Interval",
|
||||
description: "Interval in seconds to autosave data in memory to disk.",
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
"controller.http_port": {
|
||||
title: "HTTP Port",
|
||||
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
initialValue: 8080,
|
||||
},
|
||||
"controller.https_port": {
|
||||
title: "HTTPS Port",
|
||||
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"controller.bind_address": {
|
||||
title: "Bind Address",
|
||||
description: "IP address to bind the HTTP and HTTPS ports on.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"controller.external_address": {
|
||||
title: "External Address",
|
||||
description: "Public facing address the controller is hosted on.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"controller.tls_certificate": {
|
||||
title: "TLS Certificate",
|
||||
description: "Path to the certificate to use for HTTPS.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"controller.tls_private_key": {
|
||||
title: "TLS Private Key",
|
||||
description: "Path to the private key to use for HTTPS.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"controller.auth_secret": {
|
||||
access: ["controller"],
|
||||
title: "Controller Authentication Secret",
|
||||
description:
|
||||
"Secret used to generate and verify authentication tokens. " +
|
||||
"Should be a long string of random letters and numbers. " +
|
||||
"Do not share this.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"controller.heartbeat_interval": {
|
||||
title: "Heartbeat Interval",
|
||||
description: "Interval heartbeats are sent out on WebSocket connections.",
|
||||
type: "number",
|
||||
initialValue: 15,
|
||||
},
|
||||
"controller.session_timeout": {
|
||||
title: "Session Timeout",
|
||||
description: "Time in seconds before giving up resuming a dropped WebSocket session.",
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
"controller.metrics_timeout": {
|
||||
title: "Metrics Timeout",
|
||||
description: "Timeout in seconds for metrics gathering from hosts.",
|
||||
type: "number",
|
||||
initialValue: 8,
|
||||
},
|
||||
"controller.proxy_stream_timeout": {
|
||||
title: "Proxy Stream Timeout",
|
||||
description: "Timeout in seconds for proxy streams to start flowing.",
|
||||
type: "number",
|
||||
initialValue: 15,
|
||||
},
|
||||
"controller.default_mod_pack_id": {
|
||||
title: "Default Mod Pack",
|
||||
description: "Mod pack used by default for instances.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"controller.default_role_id": {
|
||||
title: "Default role",
|
||||
description: "ID of role assigned by default to new users.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
initialValue: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
ControllerConfig.registerGroup(ControllerGroup);
|
||||
|
||||
|
||||
/**
|
||||
* Host config group for {@link HostConfig}
|
||||
* @extends classes.ConfigGroup
|
||||
*/
|
||||
export class HostGroup extends classes.ConfigGroup {}
|
||||
HostGroup.defaultAccess = ["controller", "host", "control"];
|
||||
HostGroup.groupName = "host";
|
||||
HostGroup.define({
|
||||
name: "name",
|
||||
description: "Name of the host",
|
||||
type: "string",
|
||||
initial_value: "New Host",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "id",
|
||||
description: "ID of the host",
|
||||
type: "number",
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "factorio_directory",
|
||||
description: "Path to directory to look for factorio installs",
|
||||
type: "string",
|
||||
initial_value: "factorio",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "mods_directory",
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods for instances are cached.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "mods",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "instances_directory",
|
||||
description: "Path to directory to store instances in.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "instances",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "controller_url",
|
||||
description: "URL to connect to the controller at",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "http://localhost:8080/",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "controller_token",
|
||||
description: "Token to authenticate to controller with.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "enter token here",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "tls_ca",
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "public_address",
|
||||
description: "Public facing address players should connect to in order to join instances on this host",
|
||||
type: "string",
|
||||
initial_value: "localhost",
|
||||
});
|
||||
HostGroup.define({
|
||||
name: "max_reconnect_delay",
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
HostGroup.finalize();
|
||||
export interface HostConfigFields {
|
||||
"host.name": string;
|
||||
"host.id": number;
|
||||
"host.factorio_directory": string;
|
||||
"host.mods_directory": string;
|
||||
"host.instances_directory": string;
|
||||
"host.controller_url": string;
|
||||
"host.controller_token": string;
|
||||
"host.tls_ca": string | null;
|
||||
"host.public_address": string;
|
||||
"host.max_reconnect_delay": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Host Config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class HostConfig extends classes.Config {
|
||||
declare static fromJSON: (json: classes.SerializedConfig, location: classes.ConfigLocation) => HostConfig;
|
||||
get(name: "host.name"): string;
|
||||
get(name: "host.id"): number;
|
||||
get(name: "host.factorio_directory"): string;
|
||||
get(name: "host.mods_directory"): string;
|
||||
get(name: "host.instances_directory"): string;
|
||||
get(name: "host.controller_url"): string;
|
||||
get(name: "host.controller_token"): string;
|
||||
get(name: "host.tls_ca"): string | null;
|
||||
get(name: "host.public_address"): string;
|
||||
get(name: "host.max_reconnect_delay"): number;
|
||||
get(name: string): unknown;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
export class HostConfig extends classes.Config<HostConfigFields> {
|
||||
declare static fromJSON: (json: classes.ConfigSchema, location: classes.ConfigLocation) => HostConfig;
|
||||
static fieldDefinitions: classes.ConfigDefs<HostConfigFields> = {
|
||||
"host.name": {
|
||||
description: "Name of the host",
|
||||
type: "string",
|
||||
initialValue: "New Host",
|
||||
},
|
||||
"host.id": {
|
||||
description: "ID of the host",
|
||||
type: "number",
|
||||
initialValue: () => Math.random() * 2**31 | 0,
|
||||
},
|
||||
"host.factorio_directory": {
|
||||
description: "Path to directory to look for factorio installs",
|
||||
type: "string",
|
||||
initialValue: "factorio",
|
||||
},
|
||||
"host.mods_directory": {
|
||||
title: "Mods Directory",
|
||||
description: "Path to directory where mods for instances are cached.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "mods",
|
||||
},
|
||||
"host.instances_directory": {
|
||||
description: "Path to directory to store instances in.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "instances",
|
||||
},
|
||||
"host.controller_url": {
|
||||
description: "URL to connect to the controller at",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "http://localhost:8080/",
|
||||
},
|
||||
"host.controller_token": {
|
||||
access: ["host"],
|
||||
description: "Token to authenticate to controller with.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "enter token here",
|
||||
},
|
||||
"host.tls_ca": {
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"host.public_address": {
|
||||
description: "Public facing address players should connect to in order to join instances on this host",
|
||||
type: "string",
|
||||
initialValue: "localhost",
|
||||
},
|
||||
"host.max_reconnect_delay": {
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
HostConfig.registerGroup(HostGroup);
|
||||
|
||||
export interface InstanceConfigFields {
|
||||
"instance.name": string;
|
||||
"instance.id": number;
|
||||
"instance.assigned_host": number | null;
|
||||
"instance.auto_start": boolean;
|
||||
|
||||
/**
|
||||
* Instance config group for {@link classes.InstanceConfig}
|
||||
* @extends classes.ConfigGroup
|
||||
*/
|
||||
export class InstanceGroup extends classes.ConfigGroup { }
|
||||
InstanceGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceGroup.groupName = "instance";
|
||||
InstanceGroup.define({
|
||||
name: "name",
|
||||
type: "string",
|
||||
initial_value: "New Instance",
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "id",
|
||||
description: "ID of the instance",
|
||||
type: "number",
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "assigned_host",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "auto_start",
|
||||
description: "Automatically start this instance when the host hosting it is started up",
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
InstanceGroup.finalize();
|
||||
"factorio.version": string;
|
||||
"factorio.game_port": number | null;
|
||||
"factorio.rcon_port": number | null;
|
||||
"factorio.rcon_password": string | null;
|
||||
"factorio.player_online_autosave_slots": number;
|
||||
"factorio.mod_pack": number | null;
|
||||
"factorio.enable_save_patching": boolean;
|
||||
"factorio.enable_whitelist": boolean;
|
||||
"factorio.enable_authserver_bans": boolean;
|
||||
"factorio.settings": Record<string, unknown>;
|
||||
"factorio.verbose_logging": boolean;
|
||||
"factorio.strip_paths": boolean;
|
||||
"factorio.sync_adminlist": boolean;
|
||||
"factorio.sync_whitelist": boolean;
|
||||
"factorio.sync_banlist": boolean;
|
||||
"factorio.max_concurrent_commands": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factorio config group for {@link classes.InstanceConfig}
|
||||
* @extends classes.ConfigGroup
|
||||
*/
|
||||
export class FactorioGroup extends classes.ConfigGroup { }
|
||||
FactorioGroup.defaultAccess = ["controller", "host", "control"];
|
||||
FactorioGroup.groupName = "factorio";
|
||||
FactorioGroup.define({
|
||||
name: "version",
|
||||
description: "Version of the game to run, use latest to run the latest installed version",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initial_value: "latest",
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "game_port",
|
||||
description: "UDP port to run game on, uses a random port if null",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "rcon_port",
|
||||
description: "TCP port to run RCON on, uses a random port if null",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "rcon_password",
|
||||
description: "Password for RCON, randomly generated if null",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "player_online_autosave_slots",
|
||||
description:
|
||||
"Rename autosaves where players have been online since the previous autosave into a separate autosave " +
|
||||
"pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.",
|
||||
type: "number",
|
||||
initial_value: 5,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "mod_pack",
|
||||
description: "Mod pack to use on this server",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "enable_save_patching",
|
||||
description: "Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "enable_whitelist",
|
||||
description: "Turn on whitelist for joining the server.",
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "enable_authserver_bans",
|
||||
description: "Turn on Factorio.com based multiplayer bans.",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "settings",
|
||||
description: "Settings overridden in server-settings.json",
|
||||
restartRequired: true,
|
||||
restartRequiredProps: [
|
||||
"afk_autokick_interval", "allow_commands", "autosave_interval", "autosave_only_on_server", "description",
|
||||
"ignore_player_limit_for_returning_players", "max_players", "max_upload_slots",
|
||||
"max_upload_in_kilobytes_per_second", "name", "only_admins_can_pause_the_game", "game_password",
|
||||
"require_user_verification", "tags", "visibility",
|
||||
],
|
||||
type: "object",
|
||||
initial_value: {}, // See create instance handler in controller.
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "verbose_logging",
|
||||
description: "Enable verbose logging on the Factorio server",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "strip_paths",
|
||||
description: "Strip down instance paths in the log",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_adminlist",
|
||||
description: "Synchronize adminlist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_whitelist",
|
||||
description: "Synchronize whitelist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "sync_banlist",
|
||||
description: "Synchronize banlist with controller",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "max_concurrent_commands",
|
||||
description: "Maximum number of RCON commands trasmitted in parallel",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
initial_value: 5,
|
||||
});
|
||||
FactorioGroup.finalize();
|
||||
|
||||
/**
|
||||
* Instance config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class InstanceConfig extends classes.Config {
|
||||
declare static fromJSON: (json: classes.SerializedConfig, location: classes.ConfigLocation) => InstanceConfig;
|
||||
|
||||
get(name: "instance.name"): string;
|
||||
get(name: "instance.id"): number;
|
||||
get(name: "instance.assigned_host"): number | null;
|
||||
get(name: "instance.auto_start"): boolean;
|
||||
|
||||
get(name: "factorio.version"): string;
|
||||
get(name: "factorio.game_port"): number | null;
|
||||
get(name: "factorio.rcon_port"): number | null;
|
||||
get(name: "factorio.rcon_password"): string | null;
|
||||
get(name: "factorio.player_online_autosave_slots"): number;
|
||||
get(name: "factorio.mod_pack"): number | null;
|
||||
get(name: "factorio.enable_save_patching"): boolean;
|
||||
get(name: "factorio.enable_whitelist"): boolean;
|
||||
get(name: "factorio.enable_authserver_bans"): boolean;
|
||||
get(name: "factorio.settings"): Record<string, unknown>;
|
||||
get(name: "factorio.verbose_logging"): boolean;
|
||||
get(name: "factorio.strip_paths"): boolean;
|
||||
get(name: "factorio.sync_adminlist"): boolean;
|
||||
get(name: "factorio.sync_whitelist"): boolean;
|
||||
get(name: "factorio.sync_banlist"): boolean;
|
||||
get(name: "factorio.max_concurrent_commands"): number;
|
||||
get(name: string): unknown;
|
||||
get(name: string) {
|
||||
return super.get(name);
|
||||
}
|
||||
export class InstanceConfig extends classes.Config<InstanceConfigFields> {
|
||||
declare static fromJSON: (json: classes.ConfigSchema, location: classes.ConfigLocation) => InstanceConfig;
|
||||
static fieldDefinitions: classes.ConfigDefs<InstanceConfigFields> = {
|
||||
"instance.name": {
|
||||
type: "string",
|
||||
initialValue: "New Instance",
|
||||
},
|
||||
"instance.id": {
|
||||
description: "ID of the instance",
|
||||
type: "number",
|
||||
initialValue: () => Math.random() * 2**31 | 0,
|
||||
},
|
||||
"instance.assigned_host": {
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"instance.auto_start": {
|
||||
description: "Automatically start this instance when the host hosting it is started up",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"factorio.version": {
|
||||
description: "Version of the game to run, use latest to run the latest installed version",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
initialValue: "latest",
|
||||
},
|
||||
"factorio.game_port": {
|
||||
description: "UDP port to run game on, uses a random port if null",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"factorio.rcon_port": {
|
||||
description: "TCP port to run RCON on, uses a random port if null",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"factorio.rcon_password": {
|
||||
description: "Password for RCON, randomly generated if null",
|
||||
restartRequired: true,
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"factorio.player_online_autosave_slots": {
|
||||
description:
|
||||
"Rename autosaves where players have been online since the previous autosave into a separate " +
|
||||
"autosave pool with this many slots. Requires autosaves to be enabled to work. Set to 0 to disable.",
|
||||
type: "number",
|
||||
initialValue: 5,
|
||||
},
|
||||
"factorio.mod_pack": {
|
||||
description: "Mod pack to use on this server",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
optional: true,
|
||||
},
|
||||
"factorio.enable_save_patching": {
|
||||
description:
|
||||
"Patch saves with Lua code. Required for Clusterio integrations, lua modules, and most plugins.",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"factorio.enable_whitelist": {
|
||||
description: "Turn on whitelist for joining the server.",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"factorio.enable_authserver_bans": {
|
||||
description: "Turn on Factorio.com based multiplayer bans.",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"factorio.settings": {
|
||||
description: "Settings overridden in server-settings.json",
|
||||
restartRequired: true,
|
||||
restartRequiredProps: [
|
||||
"afk_autokick_interval", "allow_commands", "autosave_interval", "autosave_only_on_server",
|
||||
"description", "ignore_player_limit_for_returning_players", "max_players", "max_upload_slots",
|
||||
"max_upload_in_kilobytes_per_second", "name", "only_admins_can_pause_the_game", "game_password",
|
||||
"require_user_verification", "tags", "visibility",
|
||||
],
|
||||
type: "object",
|
||||
initialValue: {}, // See create instance handler in controller.
|
||||
},
|
||||
"factorio.verbose_logging": {
|
||||
description: "Enable verbose logging on the Factorio server",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
"factorio.strip_paths": {
|
||||
description: "Strip down instance paths in the log",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"factorio.sync_adminlist": {
|
||||
description: "Synchronize adminlist with controller",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"factorio.sync_whitelist": {
|
||||
description: "Synchronize whitelist with controller",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"factorio.sync_banlist": {
|
||||
description: "Synchronize banlist with controller",
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
},
|
||||
"factorio.max_concurrent_commands": {
|
||||
description: "Maximum number of RCON commands trasmitted in parallel",
|
||||
restartRequired: true,
|
||||
type: "number",
|
||||
initialValue: 5,
|
||||
},
|
||||
};
|
||||
}
|
||||
InstanceConfig.registerGroup(InstanceGroup);
|
||||
InstanceConfig.registerGroup(FactorioGroup);
|
||||
|
||||
|
||||
/**
|
||||
* Control config group for {@link classes.ControlConfig}
|
||||
* @extends classes.ConfigGroup
|
||||
*/
|
||||
export class ControlGroup extends classes.ConfigGroup {}
|
||||
ControlGroup.defaultAccess = ["control"];
|
||||
ControlGroup.groupName = "control";
|
||||
ControlGroup.define({
|
||||
name: "controller_url",
|
||||
description: "URL to connect to the controller at",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "controller_token",
|
||||
description: "Token to authenticate to controller with.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "tls_ca",
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "max_reconnect_delay",
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
ControlGroup.finalize();
|
||||
export interface ControlConfigFields {
|
||||
"control.controller_url": string | null;
|
||||
"control.controller_token": string | null;
|
||||
"control.tls_ca": string | null;
|
||||
"control.max_reconnect_delay": number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control config class
|
||||
* @extends classes.Config
|
||||
*/
|
||||
export class ControlConfig extends classes.Config { }
|
||||
ControlConfig.registerGroup(ControlGroup);
|
||||
export class ControlConfig extends classes.Config<ControlConfigFields> {
|
||||
declare static fromJSON: (json: classes.ConfigSchema, location: classes.ConfigLocation) => ControlConfig;
|
||||
static fieldDefinitions: classes.ConfigDefs<ControlConfigFields> = {
|
||||
"control.controller_url": {
|
||||
description: "URL to connect to the controller at",
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"control.controller_token": {
|
||||
access: ["control"],
|
||||
description: "Token to authenticate to controller with.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"control.tls_ca": {
|
||||
description: "Path to Certificate Authority to validate TLS connection to controller against.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
},
|
||||
"control.max_reconnect_delay": {
|
||||
title: "Max Reconnect Delay",
|
||||
description: "Maximum delay to wait before attempting to reconnect WebSocket",
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function validateGroup(
|
||||
pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo,
|
||||
groupName: "ControllerConfigGroup" | "HostConfigGroup" | "InstanceConfigGroup",
|
||||
function validateFields(
|
||||
pluginName: string,
|
||||
fields: Record<string, classes.FieldDefinition>,
|
||||
) {
|
||||
if (!(pluginInfo[groupName]?.prototype instanceof classes.PluginConfigGroup)) {
|
||||
throw new Error(
|
||||
`Expected ${groupName} for ${pluginInfo.name} to be a subclass of PluginConfigGroup`
|
||||
);
|
||||
}
|
||||
|
||||
if (pluginInfo[groupName]?.groupName !== pluginInfo.name) {
|
||||
throw new Error(
|
||||
`Expected ${groupName} for ${pluginInfo.name} to be named after the plugin`
|
||||
);
|
||||
for (const [name, field] of Object.entries(fields)) {
|
||||
if (!name.startsWith(`${pluginName}.`)) {
|
||||
throw new Error(
|
||||
`Expected name of config field '${name}' for ${pluginName} to start with '${pluginName}.'`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the config groups for the provided plugin infos
|
||||
* Add config fields defined by the provided plugin infos
|
||||
*
|
||||
* @param {Array<Object>} pluginInfos - Array of plugin info objects.
|
||||
*/
|
||||
export function registerPluginConfigGroups(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
|
||||
export function addPluginConfigFields(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
|
||||
function pluginConfig(
|
||||
pluginInfo: PluginNodeEnvInfo | PluginWebpackEnvInfo,
|
||||
group: "ControllerConfigGroup" | "HostConfigGroup" | "InstanceConfigGroup",
|
||||
Config: typeof classes.Config,
|
||||
kind: "controllerConfigFields" | "hostConfigFields" | "instanceConfigFields",
|
||||
Config: typeof ControllerConfig | typeof HostConfig | typeof InstanceConfig,
|
||||
) {
|
||||
if (pluginInfo[group]) {
|
||||
validateGroup(pluginInfo, group);
|
||||
Config.registerGroup(pluginInfo[group]!);
|
||||
(Config.fieldDefinitions as any)[`${pluginInfo.name}.load_plugin`] = {
|
||||
title: "Load Plugin",
|
||||
restartRequired: true,
|
||||
type: "boolean",
|
||||
initialValue: true,
|
||||
};
|
||||
|
||||
} else {
|
||||
class ConfigGroup extends classes.PluginConfigGroup { }
|
||||
ConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ConfigGroup.groupName = pluginInfo.name;
|
||||
ConfigGroup.finalize();
|
||||
Config.registerGroup(ConfigGroup);
|
||||
const fields = pluginInfo[kind];
|
||||
if (fields) {
|
||||
validateFields(pluginInfo.name, fields);
|
||||
Object.assign(Config.fieldDefinitions, fields);
|
||||
}
|
||||
}
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
pluginConfig(pluginInfo, "ControllerConfigGroup", ControllerConfig);
|
||||
pluginConfig(pluginInfo, "controllerConfigFields", ControllerConfig);
|
||||
if (pluginInfo.hostEntrypoint || pluginInfo.instanceEntrypoint) {
|
||||
pluginConfig(pluginInfo, "HostConfigGroup", HostConfig);
|
||||
pluginConfig(pluginInfo, "hostConfigFields", HostConfig);
|
||||
}
|
||||
if (pluginInfo.instanceEntrypoint) {
|
||||
pluginConfig(pluginInfo, "InstanceConfigGroup", InstanceConfig);
|
||||
pluginConfig(pluginInfo, "instanceConfigFields", InstanceConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock configs from adding more groups and make them usable
|
||||
*/
|
||||
export function finalizeConfigs() {
|
||||
ControllerConfig.finalize();
|
||||
HostConfig.finalize();
|
||||
InstanceConfig.finalize();
|
||||
ControlConfig.finalize();
|
||||
}
|
||||
@@ -177,6 +177,25 @@ export async function readStream(stream: NodeJS.ReadableStream & { isTTY?: boole
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the given string on the first instance of separator
|
||||
*
|
||||
* Splits `string` on the first instance of `separator` and returns an
|
||||
* array consisting of the string up to the separator and the string
|
||||
* after the separator. Returns an array with the string and an empty
|
||||
* string if the separator is not present.
|
||||
*
|
||||
* @param separator - Separator to split string by.
|
||||
* @param string - String to split
|
||||
* @returns string split on separator.
|
||||
*/
|
||||
export function splitOn(separator: string, string: string) {
|
||||
let index = string.indexOf(separator);
|
||||
if (index === -1) {
|
||||
return [string, ""];
|
||||
}
|
||||
return [string.slice(0, index), string.slice(index + separator.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes text for inclusion in a RegExp
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import * as libHelpers from "./helpers";
|
||||
import type { Logger } from "./logging";
|
||||
import type { PluginConfigGroup } from "./config";
|
||||
import type { FieldDefinition } from "./config";
|
||||
import type { PlayerStats } from "./data";
|
||||
|
||||
// TODO Add proper typing for plugins
|
||||
@@ -15,17 +15,17 @@ export type PluginDeclaration = {
|
||||
description?: string;
|
||||
|
||||
controllerEntrypoint?: string;
|
||||
ControllerConfigGroup?: typeof PluginConfigGroup;
|
||||
controllerConfigFields?: Record<string, FieldDefinition>;
|
||||
|
||||
hostEntrypoint?: string;
|
||||
HostConfigGroup?: typeof PluginConfigGroup;
|
||||
hostConfigFields?: Record<string, FieldDefinition>;
|
||||
|
||||
instanceEntrypoint?: string;
|
||||
InstanceConfigGroup?: typeof PluginConfigGroup;
|
||||
instanceConfigFields?: Record<string, FieldDefinition>;
|
||||
|
||||
ctlEntrypoint?: string;
|
||||
webEntrypoint?: string;
|
||||
ControlConfigGroup?: typeof PluginConfigGroup;
|
||||
controlConfigFields?: Record<string, FieldDefinition>;
|
||||
|
||||
messages?: any[];
|
||||
routes?: string[];
|
||||
|
||||
@@ -128,17 +128,14 @@ export function configCommand(yargs: any) {
|
||||
*/
|
||||
export async function handleConfigCommand(
|
||||
args: Record<string, unknown>,
|
||||
instance: libConfig.Config,
|
||||
instance: libConfig.Config<any>,
|
||||
configPath: string
|
||||
) {
|
||||
let command = (args._ as string[])[1];
|
||||
|
||||
if (command === "list") {
|
||||
for (let GroupClass of (instance.constructor as typeof libConfig.Config).groups.values()) {
|
||||
for (let def of GroupClass.definitions.values()) {
|
||||
let value = instance.get(def.fullName);
|
||||
print(`${def.fullName} ${JSON.stringify(value)}`);
|
||||
}
|
||||
for (const name of Object.keys(instance.constructor.fieldDefinitions)) {
|
||||
print(`${name} ${JSON.stringify(instance.get(name))}`);
|
||||
}
|
||||
|
||||
} else if (command === "show") {
|
||||
|
||||
@@ -104,8 +104,7 @@ export default async function bootstrap() {
|
||||
}));
|
||||
let pluginInfos = await loadPluginInfos();
|
||||
lib.registerPluginMessages(pluginInfos);
|
||||
lib.registerPluginConfigGroups(pluginInfos);
|
||||
lib.finalizeConfigs();
|
||||
lib.addPluginConfigFields(pluginInfos);
|
||||
|
||||
let wsUrl = new URL(webRoot, document.location.href);
|
||||
let controlConnector = new ControlConnector(wsUrl.href, 120, undefined);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type * as lib from "@clusterio/lib";
|
||||
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";
|
||||
@@ -6,41 +6,35 @@ import ReloadOutlined from "@ant-design/icons/ReloadOutlined";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
function getInitialValues(config: lib.Config, props: BaseConfigTreeProps) {
|
||||
function getInitialValues(config: lib.Config<any>, props: BaseConfigTreeProps) {
|
||||
let initialValues: any = {};
|
||||
for (let [name, GroupClass] of props.ConfigClass.groups) {
|
||||
let group = config.group(name);
|
||||
for (let def of GroupClass.definitions.values()) {
|
||||
if (!group.canAccess(def.name)) {
|
||||
continue;
|
||||
}
|
||||
for (let [name, def] of Object.entries(config.constructor.fieldDefinitions)) {
|
||||
if (!config.canAccess(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = group.get(def.name) as any;
|
||||
let fieldName = `${group.name}.${def.name}`;
|
||||
|
||||
if (def.type === "object") {
|
||||
for (let prop of Object.keys(value)) {
|
||||
let propPath = `${group.name}.${def.name}.${prop}`;
|
||||
initialValues[propPath] = JSON.stringify(value[prop]);
|
||||
}
|
||||
} else {
|
||||
initialValues[fieldName] = value;
|
||||
let value = config.get(name) as any;
|
||||
if (def.type === "object") {
|
||||
for (let prop of Object.keys(value)) {
|
||||
initialValues[`${name}.${prop}`] = JSON.stringify(value[prop]);
|
||||
}
|
||||
} else {
|
||||
initialValues[name] = value;
|
||||
}
|
||||
}
|
||||
return initialValues;
|
||||
}
|
||||
|
||||
type BaseConfigTreeProps = {
|
||||
ConfigClass: typeof lib.Config;
|
||||
retrieveConfig: () => Promise<lib.SerializedConfig>;
|
||||
ConfigClass: typeof lib.ControllerConfig | 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;
|
||||
};
|
||||
export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let [config, setConfig] = useState<lib.Config|null>(null);
|
||||
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());
|
||||
@@ -116,21 +110,30 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
|
||||
let treeData = [];
|
||||
let propsMap = new Map();
|
||||
for (let [name, GroupClass] of props.ConfigClass.groups) {
|
||||
let group = config.group(name);
|
||||
const groups = new Map<string, { [name: string]: lib.FieldDefinition }>();
|
||||
for (let [fieldName, def] of Object.entries(props.ConfigClass.fieldDefinitions)) {
|
||||
const [groupName, field] = lib.splitOn(".", fieldName);
|
||||
let group = groups.get(groupName);
|
||||
if (!group) {
|
||||
group = {};
|
||||
groups.set(groupName, group);
|
||||
}
|
||||
group[field] = def;
|
||||
}
|
||||
for (let [groupName, groupDefs] of groups) {
|
||||
let treeNode: TreeNode = {
|
||||
key: group.name,
|
||||
title: group.name,
|
||||
key: groupName,
|
||||
title: groupName,
|
||||
children: [],
|
||||
};
|
||||
|
||||
for (let def of GroupClass.definitions.values()) {
|
||||
if (!group.canAccess(def.name)) {
|
||||
for (let [field, def] of Object.entries(groupDefs)) {
|
||||
if (!config.canAccess(`${groupName}.${field}`)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = group.get(def.name) as any;
|
||||
let fieldName = `${group.name}.${def.name}`;
|
||||
let fieldName = `${groupName}.${field}`;
|
||||
let value = config.get(fieldName) as any;
|
||||
let childNode: ChildNode = {
|
||||
key: fieldName,
|
||||
children: [],
|
||||
@@ -140,13 +143,13 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let restartRequiredProps = new Set(def.restartRequiredProps || []);
|
||||
childNode.title = <Form.Item
|
||||
label={<>
|
||||
{def.title || def.name}
|
||||
{def.title || field}
|
||||
{!def.restartRequiredProps && def.restartRequired && restartTip}
|
||||
</>}
|
||||
tooltip={def.description}
|
||||
/>;
|
||||
for (let prop of Object.keys(value)) {
|
||||
let propPath = `${group.name}.${def.name}.${prop}`;
|
||||
let propPath = `${groupName}.${field}.${prop}`;
|
||||
let restart = Boolean(
|
||||
def.restartRequiredProps && Number(def.restartRequired) ^ Number(restartRequiredProps.has(prop))
|
||||
);
|
||||
@@ -178,7 +181,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
propsMap.set(propPath, [fieldName, prop]);
|
||||
}
|
||||
|
||||
let newPropPath = `${group.name}.${def.name}:add`;
|
||||
let newPropPath = `${groupName}.${field}:add`;
|
||||
childNode.children.push({
|
||||
key: newPropPath,
|
||||
title: <Space>
|
||||
@@ -201,11 +204,11 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
let propValue = form.getFieldValue(`${newPropPath}.value`);
|
||||
if (!Object.prototype.hasOwnProperty.call(value, propName)) {
|
||||
let newConfig = props.ConfigClass.fromJSON(config!.toJSON(), "control");
|
||||
newConfig.setProp(fieldName, propName, null);
|
||||
(newConfig as lib.Config<any>).setProp(fieldName, propName, null);
|
||||
setConfig(newConfig);
|
||||
}
|
||||
|
||||
let propPath = `${group.name}.${def.name}.${propName}`;
|
||||
let propPath = `${groupName}.${field}.${propName}`;
|
||||
form.setFieldsValue({
|
||||
[`${newPropPath}.name`]: "",
|
||||
[`${newPropPath}.value`]: "",
|
||||
@@ -224,7 +227,7 @@ export default function BaseConfigTree(props: BaseConfigTreeProps) {
|
||||
childNode.title = <Form.Item
|
||||
name={fieldName}
|
||||
label={<>
|
||||
{def.title || def.name}
|
||||
{def.title || field}
|
||||
{def.restartRequired && restartTip}
|
||||
</>}
|
||||
validateStatus={errorFields.has(fieldName) ? "error" : undefined}
|
||||
|
||||
@@ -62,7 +62,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
if (["unknown", "stopped"].includes(instance.status)) {
|
||||
let timeout = this.controller.config.get("inventory_sync.player_lock_timeout") as number * 1000;
|
||||
let timeout = this.controller.config.get("inventory_sync.player_lock_timeout") * 1000;
|
||||
for (let acquisitonRecord of this.acquiredPlayers.values()) {
|
||||
if (acquisitonRecord.instanceId === instanceId && !acquisitonRecord.expires) {
|
||||
acquisitonRecord.expires = Date.now() + timeout;
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
|
||||
class ControllerConfigGroup extends lib.PluginConfigGroup { }
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = "inventory_sync";
|
||||
ControllerConfigGroup.define({
|
||||
name: "player_lock_timeout",
|
||||
title: "Player Lock Timeout",
|
||||
description:
|
||||
"Time in seconds before the lock on a player inventory expires after an instance stops or is disconnected",
|
||||
type: "number",
|
||||
initial_value: 60,
|
||||
});
|
||||
ControllerConfigGroup.finalize();
|
||||
|
||||
class InstanceConfigGroup extends lib.PluginConfigGroup { }
|
||||
InstanceConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceConfigGroup.groupName = "inventory_sync";
|
||||
InstanceConfigGroup.define({
|
||||
name: "rcon_chunk_size",
|
||||
title: "Rcon inventory chunk size",
|
||||
description: "Divide inventories into multiple chunks before sending with rcon to prevent blocking the pipe",
|
||||
type: "number",
|
||||
initial_value: 1000,
|
||||
});
|
||||
InstanceConfigGroup.finalize();
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
"inventory_sync.rcon_chunk_size": number;
|
||||
}
|
||||
export interface ControllerConfigFields {
|
||||
"inventory_sync.player_lock_timeout": number;
|
||||
}
|
||||
}
|
||||
|
||||
lib.definePermission({
|
||||
name: "inventory_sync.inventory.view",
|
||||
@@ -39,10 +23,27 @@ export default {
|
||||
description: "Synchronizes players inventories between instances",
|
||||
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
InstanceConfigGroup,
|
||||
instanceConfigFields: {
|
||||
"inventory_sync.rcon_chunk_size": {
|
||||
title: "Rcon inventory chunk size",
|
||||
description:
|
||||
"Divide inventories into chunks of this size before sending with rcon to prevent blocking the pipe",
|
||||
type: "number",
|
||||
initialValue: 1000,
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
ControllerConfigGroup,
|
||||
controllerConfigFields: {
|
||||
"inventory_sync.player_lock_timeout": {
|
||||
title: "Player Lock Timeout",
|
||||
description:
|
||||
"Time in seconds before the lock on a player inventory expires after an instance stops " +
|
||||
"or is disconnected",
|
||||
type: "number",
|
||||
initialValue: 60,
|
||||
},
|
||||
},
|
||||
|
||||
messages: [
|
||||
messages.AcquireRequest,
|
||||
|
||||
@@ -177,7 +177,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size") as number;
|
||||
const chunkSize = this.instance.config.get("inventory_sync.rcon_chunk_size");
|
||||
const chunks = chunkify(chunkSize, JSON.stringify(response.playerData));
|
||||
this.logger.verbose(`Sending inventory for ${playerName} in ${chunks.length} chunks`);
|
||||
for (let i = 0; i < chunks.length; i++) {
|
||||
|
||||
@@ -89,7 +89,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
|
||||
for (let entry of this.players.values()) {
|
||||
if (entry.playerCode === playerCode && entry.expires > Date.now()) {
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length") as number);
|
||||
let verifyCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let secret = Buffer.from(this.controller.config.get("controller.auth_secret"), "base64");
|
||||
let verifyToken = jwt.sign(
|
||||
{
|
||||
@@ -173,8 +173,8 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
}
|
||||
|
||||
async handleFetchPlayerCodeRequest(request: FetchPlayerCodeRequest) {
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length") as number);
|
||||
let expires = Date.now() + (this.controller.config.get("player_auth.code_timeout") as number) * 1000;
|
||||
let playerCode = await generateCode(this.controller.config.get("player_auth.code_length"));
|
||||
let expires = Date.now() + this.controller.config.get("player_auth.code_timeout") * 1000;
|
||||
this.players.set(request.player, { playerCode, verifyCode: null, expires });
|
||||
return { player_code: playerCode, controller_url: this.controller.getControllerUrl() };
|
||||
}
|
||||
|
||||
+23
-19
@@ -1,24 +1,15 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
|
||||
class ControllerConfigGroup extends lib.PluginConfigGroup {}
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = "player_auth";
|
||||
ControllerConfigGroup.define({
|
||||
name: "code_length",
|
||||
title: "Code Length",
|
||||
description: "Length in characters of the generated codes.",
|
||||
type: "number",
|
||||
initial_value: 6,
|
||||
});
|
||||
ControllerConfigGroup.define({
|
||||
name: "code_timeout",
|
||||
title: "Code Timeout",
|
||||
description: "Time in seconds for the generated codes to stay valid.",
|
||||
type: "number",
|
||||
initial_value: 120,
|
||||
});
|
||||
ControllerConfigGroup.finalize();
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
"player_auth.load_plugin": boolean;
|
||||
}
|
||||
export interface ControllerConfigFields {
|
||||
"player_auth.code_length": number;
|
||||
"player_auth.code_timeout": number;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "player_auth",
|
||||
@@ -27,7 +18,20 @@ export default {
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
webEntrypoint: "./web",
|
||||
ControllerConfigGroup,
|
||||
controllerConfigFields: {
|
||||
"player_auth.code_length": {
|
||||
title: "Code Length",
|
||||
description: "Length in characters of the generated codes.",
|
||||
type: "number",
|
||||
initialValue: 6,
|
||||
},
|
||||
"player_auth.code_timeout": {
|
||||
title: "Code Timeout",
|
||||
description: "Time in seconds for the generated codes to stay valid.",
|
||||
type: "number",
|
||||
initialValue: 120,
|
||||
},
|
||||
},
|
||||
|
||||
messages: [
|
||||
messages.FetchPlayerCodeRequest,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
|
||||
class InstanceConfigGroup extends lib.PluginConfigGroup {}
|
||||
InstanceConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceConfigGroup.groupName = "statistics_exporter";
|
||||
InstanceConfigGroup.define({
|
||||
name: "command_timeout",
|
||||
title: "Command Timeout",
|
||||
description: "Timeout in seconds of the metrics command before returning results from the previous invocation.",
|
||||
type: "number",
|
||||
initial_value: 1,
|
||||
});
|
||||
InstanceConfigGroup.finalize();
|
||||
declare module "@clusterio/lib" {
|
||||
export interface InstanceConfigFields {
|
||||
"statistics_exporter.command_timeout": number;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "statistics_exporter",
|
||||
@@ -19,5 +13,13 @@ export default {
|
||||
"Provides in-game item/fluid production, builds, kills, and pollution "+
|
||||
"statistics to the cluster's Prometheus endpoint.",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
InstanceConfigGroup,
|
||||
instanceConfigFields: {
|
||||
"statistics_exporter.command_timeout": {
|
||||
title: "Command Timeout",
|
||||
description:
|
||||
"Timeout in seconds of the metrics command before returning results from the previous invocation.",
|
||||
type: "number",
|
||||
initialValue: 1,
|
||||
},
|
||||
},
|
||||
} satisfies lib.PluginDeclaration;
|
||||
@@ -109,7 +109,7 @@ export class InstancePlugin extends BaseInstancePlugin {
|
||||
// take a long time for the command to go through if the command
|
||||
// stream is overloaded. Should the timeout be exceeded the
|
||||
// previous values for the metrics will end up being sent to controller.
|
||||
let timeout = this.instance.config.get("statistics_exporter.command_timeout") as number * 1000;
|
||||
let timeout = this.instance.config.get("statistics_exporter.command_timeout") * 1000;
|
||||
await lib.timeout(this.gatherMetrics(), timeout, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
|
||||
let count = dole.doleDivider({
|
||||
object: { name: item.name, count: item.count, instanceId, instanceName },
|
||||
items: this.items,
|
||||
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers") as boolean,
|
||||
logItemTransfers: this.controller.config.get("subspace_storage.log_item_transfers"),
|
||||
logger: this.logger,
|
||||
});
|
||||
if (count > 0) {
|
||||
|
||||
@@ -1,37 +1,15 @@
|
||||
import * as lib from "@clusterio/lib";
|
||||
import * as messages from "./messages";
|
||||
|
||||
class ControllerConfigGroup extends lib.PluginConfigGroup {}
|
||||
ControllerConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
ControllerConfigGroup.groupName = "subspace_storage";
|
||||
ControllerConfigGroup.define({
|
||||
name: "division_method",
|
||||
title: "Division Method",
|
||||
description: "Method for dividing resource requests between instances.",
|
||||
type: "string",
|
||||
enum: ["simple", "dole", "neural_dole"],
|
||||
initial_value: "simple",
|
||||
});
|
||||
ControllerConfigGroup.define({
|
||||
name: "log_item_transfers",
|
||||
title: "Log Item Transfers",
|
||||
description: "Spam controller console with item transfers done.",
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
ControllerConfigGroup.finalize();
|
||||
|
||||
class InstanceConfigGroup extends lib.PluginConfigGroup {}
|
||||
InstanceConfigGroup.defaultAccess = ["controller", "host", "control"];
|
||||
InstanceConfigGroup.groupName = "subspace_storage";
|
||||
InstanceConfigGroup.define({
|
||||
name: "log_item_transfers",
|
||||
title: "Log Item Transfers",
|
||||
description: "Spam host console with item transfers done.",
|
||||
type: "boolean",
|
||||
initial_value: false,
|
||||
});
|
||||
InstanceConfigGroup.finalize();
|
||||
declare module "@clusterio/lib" {
|
||||
export interface ControllerConfigFields {
|
||||
"subspace_storage.division_method": "simple" | "dole" | "neural_dole";
|
||||
"subspace_storage.log_item_transfers": boolean;
|
||||
}
|
||||
export interface InstanceConfigFields {
|
||||
"subspace_storage.log_item_transfers": boolean;
|
||||
}
|
||||
}
|
||||
|
||||
lib.definePermission({
|
||||
name: "subspace_storage.storage.view",
|
||||
@@ -45,10 +23,31 @@ export default {
|
||||
title: "Subspace Storage",
|
||||
description: "Provides shared storage across instances for the Subspace Storage mod",
|
||||
instanceEntrypoint: "dist/plugin/instance",
|
||||
InstanceConfigGroup,
|
||||
instanceConfigFields: {
|
||||
"subspace_storage.log_item_transfers": {
|
||||
title: "Log Item Transfers",
|
||||
description: "Spam host console with item transfers done.",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
},
|
||||
|
||||
controllerEntrypoint: "dist/plugin/controller",
|
||||
ControllerConfigGroup,
|
||||
controllerConfigFields: {
|
||||
"subspace_storage.division_method": {
|
||||
title: "Division Method",
|
||||
description: "Method for dividing resource requests between instances.",
|
||||
type: "string",
|
||||
enum: ["simple", "dole", "neural_dole"],
|
||||
initialValue: "simple",
|
||||
},
|
||||
"subspace_storage.log_item_transfers": {
|
||||
title: "Log Item Transfers",
|
||||
description: "Spam controller console with item transfers done.",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
},
|
||||
},
|
||||
|
||||
messages: [
|
||||
messages.PlaceEvent,
|
||||
|
||||
@@ -17,9 +17,6 @@ describe("controller/InstanceInfo", function() {
|
||||
assert.deepEqual(InstanceInfo.fromJSON(json, "controller"), info);
|
||||
}
|
||||
|
||||
if (!lib.InstanceConfig._finalized) {
|
||||
lib.InstanceConfig.finalize();
|
||||
}
|
||||
const config = new lib.InstanceConfig("controller").toJSON();
|
||||
|
||||
check(InstanceInfo.fromJSON({ config, status: "stopped" }, "controller"));
|
||||
|
||||
@@ -7,10 +7,6 @@ const Host = require("@clusterio/host/dist/src/Host").default;
|
||||
const { _discoverInstances } = require("@clusterio/host/dist/src/Host");
|
||||
|
||||
describe("Host testing", function() {
|
||||
before(function() {
|
||||
lib.InstanceConfig.finalize();
|
||||
});
|
||||
|
||||
describe("discoverInstances()", function() {
|
||||
it("should discover test instance", async function() {
|
||||
let instancePath = path.join("test", "file", "instances");
|
||||
|
||||
@@ -11,10 +11,6 @@ const addr = lib.Address.fromShorthand;
|
||||
|
||||
|
||||
describe("class Instance", function() {
|
||||
before(function() {
|
||||
lib.InstanceConfig.finalize();
|
||||
});
|
||||
|
||||
let src;
|
||||
let dst = addr({ hostId: 1 });
|
||||
let instance;
|
||||
|
||||
@@ -205,30 +205,14 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
it("should not leak auth_secret", async function() {
|
||||
let result = await getControl().send(new lib.ControllerConfigGetRequest());
|
||||
let done = false;
|
||||
for (let group of result.groups) {
|
||||
if (group.name === "controller") {
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(group.fields, "auth_secret"), false);
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(done, "controller group not found");
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(result, "controller.auth_secret"), false);
|
||||
});
|
||||
});
|
||||
describe("controller config set", function() {
|
||||
it("sets given config option", async function() {
|
||||
await execCtl('controller config set controller.name "Test Cluster"');
|
||||
let result = await getControl().send(new lib.ControllerConfigGetRequest());
|
||||
let done = false;
|
||||
for (let group of result.groups) {
|
||||
if (group.name === "controller") {
|
||||
assert.equal(group.fields.name, "Test Cluster");
|
||||
done = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert(done, "controller group not found");
|
||||
assert.equal(result["controller.name"], "Test Cluster");
|
||||
});
|
||||
it("should not allow setting auth_secret", async function() {
|
||||
await assert.rejects(execCtl("controller config set controller.auth_secret root"));
|
||||
|
||||
+335
-625
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user