mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 15:04:52 +09:00
Add location access control to config class
Add a per field access parameter to the config class which specifies which of master, slave and control can access the given field. A location is added to the config class to specify which party owns it and fields which do not have an access parameter are not present in the config and will throw if attempted to be read from or writen to. The serialize, load, update, get, set and setProp are ammended with a location parameter which specifiy where the data is from or destined to and the access checks are performed to ensure remote access to the config is still limited by the access parameter of field. With this system in place the master config can be shared with control and slave connections without compromising secrets that should stay private. This is primarily the master authentication secret, but other plugins may define secrets it does not want to have shared with slave and/or control connections.
This commit is contained in:
1 parent
77cf4947b6
commit
2c74a67738
13 files changed
+399
-109
No files matched your search
@@ -28,6 +28,16 @@ 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:
|
||||
|
||||
**access**:
|
||||
The locations this config field can be read and modified from. This
|
||||
is an array of strings which can contain the values `"master"`,
|
||||
`"slave"` 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 `"master"` and
|
||||
`"slave"` for all of its fields 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
|
||||
|
||||
@@ -224,12 +224,13 @@ but for event handlers registered on EventEmitters it's critical that
|
||||
exceptions are catched and handled appropriately.
|
||||
|
||||
|
||||
Defining Configuration
|
||||
----------------------
|
||||
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 the
|
||||
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 `MasterConfigGroup` or `InstanceConfigGroup` in
|
||||
the `info.js` export. For example in info.js:
|
||||
@@ -237,6 +238,7 @@ the `info.js` export. For example in info.js:
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = "foo_frobber";
|
||||
MasterConfigGroup.define({
|
||||
name: "level",
|
||||
@@ -261,7 +263,7 @@ for example in the MasterPluginClass:
|
||||
}
|
||||
|
||||
The same applies for instance configs, replace "master" with "instance"
|
||||
where appropriate. See [Configuration System](configuration-system.md)
|
||||
where appropriate. See [Configuration System](config-system.md)
|
||||
for more details on how this system works.
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -104,13 +104,13 @@ instanceCommands.add(new libCommand.Command({
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("control");
|
||||
await instanceConfig.init();
|
||||
if (args.id) {
|
||||
instanceConfig.set("instance.id", args.id);
|
||||
}
|
||||
instanceConfig.set("instance.name", args.name);
|
||||
let serialized_config = instanceConfig.serialize();
|
||||
let serialized_config = instanceConfig.serialize("master");
|
||||
let response = await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
},
|
||||
}));
|
||||
@@ -792,7 +792,7 @@ async function startControl() {
|
||||
;
|
||||
|
||||
logger.verbose(`Loading config from ${args.config}`);
|
||||
let controlConfig = new libConfig.ControlConfig();
|
||||
let controlConfig = new libConfig.ControlConfig("control");
|
||||
try {
|
||||
await controlConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
|
||||
|
||||
+158
-39
@@ -7,6 +7,17 @@ const isDeepStrictEqual = require("../is_deep_strict_equal");
|
||||
const { basicType } = require("@clusterio/lib/helpers");
|
||||
|
||||
|
||||
/**
|
||||
* Invalid Access exception
|
||||
*
|
||||
* Exception class for when an access attempt is made to a field that
|
||||
* does not permitt this access is done.
|
||||
*
|
||||
* @extends Error
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class InvalidAccess extends Error { };
|
||||
|
||||
/**
|
||||
* Invalid Value exception
|
||||
*
|
||||
@@ -69,13 +80,24 @@ function splitOn(separator, string) {
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class Config extends events.EventEmitter {
|
||||
constructor() {
|
||||
/**
|
||||
* Create a new instance of the given config
|
||||
*
|
||||
* @param {string} location -
|
||||
* Location to evaluate access for this instance from
|
||||
*/
|
||||
constructor(location) {
|
||||
super();
|
||||
|
||||
if (!this.constructor._finalized) {
|
||||
throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
if (typeof location !== "string") {
|
||||
throw new Error("location must be a string");
|
||||
}
|
||||
|
||||
this.location = location;
|
||||
this._groups = new Map();
|
||||
this._unknownGroups = new Map();
|
||||
}
|
||||
@@ -83,8 +105,8 @@ class Config extends events.EventEmitter {
|
||||
async init() {
|
||||
for (let [name, GroupClass] of this.constructor._groups) {
|
||||
if (!this._groups.has(name)) {
|
||||
let group = new GroupClass();
|
||||
await group.init(this);
|
||||
let group = new GroupClass(this);
|
||||
await group.init();
|
||||
this._groups.set(name, group);
|
||||
}
|
||||
}
|
||||
@@ -101,7 +123,13 @@ class Config extends events.EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async load(serializedConfig) {
|
||||
/**
|
||||
* Load config from a serialized object
|
||||
*
|
||||
* @param {object} serializedConfig - Serialized config to load.
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
async load(serializedConfig, location = this.location) {
|
||||
this._validate(serializedConfig);
|
||||
for (let serializedGroup of serializedConfig.groups) {
|
||||
let GroupClass = this.constructor._groups.get(serializedGroup.name);
|
||||
@@ -110,8 +138,8 @@ class Config extends events.EventEmitter {
|
||||
continue;
|
||||
}
|
||||
|
||||
let group = new GroupClass();
|
||||
await group.load(this, serializedGroup);
|
||||
let group = new GroupClass(this);
|
||||
await group.load(serializedGroup, location);
|
||||
this._groups.set(group.name, group);
|
||||
}
|
||||
|
||||
@@ -121,12 +149,13 @@ class Config extends events.EventEmitter {
|
||||
/**
|
||||
* Serialize the config to a plain JavaScript object
|
||||
*
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {Object} JSON serializable representation of the config.
|
||||
*/
|
||||
serialize() {
|
||||
serialize(location = this.location) {
|
||||
let groups = [...this._unknownGroups.values()];
|
||||
for (let group of this._groups.values()) {
|
||||
groups.push(group.serialize());
|
||||
groups.push(group.serialize(location));
|
||||
}
|
||||
|
||||
return { groups };
|
||||
@@ -140,28 +169,42 @@ class Config extends events.EventEmitter {
|
||||
*
|
||||
* @param {Object} serializedConfig - Output from .serialize().
|
||||
* @param {boolean} notify - Invoke fieldChanged events if true.
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
update(serializedConfig, notify) {
|
||||
update(serializedConfig, notify, location = this.location) {
|
||||
this._validate(serializedConfig);
|
||||
for (let serializedGroup of serializedConfig.groups) {
|
||||
let group = this._groups.get(serializedGroup.name);
|
||||
if (group) {
|
||||
group.update(serializedGroup, notify);
|
||||
group.update(serializedGroup, notify, location);
|
||||
} else {
|
||||
this._unknownGroups.set(serializedGroup.name, serializedGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field can be accessed from the given location
|
||||
*
|
||||
* @param {string} name - Name of the field to check.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {boolean} true if field is accessible
|
||||
*/
|
||||
canAccess(name, location = this.location) {
|
||||
let [group, field] = splitOn(".", name);
|
||||
return this.group(group).canAccess(field, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for a config field
|
||||
*
|
||||
* @param {string} name - Name of field to get.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {*} the value for the field.
|
||||
*/
|
||||
get(name) {
|
||||
get(name, location = this.location) {
|
||||
let [group, field] = splitOn(".", name);
|
||||
return this.group(group).get(field);
|
||||
return this.group(group).get(field, location);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,10 +212,11 @@ class Config extends events.EventEmitter {
|
||||
*
|
||||
* @param {string} name - Name of field to set.
|
||||
* @param {*} value - the value to set on field
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
set(name, value) {
|
||||
set(name, value, location = this.location) {
|
||||
let [group, field] = splitOn(".", name);
|
||||
this.group(group).set(field, value);
|
||||
this.group(group).set(field, value, location);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,10 +225,11 @@ class Config extends events.EventEmitter {
|
||||
* @param {string} name - Name of field to set property on.
|
||||
* @param {string} prop - Name of property to set on field.
|
||||
* @param {*} value - the value to set the property to.
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
setProp(name, prop, value) {
|
||||
setProp(name, prop, value, location = this.location) {
|
||||
let [group, field] = splitOn(".", name);
|
||||
this.group(group).setProp(field, prop, value);
|
||||
this.group(group).setProp(field, prop, value, location);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,15 +325,42 @@ class ConfigGroup {
|
||||
*
|
||||
* After the creation of the new config group you have to call
|
||||
* either .init() or .load() on it in order to fully initialize it.
|
||||
*
|
||||
* @param {module:lib/config.Config} config -
|
||||
* Parent config for this group instance.
|
||||
*/
|
||||
constructor() {
|
||||
constructor(config) {
|
||||
if (!this.constructor._finalized) {
|
||||
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`);
|
||||
}
|
||||
|
||||
this._fields = new Map();
|
||||
this._unknownFields = new Map();
|
||||
this._config = null;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access for a field
|
||||
*
|
||||
* @param {object} def - definition for for the field to check.
|
||||
* @param {string} remote - location of remote access to check for.
|
||||
* @param {boolean} error - throw on failure to pass check.
|
||||
* @throws {module:lib/config.InvalidAccess}
|
||||
* if access is not granted and error is true.
|
||||
* @returns {boolean} true if access is granted
|
||||
* @private
|
||||
*/
|
||||
_checkAccess(def, remote, error) {
|
||||
for (let loc of [remote, this.config.location]) {
|
||||
if (!def.access.includes(loc)) {
|
||||
if (error) {
|
||||
throw new InvalidAccess(`Field '${def.name}' is not accessible from ${loc}`);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,16 +368,15 @@ class ConfigGroup {
|
||||
*
|
||||
* Computes and assigns the initial values for all fields of the
|
||||
* config group.
|
||||
*
|
||||
* @param {module:lib/config.Config} config - Config group belongs to.
|
||||
*/
|
||||
async init(config) {
|
||||
this._config = config;
|
||||
|
||||
async init() {
|
||||
for (let [name, def] of this.constructor._definitions) {
|
||||
if (this._fields.has(name)) {
|
||||
continue;
|
||||
}
|
||||
if (!def.access.includes(this.config.location)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
if (typeof def.initial_value === "function") {
|
||||
@@ -326,13 +397,13 @@ class ConfigGroup {
|
||||
* group and then initializes any possible missing fields from it to
|
||||
* their default values.
|
||||
*
|
||||
* @param {module:lib/config.Config} config - Config group belongs to.
|
||||
* @param {Object} serializedGroup -
|
||||
* Result from a previous call to .serialize().
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
async load(config, serializedGroup) {
|
||||
this.update(serializedGroup, false);
|
||||
await this.init(config);
|
||||
async load(serializedGroup, location = this.config.location) {
|
||||
this.update(serializedGroup, false, location);
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,16 +415,34 @@ class ConfigGroup {
|
||||
return this.constructor.groupName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if field can be accessed from the given location
|
||||
*
|
||||
* @param {string} name - Name of field to check.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {boolean} true if field is accessible
|
||||
*/
|
||||
canAccess(name, location = this.config.location) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
return this._checkAccess(def, location, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value for field
|
||||
*
|
||||
* @param {string} name - Name of field to get.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {*} Value stored for the field.
|
||||
*/
|
||||
get(name) {
|
||||
if (!this.constructor._definitions.has(name)) {
|
||||
get(name, location = this.config.location) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
this._checkAccess(def, location, true);
|
||||
|
||||
return this._fields.get(name);
|
||||
}
|
||||
@@ -366,14 +455,16 @@ class ConfigGroup {
|
||||
*
|
||||
* @param {string} name - Name of field to set.
|
||||
* @param {*} value - Value to set for field.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @throws {module:lib/config.InvalidField} if field is not defined.
|
||||
* @throws {module:lib/config.InvalidValue} if value is not allowed for the field.
|
||||
*/
|
||||
set(name, value) {
|
||||
set(name, value, location = this.config.location) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
this._checkAccess(def, location, true);
|
||||
|
||||
// Empty strings are treates as null
|
||||
if (value === "") {
|
||||
@@ -418,8 +509,8 @@ class ConfigGroup {
|
||||
|
||||
let prev = this._fields.get(name);
|
||||
this._fields.set(name, value);
|
||||
if (this._config && !isDeepStrictEqual(value, prev)) {
|
||||
this._config.emit("fieldChanged", this, name, prev);
|
||||
if (this.config && !isDeepStrictEqual(value, prev)) {
|
||||
this.config.emit("fieldChanged", this, name, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,14 +523,16 @@ class ConfigGroup {
|
||||
* @param {string} name - Name of field to set property on.
|
||||
* @param {string} prop - Name of property to set on field.
|
||||
* @param {*} value - the value to set the property to.
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @throws {module:lib/config.InvalidField} if field is not defined.
|
||||
* @throws {module:lib/config.InvalidValue} if field is not an object.
|
||||
*/
|
||||
setProp(name, prop, value) {
|
||||
setProp(name, prop, value, location = this.config.location) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
this._checkAccess(def, location, true);
|
||||
|
||||
if (def.type !== "object") {
|
||||
throw new InvalidValue(`Cannot set property on non-object field '${name}'`);
|
||||
@@ -454,8 +547,8 @@ class ConfigGroup {
|
||||
delete updated[prop];
|
||||
}
|
||||
this._fields.set(name, updated);
|
||||
if (this._config && !isDeepStrictEqual(updated, prev)) {
|
||||
this._config.emit("fieldChanged", this, name, prev);
|
||||
if (this.config && !isDeepStrictEqual(updated, prev)) {
|
||||
this.config.emit("fieldChanged", this, name, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,8 +561,9 @@ class ConfigGroup {
|
||||
* @param {Object} serializedGroup -
|
||||
* Result from a previous call to .serialize().
|
||||
* @param {boolean} notify - Invoke fieldChanged events if true.
|
||||
* @param {string=} location - Location used for access control.
|
||||
*/
|
||||
update(serializedGroup, notify) {
|
||||
update(serializedGroup, notify, location = this.config.location) {
|
||||
if (basicType(serializedGroup) !== "object") {
|
||||
throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`);
|
||||
}
|
||||
@@ -489,6 +583,10 @@ class ConfigGroup {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!this._checkAccess(def, location, false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (def.optional) {
|
||||
if (value !== null && basicType(value) !== def.type) {
|
||||
continue;
|
||||
@@ -504,8 +602,8 @@ class ConfigGroup {
|
||||
|
||||
let prev = this._fields.get(name);
|
||||
this._fields.set(name, value);
|
||||
if (notify && this._config && !isDeepStrictEqual(value, prev)) {
|
||||
this._config.emit("fieldChanged", this, name, prev);
|
||||
if (notify && this.config && !isDeepStrictEqual(value, prev)) {
|
||||
this.config.emit("fieldChanged", this, name, prev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,11 +611,17 @@ class ConfigGroup {
|
||||
/**
|
||||
* Serialize the config to a plain JavaScript object
|
||||
*
|
||||
* @param {string=} location - Location used for access control.
|
||||
* @returns {Object} JSON serializable representation of the group.
|
||||
*/
|
||||
serialize() {
|
||||
serialize(location = this.config.location) {
|
||||
let fields = {};
|
||||
for (let [name, value] of this._fields) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!this._checkAccess(def, location, false)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fields[name] = value;
|
||||
}
|
||||
|
||||
@@ -557,6 +661,9 @@ class ConfigGroup {
|
||||
* Define a new configuration field
|
||||
*
|
||||
* @param {Object} item - Field definition.
|
||||
* @param {Array<string>=} item.access -
|
||||
* Array of strings declaring where this config entry can be read
|
||||
* and modified from.
|
||||
* @param {string} item.type -
|
||||
* string declaring the type of the config value, supports
|
||||
* boolean, string, number and object.
|
||||
@@ -609,6 +716,12 @@ class ConfigGroup {
|
||||
}
|
||||
break;
|
||||
|
||||
case "access":
|
||||
if (valueType !== "array") {
|
||||
throw new Error("access must be an array");
|
||||
}
|
||||
break;
|
||||
|
||||
case "enum":
|
||||
if (valueType !== "array") {
|
||||
throw new Error("enum must be an array");
|
||||
@@ -655,11 +768,16 @@ class ConfigGroup {
|
||||
}
|
||||
|
||||
let computed = {
|
||||
access: this.defaultAccess,
|
||||
optional: false,
|
||||
fullName: `${this.groupName}.${item.name}`,
|
||||
...item,
|
||||
};
|
||||
|
||||
if (!computed.access) {
|
||||
throw new Error(`access not set and no defaultAccess defined for ${this.name}`);
|
||||
}
|
||||
|
||||
if (!computed.optional && computed.initial_value === undefined) {
|
||||
throw new Error(`Non-optional field ${item.name} needs an initial_value`);
|
||||
}
|
||||
@@ -706,6 +824,7 @@ class PluginConfigGroup extends ConfigGroup {
|
||||
|
||||
|
||||
module.exports = {
|
||||
InvalidAccess,
|
||||
InvalidField,
|
||||
InvalidValue,
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ const classes = require("./classes");
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class MasterGroup extends classes.ConfigGroup { }
|
||||
MasterGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterGroup.groupName = "master";
|
||||
MasterGroup.define({
|
||||
name: "name",
|
||||
@@ -69,6 +70,7 @@ MasterGroup.define({
|
||||
optional: true,
|
||||
});
|
||||
MasterGroup.define({
|
||||
access: ["master"],
|
||||
name: "auth_secret",
|
||||
title: "Master Authentication Secret",
|
||||
description:
|
||||
@@ -124,6 +126,7 @@ MasterConfig.registerGroup(MasterGroup);
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class SlaveGroup extends classes.ConfigGroup {}
|
||||
SlaveGroup.defaultAccess = ["master", "slave", "control"];
|
||||
SlaveGroup.groupName = "slave";
|
||||
SlaveGroup.define({
|
||||
name: "name",
|
||||
@@ -197,6 +200,7 @@ SlaveConfig.registerGroup(SlaveGroup);
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class InstanceGroup extends classes.ConfigGroup { }
|
||||
InstanceGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceGroup.groupName = "instance";
|
||||
InstanceGroup.define({
|
||||
name: "name",
|
||||
@@ -228,6 +232,7 @@ InstanceGroup.finalize();
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class FactorioGroup extends classes.ConfigGroup { }
|
||||
FactorioGroup.defaultAccess = ["master", "slave", "control"];
|
||||
FactorioGroup.groupName = "factorio";
|
||||
FactorioGroup.define({
|
||||
name: "version",
|
||||
@@ -325,6 +330,7 @@ InstanceConfig.registerGroup(FactorioGroup);
|
||||
* @memberof module:lib/config
|
||||
*/
|
||||
class ControlGroup extends classes.ConfigGroup {}
|
||||
ControlGroup.defaultAccess = ["control"];
|
||||
ControlGroup.groupName = "control";
|
||||
ControlGroup.define({
|
||||
name: "master_url",
|
||||
@@ -390,6 +396,7 @@ function registerPluginConfigGroups(pluginInfos) {
|
||||
|
||||
} else {
|
||||
class MasterConfigGroup extends classes.PluginConfigGroup { }
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = pluginInfo.name;
|
||||
MasterConfigGroup.finalize();
|
||||
MasterConfig.registerGroup(MasterConfigGroup);
|
||||
@@ -402,6 +409,7 @@ function registerPluginConfigGroups(pluginInfos) {
|
||||
|
||||
} else {
|
||||
class InstanceConfigGroup extends classes.PluginConfigGroup { }
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.groupName = pluginInfo.name;
|
||||
InstanceConfigGroup.finalize();
|
||||
InstanceConfig.registerGroup(InstanceConfigGroup);
|
||||
|
||||
+14
-14
@@ -372,7 +372,7 @@ async function loadInstances(databaseDirectory, file) {
|
||||
try {
|
||||
let serialized = JSON.parse(await fs.readFile(filePath));
|
||||
for (let serializedConfig of serialized) {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
await instanceConfig.load(serializedConfig);
|
||||
let status = instanceConfig.get("instance.assigned_slave") === null ? "unassigned" : "unknown";
|
||||
instances.set(instanceConfig.get("instance.id"), { config: instanceConfig, status });
|
||||
@@ -707,7 +707,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async createSlaveConfigRequestHandler(message) {
|
||||
let slaveConfig = new libConfig.SlaveConfig();
|
||||
let slaveConfig = new libConfig.SlaveConfig("control");
|
||||
await slaveConfig.init();
|
||||
|
||||
slaveConfig.set("slave.master_url", getMasterUrl());
|
||||
@@ -739,7 +739,7 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
// XXX should probably add a hook for slave reuqests?
|
||||
async createInstanceRequestHandler(message) {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
await instanceConfig.load(message.data.serialized_config);
|
||||
|
||||
let instanceId = instanceConfig.get("instance.id");
|
||||
@@ -795,7 +795,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
return {
|
||||
serialized_config: instance.config.serialize(),
|
||||
serialized_config: instance.config.serialize("control"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -806,7 +806,7 @@ class ControlConnection extends BaseConnection {
|
||||
if (connection) {
|
||||
await libLink.messages.assignInstance.send(connection, {
|
||||
instance_id: instance.config.get("instance.id"),
|
||||
serialized_config: instance.config.serialize(),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -827,7 +827,7 @@ class ControlConnection extends BaseConnection {
|
||||
throw new libErrors.RequestError("Setting instance.id is not supported");
|
||||
}
|
||||
|
||||
instance.config.set(message.data.field, message.data.value);
|
||||
instance.config.set(message.data.field, message.data.value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
}
|
||||
|
||||
@@ -838,7 +838,7 @@ class ControlConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
let { field, prop, value } = message.data;
|
||||
instance.config.setProp(field, prop, value);
|
||||
instance.config.setProp(field, prop, value, "control");
|
||||
await this.updateInstanceConfig(instance);
|
||||
}
|
||||
|
||||
@@ -877,7 +877,7 @@ class ControlConnection extends BaseConnection {
|
||||
if (slave_id !== null) {
|
||||
await libLink.messages.assignInstance.send(newSlaveConnection, {
|
||||
instance_id,
|
||||
serialized_config: instance.config.serialize(),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1202,15 +1202,15 @@ class SlaveConnection extends BaseConnection {
|
||||
if (instance.config.get("instance.assigned_slave") === this._id) {
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instance.config.get("instance.id"),
|
||||
serialized_config: instance.config.serialize(),
|
||||
serialized_config: instance.config.serialize("slave"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign instances the slave has but master does not
|
||||
for (let instance of message.data.instances) {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
await instanceConfig.load(instance.serialized_config);
|
||||
let instanceConfig = new libConfig.InstanceConfig("master");
|
||||
await instanceConfig.load(instance.serialized_config, "slave");
|
||||
|
||||
let masterInstance = db.instances.get(instanceConfig.get("instance.id"));
|
||||
if (masterInstance) {
|
||||
@@ -1239,7 +1239,7 @@ class SlaveConnection extends BaseConnection {
|
||||
});
|
||||
await libLink.messages.assignInstance.send(this, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
serialized_config: instanceConfig.serialize("slave"),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1797,7 +1797,7 @@ async function handleBootstrapCommand(args) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
let controlConfig = new libConfig.ControlConfig();
|
||||
let controlConfig = new libConfig.ControlConfig("control");
|
||||
await controlConfig.init();
|
||||
|
||||
controlConfig.set("control.master_url", getMasterUrl());
|
||||
@@ -1909,7 +1909,7 @@ async function initialize() {
|
||||
|
||||
masterConfigPath = args.config;
|
||||
logger.info(`Loading config from ${masterConfigPath}`);
|
||||
masterConfig = new libConfig.MasterConfig();
|
||||
masterConfig = new libConfig.MasterConfig("master");
|
||||
try {
|
||||
await masterConfig.load(JSON.parse(await fs.readFile(masterConfigPath)));
|
||||
|
||||
|
||||
@@ -799,7 +799,7 @@ async function discoverInstances(instancesDir) {
|
||||
let instanceInfos = new Map();
|
||||
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
let configPath = path.join(instancesDir, entry.name, "instance.json");
|
||||
|
||||
try {
|
||||
@@ -1175,17 +1175,17 @@ class Slave extends libLink.Link {
|
||||
let { instance_id, serialized_config } = message.data;
|
||||
let instanceInfo = this.instanceInfos.get(instance_id);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true);
|
||||
instanceInfo.config.update(serialized_config, true, "master");
|
||||
logger.verbose(`Updated config for ${instanceInfo.path}`);
|
||||
|
||||
} else {
|
||||
instanceInfo = this.discoveredInstanceInfos.get(instance_id);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config, true);
|
||||
instanceInfo.config.update(serialized_config, true, "master");
|
||||
|
||||
} else {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
await instanceConfig.load(serialized_config);
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
await instanceConfig.load(serialized_config, "master");
|
||||
|
||||
// XXX: race condition on multiple simultanious calls
|
||||
let instanceDir = await this._findNewInstanceDir(instanceConfig.get("instance.name"));
|
||||
@@ -1348,7 +1348,7 @@ class Slave extends libLink.Link {
|
||||
for (let [instanceId, instanceInfo] of this.discoveredInstanceInfos) {
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
list.push({
|
||||
serialized_config: instanceInfo.config.serialize(),
|
||||
serialized_config: instanceInfo.config.serialize("master"),
|
||||
status: instanceConnection ? instanceConnection.status : "stopped",
|
||||
});
|
||||
}
|
||||
@@ -1561,7 +1561,7 @@ async function startSlave() {
|
||||
libConfig.finalizeConfigs();
|
||||
|
||||
logger.info(`Loading config from ${args.config}`);
|
||||
let slaveConfig = new libConfig.SlaveConfig();
|
||||
let slaveConfig = new libConfig.SlaveConfig("slave");
|
||||
try {
|
||||
await slaveConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
|
||||
|
||||
@@ -333,14 +333,14 @@ function InstanceConfigTree(props) {
|
||||
instance_id: props.id,
|
||||
});
|
||||
|
||||
let newConfig = new libConfig.InstanceConfig();
|
||||
let newConfig = new libConfig.InstanceConfig("control");
|
||||
await newConfig.load(result.serialized_config);
|
||||
setConfig(newConfig);
|
||||
}
|
||||
|
||||
useEffect(() => { updateConfig(); }, [props.id]);
|
||||
|
||||
function renderInput(group, def) {
|
||||
function renderInput(def) {
|
||||
if (def.type === "boolean") {
|
||||
return <Checkbox/>;
|
||||
}
|
||||
@@ -387,6 +387,10 @@ function InstanceConfigTree(props) {
|
||||
};
|
||||
|
||||
for (let def of GroupClass.definitions.values()) {
|
||||
if (!group.canAccess(def.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = group.get(def.name);
|
||||
let fieldName = `${group.name}.${def.name}`;
|
||||
let childNode = {
|
||||
@@ -445,7 +449,7 @@ function InstanceConfigTree(props) {
|
||||
let propName = form.getFieldValue(`${newPropPath}.name`);
|
||||
let propValue = form.getFieldValue(`${newPropPath}.value`);
|
||||
if (!Object.prototype.hasOwnProperty.call(value, propName)) {
|
||||
let newConfig = new libConfig.InstanceConfig();
|
||||
let newConfig = new libConfig.InstanceConfig("control");
|
||||
await newConfig.load(config.serialize());
|
||||
newConfig.setProp(fieldName, propName, null);
|
||||
setConfig(newConfig);
|
||||
@@ -475,7 +479,7 @@ function InstanceConfigTree(props) {
|
||||
tooltip={def.description}
|
||||
valuePropName={def.type === "boolean" ? "checked" : "value"}
|
||||
>
|
||||
{renderInput(group, def)}
|
||||
{renderInput(def)}
|
||||
</Form.Item>;
|
||||
initialValues[fieldName] = value;
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ export default function InstancesPage() {
|
||||
title: "Name",
|
||||
}],
|
||||
insert: async args => {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("control");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", args.name);
|
||||
let serialized_config = instanceConfig.serialize();
|
||||
let serialized_config = instanceConfig.serialize("master");
|
||||
let response = await libLink.messages.createInstance.send(control, { serialized_config });
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
|
||||
class InstanceConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.groupName = "statistics_exporter";
|
||||
InstanceConfigGroup.define({
|
||||
name: "command_timeout",
|
||||
|
||||
@@ -3,6 +3,7 @@ const libLink = require("@clusterio/lib/link");
|
||||
const libConfig = require("@clusterio/lib/config");
|
||||
|
||||
class MasterConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
MasterConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
MasterConfigGroup.groupName = "subspace_storage";
|
||||
MasterConfigGroup.define({
|
||||
name: "autosave_interval",
|
||||
@@ -29,6 +30,7 @@ MasterConfigGroup.define({
|
||||
MasterConfigGroup.finalize();
|
||||
|
||||
class InstanceConfigGroup extends libConfig.PluginConfigGroup {}
|
||||
InstanceConfigGroup.defaultAccess = ["master", "slave", "control"];
|
||||
InstanceConfigGroup.groupName = "subspace_storage";
|
||||
InstanceConfigGroup.define({
|
||||
name: "log_item_transfers",
|
||||
|
||||
+177
-33
@@ -1,11 +1,15 @@
|
||||
"use strict";
|
||||
const classes = require("@clusterio/lib/config/classes");
|
||||
const assert = require("assert").strict;
|
||||
const events = require("events");
|
||||
|
||||
|
||||
describe("lib/config/classes", function() {
|
||||
describe("ConfigGroup", function() {
|
||||
let mockConfig = new events.EventEmitter();
|
||||
mockConfig.location = "local";
|
||||
class TestGroup extends classes.ConfigGroup { }
|
||||
TestGroup.defaultAccess = ["local", "remote"];
|
||||
TestGroup.groupName = "test_group";
|
||||
describe(".define()", function() {
|
||||
it("should throw if groupName is not set", function() {
|
||||
@@ -23,6 +27,10 @@ describe("lib/config/classes", function() {
|
||||
});
|
||||
it("should throw on invalid properties", function() {
|
||||
for (let [def, error] of [
|
||||
[
|
||||
{ access: "local", name: "a", type: "string", optional: true },
|
||||
"access must be an array",
|
||||
],
|
||||
[
|
||||
{ name: "a", type: "invalid", optional: true },
|
||||
"invalid is not a valid type",
|
||||
@@ -59,6 +67,14 @@ describe("lib/config/classes", function() {
|
||||
assert.throws(() => TestGroup.define(def), new Error(error));
|
||||
}
|
||||
});
|
||||
it("should throw on defaultAccess not set", function() {
|
||||
class NoAccessGroup extends classes.ConfigGroup { }
|
||||
NoAccessGroup.groupName = "no_access";
|
||||
assert.throws(
|
||||
() => NoAccessGroup.define({ name: "a", type: "string", optional: true }),
|
||||
new Error("access not set and no defaultAccess defined for NoAccessGroup")
|
||||
);
|
||||
});
|
||||
it("should throw on missing properties", function() {
|
||||
assert.throws(
|
||||
() => TestGroup.define({ type: "string", optional: true }),
|
||||
@@ -74,11 +90,13 @@ describe("lib/config/classes", function() {
|
||||
TestGroup.define({ name: "func", type: "number", initial_value: () => 42 });
|
||||
TestGroup.define({ name: "bool", type: "boolean", initial_value: false, optional: true });
|
||||
TestGroup.define({ name: "json", type: "object", initial_value: {}, optional: true });
|
||||
TestGroup.define({ name: "priv", access: ["local"], type: "string", optional: true });
|
||||
|
||||
assert(TestGroup._definitions.has("test"), "field test was not defined");
|
||||
assert(TestGroup._definitions.has("func"), "field func was not defined");
|
||||
assert(TestGroup._definitions.has("bool"), "field bool was not defined");
|
||||
assert(TestGroup._definitions.has("json"), "field json was not defined");
|
||||
assert(TestGroup._definitions.has("priv"), "field priv was not defined");
|
||||
});
|
||||
|
||||
it("should throw on already defined entries", function() {
|
||||
@@ -90,6 +108,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
it("should set the entries on the class", function() {
|
||||
let field = {
|
||||
access: ["local", "remote"],
|
||||
type: "string",
|
||||
name: "enum",
|
||||
title: "Enum",
|
||||
@@ -122,7 +141,7 @@ describe("lib/config/classes", function() {
|
||||
});
|
||||
it("should throw when creating an instance before finalize", function() {
|
||||
assert.throws(
|
||||
() => new TestGroup(),
|
||||
() => new TestGroup(mockConfig),
|
||||
new Error("Cannot instantiate incomplete ConfigGroup class TestGroup")
|
||||
);
|
||||
});
|
||||
@@ -141,24 +160,31 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should create an instance", function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".init()", function() {
|
||||
it("should initialize all fields with defaults", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
|
||||
assert.equal(testInstance.get("enum"), "b");
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
assert.equal(testInstance.get("func"), 42);
|
||||
assert.equal(testInstance.get("priv"), null);
|
||||
});
|
||||
it("should not initalize inaccessible fields", async function() {
|
||||
let testInstance = new TestGroup({ location: "remote" });
|
||||
await testInstance.init();
|
||||
assert.equal(testInstance._fields.get("enum"), "b");
|
||||
assert.equal(testInstance._fields.get("priv"), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get name", function() {
|
||||
it("should give the name of the group", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
assert.equal(testInstance.name, "test_group");
|
||||
});
|
||||
@@ -166,10 +192,26 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".serialize()", function() {
|
||||
it("should serialize a basic group", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
|
||||
assert.deepEqual(testInstance.serialize(), {
|
||||
name: "test_group",
|
||||
fields: {
|
||||
enum: "b",
|
||||
test: null,
|
||||
func: 42,
|
||||
bool: false,
|
||||
json: {},
|
||||
priv: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
it("should leave out inaccessible fields", async function() {
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
|
||||
assert.deepEqual(testInstance.serialize("remote"), {
|
||||
name: "test_group",
|
||||
fields: {
|
||||
enum: "b",
|
||||
@@ -184,29 +226,36 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".load()", function() {
|
||||
it("should load a basic group", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.load(null, { name: "test_group", fields: {
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: {
|
||||
enum: "c",
|
||||
test: "blah",
|
||||
func: 22,
|
||||
bool: null,
|
||||
json: { valid: true },
|
||||
priv: "bar",
|
||||
}});
|
||||
|
||||
assert.equal(testInstance.get("enum"), "c");
|
||||
assert.equal(testInstance.get("test"), "blah");
|
||||
assert.equal(testInstance.get("func"), 22);
|
||||
assert.equal(testInstance.get("bool"), null);
|
||||
assert.equal(testInstance.get("priv"), "bar");
|
||||
assert.deepEqual(testInstance.get("json"), { valid: true });
|
||||
});
|
||||
it("should load defaults for missing serialized fields", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.load(null, { name: "test_group", fields: { enum: "a" } });
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: { enum: "a" } });
|
||||
|
||||
assert.equal(testInstance.get("enum"), "a");
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
assert.equal(testInstance.get("func"), 42);
|
||||
});
|
||||
it("should ignore inaccessible fields", async function() {
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: { priv: "bad" } }, "remote");
|
||||
assert.equal(testInstance.get("priv"), null);
|
||||
});
|
||||
|
||||
it("should preserve unknown fields when serialized back", async function() {
|
||||
let testFields = {
|
||||
@@ -215,13 +264,14 @@ describe("lib/config/classes", function() {
|
||||
func: 50,
|
||||
bool: false,
|
||||
json: {},
|
||||
priv: null,
|
||||
alpha: null,
|
||||
beta: "decay",
|
||||
gamma: 99,
|
||||
};
|
||||
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.load(null, { name: "test_group", fields: testFields });
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: testFields });
|
||||
|
||||
assert.deepEqual(testInstance.serialize().fields, testFields);
|
||||
});
|
||||
@@ -229,7 +279,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".update()", function() {
|
||||
it("should skip updating invalid values", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
testInstance.update({ name: "test_group", fields: { enum: "wrong", test: 3, func: null }}, false);
|
||||
|
||||
@@ -237,9 +287,19 @@ describe("lib/config/classes", function() {
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
assert.equal(testInstance.get("func"), 42);
|
||||
});
|
||||
it("should skip updating inaccessible fields", async function() {
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.init();
|
||||
testInstance.update(
|
||||
{ name: "test_group", fields: { test: "a", priv: "bad" }}, false, "remote"
|
||||
);
|
||||
|
||||
assert.equal(testInstance.get("test"), "a");
|
||||
assert.equal(testInstance.get("priv"), null);
|
||||
});
|
||||
|
||||
it("should throw on invalid input", function() {
|
||||
let testInstance = new TestGroup();
|
||||
let testInstance = new TestGroup(mockConfig);
|
||||
assert.throws(
|
||||
() => testInstance.update(),
|
||||
new Error("Expected object, not undefined for ConfigGroup")
|
||||
@@ -263,13 +323,52 @@ describe("lib/config/classes", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".canAccess()", function() {
|
||||
let testInstance;
|
||||
before(async function() {
|
||||
testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: {
|
||||
enum: "a", priv: "blah", func: 27,
|
||||
}});
|
||||
});
|
||||
it("should return true for fields that are accessible", function() {
|
||||
for (let field of ["enum", "test", "func", "bool", "json"]) {
|
||||
assert.equal(testInstance.canAccess(field), true);
|
||||
assert.equal(testInstance.canAccess(field, "local"), true);
|
||||
assert.equal(testInstance.canAccess(field, "remote"), true);
|
||||
}
|
||||
assert.equal(testInstance.canAccess("priv"), true);
|
||||
assert.equal(testInstance.canAccess("priv", "local"), true);
|
||||
assert.equal(testInstance.canAccess("priv", "remote"), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".get()", function() {
|
||||
let testInstance;
|
||||
before(async function() {
|
||||
testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: {
|
||||
enum: "a", priv: "blah", func: 27,
|
||||
}});
|
||||
});
|
||||
it("should retreive value of field", function() {
|
||||
assert.equal(testInstance.get("enum"), "a");
|
||||
assert.equal(testInstance.get("priv"), "blah");
|
||||
assert.equal(testInstance.get("func"), 27);
|
||||
});
|
||||
it("should throw if field is inaccessible", function() {
|
||||
assert.throws(
|
||||
() => testInstance.get("priv", "remote"),
|
||||
new classes.InvalidAccess("Field 'priv' is not accessible from remote")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".set()", function() {
|
||||
let testInstance;
|
||||
before(async function() {
|
||||
testInstance = new TestGroup();
|
||||
await testInstance.load(null, { name: "test_group", fields: {
|
||||
enum: "a", test: "blah", func: 27,
|
||||
}});
|
||||
testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: { enum: "a", test: "blah", func: 27 }});
|
||||
});
|
||||
|
||||
it("should throw if field does not exist", function() {
|
||||
@@ -279,6 +378,13 @@ describe("lib/config/classes", function() {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if field is inaccesible", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("priv", "bad", "remote"),
|
||||
new classes.InvalidAccess("Field 'priv' is not accessible from remote")
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if field is not in enum", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("enum", "bar"),
|
||||
@@ -366,8 +472,8 @@ describe("lib/config/classes", function() {
|
||||
describe(".setProp()", function() {
|
||||
let testInstance;
|
||||
before(async function() {
|
||||
testInstance = new TestGroup();
|
||||
await testInstance.load(null, { name: "test_group", fields: {
|
||||
testInstance = new TestGroup(mockConfig);
|
||||
await testInstance.load({ name: "test_group", fields: {
|
||||
enum: "a", test: "blah", func: 27,
|
||||
}});
|
||||
});
|
||||
@@ -379,9 +485,16 @@ describe("lib/config/classes", function() {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if field is inaccesible", function() {
|
||||
assert.throws(
|
||||
() => testInstance.setProp("priv", "prop", "bad", "remote"),
|
||||
new classes.InvalidAccess("Field 'priv' is not accessible from remote")
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw if field is not an object", function() {
|
||||
assert.throws(
|
||||
() => testInstance.setProp("enum", "a"),
|
||||
() => testInstance.setProp("enum", "prop", "a"),
|
||||
new classes.InvalidField("Cannot set property on non-object field 'enum'")
|
||||
);
|
||||
});
|
||||
@@ -408,8 +521,10 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe("Config", function() {
|
||||
class AlphaGroup extends classes.ConfigGroup { }
|
||||
AlphaGroup.defaultAccess = ["local", "remote"];
|
||||
AlphaGroup.groupName = "alpha";
|
||||
class BetaGroup extends classes.ConfigGroup { }
|
||||
BetaGroup.defaultAccess = ["local", "remote"];
|
||||
BetaGroup.groupName = "beta";
|
||||
class TestConfig extends classes.Config { }
|
||||
|
||||
@@ -454,20 +569,26 @@ describe("lib/config/classes", function() {
|
||||
describe("constructor", function() {
|
||||
it("should throw if config is not finalized", function() {
|
||||
assert.throws(
|
||||
() => new TestConfig(),
|
||||
() => new TestConfig("local"),
|
||||
new Error("Cannot instantiate incomplete Config class TestConfig")
|
||||
);
|
||||
});
|
||||
it("should construct a finalized class", function() {
|
||||
TestConfig.finalize();
|
||||
let config = new TestConfig();
|
||||
let config = new TestConfig("local");
|
||||
assert(config);
|
||||
});
|
||||
it("should throw if location is not passed", function() {
|
||||
assert.throws(
|
||||
() => new TestConfig(),
|
||||
new Error("location must be a string")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".init()", function() {
|
||||
it("should initialize all fields", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
assert.equal(testInstance.get("alpha.foo"), null);
|
||||
assert.deepEqual(testInstance.get("beta.bar"), {});
|
||||
@@ -476,7 +597,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".serialize()", function() {
|
||||
it("should serialize a basic config", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
assert.deepEqual(testInstance.serialize(), {
|
||||
groups: [
|
||||
@@ -495,7 +616,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".load()", function() {
|
||||
it("should reject on incorrect input passed", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await assert.rejects(
|
||||
testInstance.load(null),
|
||||
new Error("Expected object, not null for config")
|
||||
@@ -511,7 +632,7 @@ describe("lib/config/classes", function() {
|
||||
});
|
||||
|
||||
it("should load defaults for missing serialized groups", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.load({ groups: [{ name: "alpha", fields: { foo: "a" }}] });
|
||||
|
||||
assert.equal(testInstance.get("alpha.foo"), "a");
|
||||
@@ -534,7 +655,7 @@ describe("lib/config/classes", function() {
|
||||
},
|
||||
];
|
||||
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.load({ groups: testGroups });
|
||||
|
||||
assert.deepEqual(testInstance.serialize().groups, testGroups);
|
||||
@@ -543,7 +664,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".update()", function() {
|
||||
it("should update a basic config", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
testInstance.update({ groups: [
|
||||
{
|
||||
@@ -577,7 +698,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".group()", function() {
|
||||
it("should throw if instance is not initialized", function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
assert.throws(
|
||||
() => testInstance.group("test"),
|
||||
new Error("TestConfig instance is uninitialized")
|
||||
@@ -585,16 +706,39 @@ describe("lib/config/classes", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".canAccess()", function() {
|
||||
it("should throw if group does not exist", async function() {
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
assert.throws(() => testInstance.canAccess("invalid"), new Error("No config group named 'invalid'"));
|
||||
});
|
||||
|
||||
it("should throw if no field is specified", async function() {
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
assert.throws(() => testInstance.canAccess("alpha"), new Error("No field named ''"));
|
||||
});
|
||||
|
||||
it("should return true for an accessible field", async function() {
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
assert.equal(testInstance.canAccess("alpha.foo"), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".get()", function() {
|
||||
it("should throw if group does not exist", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
assert.throws(() => testInstance.get("invalid"), new Error("No config group named 'invalid'"));
|
||||
});
|
||||
|
||||
it("should throw if no field is specified", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
assert.throws(() => testInstance.get("alpha"), new Error("No field named ''"));
|
||||
@@ -603,7 +747,7 @@ describe("lib/config/classes", function() {
|
||||
|
||||
describe(".set()", function() {
|
||||
it("should set the value", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
let testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
testInstance.set("alpha.foo", "new value");
|
||||
@@ -621,7 +765,7 @@ describe("lib/config/classes", function() {
|
||||
let testInstance;
|
||||
let called;
|
||||
beforeEach(async function() {
|
||||
testInstance = new TestConfig();
|
||||
testInstance = new TestConfig("local");
|
||||
await testInstance.init();
|
||||
|
||||
called = false;
|
||||
|
||||
+3
-3
@@ -15,7 +15,7 @@ describe("Slave testing", function() {
|
||||
describe("class Instance", function() {
|
||||
let instance;
|
||||
before(async function() {
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", "foo");
|
||||
instance = new slave._Instance({}, new libLink.VirtualConnector(), "dir", "factorioDir", instanceConfig);
|
||||
@@ -96,7 +96,7 @@ describe("Slave testing", function() {
|
||||
await fs.outputFile(path.join(testDir, "shared", "mod_b.zip"), "b");
|
||||
await fs.outputFile(path.join(testDir, "shared", "mod.dat"), "c");
|
||||
|
||||
let instanceConfig = new libConfig.InstanceConfig();
|
||||
let instanceConfig = new libConfig.InstanceConfig("slave");
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", "test");
|
||||
instance = new slave._Instance(
|
||||
@@ -149,7 +149,7 @@ describe("Slave testing", function() {
|
||||
let instancePath = path.join("test", "file", "instances");
|
||||
let instanceInfos = await slave._discoverInstances(instancePath);
|
||||
|
||||
let referenceConfig = new libConfig.InstanceConfig();
|
||||
let referenceConfig = new libConfig.InstanceConfig("slave");
|
||||
await referenceConfig.init();
|
||||
referenceConfig.set("instance.id", 1);
|
||||
referenceConfig.set("instance.name", "test");
|
||||
|
||||
Reference in new issue
Block a user