mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Implement new configuration system
Replace current configuration mess with a trimmed down version of the original vision of #200. Configs are collection of config groups that have fields that can be set. These groups allow organizing the fields as well as opertunity for plugins to add their own groups. By default plugins have an auto generated group with the enabled field.
This commit is contained in:
1 parent
370ee9ed04
commit
5e5149dc93
24 files changed
+2364
-498
No files matched your search
+1
-1
@@ -32,7 +32,7 @@ Factorio*
|
||||
/instances
|
||||
/sharedMods
|
||||
/database
|
||||
/config.json
|
||||
/config-master.json
|
||||
/config-control.json
|
||||
/config-slave.json
|
||||
package-lock.json
|
||||
|
||||
+11
-4
@@ -22,6 +22,9 @@ Version 2.0.0
|
||||
- Rewritten the plugin system from scratch. Plugins now inherit from
|
||||
a base class and use the same WebSocket connection Clusterio uses to
|
||||
communicate.
|
||||
- New configuration system with support for initializing configs when needed,
|
||||
modifying config entries using built-in commands, and updating instance
|
||||
configuration remotely.
|
||||
|
||||
### Features
|
||||
|
||||
@@ -38,7 +41,6 @@ Version 2.0.0
|
||||
- Added plugins directory to the views path. This makes it possible for
|
||||
plugins to render their own ejs views or pages in their own folders by
|
||||
using paths of the format "pluginName/path/to/page-or-view".
|
||||
- Added instanceName entry to the config passed to instance plugins.
|
||||
- Replaced the per instance copy of the shared Factorio mods with
|
||||
symlinks. On Windows hard links are used instead due to the
|
||||
privileges requirements of symlinks.
|
||||
@@ -63,16 +65,18 @@ Version 2.0.0
|
||||
- Removed factorio_version from config. The version installed is auto
|
||||
detected and used instead.
|
||||
- Removed the playerManager specific command CLI tools/delete_player.js.
|
||||
- Creating an instance, creating a save for an instance and starting an
|
||||
instance is now three separate commands.
|
||||
- Creating an instance, assigning it to a slave, creating a save for an
|
||||
instance and starting an instance is now four separate commands.
|
||||
- Removed oddball limits to slaves.json size.
|
||||
- Moved slave specific configuration into its own configuration file.
|
||||
- Moved slave specific and instance specific configuration into their own
|
||||
configuration files.
|
||||
- Removed unused binary option from plugin config.
|
||||
- Removed info and shout command from globalChat plugin
|
||||
- Removed mirrorAllChat and enableCrossServerShout configuration options for
|
||||
globalChat plugin.
|
||||
- Removed UPSdisplay plugin. UPS statistics is exported by the statistics
|
||||
exporter plugin.
|
||||
- Master server now defaults to hosting on https on port 8443.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -136,6 +140,9 @@ Version 2.0.0
|
||||
concurrent commands.
|
||||
- Removed allowRemoteCommandExecution config option. Remote commands are
|
||||
always allowed with the move to master managing slaves/instances.
|
||||
- Removed `--databaseDirectory`, `--masterPort`, and `--sslPort` command line
|
||||
arguments from the master server.
|
||||
- Implemented a new config system that replaces the old. Breaks all plugins.
|
||||
|
||||
|
||||
Version 1.2.4
|
||||
|
||||
@@ -113,8 +113,6 @@ Master and all slaves:
|
||||
wget -O factorio.tar.gz https://www.factorio.com/get-download/latest/headless/linux64
|
||||
tar -xf factorio.tar.gz
|
||||
npm install --only=production
|
||||
cp config.json.dist config.json
|
||||
node ./lib/npmPostinstall
|
||||
|
||||
downloads and installs nodejs, git and clusterio. To specify a version, change "latest" in the link to a version number like 0.14.21.
|
||||
|
||||
@@ -122,12 +120,6 @@ Optional step (if you want to use pm2):
|
||||
|
||||
sudo npm install pm2 -g
|
||||
|
||||
Now you need to edit the `config.json` file. If you skip this step nothing will work.
|
||||
Pretty much all the blank fields should be filled in, except on the master where a few can be omitted.
|
||||
|
||||
* You get your factorio matchmaking token from factorio.com
|
||||
|
||||
* The `masterAuthSecret` should never be touched unless you want to invalidate everyones authentication tokens
|
||||
**Ubuntu with Docker**
|
||||
|
||||
The Docker support for Clusterio is curently broken. If you need it
|
||||
@@ -174,7 +166,6 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
git clone -b master https://github.com/clusterio/factorioClusterio
|
||||
cd factorioClusterio
|
||||
npm install --only=production
|
||||
copy config.json.dist config.json
|
||||
|
||||
2. Obtain Factorio by either of these two methods:
|
||||
|
||||
@@ -195,21 +186,6 @@ reboot when you are done, then proceed to the next steps. *reboots matter*
|
||||
3. Rename the folder to "factorio"
|
||||
|
||||
|
||||
3. Open `config.json` with a text editor and configure as desired.
|
||||
|
||||
4. Run `node master` to generate the athentication token into secret-api-token.txt
|
||||
|
||||
**Server Host**
|
||||
|
||||
1. Do step 1 and 2 of the Master section above *OR* use the same folder that was created in that section.
|
||||
|
||||
Change `masterURL `to something like `http://203.0.113.33:8080` (provided by master server owner)
|
||||
|
||||
Change `masterAuthToken` to the value found in `secret-api-token.txt` on the master server
|
||||
|
||||
Repeat step 4 for more servers on one machine. You should be able to find its port by looking at the slave section on master:8080 (the web interface)
|
||||
|
||||
|
||||
## Running Clusterio
|
||||
|
||||
After following the installation instructions you can use the following
|
||||
@@ -222,7 +198,7 @@ It's necessary to run the master server in order for anything to work.
|
||||
Once you've completed the setup run the following command to start it
|
||||
up:
|
||||
|
||||
node master
|
||||
node master run
|
||||
|
||||
Or with pm2 (it's recommened to run it without pm2 first):
|
||||
|
||||
@@ -234,20 +210,21 @@ Or with pm2 (it's recommened to run it without pm2 first):
|
||||
Slaves connect to the master server and are managed remotely from the
|
||||
master server. In order for slaves to connect to the master server they
|
||||
need an authentication token from the master server. This token is
|
||||
written to secret-api-token.txt on the master server the first time it
|
||||
is started.
|
||||
written to secret-api-token.txt on the master server when it is started
|
||||
up.
|
||||
|
||||
To set up the configuration for a new local slave run the following.
|
||||
|
||||
node client create-config --name "Local" --token "<token>"
|
||||
node client config set slave.name "Local"
|
||||
node client config set slave.master_token "<token>"
|
||||
|
||||
This will write a new `config-slave.json` file in the current directory
|
||||
(you can change the location with the `--config` option) with the name
|
||||
and token provided. If you are connecting to a remote master server you
|
||||
will also need to pass the `--url <url to master server>` option.
|
||||
will also need to set the `slave.master_url` option to that url.
|
||||
|
||||
You can edit the config of a slave with the `node client edit-config`
|
||||
command. Use `node client edit-config --help` for more information.
|
||||
You can list the config of a slave with the `node client config list`
|
||||
command. Use `node client config --help` for more information.
|
||||
|
||||
Once the config is set up run the slave with
|
||||
|
||||
@@ -260,21 +237,26 @@ Instances are created, managed and started from the master server. For
|
||||
now the only interface available is the `clusterctl` command line tool
|
||||
included in Clusterio. You can run this tool from any slave, or the
|
||||
master server without having to set up a config, if you want to manage
|
||||
the cluster from somewhere else see `node clusterctl create-config`.
|
||||
the cluster from somewhere else you will need to set the
|
||||
`control.master_url` and `control.master_token` options with the `node
|
||||
clusterctl control-config set` command:
|
||||
|
||||
The basic operations to start a new instance is the following
|
||||
|
||||
node clusterctl create-instance --slave "Local" --name "My instance"
|
||||
node clusterctl create-instance --name "My instance"
|
||||
node clusterctl assign-instance --instance "My instance" --slave "Local"
|
||||
node clusterctl create-save --instance "My instance"
|
||||
node clusterctl start-instance --instance "My instance"
|
||||
|
||||
The first line creates the files and database entries for a new
|
||||
instance. The second line creates a new savegame for the instance. You
|
||||
could also upload your own save to the instance directory instead. And
|
||||
finally the third line starts the instance.
|
||||
The first line creates the instance configuration on the master server.
|
||||
The second assigns the instance to a slave which creates the instance
|
||||
directory and files needed to run the instance on the given slave. The
|
||||
third line creates a new savegame for the instance. You could also
|
||||
upload your own save to the instance directory instead. And finally the
|
||||
fourth line starts the instance.
|
||||
|
||||
There are many more commands available with clusterctl. See
|
||||
`node clusterctl --help` for more information.
|
||||
`node clusterctl --help` for a full list of them.
|
||||
|
||||
|
||||
## Plugins
|
||||
@@ -285,10 +267,6 @@ Here are the known Clusterio plugins in the wild:
|
||||
|
||||
## Common problems
|
||||
|
||||
### Cannot find module: `./config`
|
||||
|
||||
Copy your config.json.dist to config.json and configure it.
|
||||
|
||||
### EACCESS [...] LISTEN 443
|
||||
|
||||
Some systems don't let non root processes listen to ports below 1000. Either run with `sudo` or change config.json to use higher port numbers.
|
||||
|
||||
@@ -12,6 +12,7 @@ const plugin = require("lib/plugin");
|
||||
const errors = require("lib/errors");
|
||||
const prometheus = require('lib/prometheus');
|
||||
const luaTools = require('lib/luaTools');
|
||||
const config = require('lib/config');
|
||||
|
||||
|
||||
/**
|
||||
@@ -24,20 +25,12 @@ class Instance extends link.Link{
|
||||
this._dir = dir;
|
||||
|
||||
this.plugins = new Map();
|
||||
|
||||
// This is expected to change with the config system rewrite
|
||||
this.config = {
|
||||
id: instanceConfig.id,
|
||||
name: instanceConfig.name,
|
||||
gamePort: instanceConfig.factorioPort,
|
||||
rconPort: instanceConfig.clientPort,
|
||||
rconPassword: instanceConfig.clientPassword,
|
||||
}
|
||||
this.config = instanceConfig;
|
||||
|
||||
let serverOptions = {
|
||||
gamePort: this.config.gamePort,
|
||||
rconPort: this.config.rconPort,
|
||||
rconPassword: this.config.rconPassword,
|
||||
gamePort: this.config.get('factorio.game_port'),
|
||||
rconPort: this.config.get('factorio.rcon_port'),
|
||||
rconPassword: this.config.get('factorio.rcon_password'),
|
||||
};
|
||||
|
||||
this.server = new factorio.FactorioServer(
|
||||
@@ -45,7 +38,7 @@ class Instance extends link.Link{
|
||||
);
|
||||
|
||||
this.server.on('output', (output) => {
|
||||
link.messages.instanceOutput.send(this, { instance_id: this.config.id, output })
|
||||
link.messages.instanceOutput.send(this, { instance_id: this.config.get("instance.id"), output })
|
||||
|
||||
for (let [name, plugin] of this.plugins) {
|
||||
plugin.onOutput(output).catch(err => {
|
||||
@@ -60,7 +53,7 @@ class Instance extends link.Link{
|
||||
|
||||
// load plugins
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
if (!pluginInfo.enabled || !pluginInfo.instanceEntrypoint) {
|
||||
if (!pluginInfo.instanceEntrypoint || this.config.group(pluginInfo.name).get("enabled")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -76,6 +69,30 @@ class Instance extends link.Link{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the server-settings.json file
|
||||
*
|
||||
* Generate the server-settings.json file from the example file in the
|
||||
* data directory and override any settings configured in the instance's
|
||||
* factorio_settings config entry.
|
||||
*/
|
||||
async writeServerSettings() {
|
||||
let serverSettings = await this.server.exampleSettings();
|
||||
let overrides = this.config.get('factorio.settings');
|
||||
|
||||
for (let [key, value] of Object.entries(overrides)) {
|
||||
if (!Object.hasOwnProperty.call(serverSettings, key)) {
|
||||
console.log(`Warning: Server settings does not have the property '${key}'`);
|
||||
}
|
||||
serverSettings[key] = value;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
this.server.writePath("server-settings.json"),
|
||||
JSON.stringify(serverSettings, null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new empty instance directory
|
||||
*
|
||||
@@ -89,56 +106,17 @@ class Instance extends link.Link{
|
||||
* @param {String} factorioDir - Path to factorio installation.
|
||||
* @param {Object} options - Options for new instance.
|
||||
*/
|
||||
static async create(id, instanceDir, factorioDir, options) {
|
||||
let instanceConfig = {
|
||||
id,
|
||||
name: options.name,
|
||||
factorioPort: null,
|
||||
clientPort: null,
|
||||
clientPassword: null,
|
||||
}
|
||||
|
||||
static async create(instanceConfig, instanceDir, factorioDir) {
|
||||
console.log(`Clusterio | Creating ${instanceDir}`);
|
||||
await fs.ensureDir(instanceDir);
|
||||
await fs.ensureDir(path.join(instanceDir, "script-output"));
|
||||
await fs.ensureDir(path.join(instanceDir, "saves"));
|
||||
|
||||
// save instance config
|
||||
await fs.outputFile(path.join(instanceDir, "config.json"), JSON.stringify(instanceConfig, null, 4));
|
||||
|
||||
let serverSettings = await factorio.FactorioServer.exampleSettings(path.join(factorioDir, "data"));
|
||||
let gameName = "Clusterio instance: " + options.name;
|
||||
if (options.username) {
|
||||
gameName = options.username + "'s clusterio " + options.name;
|
||||
}
|
||||
|
||||
let overrides = {
|
||||
"name": gameName,
|
||||
"description": options.description,
|
||||
"tags": ["clusterio"],
|
||||
"visibility": options.visibility,
|
||||
"username": options.username,
|
||||
"token": options.token,
|
||||
"game_password": options.game_password,
|
||||
"require_user_verification": options.verify_user_identity,
|
||||
"allow_commands": options.allow_commands,
|
||||
"auto_pause": options.auto_pause,
|
||||
};
|
||||
|
||||
for (let [key, value] of Object.entries(overrides)) {
|
||||
if (!Object.hasOwnProperty.call(serverSettings, key)) {
|
||||
throw Error(`Expected server settings to have a ${key} property`);
|
||||
}
|
||||
serverSettings[key] = value;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(instanceDir, "server-settings.json"),
|
||||
JSON.stringify(serverSettings, null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
async start(saveName, slaveConfig, socket) {
|
||||
console.log("Clusterio | Writing server-settings.json");
|
||||
await this.writeServerSettings();
|
||||
|
||||
console.log("Clusterio | Rotating old logs...");
|
||||
// clean old log file to avoid crash
|
||||
try{
|
||||
@@ -231,8 +209,9 @@ class Instance extends link.Link{
|
||||
* Update instance information on the Factorio side
|
||||
*/
|
||||
async updateInstanceData() {
|
||||
let name = luaTools.escapeString(this.config.name);
|
||||
await this.server.sendRcon(`/sc clusterio_private.update_instance(${this.config.id}, "${name}")`, true);
|
||||
let name = luaTools.escapeString(this.config.get("instance.name"));
|
||||
let id = this.config.get("instance.id");
|
||||
await this.server.sendRcon(`/sc clusterio_private.update_instance(${id}, "${name}")`, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,7 +233,7 @@ class Instance extends link.Link{
|
||||
collector instanceof prometheus.ValueCollector
|
||||
&& collector.metric.labels.includes('instance_id')
|
||||
) {
|
||||
collector.removeAll({ instance_id: String(this.config.id) });
|
||||
collector.removeAll({ instance_id: String(this.config.get("instance.id")) });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,6 +267,9 @@ class Instance extends link.Link{
|
||||
}
|
||||
|
||||
async createSaveRequestHandler() {
|
||||
console.log("Clusterio | Writing server-settings.json");
|
||||
await this.writeServerSettings();
|
||||
|
||||
console.log("Creating save .....");
|
||||
|
||||
await this.server.create("world");
|
||||
@@ -309,7 +291,7 @@ class Instance extends link.Link{
|
||||
* This should not be used for filesystem paths. See .path() for that.
|
||||
*/
|
||||
get name() {
|
||||
return this.config.name;
|
||||
return this.config.get("instance.name");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -330,7 +312,7 @@ class Instance extends link.Link{
|
||||
*
|
||||
* Looks through all sub-dirs of the provided directory for valid
|
||||
* instance definitions and returns a mapping of instance id to
|
||||
* instance info objects.
|
||||
* instance config objects.
|
||||
*
|
||||
* @returns {Map<integer, Object>} mapping between instance
|
||||
*/
|
||||
@@ -338,10 +320,12 @@ async function discoverInstances(instancesDir, logger) {
|
||||
let instanceInfos = new Map();
|
||||
for (let entry of await fs.readdir(instancesDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
let instanceConfig;
|
||||
let configPath = path.join(instancesDir, entry.name, "config.json");
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
let configPath = path.join(instancesDir, entry.name, "instance.json");
|
||||
|
||||
try {
|
||||
instanceConfig = JSON.parse(await fs.readFile(configPath));
|
||||
await instanceConfig.load(JSON.parse(await fs.readFile(configPath)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
continue; // Ignore folders without config.json
|
||||
@@ -351,20 +335,9 @@ async function discoverInstances(instancesDir, logger) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// XXX should probably validate the entire config with a JSON Schema.
|
||||
if (typeof instanceConfig.id !== "number" || isNaN(instanceConfig.id)) {
|
||||
logger.error(`${configPath} is missing id`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof instanceConfig.name !== "string") {
|
||||
logger.error(`${configPath} is missing name`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let instancePath = path.join(instancesDir, entry.name);
|
||||
logger.log(`found instance ${instanceConfig.name} in ${instancePath}`);
|
||||
instanceInfos.set(instanceConfig.id, {
|
||||
logger.log(`found instance ${instanceConfig.get("instance.name")} in ${instancePath}`);
|
||||
instanceInfos.set(instanceConfig.get("instance.id"), {
|
||||
path: instancePath,
|
||||
config: instanceConfig,
|
||||
});
|
||||
@@ -387,15 +360,13 @@ class InstanceConnection extends link.Link {
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instanceId = message.data.instance_id;
|
||||
if (!this.slave.instanceInfos.has(instanceId)) {
|
||||
let instanceConnection = this.slave.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
// Instance is probably on another slave
|
||||
await this.slave.forwardEventToMaster(message, event);
|
||||
return;
|
||||
}
|
||||
|
||||
let instanceConnection = this.slave.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) { return; }
|
||||
|
||||
event.send(instanceConnection, message.data);
|
||||
}
|
||||
|
||||
@@ -417,10 +388,10 @@ class InstanceConnection extends link.Link {
|
||||
|
||||
class SlaveConnector extends link.SocketIOClientConnector {
|
||||
constructor(slaveConfig) {
|
||||
super(slaveConfig.masterURL, slaveConfig.masterAuthToken);
|
||||
super(slaveConfig.get("slave.master_url"), slaveConfig.get("slave.master_token"));
|
||||
|
||||
this.id = slaveConfig.id;
|
||||
this.name = slaveConfig.name;
|
||||
this.id = slaveConfig.get("slave.id");
|
||||
this.name = slaveConfig.get("slave.name");
|
||||
}
|
||||
|
||||
register() {
|
||||
@@ -452,15 +423,7 @@ class Slave extends link.Link {
|
||||
plugin.attachPluginMessages(this, pluginInfo, null);
|
||||
}
|
||||
|
||||
this.config = {
|
||||
id: slaveConfig.id,
|
||||
name: slaveConfig.name,
|
||||
instancesDir: slaveConfig.instanceDirectory,
|
||||
factorioDir: slaveConfig.factorioDirectory,
|
||||
masterUrl: slaveConfig.masterURL,
|
||||
masterToken: slaveConfig.masterAuthToken,
|
||||
publicAddress: slaveConfig.publicIP,
|
||||
}
|
||||
this.config = slaveConfig;
|
||||
|
||||
this.instanceConnections = new Map();
|
||||
this.instanceInfos = new Map();
|
||||
@@ -474,7 +437,7 @@ class Slave extends link.Link {
|
||||
}
|
||||
|
||||
// For now add dashes until an unused directory name is found
|
||||
let dir = path.join(this.config.instancesDir, name);
|
||||
let dir = path.join(this.config.get("slave.instances_directory"), name);
|
||||
while (await fs.pathExists(dir)) {
|
||||
dir += '-';
|
||||
}
|
||||
@@ -490,7 +453,7 @@ class Slave extends link.Link {
|
||||
|
||||
let instanceConnection = this.instanceConnections.get(instanceId);
|
||||
if (!instanceConnection) {
|
||||
throw new errors.RequestError(`Instance ID ${instanceId} is not active`);
|
||||
throw new errors.RequestError(`Instance ID ${instanceId} is not running`);
|
||||
}
|
||||
|
||||
return await request.send(instanceConnection, message.data);
|
||||
@@ -512,17 +475,40 @@ class Slave extends link.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async createInstanceRequestHandler(message) {
|
||||
let { id, options } = message.data;
|
||||
if (this.instanceInfos.has(id)) {
|
||||
throw new Error(`Instance with ID ${id} already exists`);
|
||||
async assignInstanceRequestHandler(message) {
|
||||
let { instance_id, serialized_config } = message.data;
|
||||
let instanceInfo = this.instanceInfos.get(instance_id);
|
||||
if (instanceInfo) {
|
||||
instanceInfo.config.update(serialized_config);
|
||||
console.log(`Clusterio | Updated config for ${instanceInfo.path}`);
|
||||
// TODO: Notify of update if instance is running
|
||||
|
||||
} else {
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.load(serialized_config);
|
||||
|
||||
// XXX: race condition on multiple simultanious calls
|
||||
let instanceDir = await this._findNewInstanceDir(instanceConfig.get("instance.name"));
|
||||
|
||||
await Instance.create(instanceConfig, instanceDir, this.config.get("slave.factorio_directory"));
|
||||
instanceInfo = {
|
||||
path: instanceDir,
|
||||
config: instanceConfig,
|
||||
};
|
||||
this.instanceInfos.set(instance_id, instanceInfo);
|
||||
console.log(`Clusterio | assigned instance ${instanceConfig.get("instance.name")}`);
|
||||
}
|
||||
|
||||
// XXX: race condition on multiple simultanious calls
|
||||
let instanceDir = await this._findNewInstanceDir(options.name);
|
||||
|
||||
await Instance.create(id, instanceDir, this.config.factorioDir, options);
|
||||
await this.updateInstances();
|
||||
// save a copy of the instance config
|
||||
let warnedOutput = Object.assign(
|
||||
{ _warning: "Changes to this file will be overwritten by the master server's copy." },
|
||||
instanceInfo.config.serialize()
|
||||
);
|
||||
await fs.outputFile(
|
||||
path.join(instanceInfo.path, "instance.json"),
|
||||
JSON.stringify(warnedOutput, null, 4)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -535,13 +521,13 @@ class Slave extends link.Link {
|
||||
}
|
||||
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new errors.RequestError(`Instance with ID ${instanceId} is active`);
|
||||
throw new errors.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
|
||||
let [connectionClient, connectionServer] = link.VirtualConnector.makePair();
|
||||
let instanceConnection = new InstanceConnection(connectionServer, this);
|
||||
let instance = new Instance(
|
||||
connectionClient, instanceInfo.path, this.config.factorioDir, instanceInfo.config
|
||||
connectionClient, instanceInfo.path, this.config.get("slave.factorio_directory"), instanceInfo.config
|
||||
);
|
||||
await instance.init(this.pluginInfos);
|
||||
|
||||
@@ -564,7 +550,7 @@ class Slave extends link.Link {
|
||||
for await (let result of prometheus.defaultRegistry.collect()) {
|
||||
if (result.metric.name.startsWith('process_')) {
|
||||
results.push(prometheus.serializeResult(result, {
|
||||
addLabels: { 'slave_id': String(this.config.id) },
|
||||
addLabels: { 'slave_id': String(this.config.get("slave.id")) },
|
||||
metricName: result.metric.name.replace('process_', 'clusterio_slave_'),
|
||||
}));
|
||||
|
||||
@@ -596,7 +582,7 @@ class Slave extends link.Link {
|
||||
async deleteInstanceRequestHandler(message) {
|
||||
let instanceId = message.data.instance_id;
|
||||
if (this.instanceConnections.has(instanceId)) {
|
||||
throw new errors.RequestError(`Instance with ID ${instanceId} is active`);
|
||||
throw new errors.RequestError(`Instance with ID ${instanceId} is running`);
|
||||
}
|
||||
|
||||
let instanceInfo = this.instanceInfos.get(instanceId);
|
||||
@@ -605,7 +591,7 @@ class Slave extends link.Link {
|
||||
}
|
||||
|
||||
await fs.remove(instanceInfo.path);
|
||||
await this.updateInstances();
|
||||
this.instanceInfos.delete(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -615,12 +601,11 @@ class Slave extends link.Link {
|
||||
* the slave and master server with the new list of instances.
|
||||
*/
|
||||
async updateInstances() {
|
||||
this.instanceInfos = await discoverInstances(this.config.instancesDir, console);
|
||||
this.instanceInfos = await discoverInstances(this.config.get("slave.instances_directory"), console);
|
||||
let list = [];
|
||||
for (let instanceInfo of this.instanceInfos.values()) {
|
||||
list.push({
|
||||
id: instanceInfo.config.id,
|
||||
name: instanceInfo.config.name,
|
||||
serialized_config: instanceInfo.config.serialize(),
|
||||
});
|
||||
}
|
||||
link.messages.updateInstances.send(this, { instances: list });
|
||||
@@ -755,80 +740,42 @@ async function startClient() {
|
||||
default: 'config-slave.json',
|
||||
type: 'string',
|
||||
})
|
||||
.command('create-config', "Create slave config", (yargs) => {
|
||||
yargs.options({
|
||||
'name': { describe: "Name of the slave", nargs: 1, type: 'string', demandOption: true },
|
||||
'url': { describe: "Master URL", nargs: 1, type: 'string', default: "http://localhost:8080/" },
|
||||
'token': { describe: "Master token", nargs: 1, type: 'string', demandOption: true },
|
||||
'ip': { describe: "Public facing IP", nargs: 1, type: 'string', default: "localhost" },
|
||||
'instances-dir': { describe: "Instances directory", nargs: 1, type: 'string', default: "instances" },
|
||||
'factorio-dir': { describe: "Factorio directory", nargs: 1, type: 'string', default: "factorio" },
|
||||
'id': {
|
||||
describe: "Numeric id of the slave",
|
||||
nargs: 1,
|
||||
type: 'number',
|
||||
default: Math.random() * 2**31 | 0,
|
||||
defaultDescription: "random id",
|
||||
},
|
||||
});
|
||||
})
|
||||
.command('edit-config', "Edit slave config", (yargs) => {
|
||||
yargs.options({
|
||||
'name': { describe: "Set name of the slave", nargs: 1, type: 'string' },
|
||||
'url': { describe: "Set master URL", nargs: 1, type: 'string' },
|
||||
'token': { describe: "Set master token", nargs: 1, type: 'string' },
|
||||
'ip': { describe: "Set public facing IP", nargs: 1, type: 'string' },
|
||||
'instances-dir': { describe: "Set instances directory", nargs: 1, type: 'string' },
|
||||
'factorio-dir': { describe: "Set Factorio directory", nargs: 1, type: 'string' },
|
||||
'id': { describe: "Set id of the slave", nargs: 1, type: 'number' },
|
||||
});
|
||||
})
|
||||
.command('show-config', "Show slave config")
|
||||
.command("config", "Manage Slave config", config.configCommand)
|
||||
.command('start', "Start slave")
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
.argv
|
||||
;
|
||||
|
||||
console.log("Loading Plugin info");
|
||||
let pluginInfos = await plugin.loadPluginInfos("plugins");
|
||||
config.registerPluginConfigGroups(pluginInfos);
|
||||
config.finalizeConfigs();
|
||||
|
||||
console.log(`Loading config from ${args.config}`);
|
||||
let slaveConfig = new config.SlaveConfig();
|
||||
try {
|
||||
await slaveConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log("Config not found, initializing new config");
|
||||
await slaveConfig.init();
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let command = args._[0];
|
||||
|
||||
if (command === "create-config") {
|
||||
await fs.outputFile(args.config, JSON.stringify({
|
||||
name: args.name,
|
||||
masterURL: args.url,
|
||||
masterAuthToken: args.token,
|
||||
publicIP: args.ip,
|
||||
instanceDirectory: args.instancesDir,
|
||||
factorioDirectory: args.factorioDir,
|
||||
id: args.id,
|
||||
}, null, 4), { flag: 'wx' });
|
||||
return;
|
||||
|
||||
} else if (command == "edit-config") {
|
||||
let slaveConfig = JSON.parse(await fs.readFile(args.config));
|
||||
if ('name' in args) slaveConfig.name = args.name;
|
||||
if ('url' in args) slaveConfig.masterURL = args.url;
|
||||
if ('token' in args) slaveConfig.masterAuthToken = args.token;
|
||||
if ('ip' in args) slaveConfig.publicIP = args.ip;
|
||||
if ('instancesDir' in args) slaveConfig.instanceDirectory = args.instancesDir;
|
||||
if ('factorioDir' in args) slaveConfig.factorioDirectory = args.factorioDir;
|
||||
if ('id' in args) slaveConfig.id = args.id;
|
||||
await fs.outputFile(args.config, JSON.stringify(slaveConfig, null, 4));
|
||||
return;
|
||||
|
||||
} else if (command == "show-config") {
|
||||
let slaveConfig = JSON.parse(await fs.readFile(args.config));
|
||||
console.log(slaveConfig);
|
||||
if (command === "config") {
|
||||
await config.handleConfigCommand(args, slaveConfig, args.config);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here the command was start
|
||||
|
||||
// handle commandline parameters
|
||||
console.log(`Loading ${args.config}`);
|
||||
const slaveConfig = JSON.parse(await fs.readFile(args.config));
|
||||
|
||||
await fs.ensureDir(slaveConfig.instanceDirectory);
|
||||
await fs.ensureDir(slaveConfig.get("slave.instances_directory"));
|
||||
await fs.ensureDir("sharedMods");
|
||||
await fs.ensureDir("modules");
|
||||
|
||||
@@ -837,27 +784,26 @@ async function startClient() {
|
||||
process.title = "clusterioClient";
|
||||
|
||||
// make sure we have the master access token
|
||||
if(!slaveConfig.masterAuthToken || typeof slaveConfig.masterAuthToken !== "string"){
|
||||
if (slaveConfig.get("slave.master_token") === "enter token here") {
|
||||
console.error("ERROR invalid config!");
|
||||
console.error(
|
||||
"Master server now needs an access token for write operations. As clusterio\n"+
|
||||
"slaves depends upon this, please add your token to config.json in the field\n"+
|
||||
"named masterAuthToken. You can retrieve your auth token from the master in\n"+
|
||||
"secret-api-token.txt after running it once."
|
||||
"slaves depends upon this, please set your token using the command node client\n"+
|
||||
"config set slave.master_token <token>. You can retrieve your auth token from\n"+
|
||||
"the master in secret-api-token.txt after running it once."
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// make sure url ends with /
|
||||
if (!slaveConfig.masterURL.endsWith("/")) {
|
||||
if (!slaveConfig.get("slave.master_url").endsWith("/")) {
|
||||
console.error("ERROR invalid config!");
|
||||
console.error("masterURL (set with --url) must end with '/'");
|
||||
console.error("slave.master_url must end with '/'");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
let pluginInfos = await plugin.getPluginInfos("plugins");
|
||||
let slaveConnector = new SlaveConnector(slaveConfig);
|
||||
let slave = new Slave(slaveConnector, slaveConfig, pluginInfos);
|
||||
|
||||
|
||||
+177
-75
@@ -8,6 +8,7 @@ const chalk = require("chalk");
|
||||
|
||||
const link = require("lib/link");
|
||||
const errors = require("lib/errors");
|
||||
const config = require("lib/config");
|
||||
|
||||
|
||||
/**
|
||||
@@ -45,7 +46,7 @@ function formatOutputColored(output) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Represens a command that can be runned by clusterctl
|
||||
* Represents a command that can be runned by clusterctl
|
||||
*/
|
||||
class Command {
|
||||
constructor({ definition, handler }) {
|
||||
@@ -69,16 +70,16 @@ class Command {
|
||||
* Resolves a string with either an instance name or an id into an integer
|
||||
* with the instance ID.
|
||||
*
|
||||
* @param link - link to master server to query instance on.
|
||||
* @param instance - string with name or id of instance.
|
||||
* @param client - link to master server to query instance on.
|
||||
* @param instanceName - string with name or id of instance.
|
||||
* @returns {number} instance ID.
|
||||
*/
|
||||
async function resolveInstance(control, instanceName) {
|
||||
async function resolveInstance(client, instanceName) {
|
||||
let instanceId;
|
||||
if (/^-?\d+$/.test(instanceName)) {
|
||||
instanceId = parseInt(instanceName, 10);
|
||||
} else {
|
||||
let response = await link.messages.listInstances.send(control);
|
||||
let response = await link.messages.listInstances.send(client);
|
||||
for (let instance of response.list) {
|
||||
if (instance.name === instanceName) {
|
||||
instanceId = instance.id;
|
||||
@@ -94,6 +95,37 @@ async function resolveInstance(control, instanceName) {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a string into a slave ID
|
||||
*
|
||||
* Resolves a string with either an slave name or an id into an integer with
|
||||
* the slave ID.
|
||||
*
|
||||
* @param client - link to master server to query slave on.
|
||||
* @param slaveName - string with name or id of slave.
|
||||
* @returns {number} slave ID.
|
||||
*/
|
||||
async function resolveSlave(client, slaveName) {
|
||||
let slaveId;
|
||||
if (/^-?\d+$/.test(slaveName)) {
|
||||
slaveId = parseInt(slaveName, 10);
|
||||
} else {
|
||||
let response = await link.messages.listSlaves.send(client);
|
||||
for (let slave of response.list) {
|
||||
if (slave.name === slaveName) {
|
||||
slaveId = slave.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slaveId === undefined) {
|
||||
throw new errors.CommandError(`No slave named ${slaveName}`);
|
||||
}
|
||||
}
|
||||
|
||||
return slaveId;
|
||||
}
|
||||
|
||||
let commands = [];
|
||||
commands.push(new Command({
|
||||
definition: ["list-slaves", "List slaves connected to the master"],
|
||||
@@ -113,30 +145,88 @@ commands.push(new Command({
|
||||
|
||||
commands.push(new Command({
|
||||
definition: ['create-instance', "Create an instance", (yargs) => {
|
||||
yargs.options({
|
||||
'name': { describe: "Name of the instance", nargs: 1, type: 'string', demandOption: true },
|
||||
'slave': { describe: "Slave to create on", nargs: 1, type: 'string', demandOption: true },
|
||||
// XXX TODO: set any specific options?
|
||||
yargs.option('name', {
|
||||
type: "string",
|
||||
nargs: 1,
|
||||
describe: "Instance name",
|
||||
group: "Instance Config:",
|
||||
});
|
||||
yargs.option('id', {
|
||||
type: "number",
|
||||
nargs: 1,
|
||||
describe: "Instance ID",
|
||||
group: "Instance Config:",
|
||||
});
|
||||
yargs.option('base', {
|
||||
type: "boolean",
|
||||
nargs: 0,
|
||||
describe: "Specify this is a base config",
|
||||
group: "Instance Config:"
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let slaveId;
|
||||
if (/^-?\d+$/.test(args.slave)) {
|
||||
slaveId = parseInt(args.slave, 10);
|
||||
} else {
|
||||
let response = await link.messages.listSlaves.send(control);
|
||||
for (let slave of response.list) {
|
||||
if (slave.name === args.slave) {
|
||||
slaveId = slave.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.init();
|
||||
if (args.id) {
|
||||
instanceConfig.set("instance.id", args.id);
|
||||
}
|
||||
if (args.name) {
|
||||
instanceConfig.set("instance.name", args.name);
|
||||
}
|
||||
let serialized_config = instanceConfig.serialize();
|
||||
let response = await link.messages.createInstance.send(control, { serialized_config });
|
||||
},
|
||||
}));
|
||||
|
||||
if (slaveId === undefined) {
|
||||
throw new errors.CommandError(`No slave named ${args.slave}`);
|
||||
commands.push(new Command({
|
||||
definition: ["list-instance-config", "List configuration for an instance", (yargs) => {
|
||||
yargs.option({
|
||||
"instance": { describe: "Instance to list config for", nargs: 1, type: "string", demandOption: true },
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await resolveInstance(control, args.instance);
|
||||
let response = await link.messages.getInstanceConfig.send(control, { instance_id: instanceId });
|
||||
|
||||
for (let group of response.serialized_config.groups) {
|
||||
for (let [name, value] of Object.entries(group.fields)) {
|
||||
console.log(`${group.name}.${name} ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
let response = await link.messages.createInstanceCommand.send(control, {
|
||||
name: args.name,
|
||||
},
|
||||
}));
|
||||
|
||||
commands.push(new Command({
|
||||
definition: ["set-instance-config", "Set field in instance config", (yargs) => {
|
||||
yargs.option({
|
||||
"instance": { describe: "Instance to set config on", nargs: 1, type: "string", demandOption: true },
|
||||
"field": { describe: "Field to set", nargs: 1, type: "string", demandOption: true },
|
||||
"value": { describe: "Value to set", nargs: 1, type: "string", demandOption: true },
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await resolveInstance(control, args.instance);
|
||||
await link.messages.setInstanceConfigField.send(control, {
|
||||
instance_id: instanceId,
|
||||
field: args.field,
|
||||
value: args.value,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
commands.push(new Command({
|
||||
definition: ['assign-instance', "Assign instance to a slave", (yargs) => {
|
||||
yargs.options({
|
||||
'instance': { describe: "Instance to assign", nargs: 1, type: 'string', demandOption: true },
|
||||
'slave': { describe: "Slave to assign to", nargs: 1, type: 'string', demandOption: true },
|
||||
});
|
||||
}],
|
||||
handler: async function(args, control) {
|
||||
let instanceId = await resolveInstance(control, args.instance);
|
||||
let slaveId = await resolveSlave(control, args.slave);
|
||||
await link.messages.assignInstanceCommand.send(control, {
|
||||
instance_id: instanceId,
|
||||
slave_id: slaveId,
|
||||
});
|
||||
},
|
||||
@@ -256,20 +346,24 @@ class Control extends link.Link {
|
||||
}
|
||||
}
|
||||
|
||||
async function findCredentials() {
|
||||
async function findCredentials(controlConfig) {
|
||||
// Try looking in the default master config location
|
||||
try {
|
||||
let masterConfig = JSON.parse(await fs.readFile("config.json"));
|
||||
let token = jwt.sign({ id: "api" }, masterConfig.masterAuthSecret, { expiresIn: 600 });
|
||||
let masterConfig = new config.MasterConfig();
|
||||
await masterConfig.load(JSON.parse(await fs.readFile("config-master.json")));
|
||||
|
||||
let token = jwt.sign({ id: "api" }, masterConfig.get("master.auth_secret"), { expiresIn: 600 });
|
||||
controlConfig.set("control.master_token", token);
|
||||
|
||||
let url;
|
||||
if (masterConfig.sslPort) {
|
||||
url = `https://localhost:${masterConfig.sslPort}`;
|
||||
if (masterConfig.get("master.https_port")) {
|
||||
url = `https://localhost:${masterConfig.get("master.https_port")}`;
|
||||
} else {
|
||||
url = `http://localhost:${masterConfig.masterPort}`;
|
||||
url = `http://localhost:${masterConfig.get("master.http_port")}`;
|
||||
}
|
||||
controlConfig.set("control.master_url", url);
|
||||
|
||||
return { url, token };
|
||||
return;
|
||||
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
@@ -282,11 +376,12 @@ async function findCredentials() {
|
||||
// authorizations, meaning that you cannot use a slave token to control
|
||||
// the cluster.
|
||||
try {
|
||||
let slaveConfig = JSON.parse(await fs.readFile("config-slave.json"));
|
||||
return {
|
||||
url: slaveConfig.masterURL,
|
||||
token: slaveConfig.masterAuthToken,
|
||||
}
|
||||
let slaveConfig = new config.SlaveConfig();
|
||||
await slaveConfig.load(JSON.parse(await fs.readFile("config-slave.json")));
|
||||
|
||||
controlConfig.set("control.master_token", slaveConfig.get("slave.master_token"));
|
||||
controlConfig.set("control.master_url", slaveConfig.get("slave.master_url"));
|
||||
return;
|
||||
|
||||
} catch (err) {
|
||||
if (err.code !== "ENOENT") {
|
||||
@@ -298,6 +393,27 @@ async function findCredentials() {
|
||||
}
|
||||
|
||||
|
||||
class ControlGroup extends config.ConfigGroup {}
|
||||
ControlGroup.groupName = "control";
|
||||
ControlGroup.define({
|
||||
name: "master_url",
|
||||
description: "URL to connect to the master server at",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.define({
|
||||
name: "master_token",
|
||||
description: "Token to authenticate to master server with.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
ControlGroup.finalize();
|
||||
|
||||
class ControlConfig extends config.Config { }
|
||||
ControlConfig.registerGroup(ControlGroup);
|
||||
ControlConfig.finalize();
|
||||
|
||||
|
||||
async function startControl() {
|
||||
yargs
|
||||
.scriptName("clusterctl")
|
||||
@@ -309,19 +425,8 @@ async function startControl() {
|
||||
defaultDescription: "auto",
|
||||
type: 'string',
|
||||
})
|
||||
.command('create-config', "Create control config", (yargs) => {
|
||||
yargs.options({
|
||||
'url': { describe: "Master URL", nargs: 1, type: 'string', default: "http://localhost:8080/" },
|
||||
'token': { describe: "Master token", nargs: 1, type: 'string', demandOption: true },
|
||||
});
|
||||
})
|
||||
.command('edit-config', "Edit control config", (yargs) => {
|
||||
yargs.options({
|
||||
'url': { describe: "Set master URL", nargs: 1, type: 'string' },
|
||||
'token': { describe: "Set master token", nargs: 1, type: 'string' },
|
||||
});
|
||||
})
|
||||
.command('show-config', "Show control config")
|
||||
.command("control-config", "Manage Control config", config.configCommand)
|
||||
.wrap(yargs.terminalWidth())
|
||||
;
|
||||
|
||||
for (let command of commands.values()) {
|
||||
@@ -333,40 +438,37 @@ async function startControl() {
|
||||
.argv
|
||||
;
|
||||
|
||||
let commandName = args._[0];
|
||||
if (commandName === "create-config") {
|
||||
await fs.outputFile(args.config, JSON.stringify({
|
||||
url: args.url,
|
||||
token: args.token,
|
||||
}, null, 4), { flag: 'wx' });
|
||||
return;
|
||||
config.finalizeConfigs();
|
||||
|
||||
} else if (commandName === "edit-config") {
|
||||
let controlConfig = JSON.parse(await fs.readFile(args.config));
|
||||
if ('url' in args) controlConfig.url = args.url;
|
||||
if ('token' in args) controlConfig.token = args.token;
|
||||
await fs.outputFile(args.config, JSON.stringify(controlConfig, null, 4));
|
||||
return;
|
||||
|
||||
} else if (commandName === "show-config") {
|
||||
let controlConfig = JSON.parse(await fs.readFile(args.config));
|
||||
console.log(controlConfig);
|
||||
return;
|
||||
}
|
||||
|
||||
// The remaining commands require connecting to the master server.
|
||||
let controlConfig;
|
||||
console.log(`Loading config from ${args.config}`);
|
||||
let controlConfig = new ControlConfig();
|
||||
try {
|
||||
controlConfig = JSON.parse(await fs.readFile(args.config));
|
||||
await controlConfig.load(JSON.parse(await fs.readFile(args.config)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === "ENOENT") {
|
||||
controlConfig = await findCredentials();
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log("Config not found, initializing new config");
|
||||
await controlConfig.init();
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let controlConnector = new ControlConnector(controlConfig.url, controlConfig.token);
|
||||
let commandName = args._[0];
|
||||
if (commandName === "control-config") {
|
||||
await config.handleConfigCommand(args, controlConfig, args.config);
|
||||
return;
|
||||
}
|
||||
|
||||
// The remaining commands require connecting to the master server.
|
||||
if (!controlConfig.get("control.master_url") || !controlConfig.get("control.master_token")) {
|
||||
await findCredentials(controlConfig);
|
||||
}
|
||||
|
||||
let controlConnector = new ControlConnector(
|
||||
controlConfig.get("control.master_url"), controlConfig.get("control.master_token")
|
||||
);
|
||||
let control = new Control(controlConnector);
|
||||
await controlConnector.connect();
|
||||
|
||||
@@ -386,7 +488,7 @@ async function startControl() {
|
||||
process.exitCode = 1;
|
||||
|
||||
} else {
|
||||
control.close("error");
|
||||
controlConnector.close("error");
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
Configuration System
|
||||
====================
|
||||
|
||||
<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 master server, instances, slaves, and the clusterctl utility.
|
||||
Plugins get their own config group for the master server 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.
|
||||
|
||||
The built-in config groups and their fields are defined in
|
||||
[/lib/config/definitions.js](lib/config/definitions.js) while plugins
|
||||
can add their own config groups to the `MasterConfigGroup` 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.
|
||||
|
||||
|
||||
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:
|
||||
|
||||
**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.
|
||||
|
||||
**description** (optional):
|
||||
Text used to describe the config field in user interfaces.
|
||||
|
||||
**enum** (optional):
|
||||
Array of acceptable values this field can have. Attempts to set a
|
||||
value not in this array will be rejected, which makes it useful for
|
||||
making enumareted list of choices.
|
||||
|
||||
**optional** (optional):
|
||||
True if this field can be set to null. Defaults to false.
|
||||
|
||||
**initial_value** (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.
|
||||
|
||||
If the value for a field stored in on disk somehow ends up being invalid
|
||||
the field will take on the initial\_value instead when the config is
|
||||
loaded.
|
||||
|
||||
|
||||
Accessing Fields
|
||||
----------------
|
||||
|
||||
On the Config instance there's two ways to access fields: through the
|
||||
set and get methods using dot notation. E.g.:
|
||||
|
||||
let value = configInstance.get("group.field");
|
||||
|
||||
Or by getting the group first and then calling set or get on that.
|
||||
E.g.:
|
||||
|
||||
let value = configInstance.group("group").get("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.
|
||||
|
||||
|
||||
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. 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 master server and serialized
|
||||
and sent over to the relevant slaves on startup. This makes it possible
|
||||
to edit instance configs when the slave the instance is on is offline.
|
||||
A copy is also stored in the instance directory on the slave, and this
|
||||
config will be deliverd to the master server on slave startup, which
|
||||
enables instances to be copied between clusters as long as their IDs are
|
||||
unique.
|
||||
@@ -11,6 +11,7 @@ Contents
|
||||
|
||||
- [Plugin Structure](#plugin-structure)
|
||||
- [Defining the plugin class](#defining-the-plugin-class)
|
||||
- [Plugin Configuration](#plugin-configuration)
|
||||
- [Communicating with Factorio](#communicating-with-factorio)
|
||||
- [Defining Link Messages](#defining-link-messages)
|
||||
- [Defining Events](#defining-events)
|
||||
@@ -76,12 +77,22 @@ The following properties are recognized:
|
||||
for it to be able to send any messages to other instances it will
|
||||
still have to be loaded on the master server.
|
||||
|
||||
**InstanceConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` to defining the per instance
|
||||
configuration fields for this plugin. See [Plugin
|
||||
Configuration](#plugin-configuration)
|
||||
|
||||
**masterEntrypoint**:
|
||||
Path to a Node.js module relative to the plugin directory which
|
||||
contains the MasterPlugin class definiton for this plugin. This is
|
||||
an optional parameter. A plugin can be made that only runs on the
|
||||
master server.
|
||||
|
||||
**MasterConfigGroup**:
|
||||
Subclass of `PluginConfigGroup` to defining the master server
|
||||
configuration fields for this plugin. See [Plugin
|
||||
Configuration](#plugin-configuration)
|
||||
|
||||
**messages**:
|
||||
Object with link messages definitions for this plugin. See guide
|
||||
for [defining link messages](#defining-link-messages) below.
|
||||
@@ -142,6 +153,47 @@ result in unpredicatable behaviour. The async init method always called
|
||||
immediatly after the constructor so there's little reason to do this.
|
||||
|
||||
|
||||
Defining 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
|
||||
`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:
|
||||
|
||||
const config = require("lib/config");
|
||||
|
||||
class MasterConfigGroup extends config.PluginConfigGroup { }
|
||||
MasterConfigGroup.groupName = "foo_frobber";
|
||||
MasterConfigGroup.define({
|
||||
name: "level",
|
||||
description: "Level of frobnication done",
|
||||
type: "number",
|
||||
initial_value: 2,
|
||||
});
|
||||
MasterConfigGroup.finalize();
|
||||
|
||||
module.exports = {
|
||||
...
|
||||
MasterConfigGroup: MasterConfigGroup,
|
||||
};
|
||||
|
||||
Code inside the `MasterPlugin` class will then be able to access the
|
||||
level config field through the `Config` object at `this.master.config`,
|
||||
for example in the MasterPluginClass:
|
||||
|
||||
async init() {
|
||||
let level = this.master.config.get("foo_frobber.level");
|
||||
console.log("I got a frobnication level of", level);
|
||||
}
|
||||
|
||||
The same applies for instance configs, replace "master" with "instance"
|
||||
where appropriate. See [Configuration System](configuration-system.md)
|
||||
for more details on how this system works.
|
||||
|
||||
|
||||
Communicating with Factorio
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* @file Configuration classes
|
||||
* @author Hornwitser
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* Invalid Value exception
|
||||
*
|
||||
* Exception class for when invalid values are attempted to be set on a
|
||||
* field.
|
||||
*/
|
||||
class InvalidValue extends Error { };
|
||||
|
||||
/**
|
||||
* Invalid Field exception
|
||||
*
|
||||
* Exception class for when a group or field that does not exist is
|
||||
* attempted to be accessed.
|
||||
*/
|
||||
class InvalidField extends Error { };
|
||||
|
||||
/**
|
||||
* Split the given string on the first instance of separator
|
||||
*
|
||||
* Splits @p string on the first instance of @p 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.
|
||||
*
|
||||
* @returns {Array} string split on separator.
|
||||
*/
|
||||
function splitOn(separator, string) {
|
||||
let index = string.indexOf(separator);
|
||||
if (index === -1) {
|
||||
return [string, ''];
|
||||
}
|
||||
return [string.slice(0, index), string.slice(index + separator.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string describing the type of the value passed
|
||||
*
|
||||
* Works the same as typeof, excpet that null and array types get their
|
||||
* own string.
|
||||
*
|
||||
* @param value - value to return the type of.
|
||||
* @returns {string} basic type of the value passed.
|
||||
*/
|
||||
function basicType(value) {
|
||||
if (value === null) { return "null"; }
|
||||
if (value instanceof Array) { return "array"; }
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collection of config groups
|
||||
*
|
||||
* Represents a collection of ConfigGroup instances that can be
|
||||
* managed as one.
|
||||
*/
|
||||
class Config {
|
||||
constructor() {
|
||||
if (!this.constructor._finalized) {
|
||||
throw new Error(`Cannot instantiate incomplete Config class ${this.constructor.name}`)
|
||||
}
|
||||
|
||||
this._groups = new Map();
|
||||
this._unknownGroups = new Map();
|
||||
}
|
||||
|
||||
async init() {
|
||||
for (let [name, GroupClass] of this.constructor._groups) {
|
||||
if (!this._groups.has(name)) {
|
||||
let group = new GroupClass();
|
||||
await group.init();
|
||||
this._groups.set(name, group);
|
||||
}
|
||||
}
|
||||
this._initialized = true;
|
||||
}
|
||||
|
||||
_validate(serializedConfig) {
|
||||
if (basicType(serializedConfig) !== "object") {
|
||||
throw new Error(`Expected object, not ${basicType(serializedConfig)} for config`);
|
||||
}
|
||||
|
||||
if (basicType(serializedConfig.groups) !== "array") {
|
||||
throw new Error(`Expected groups to be an array, not ${basicType(serializedConfig.groups)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async load(serializedConfig) {
|
||||
this._validate(serializedConfig);
|
||||
for (let serializedGroup of serializedConfig.groups) {
|
||||
let GroupClass = this.constructor._groups.get(serializedGroup.name);
|
||||
if (!GroupClass) {
|
||||
this._unknownGroups.set(serializedGroup.name, serializedGroup);
|
||||
continue;
|
||||
}
|
||||
|
||||
let group = new GroupClass();
|
||||
await group.load(serializedGroup);
|
||||
this._groups.set(GroupClass.groupName, group);
|
||||
}
|
||||
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the config to a plain JavaScript object
|
||||
*/
|
||||
serialize() {
|
||||
let groups = [...this._unknownGroups.values()];
|
||||
for (let group of this._groups.values()) {
|
||||
groups.push(group.serialize());
|
||||
}
|
||||
|
||||
return { groups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the configuration based on a serialized config
|
||||
*
|
||||
* Updates all groups in the config with groups in the serialized
|
||||
* config passed.
|
||||
*/
|
||||
update(serializedConfig) {
|
||||
this._validate(serializedConfig);
|
||||
for (let serializedGroup of serializedConfig.groups) {
|
||||
let group = this._groups.get(serializedGroup.name);
|
||||
if (group) {
|
||||
group.update(serializedGroup);
|
||||
} else {
|
||||
this._unknownGroups.set(serializedGroup.name, serializedGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for a config field
|
||||
*/
|
||||
get(name) {
|
||||
let [group, field] = splitOn('.', name);
|
||||
return this.group(group).get(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a config field
|
||||
*/
|
||||
set(name, value) {
|
||||
let [group, field] = splitOn('.', name);
|
||||
return this.group(group).set(field, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the config group instance with the given name
|
||||
*/
|
||||
group(name) {
|
||||
if (!this._initialized) {
|
||||
throw new Error(`${this.constructor.name} instance is uninitialized`)
|
||||
}
|
||||
let group = this._groups.get(name);
|
||||
if (!group) {
|
||||
throw new InvalidField(`No config group named '${name}'`);
|
||||
}
|
||||
return group;
|
||||
}
|
||||
|
||||
static _initSubclass() {
|
||||
// These properties are initialized here in order to ensure that
|
||||
// they end up on the sub-class.
|
||||
if (!this._initialized) {
|
||||
this._initialized = true;
|
||||
this._finalized = false;
|
||||
this._groups = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of group name to group class
|
||||
*
|
||||
* Returns the internal mapping of group names to group classes that
|
||||
* mave been registered with this config. Note that mutating this
|
||||
* object will lead to unexpected behaviour.
|
||||
*
|
||||
* @returns {Map<string, GroupClass>} config groups
|
||||
*/
|
||||
static get groups() {
|
||||
this._initSubclass();
|
||||
return this._groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks this Config class from adding more groups.
|
||||
*/
|
||||
static finalize() {
|
||||
this._initSubclass();
|
||||
this._finalized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a ConfigGroup with this Config
|
||||
*
|
||||
* Adds the ConfigGroup to this config as a group that will get
|
||||
* initialized, loaded and serialized with this config class.
|
||||
*/
|
||||
static registerGroup(group) {
|
||||
this._initSubclass();
|
||||
|
||||
if (this._finalized) {
|
||||
throw new Error("Cannot register group on finalized config");
|
||||
}
|
||||
|
||||
if (!group._finalized) {
|
||||
throw new Error("Group must be finalized before it can be registered");
|
||||
}
|
||||
|
||||
if (this._groups.has(group.groupName)) {
|
||||
throw new Error(`${group.groupName} has already been registered`);
|
||||
}
|
||||
|
||||
this._groups.set(group.groupName, group);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Collection of related config entries
|
||||
*/
|
||||
class ConfigGroup {
|
||||
/**
|
||||
* Creates a new config group.
|
||||
*
|
||||
* After the creation of the new config group you have to call
|
||||
* either .init() or .load() on it in order to fully initialize it.
|
||||
*/
|
||||
constructor(serializedGroup) {
|
||||
if (!this.constructor._finalized) {
|
||||
throw new Error(`Cannot instantiate incomplete ConfigGroup class ${this.constructor.name}`)
|
||||
}
|
||||
|
||||
this._fields = new Map();
|
||||
this._unknownFields = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize new config group
|
||||
*
|
||||
* Computes and assigns the initial values for all fields of the
|
||||
* config group.
|
||||
*/
|
||||
async init() {
|
||||
for (let [name, def] of this.constructor._definitions) {
|
||||
if (this._fields.has(name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let value = null;
|
||||
if (typeof def.initial_value === "function") {
|
||||
value = await def.initial_value()
|
||||
|
||||
} else if (def.initial_value !== undefined) {
|
||||
value = def.initial_value;
|
||||
}
|
||||
|
||||
this._fields.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from serialized group
|
||||
*
|
||||
* Loads all the values from the passed serialized version of the
|
||||
* group and then initializes any possible missing fields from it to
|
||||
* their default values.
|
||||
*/
|
||||
async load(serializedGroup) {
|
||||
this.update(serializedGroup);
|
||||
await this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value for field
|
||||
*/
|
||||
get(name) {
|
||||
if (!this.constructor._definitions.has(name)) {
|
||||
throw new InvalidField(`No field named '${name}'`);
|
||||
}
|
||||
|
||||
return this._fields.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value of field
|
||||
*
|
||||
* The field must be defined for the config group and the value is
|
||||
* type checked against the field definition.
|
||||
*
|
||||
* @throws {InvalidValue} if value is not allowed for the field.
|
||||
*/
|
||||
set(name, value) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
throw new Error(`No field named '${name}'`);
|
||||
}
|
||||
|
||||
// Empty strings are treates as null
|
||||
if (value === "") {
|
||||
value = null;
|
||||
}
|
||||
|
||||
if (def.enum && !def.enum.includes(value)) {
|
||||
throw new InvalidValue(`Expected one of [${def.enum.join(', ')}], not ${value}`);
|
||||
}
|
||||
|
||||
if (basicType(value) === "string") {
|
||||
if (def.type === "boolean") {
|
||||
if (value === "true") { value = true; }
|
||||
else if (value === "false") { value = false; }
|
||||
|
||||
} else if (def.type === "number") {
|
||||
let numberRegExp = /^[+-]?(Infinity|\d+\.(\d+)?([eE][+-]?\d+)?|\.?\d+([eE][+-]?\d+)?)$/;
|
||||
if (numberRegExp.test(value.trim())) {
|
||||
value = Number.parseFloat(value);
|
||||
}
|
||||
|
||||
} else if (def.type === "object" ) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (err) {
|
||||
throw new InvalidValue(`Error parsing value for ${name}: ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (value === null) {
|
||||
if (!def.optional) {
|
||||
throw new InvalidValue(`Field ${name} cannot be null`)
|
||||
}
|
||||
|
||||
} else {
|
||||
if (basicType(value) !== def.type) {
|
||||
throw new InvalidValue(`Expected type of ${name} to be ${def.type}, not ${basicType(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._fields.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a config group from a serialized group
|
||||
*
|
||||
* Overwrites the values of all the fields in this config group that
|
||||
* has a valid value in the passed serialized group with that value.
|
||||
*/
|
||||
update(serializedGroup) {
|
||||
if (basicType(serializedGroup) !== "object") {
|
||||
throw new Error(`Expected object, not ${basicType(serializedGroup)} for ConfigGroup`);
|
||||
}
|
||||
|
||||
if (serializedGroup.name !== this.constructor.groupName) {
|
||||
throw new Error(`Expected group name ${this.constructor.groupName}, not ${serializedGroup.name}`);
|
||||
}
|
||||
|
||||
if (basicType(serializedGroup.fields) !== "object") {
|
||||
throw new Error(`Expected fields to be an object, not ${basicType(serializedGroup.fields)}`);
|
||||
}
|
||||
|
||||
for (let [name, value] of Object.entries(serializedGroup.fields)) {
|
||||
let def = this.constructor._definitions.get(name);
|
||||
if (!def) {
|
||||
this._unknownFields.set(name, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (def.optional) {
|
||||
if (value !== null && basicType(value) !== def.type) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (basicType(value) !== def.type) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (def.enum && !def.enum.includes(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._fields.set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the config to a plain JavaScript object
|
||||
*/
|
||||
serialize() {
|
||||
let fields = {};
|
||||
for (let [name, value] of this._fields) {
|
||||
fields[name] = value;
|
||||
}
|
||||
|
||||
for (let [name, value] of this._unknownFields) {
|
||||
fields[name] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.constructor.groupName,
|
||||
fields,
|
||||
};
|
||||
}
|
||||
|
||||
static _initSubclass() {
|
||||
// These properties are initialized here in order to ensure that
|
||||
// they end up on the sub-class.
|
||||
if (!this._initialized) {
|
||||
this._initialized = true;
|
||||
this._definitions = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping of config name to field meta data
|
||||
*
|
||||
* Returns the internal meta data for the fields of this class.
|
||||
* Note that mutating this object will lead to unexpected behaviour.
|
||||
*
|
||||
* @returns {Map<string, Object>} field meta data
|
||||
*/
|
||||
static get definitions() {
|
||||
this._initSubclass();
|
||||
return this._definitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a new configuration field
|
||||
*
|
||||
* The following properties are supported:
|
||||
* - type:
|
||||
* string declaring the type of the config value, supports
|
||||
* boolean, string, number and object.
|
||||
* - name:
|
||||
* Name of the configuration field. Should follow the
|
||||
* lower_case_underscore style.
|
||||
* - title:
|
||||
* Text used to identify the config field in user interfaces.
|
||||
* - description:
|
||||
* Text used to describe the config field in user interfaces.
|
||||
* - enum (optional):
|
||||
* Array of all values that are acceptible. Useful for making
|
||||
* enumerated values.
|
||||
* - optional (optional):
|
||||
* True if this field can be set to null. Defaults to false.
|
||||
* - initial_value (optional):
|
||||
* Value this config field should take in a newly initialized
|
||||
* config. This can also be an async function returning the
|
||||
* value to use.
|
||||
*/
|
||||
static define(item) {
|
||||
if (this._finalized) {
|
||||
throw new Error(`Cannot define field for ConfigGroup class ${this.name} after it has been finalized`)
|
||||
}
|
||||
|
||||
if (basicType(this.groupName) !== "string") {
|
||||
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`)
|
||||
}
|
||||
|
||||
this._initSubclass();
|
||||
|
||||
let itemKeys = Object.keys(item);
|
||||
|
||||
// Check for required properties
|
||||
["name", "type"].forEach(prop => {
|
||||
if (!itemKeys.includes(prop)) {
|
||||
throw new Error(`${prop} is required when defining an field`);
|
||||
}
|
||||
})
|
||||
|
||||
// Validate properties
|
||||
let validTypes = ["boolean", "string", "number", "object"];
|
||||
Object.keys(item).forEach(key => {
|
||||
let value = item[key];
|
||||
let valueType = basicType(value);
|
||||
switch (key) {
|
||||
case "type":
|
||||
if (!validTypes.includes(value)) {
|
||||
throw new Error(`${value} is not a valid type`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "enum":
|
||||
if (valueType !== "array") {
|
||||
throw new Error("enum must be an array");
|
||||
}
|
||||
break;
|
||||
|
||||
case "name":
|
||||
if (valueType !== "string") {
|
||||
throw new Error("name must be a string");
|
||||
}
|
||||
break;
|
||||
|
||||
case "title":
|
||||
if (valueType !== "string") {
|
||||
throw new Error("title must be a string");
|
||||
}
|
||||
break;
|
||||
|
||||
case "description":
|
||||
if (valueType !== "string") {
|
||||
throw new Error("description must be a string");
|
||||
}
|
||||
break;
|
||||
|
||||
case "optional":
|
||||
if (valueType !== "boolean") {
|
||||
throw new Error("optional must be a boolean");
|
||||
}
|
||||
break;
|
||||
|
||||
case "initial_value":
|
||||
if (valueType !== item.type && valueType !== "function") {
|
||||
throw new Error("initial_value must match the type or be a function");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown property ${key}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (this._definitions.has(item.name)) {
|
||||
throw new Error(`Config field ${item.name} has already been defined`);
|
||||
}
|
||||
|
||||
let computed = Object.assign({
|
||||
optional: false,
|
||||
fullName: `${this.groupName}.${item.name}`,
|
||||
}, item)
|
||||
|
||||
if (!computed.optional && computed.initial_value === undefined) {
|
||||
throw new Error(`Non-optional field ${item.name} needs an initial_value`);
|
||||
}
|
||||
|
||||
this._definitions.set(item.name, computed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks this ConfigGroup class from adding more definitons.
|
||||
*/
|
||||
static finalize() {
|
||||
if (basicType(this.groupName) !== "string") {
|
||||
throw new Error(`Expected ConfigGroup class ${this.name} to have the groupName property set to a string`)
|
||||
}
|
||||
|
||||
this._initSubclass();
|
||||
this._finalized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Config group for plugins
|
||||
*
|
||||
* Grouping that includes per plugin fields clusterio uses to manage
|
||||
* plugins. This is currently only the enabled field. It is otherwise
|
||||
* identical to the ConfigGroup class.
|
||||
*/
|
||||
class PluginConfigGroup extends ConfigGroup {
|
||||
static _initSubclass() {
|
||||
if (!this._initialized) {
|
||||
super._initSubclass();
|
||||
this.define({
|
||||
name: "enabled",
|
||||
description: "Enable plugin",
|
||||
type: "boolean",
|
||||
initial_value: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
InvalidField,
|
||||
InvalidValue,
|
||||
|
||||
Config,
|
||||
ConfigGroup,
|
||||
PluginConfigGroup,
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* @file Core definitions for the configuration system
|
||||
* @author Hornwitser
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
const util = require("util");
|
||||
const crypto = require("crypto");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
|
||||
const classes = require("./classes");
|
||||
|
||||
|
||||
class MasterGroup extends classes.ConfigGroup { }
|
||||
MasterGroup.groupName = "master"
|
||||
MasterGroup.define({
|
||||
name: "database_directory",
|
||||
title: "Database directory",
|
||||
description: "Directory where item and configuration data is stored.",
|
||||
type: "string",
|
||||
initial_value: "database",
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "factorio_directory",
|
||||
description: "Path to directory to look for factorio installs",
|
||||
type: "string",
|
||||
initial_value: "factorio"
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "http_port",
|
||||
title: "HTTP Port",
|
||||
description: "Port to listen for HTTP connections on, set to null to not listen for HTTP connections.",
|
||||
type: "number",
|
||||
optional: true
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "https_port",
|
||||
title: "HTTPS Port",
|
||||
description: "Port to listen for HTTPS connection on, set to null to not listen for HTTPS connections.",
|
||||
type: "number",
|
||||
optional: true,
|
||||
initial_value: 8443,
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "tls_certificate",
|
||||
title: "TLS Certificate",
|
||||
description: "Path to the certificate to use for HTTPS.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
initial_value: "database/certificates/cert.crt",
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "tls_private_key",
|
||||
title: "TLS Private Key",
|
||||
description: "Path to the private key to use for HTTPS.",
|
||||
type: "string",
|
||||
optional: true,
|
||||
initial_value: "database/certificates/cert.key",
|
||||
});
|
||||
MasterGroup.define({
|
||||
name: "auth_secret",
|
||||
title: "Master 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.",
|
||||
type: "string",
|
||||
initial_value: async function() {
|
||||
console.log("Generating new master authentication secret");
|
||||
let asyncRandomBytes = util.promisify(crypto.randomBytes);
|
||||
let bytes = await asyncRandomBytes(256);
|
||||
return bytes.toString("base64");
|
||||
},
|
||||
});
|
||||
MasterGroup.finalize();
|
||||
|
||||
class MasterConfig extends classes.Config { }
|
||||
MasterConfig.registerGroup(MasterGroup);
|
||||
|
||||
|
||||
class SlaveGroup extends classes.ConfigGroup {}
|
||||
SlaveGroup.groupName = "slave";
|
||||
SlaveGroup.define({
|
||||
name: "name",
|
||||
description: "Name of the slave",
|
||||
type: "string",
|
||||
initial_value: "New Slave",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "id",
|
||||
description: "ID of the slave",
|
||||
type: "number",
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "factorio_directory",
|
||||
description: "Path to directory to look for factorio installs",
|
||||
type: "string",
|
||||
initial_value: "factorio"
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "instances_directory",
|
||||
description: "Path to directory to store instances in.",
|
||||
type: "string",
|
||||
initial_value: "instances",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "master_url",
|
||||
description: "URL to connect to the master server at",
|
||||
type: "string",
|
||||
initial_value: "https://localhost:8443/",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "master_token",
|
||||
description: "Token to authenticate to master server with.",
|
||||
type: "string",
|
||||
initial_value: "enter token here",
|
||||
});
|
||||
SlaveGroup.define({
|
||||
name: "public_address",
|
||||
description: "Public facing address players should connect to in order to join instances on this slave",
|
||||
type: "string",
|
||||
initial_value: "localhost",
|
||||
});
|
||||
SlaveGroup.finalize();
|
||||
|
||||
class SlaveConfig extends classes.Config { }
|
||||
SlaveConfig.registerGroup(SlaveGroup);
|
||||
|
||||
|
||||
class InstanceGroup extends classes.ConfigGroup { }
|
||||
InstanceGroup.groupName = "instance"
|
||||
InstanceGroup.define({
|
||||
name: "name",
|
||||
type: "string",
|
||||
initial_value: "New Instance",
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "id",
|
||||
description: "ID of the slave",
|
||||
type: "number",
|
||||
initial_value: () => Math.random() * 2**31 | 0,
|
||||
});
|
||||
InstanceGroup.define({
|
||||
name: "assigned_slave",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
InstanceGroup.finalize();
|
||||
|
||||
class FactorioGroup extends classes.ConfigGroup { }
|
||||
FactorioGroup.groupName = "factorio";
|
||||
FactorioGroup.define({
|
||||
name: "game_port",
|
||||
description: "UDP port to run game on, uses a random port if null",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "rcon_port",
|
||||
description: "TCP port to run RCON on, uses a random port if null",
|
||||
type: "number",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "rcon_password",
|
||||
description: "Password for RCON, randomly generated if null",
|
||||
type: "string",
|
||||
optional: true,
|
||||
});
|
||||
FactorioGroup.define({
|
||||
name: "settings",
|
||||
description: "Settings overridden in server-settings.json",
|
||||
type: "object",
|
||||
initial_value: {
|
||||
"tags": ["clusterio"],
|
||||
"auto_pause": false,
|
||||
},
|
||||
});
|
||||
FactorioGroup.finalize();
|
||||
|
||||
class InstanceConfig extends classes.Config { }
|
||||
InstanceConfig.registerGroup(InstanceGroup);
|
||||
InstanceConfig.registerGroup(FactorioGroup);
|
||||
|
||||
|
||||
function validateGroup(pluginInfo, groupName) {
|
||||
if (!pluginInfo[groupName] instanceof classes.PluginConfigGroup) {
|
||||
throw new Error(
|
||||
`Expected ${groupName} for ${pluginInfo.name} to be an instance of PluginConfigGroup`
|
||||
);
|
||||
}
|
||||
|
||||
if (pluginInfo[groupName].groupName !== pluginInfo.name) {
|
||||
throw new Error(
|
||||
`Expected ${groupName} for ${pluginInfo.name} to be named after the plugin`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the config groups for the provided plugin infos
|
||||
*/
|
||||
function registerPluginConfigGroups(pluginInfos) {
|
||||
for (let pluginInfo of pluginInfos) {
|
||||
if (pluginInfo.MasterConfigGroup) {
|
||||
validateGroup(pluginInfo, "MasterConfigGroup");
|
||||
MasterConfig.registerGroup(pluginInfo.MasterConfigGroup);
|
||||
|
||||
} else {
|
||||
class MasterConfigGroup extends classes.PluginConfigGroup { }
|
||||
MasterConfigGroup.groupName = pluginInfo.name;
|
||||
MasterConfigGroup.finalize();
|
||||
MasterConfig.registerGroup(MasterConfigGroup);
|
||||
}
|
||||
|
||||
if (pluginInfo.instanceEntrypoint) {
|
||||
if (pluginInfo.InstanceConfigGroup) {
|
||||
validateGroup(pluginInfo, "InstanceConfigGroup");
|
||||
InstanceConfig.registerGroup(pluginInfo.InstanceConfigGroup);
|
||||
|
||||
} else {
|
||||
class InstanceConfigGroup extends classes.PluginConfigGroup { }
|
||||
InstanceConfigGroup.groupName = pluginInfo.name;
|
||||
InstanceConfigGroup.finalize();
|
||||
InstanceConfig.registerGroup(InstanceConfigGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock configs from adding more groups and make them usable
|
||||
*/
|
||||
function finalizeConfigs() {
|
||||
MasterConfig.finalize();
|
||||
SlaveConfig.finalize();
|
||||
InstanceConfig.finalize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Yargs config command
|
||||
*
|
||||
* Can be passed to yargs.command to implement a config command. Use
|
||||
* handleConfigCommand to do the requested action.
|
||||
*/
|
||||
function configCommand(yargs) {
|
||||
yargs
|
||||
.command("set <field> [value]", "Set config field")
|
||||
.command("show <field>", "Show value of the given config field")
|
||||
.command("list", "List all configuration fields and their values")
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle yargs command
|
||||
*
|
||||
* Handle the actions that are made available by configCommand.
|
||||
*/
|
||||
async function handleConfigCommand(args, instance, configPath) {
|
||||
let command = args._[1];
|
||||
|
||||
if (command === "list") {
|
||||
for (let GroupClass of instance.constructor.groups.values()) {
|
||||
for (let def of GroupClass.definitions.values()) {
|
||||
let value = instance.get(def.fullName);
|
||||
console.log(`${def.fullName} ${JSON.stringify(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
} else if (command === "show") {
|
||||
try {
|
||||
console.log(instance.get(args.field));
|
||||
} catch (err) {
|
||||
if (err instanceof classes.InvalidField) {
|
||||
console.error(err.message);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (command === "set") {
|
||||
if (args.value === undefined) {
|
||||
args.value = null;
|
||||
}
|
||||
|
||||
try {
|
||||
instance.set(args.field, args.value);
|
||||
await fs.outputFile(configPath, JSON.stringify(instance.serialize(), null, 4));
|
||||
} catch (err) {
|
||||
if (err instanceof classes.InvalidField || err instanceof classes.InvalidValue) {
|
||||
console.error(err.message);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
MasterGroup,
|
||||
SlaveGroup,
|
||||
InstanceGroup,
|
||||
FactorioGroup,
|
||||
|
||||
MasterConfig,
|
||||
SlaveConfig,
|
||||
InstanceConfig,
|
||||
|
||||
registerPluginConfigGroups,
|
||||
finalizeConfigs,
|
||||
configCommand,
|
||||
handleConfigCommand,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @file Configuration System
|
||||
* @author Hornwitser
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
...require("./classes"),
|
||||
...require("./definitions"),
|
||||
}
|
||||
@@ -809,11 +809,10 @@ class FactorioServer extends events.EventEmitter {
|
||||
*
|
||||
* Loads server-settings.example.json from the data dir.
|
||||
*
|
||||
* @param {string} dataDir - The data directory for Factorio
|
||||
* @returns {object} the parsed server-settings.
|
||||
*/
|
||||
static async exampleSettings(dataDir) {
|
||||
return JSON.parse(await fs.readFile(path.join(dataDir, "server-settings.example.json"), "utf-8"));
|
||||
async exampleSettings(dataDir) {
|
||||
return JSON.parse(await fs.readFile(this.dataPath("server-settings.example.json"), "utf-8"));
|
||||
}
|
||||
|
||||
async _writeConfigIni() {
|
||||
|
||||
+43
-29
@@ -199,11 +199,11 @@ messages.listInstances = new Request({
|
||||
type: "array",
|
||||
items: {
|
||||
additionalProperties: false,
|
||||
required: ["name", "id", "slave_id"],
|
||||
required: ["name", "id", "assigned_slave"],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"id": { type: "integer" },
|
||||
"slave_id": { type: "integer" },
|
||||
"assigned_slave": { type: ["null", "integer"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -221,44 +221,59 @@ messages.setInstanceOutputSubscriptions = new Request({
|
||||
},
|
||||
});
|
||||
|
||||
messages.createInstanceCommand = new Request({
|
||||
messages.createInstance = new Request({
|
||||
type: 'create_instance',
|
||||
links: ['control-master'],
|
||||
requestProperties: {
|
||||
"name": { type: "string" },
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.getInstanceConfig = new Request({
|
||||
type: "get_instance_config",
|
||||
links: ["control-master"],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
},
|
||||
responseProperties: {
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.setInstanceConfigField = new Request({
|
||||
type: "set_instance_config_field",
|
||||
links: ["control-master"],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"field": { type: "string" },
|
||||
"value": { type: "string" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.assignInstanceCommand = new Request({
|
||||
type: 'assign_instance_command',
|
||||
links: ['control-master'],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
"slave_id": { type: "integer" },
|
||||
},
|
||||
});
|
||||
|
||||
messages.createInstance = new Request({
|
||||
type: 'create_instance',
|
||||
messages.assignInstance = new Request({
|
||||
type: 'assign_instance',
|
||||
links: ['master-slave'],
|
||||
requestProperties: {
|
||||
"id": { type: "integer" },
|
||||
"options": {
|
||||
additionalProperties: false,
|
||||
required: [
|
||||
"name", "description", "visibility", "username", "token", "game_password",
|
||||
"verify_user_identity", "allow_commands", "auto_pause",
|
||||
],
|
||||
properties: {
|
||||
"name": { type: "string" },
|
||||
"description": { type: "string" },
|
||||
"visibility": { type: "object" },
|
||||
"username": { type: "string" },
|
||||
"token": { type: "string" },
|
||||
"game_password": { type: "string" },
|
||||
"verify_user_identity": { type: "boolean" },
|
||||
"allow_commands": { type: "string" },
|
||||
"auto_pause": { type: "boolean" },
|
||||
},
|
||||
},
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
});
|
||||
forwardTo: 'instance',
|
||||
})
|
||||
|
||||
messages.startInstance = new Request({
|
||||
type: 'start_instance',
|
||||
links: ['control-master', 'master-slave', 'slave-instance'],
|
||||
requestProperties: {
|
||||
"instance_id": { type: "integer" },
|
||||
},
|
||||
forwardTo: 'instance',
|
||||
});
|
||||
|
||||
@@ -447,10 +462,9 @@ messages.updateInstances = new Event({
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
required: ["id", "name"],
|
||||
required: ["serialized_config"],
|
||||
properties: {
|
||||
"id": { type: "integer" },
|
||||
"name": { type: "string" },
|
||||
"serialized_config": { type: "object" },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+11
-3
@@ -106,7 +106,16 @@ class BaseMasterPlugin {
|
||||
async onExit() { }
|
||||
}
|
||||
|
||||
async function getPluginInfos(baseDir) {
|
||||
/**
|
||||
* Load plugin information
|
||||
*
|
||||
* Searches through the plugin directory and loads each plugin's info
|
||||
* module. Once loaded the info modules will not be reloaded should this
|
||||
* function be called again.
|
||||
*
|
||||
* @returns {Map<string, Object>} mapping of plugin name to info module.
|
||||
*/
|
||||
async function loadPluginInfos(baseDir) {
|
||||
let plugins = [];
|
||||
for (let pluginDir of await fs.readdir(baseDir)) {
|
||||
let pluginInfo;
|
||||
@@ -129,7 +138,6 @@ async function getPluginInfos(baseDir) {
|
||||
);
|
||||
}
|
||||
|
||||
pluginInfo.enabled = !await fs.exists(path.join(baseDir, pluginDir, "DISABLED"));
|
||||
plugins.push(pluginInfo);
|
||||
}
|
||||
return plugins;
|
||||
@@ -155,6 +163,6 @@ module.exports = {
|
||||
BaseInstancePlugin,
|
||||
BaseMasterPlugin,
|
||||
|
||||
getPluginInfos,
|
||||
loadPluginInfos,
|
||||
attachPluginMessages,
|
||||
};
|
||||
@@ -13,23 +13,21 @@ node master
|
||||
// const updater = require("./updater");
|
||||
// updater.update().then(console.log);
|
||||
|
||||
// argument parsing
|
||||
const args = require('minimist')(process.argv.slice(2));
|
||||
|
||||
const deepmerge = require("deepmerge");
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const jwt = require('jsonwebtoken');
|
||||
const crypto = require('crypto');
|
||||
const base64url = require('base64url');
|
||||
const moment = require("moment");
|
||||
const request = require("request");
|
||||
const setBlocking = require("set-blocking");
|
||||
const events = require("events");
|
||||
const yargs = require("yargs");
|
||||
const version = require("./package").version;
|
||||
|
||||
// constants
|
||||
let config = {};
|
||||
// ugly globals
|
||||
let masterConfig;
|
||||
let masterConfigPath;
|
||||
|
||||
// homebrew modules
|
||||
const generateSSLcert = require("lib/generateSSLcert");
|
||||
@@ -40,6 +38,7 @@ const link = require("lib/link");
|
||||
const errors = require("lib/errors");
|
||||
const plugin = require("lib/plugin");
|
||||
const prometheus = require("lib/prometheus");
|
||||
const config = require("lib/config");
|
||||
|
||||
// homemade express middleware for token auth
|
||||
const authenticate = require("lib/authenticate");
|
||||
@@ -160,15 +159,46 @@ async function loadMap(databaseDirectory, file) {
|
||||
return await database.loadJsonArrayAsMap(databasePath);
|
||||
}
|
||||
|
||||
async function loadInstances(databaseDirectory, file) {
|
||||
let filePath = path.join(databaseDirectory, file);
|
||||
console.log(`Loading ${filePath}`);
|
||||
|
||||
let instances = new Map();
|
||||
try {
|
||||
let serialized = JSON.parse(await fs.readFile(filePath));
|
||||
for (let serializedConfig of serialized) {
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.load(serializedConfig);
|
||||
instances.set(instanceConfig.get("instance.id"), instanceConfig);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save Map to JSON file in the database directory.
|
||||
*/
|
||||
async function saveMap(databaseDirectory, file, map) {
|
||||
let databasePath = path.resolve(config.databaseDirectory, file);
|
||||
let databasePath = path.resolve(databaseDirectory, file);
|
||||
console.log(`Saving ${databasePath}`);
|
||||
await database.saveMapAsJsonArray(databasePath, map);
|
||||
}
|
||||
|
||||
async function saveInstances(databaseDirectory, file, instances) {
|
||||
let filePath = path.join(databaseDirectory, file);
|
||||
let serialized = [];
|
||||
for (let instanceConfig of instances.values()) {
|
||||
serialized.push(instanceConfig.serialize());
|
||||
}
|
||||
|
||||
await fs.outputFile(filePath, JSON.stringify(serialized, null, 4));
|
||||
}
|
||||
|
||||
/**
|
||||
* Innitiate shutdown of master server
|
||||
@@ -177,8 +207,11 @@ async function shutdown() {
|
||||
console.log('Shutting down');
|
||||
let exitStartTime = Date.now();
|
||||
try {
|
||||
await saveMap(config.databaseDirectory, "slaves.json", db.slaves);
|
||||
await saveMap(config.databaseDirectory, "instances.json", db.instances);
|
||||
console.log("Saving configs");
|
||||
await fs.outputFile(masterConfigPath, JSON.stringify(masterConfig.serialize(), null, 4));
|
||||
|
||||
await saveMap(masterConfig.get('master.database_directory'), "slaves.json", db.slaves);
|
||||
await saveInstances(masterConfig.get('master.database_directory'), "instances.json", db.instances);
|
||||
|
||||
for (let [pluginName, masterPlugin] of masterPlugins) {
|
||||
let startTime = Date.now();
|
||||
@@ -249,7 +282,7 @@ GET endpoint. Returns factorio's base locale as a JSON object.
|
||||
app.get("/api/getFactorioLocale", function(req,res){
|
||||
endpointHitCounter.labels(req.route.path).inc();
|
||||
if (!localeCache) {
|
||||
factorio.getLocale(path.join(config.factorioDirectory, "data"), "en").then(locale => {
|
||||
factorio.getLocale(path.join(masterConfig.get("master.factorio_directory"), "data"), "en").then(locale => {
|
||||
localeCache = locale;
|
||||
res.send(localeCache);
|
||||
});
|
||||
@@ -271,12 +304,17 @@ class BaseConnection extends link.Link {
|
||||
}
|
||||
|
||||
async forwardRequestToInstance(message, request) {
|
||||
let instance = db.instances.get(message.data.instance_id);
|
||||
if (!instance) {
|
||||
throw new errors.RequestError(`Instance with ID ${instance_id} does not exist`);
|
||||
let instanceConfig = db.instances.get(message.data.instance_id);
|
||||
if (!instanceConfig) {
|
||||
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
let connection = slaveConnections.get(instance.slaveId);
|
||||
let slaveId = instanceConfig.get('instance.assigned_slave');
|
||||
if (slaveId === null) {
|
||||
throw new errors.RequestError("Instance is not assigned to a slave");
|
||||
}
|
||||
|
||||
let connection = slaveConnections.get(slaveId);
|
||||
if (!connection) {
|
||||
throw new errors.RequestError("Slave containing instance is not connected");
|
||||
}
|
||||
@@ -285,10 +323,13 @@ class BaseConnection extends link.Link {
|
||||
}
|
||||
|
||||
async forwardEventToInstance(message, event) {
|
||||
let instance = db.instance.get(message.data.instance_id);
|
||||
if (!instance) { return; }
|
||||
let instanceConfig = db.instances.get(message.data.instance_id);
|
||||
if (!instanceConfig) { return; }
|
||||
|
||||
let connection = slaveConnection.get(instance.slaveId);
|
||||
let slaveId = instanceConfig.get('instance.assigned_slave');
|
||||
if (slaveId === null) { return; }
|
||||
|
||||
let connection = slaveConnection.get(slaveId);
|
||||
if (!connection) { return; }
|
||||
|
||||
event.send(connection, message.data);
|
||||
@@ -339,37 +380,92 @@ class ControlConnection extends BaseConnection {
|
||||
|
||||
async listInstancesRequestHandler(message) {
|
||||
let list = [];
|
||||
for (let instance of db.instances.values()) {
|
||||
for (let instanceConfig of db.instances.values()) {
|
||||
list.push({
|
||||
id: instance.id,
|
||||
name: instance.name,
|
||||
slave_id: instance.slaveId,
|
||||
id: instanceConfig.get("instance.id"),
|
||||
name: instanceConfig.get("instance.name"),
|
||||
assigned_slave: instanceConfig.get("instance.assigned_slave"),
|
||||
});
|
||||
}
|
||||
return { list };
|
||||
}
|
||||
|
||||
// XXX should probably add a hook for slave reuqests?
|
||||
async createInstanceCommandRequestHandler(message) {
|
||||
let { slave_id, name } = message.data;
|
||||
let connection = slaveConnections.get(slave_id);
|
||||
if (!connection) {
|
||||
throw new errors.RequestError("Slave is not connected");
|
||||
async createInstanceRequestHandler(message) {
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.load(message.data.serialized_config);
|
||||
|
||||
let instanceId = instanceConfig.get("instance.id")
|
||||
if (db.instances.has(instanceId)) {
|
||||
throw new errors.RequestError(`Instance with ID ${instanceId} already exists`);
|
||||
}
|
||||
db.instances.set(instanceId, instanceConfig);
|
||||
}
|
||||
|
||||
async getInstanceConfigRequestHandler(message) {
|
||||
let instanceConfig = db.instances.get(message.data.instance_id);
|
||||
if (!instanceConfig) {
|
||||
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
await link.messages.createInstance.send(connection, {
|
||||
id: Math.random() * 2**31 | 0, // TODO: add id option
|
||||
options: {
|
||||
'name': name,
|
||||
'description': config.description,
|
||||
'visibility': config.visibility,
|
||||
'username': config.username,
|
||||
'token': config.token,
|
||||
'game_password': config.game_password,
|
||||
'verify_user_identity': config.verify_user_identity,
|
||||
'allow_commands': config.allow_commands,
|
||||
'auto_pause': config.auto_pause,
|
||||
return {
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
async setInstanceConfigFieldRequestHandler(message) {
|
||||
let instanceConfig = db.instances.get(message.data.instance_id);
|
||||
if (!instanceConfig) {
|
||||
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
if (message.data.field === "instance.assigned_slave") {
|
||||
throw new errors.RequestError("instance.assigned_slave must be set through the assign-slave interface");
|
||||
}
|
||||
|
||||
if (message.data.field === "instance.id") {
|
||||
// XXX is this worth implementing? It's race condition galore.
|
||||
throw new errors.RequestError("Setting instance.id is not supported");
|
||||
}
|
||||
|
||||
instanceConfig.set(message.data.field, message.data.value);
|
||||
|
||||
// Push updated config to slave
|
||||
let slaveId = instanceConfig.get('instance.assigned_slave');
|
||||
if (slaveId) {
|
||||
let connection = slaveConnections.get(slaveId);
|
||||
if (connection) {
|
||||
await link.messages.assignInstance.send(connection, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async assignInstanceCommandRequestHandler(message, request) {
|
||||
let instanceConfig = db.instances.get(message.data.instance_id);
|
||||
if (!instanceConfig) {
|
||||
throw new errors.RequestError(`Instance with ID ${message.data.instance_id} does not exist`);
|
||||
}
|
||||
|
||||
// XXX: Should the instance be stopped if it's running on another slave?
|
||||
|
||||
let connection = slaveConnections.get(message.data.slave_id);
|
||||
if (!connection) {
|
||||
// The case of the slave not getting the assign instance message
|
||||
// still have to be handled, so it's not a requirement that the
|
||||
// target slave be connected to the master while doing the
|
||||
// assignment, but it is IMHO a better user experience if this
|
||||
// is the case.
|
||||
throw new errors.RequestError("Target slave is not connected to the master server");
|
||||
}
|
||||
|
||||
instanceConfig.set("instance.assigned_slave", message.data.slave_id)
|
||||
|
||||
return await link.messages.assignInstance.send(connection, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -407,18 +503,29 @@ class SlaveConnection extends BaseConnection {
|
||||
}
|
||||
|
||||
async updateInstancesEventHandler(message) {
|
||||
// Prune any instances previously had by the slave
|
||||
for (let id of [...db.instances.keys()]) {
|
||||
if (db.instances.get(id).slaveId === this._id) {
|
||||
db.instances.delete(id);
|
||||
// Push updated instance configs
|
||||
for (let instanceConfig of db.instances.values()) {
|
||||
if (instanceConfig.get("instance.assigned_slave") === this._id) {
|
||||
await link.messages.assignInstance.send(this, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign instances the slave has but master does not
|
||||
for (let instance of message.data.instances) {
|
||||
db.instances.set(instance.id, {
|
||||
id: instance.id,
|
||||
name: instance.name,
|
||||
slaveId: this._id,
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.load(instance.serialized_config);
|
||||
if (db.instances.has(instanceConfig.get("instance.id"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
instanceConfig.set("instance.assigned_slave", this._id);
|
||||
db.instances.set(instanceConfig.get("instance.id"), instanceConfig);
|
||||
await link.messages.assignInstance.send(this, {
|
||||
instance_id: instanceConfig.get("instance.id"),
|
||||
serialized_config: instanceConfig.serialize(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -548,16 +655,16 @@ io.on('connection', function (socket) {
|
||||
|
||||
// handle plugins on the master
|
||||
var masterPlugins = new Map();
|
||||
async function pluginManagement(){
|
||||
async function pluginManagement(pluginInfos) {
|
||||
let startPluginLoad = Date.now();
|
||||
masterPlugins = await loadPlugins();
|
||||
masterPlugins = await loadPlugins(pluginInfos);
|
||||
console.log("All plugins loaded in "+(Date.now() - startPluginLoad)+"ms");
|
||||
}
|
||||
|
||||
async function loadPlugins() {
|
||||
async function loadPlugins(pluginInfos) {
|
||||
let plugins = new Map();
|
||||
for (pluginInfo of await plugin.getPluginInfos("plugins")) {
|
||||
if (!pluginInfo.enabled) {
|
||||
for (pluginInfo of pluginInfos) {
|
||||
if (!masterConfig.group(pluginInfo.name).get("enabled")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -604,6 +711,10 @@ function listen(server, ...args) {
|
||||
});
|
||||
}
|
||||
|
||||
function _setConfig(config) {
|
||||
masterConfig = config;
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
// Set the process title, shows up as the title of the CMD window on windows
|
||||
// and as the process name in ps/top on linux.
|
||||
@@ -612,25 +723,54 @@ async function startServer() {
|
||||
// add better stack traces on promise rejection
|
||||
process.on('unhandledRejection', r => console.log(r));
|
||||
|
||||
console.log(`Requiring config from ${args.config || './config'}`);
|
||||
config = require(args.config || './config');
|
||||
// argument parsing
|
||||
let args = yargs
|
||||
.scriptName("master")
|
||||
.usage("$0 <command> [options]")
|
||||
.option("config", {
|
||||
nargs: 1,
|
||||
describe: "master config file to use",
|
||||
default: "config-master.json",
|
||||
type: "string",
|
||||
})
|
||||
.command("config", "Manage Master config", config.configCommand)
|
||||
.command("run", "Run master server")
|
||||
.demandCommand(1, "You need to specify a command to run")
|
||||
.strict()
|
||||
.argv
|
||||
;
|
||||
|
||||
/** Sync */
|
||||
function randomStringAsBase64Url(size) {
|
||||
return base64url(crypto.randomBytes(size));
|
||||
console.log("Loading Plugin info");
|
||||
let pluginInfos = await plugin.loadPluginInfos("plugins")
|
||||
config.registerPluginConfigGroups(pluginInfos);
|
||||
config.finalizeConfigs();
|
||||
|
||||
masterConfigPath = args.config;
|
||||
console.log(`Loading config from ${masterConfigPath}`);
|
||||
masterConfig = new config.MasterConfig();
|
||||
try {
|
||||
await masterConfig.load(JSON.parse(await fs.readFile(masterConfigPath)));
|
||||
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log("Config not found, initializing new config");
|
||||
await masterConfig.init();
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync("secret-api-token.txt")) {
|
||||
config.masterAuthSecret = randomStringAsBase64Url(256);
|
||||
fs.writeFileSync("config.json",JSON.stringify(config, null, 4));
|
||||
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
|
||||
expiresIn: 86400*365 // expires in 1 year
|
||||
}));
|
||||
console.log("Generated new master authentication private key!");
|
||||
process.exit(0);
|
||||
let command = args._[0];
|
||||
if (command === "config") {
|
||||
await config.handleConfigCommand(args, masterConfig, masterConfigPath);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we get here the command was run
|
||||
|
||||
// write an auth token to file
|
||||
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, config.masterAuthSecret, {
|
||||
fs.writeFileSync("secret-api-token.txt", jwt.sign({ id: "api" }, masterConfig.get('master.auth_secret'), {
|
||||
expiresIn: 86400*365 // expires in 1 year
|
||||
}));
|
||||
|
||||
@@ -653,33 +793,34 @@ async function startServer() {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
config.databaseDirectory = args.databaseDirectory || config.databaseDirectory || "./database";
|
||||
await fs.ensureDir(config.databaseDirectory);
|
||||
await fs.ensureDir(masterConfig.get('master.database_directory'));
|
||||
|
||||
db.slaves = await loadMap(config.databaseDirectory, "slaves.json");
|
||||
db.instances = await loadMap(config.databaseDirectory, "instances.json");
|
||||
db.slaves = await loadMap(masterConfig.get('master.database_directory'), "slaves.json");
|
||||
db.instances = await loadInstances(masterConfig.get('master.database_directory'), "instances.json");
|
||||
|
||||
authenticate.setAuthSecret(config.masterAuthSecret);
|
||||
authenticate.setAuthSecret(masterConfig.get('master.auth_secret'));
|
||||
|
||||
// Make sure we're actually going to listen on a port
|
||||
let httpPort = args.masterPort || config.masterPort;
|
||||
let httpsPort = args.sslPort || config.sslPort;
|
||||
let httpPort = masterConfig.get('master.http_port');
|
||||
let httpsPort = masterConfig.get('master.https_port');
|
||||
if (!httpPort && !httpsPort) {
|
||||
console.error("Error: at least one of httpPort and sslPort must be configured");
|
||||
console.error("Error: at least one of http_port and https_port must be configured");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let tls_cert = masterConfig.get('master.tls_certificate');
|
||||
let tls_key = masterConfig.get('master.tls_private_key');
|
||||
// Create a self signed certificate if the certificate files doesn't exist
|
||||
if (httpsPort && !await fs.exists(config.sslCert) && !await fs.exists(config.sslPrivKey)) {
|
||||
if (httpsPort && !await fs.exists(tls_cert) && !await fs.exists(tls_key)) {
|
||||
await generateSSLcert({
|
||||
sslCertPath: config.sslCert,
|
||||
sslPrivKeyPath: config.sslPrivKey,
|
||||
sslCertPath: tls_cert,
|
||||
sslPrivKeyPath: tls_key,
|
||||
doLogging: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Load plugins
|
||||
await pluginManagement();
|
||||
await pluginManagement(pluginInfos);
|
||||
|
||||
// Only start listening for connections after all plugins have loaded
|
||||
if (httpPort) {
|
||||
@@ -692,8 +833,8 @@ async function startServer() {
|
||||
if (httpsPort) {
|
||||
let certificate, privateKey;
|
||||
try {
|
||||
certificate = await fs.readFile(config.sslCert);
|
||||
privateKey = await fs.readFile(config.sslPrivKey);
|
||||
certificate = await fs.readFile(tls_cert);
|
||||
privateKey = await fs.readFile(tls_key);
|
||||
|
||||
} catch (err) {
|
||||
throw new errors.StartupError(
|
||||
@@ -719,7 +860,7 @@ module.exports = {
|
||||
|
||||
// For testing only
|
||||
_db: db,
|
||||
_config: config,
|
||||
_setConfig,
|
||||
_SocketIOServerConnector: SocketIOServerConnector,
|
||||
_controlConnections: controlConnections,
|
||||
_ControlConnection: ControlConnection,
|
||||
|
||||
@@ -55,7 +55,7 @@ class neuralDole {
|
||||
divider({
|
||||
res,
|
||||
object,
|
||||
config,
|
||||
masterConfig,
|
||||
prometheusImportGauge
|
||||
}){
|
||||
let magicData = doleNN.Dose(
|
||||
@@ -83,7 +83,7 @@ class neuralDole {
|
||||
|
||||
// Remove item from DB and send it
|
||||
this.items.removeItem(object.name, magicData[0]);
|
||||
if(config.logItemTransfers){
|
||||
if (masterConfig.get('log_item_transfers')) {
|
||||
console.log("removed: " + object.name + " " + magicData[0] + " . " + this.items.getItemCount(object.name) + " and sent to " + object.instanceID + " | " + object.instanceName);
|
||||
}
|
||||
prometheusImportGauge.labels(object.instanceID, object.name).inc(Number(magicData[0]) || 0);
|
||||
@@ -101,7 +101,7 @@ function doleDivider({
|
||||
itemCount,
|
||||
object,
|
||||
items,
|
||||
config,
|
||||
masterConfig,
|
||||
prometheusDoleFactorGauge,
|
||||
prometheusImportGauge,
|
||||
req,res,
|
||||
@@ -120,7 +120,7 @@ function doleDivider({
|
||||
if (itemCount > object.count) {
|
||||
//If successful, increase dole
|
||||
_doleDivisionFactor[object.name] = Math.max((_doleDivisionFactor[object.name]||0)||1, 1) - 1;
|
||||
if (config.logItemTransfers) {
|
||||
if (masterConfig.get('log_item_transfers')) {
|
||||
console.log("removed: " + object.name + " " + object.count + " . " + itemCount + " and sent to " + object.instanceID + " | " + object.instanceName);
|
||||
}
|
||||
items.removeItem(object.name, object.count)
|
||||
|
||||
+35
-20
@@ -3,11 +3,19 @@ const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const link = require("lib/link");
|
||||
const config = require("lib/config");
|
||||
const client = require("../client");
|
||||
|
||||
describe("Client testing", function() {
|
||||
describe("class Instance", function() {
|
||||
let instance = new client._Instance(new link.VirtualConnector(), "dir", "factorioDir", { name:"foo" });
|
||||
let instance;
|
||||
before(async function() {
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", "foo");
|
||||
instance = new client._Instance(new link.VirtualConnector(), "dir", "factorioDir", instanceConfig);
|
||||
});
|
||||
|
||||
describe(".name", function() {
|
||||
it("should give the name of the instance", function() {
|
||||
assert.equal(instance.name, "foo");
|
||||
@@ -77,24 +85,31 @@ describe("Client testing", function() {
|
||||
});
|
||||
|
||||
describe("symlinkMods()", function() {
|
||||
// Remove previous test output
|
||||
let testDir = path.join("test", "temp", "symlink");
|
||||
fs.removeSync(testDir);
|
||||
|
||||
// Add some test mods
|
||||
fs.outputFileSync(path.join(testDir, "shared", "mod_a.zip"), "a");
|
||||
fs.outputFileSync(path.join(testDir, "shared", "mod_b.zip"), "b");
|
||||
fs.outputFileSync(path.join(testDir, "shared", "mod.dat"), "c");
|
||||
let instance = new client._Instance(
|
||||
new link.VirtualConnector(), path.join(testDir, "instance"), "factorioDir", { name: "test" }
|
||||
);
|
||||
fs.outputFileSync(instance.path("mods", "mod_i.zip"), "i");
|
||||
|
||||
let discardingLogger = {
|
||||
warning: function() { },
|
||||
log: function() { },
|
||||
}
|
||||
|
||||
let instance;
|
||||
before(async function() {
|
||||
// Remove previous test output
|
||||
await fs.remove(testDir);
|
||||
|
||||
// Add some test mods
|
||||
await fs.outputFile(path.join(testDir, "shared", "mod_a.zip"), "a");
|
||||
await fs.outputFile(path.join(testDir, "shared", "mod_b.zip"), "b");
|
||||
await fs.outputFile(path.join(testDir, "shared", "mod.dat"), "c");
|
||||
|
||||
let instanceConfig = new config.InstanceConfig();
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.name", "test");
|
||||
instance = new client._Instance(
|
||||
new link.VirtualConnector(), path.join(testDir, "instance"), "factorioDir", instanceConfig
|
||||
);
|
||||
await fs.outputFile(instance.path("mods", "mod_i.zip"), "i");
|
||||
});
|
||||
|
||||
it("should link mods and data files", async function() {
|
||||
await client._symlinkMods(instance, path.join(testDir, "shared"), discardingLogger);
|
||||
|
||||
@@ -139,15 +154,15 @@ describe("Client testing", function() {
|
||||
let logger = { log: () => {}, error: () => {} };
|
||||
let instancePath = path.join("test", "file", "instances");
|
||||
let instanceInfos = await client._discoverInstances(instancePath, logger);
|
||||
|
||||
let referenceConfig = new config.InstanceConfig();
|
||||
await referenceConfig.init();
|
||||
referenceConfig.set("instance.id", 1);
|
||||
referenceConfig.set("instance.name", "test");
|
||||
|
||||
assert.deepEqual(instanceInfos, new Map([
|
||||
[1, {
|
||||
config: {
|
||||
id: 1,
|
||||
name: "test",
|
||||
factorioPort: null,
|
||||
clientPort: null,
|
||||
clientPassword: null
|
||||
},
|
||||
config: referenceConfig,
|
||||
path: path.join(instancePath, "test"),
|
||||
}],
|
||||
]));
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ describe("clusterctl", function() {
|
||||
seq: 1, type: 'list_instances_response',
|
||||
data: {
|
||||
seq: message.seq,
|
||||
list: [{ id: 57, slave_id: 4, name: 'Test Instance' }],
|
||||
list: [{ id: 57, assigned_slave: 4, name: 'Test Instance' }],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"id": 1,
|
||||
"name": "test",
|
||||
"factorioPort": null,
|
||||
"clientPort": null,
|
||||
"clientPassword": null
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "instance",
|
||||
"fields": {
|
||||
"id": 1,
|
||||
"name": "test"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,7 @@ const path = require("path");
|
||||
|
||||
const link = require("lib/link");
|
||||
const plugin = require("lib/plugin");
|
||||
const config = require("lib/config");
|
||||
const master = require("../../master");
|
||||
const client = require("../../client");
|
||||
const clusterctl = require("../../clusterctl");
|
||||
@@ -29,24 +30,22 @@ describe("Integration of Clusterio", function() {
|
||||
test.timeout(20000);
|
||||
}
|
||||
|
||||
async function createInstanceConfig(id, name, assignedSlave) {
|
||||
let instanceConfig = new config.InstanceConfig()
|
||||
await instanceConfig.init();
|
||||
instanceConfig.set("instance.id", id);
|
||||
instanceConfig.set("instance.name", name);
|
||||
instanceConfig.set("instance.assigned_slave", assignedSlave);
|
||||
return instanceConfig;
|
||||
}
|
||||
|
||||
before(async function() {
|
||||
master._db.slaves = new Map();
|
||||
master._db.instances = new Map([
|
||||
[11, { id: 11, name: "foo", slaveId: 4 }],
|
||||
[21, { id: 21, name: "bar", slaveId: 5 }],
|
||||
[11, await createInstanceConfig(11, "foo", 4)],
|
||||
[21, await createInstanceConfig(21, "bar", 5)],
|
||||
]);
|
||||
|
||||
Object.assign(master._config, {
|
||||
description: "test",
|
||||
visibility: { public: false, lan: false },
|
||||
username: "test",
|
||||
token: "",
|
||||
game_password: "",
|
||||
verify_user_identity: false,
|
||||
allow_commands: "admins-only",
|
||||
auto_pause: false,
|
||||
});
|
||||
|
||||
let [controlClient, controlServer] = link.VirtualConnector.makePair();
|
||||
testControl = new clusterctl._Control(controlClient);
|
||||
testControlConnection = new master._ControlConnection({ agent: "test", version }, controlServer);
|
||||
@@ -54,16 +53,19 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
await fs.remove(instancesDir);
|
||||
await fs.ensureDir(instancesDir)
|
||||
|
||||
let slaveConfig = new config.SlaveConfig();
|
||||
await slaveConfig.init();
|
||||
slaveConfig.set("slave.id", 4);
|
||||
slaveConfig.set("slave.name", "slave");
|
||||
slaveConfig.set("slave.instances_directory", instancesDir);
|
||||
slaveConfig.set("slave.factorio_directory", "factorio");
|
||||
slaveConfig.set("slave.master_url", "http://invalid");
|
||||
slaveConfig.set("slave.master_token", "invalid");
|
||||
slaveConfig.set("slave.public_address", "invalid");
|
||||
|
||||
let [slaveClient, slaveServer] = link.VirtualConnector.makePair();
|
||||
testSlave = new client._Slave(slaveClient, {
|
||||
id: 4,
|
||||
name: "slave",
|
||||
instanceDirectory: instancesDir,
|
||||
factorioDirectory: "factorio",
|
||||
masterURL: "http://invalid",
|
||||
masterAuthToken: "invalid",
|
||||
publicIP: "invalid",
|
||||
}, await plugin.getPluginInfos("plugins"));
|
||||
testSlave = new client._Slave(slaveClient, slaveConfig, await plugin.loadPluginInfos("plugins"));
|
||||
testSlaveConnection = new master._SlaveConnection({ agent: "test", version, name: "slave", id: 4}, slaveServer);
|
||||
master._slaveConnections.set(4, testSlaveConnection);
|
||||
});
|
||||
@@ -82,7 +84,14 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
describe("create-instances", function() {
|
||||
it("creates the instance", async function() {
|
||||
await clusterctl._commands.get("create-instance").run({name: "test", slave: "slave"}, testControl);
|
||||
await clusterctl._commands.get("create-instance").run({id: 44, name: "test"}, testControl);
|
||||
assert(master._db.instances.has(44), "Instance was not created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assign-instance", function() {
|
||||
it("creates the instance", async function() {
|
||||
await clusterctl._commands.get("assign-instance").run({instance: "test", slave: "slave"}, testControl);
|
||||
assert(await fs.exists(path.join(instancesDir, "test")), "Instance was not created");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("Integration of lib/factorio/server", function() {
|
||||
describe(".exampleSettings()", function() {
|
||||
let settings;
|
||||
it("returns an object", async function() {
|
||||
settings = await factorio.FactorioServer.exampleSettings(path.join("factorio", "data"));
|
||||
settings = await server.exampleSettings();
|
||||
assert(typeof settings === "object");
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
"use strict";
|
||||
const classes = require("lib/config/classes");
|
||||
const assert = require("assert").strict;
|
||||
|
||||
|
||||
describe("lib/config/classes", function() {
|
||||
describe("ConfigGroup", function() {
|
||||
class TestGroup extends classes.ConfigGroup { }
|
||||
TestGroup.groupName = "test_group";
|
||||
describe(".define()", function() {
|
||||
it("should throw if groupName is not set", function() {
|
||||
class NoGroupName extends classes.ConfigGroup { }
|
||||
assert.throws(
|
||||
() => NoGroupName.define({ name: "a", type: "string", optional: true }),
|
||||
new Error("Expected ConfigGroup class NoGroupName to have the groupName property set to a string")
|
||||
);
|
||||
});
|
||||
it("should throw on unknown properties", function() {
|
||||
assert.throws(
|
||||
() => TestGroup.define({ name: "a", type: "string", optional: true, invalid: true }),
|
||||
new Error("Unknown property invalid")
|
||||
);
|
||||
});
|
||||
it("should throw on invalid properties", function() {
|
||||
for (let [def, error] of [
|
||||
[
|
||||
{ name: "a", type: "invalid", optional: true },
|
||||
"invalid is not a valid type"
|
||||
],
|
||||
[
|
||||
{ name: [], type: "string", optional: true },
|
||||
"name must be a string"
|
||||
],
|
||||
[
|
||||
{ name: "a", type: "string", enum: true, optional: true },
|
||||
"enum must be an array"
|
||||
],
|
||||
[
|
||||
{ name: "a", title: 2, type: "string", optional: true },
|
||||
"title must be a string"
|
||||
],
|
||||
[
|
||||
{ name: "a", description: 2, type: "string", optional: true },
|
||||
"description must be a string"
|
||||
],
|
||||
[
|
||||
{ name: "a", type: "string", optional: "yes" },
|
||||
"optional must be a boolean"
|
||||
],
|
||||
[
|
||||
{ name: "a", type: "string", initial_value: 1 },
|
||||
"initial_value must match the type or be a function"
|
||||
],
|
||||
[
|
||||
{ name: "a", type: "string" },
|
||||
"Non-optional field a needs an initial_value"
|
||||
],
|
||||
]) {
|
||||
assert.throws(() => TestGroup.define(def), new Error(error));
|
||||
}
|
||||
});
|
||||
it("should throw on missing properties", function() {
|
||||
assert.throws(
|
||||
() => TestGroup.define({ type: "string", optional: true }),
|
||||
new Error("name is required when defining an field")
|
||||
);
|
||||
assert.throws(
|
||||
() => TestGroup.define({ name: "a", optional: true }),
|
||||
new Error("type is required when defining an field")
|
||||
);
|
||||
});
|
||||
it("should allow defining entries", function() {
|
||||
TestGroup.define({ name: "test", type: "string", optional: true });
|
||||
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: {} });
|
||||
|
||||
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");
|
||||
});
|
||||
|
||||
it("should throw on already defined entries", function() {
|
||||
assert.throws(
|
||||
() => TestGroup.define({ name: "test", type: "string", optional: true }),
|
||||
new Error("Config field test has already been defined")
|
||||
);
|
||||
});
|
||||
|
||||
it("should set the entries on the class", function() {
|
||||
let field = {
|
||||
type: "string",
|
||||
name: "enum",
|
||||
title: "Enum",
|
||||
description: "Enum thingy",
|
||||
enum: ["a", "b", "c"],
|
||||
optional: true,
|
||||
initial_value: "b",
|
||||
}
|
||||
TestGroup.define(field);
|
||||
// Generated properties
|
||||
field.fullName = "test_group.enum";
|
||||
|
||||
assert.deepEqual(TestGroup._definitions.get("enum"), field);
|
||||
})
|
||||
});
|
||||
|
||||
describe("get definitions", function() {
|
||||
it("should give the field definitions", function() {
|
||||
assert.equal(TestGroup.definitions, TestGroup._definitions);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".finalize()", function() {
|
||||
it("should throw if groupName is not set", function() {
|
||||
class NoGroupName extends classes.ConfigGroup { }
|
||||
assert.throws(
|
||||
() => NoGroupName.finalize(),
|
||||
new Error("Expected ConfigGroup class NoGroupName to have the groupName property set to a string")
|
||||
);
|
||||
});
|
||||
it("should throw when creating an instance before finalize", function() {
|
||||
assert.throws(
|
||||
() => new TestGroup(),
|
||||
new Error("Cannot instantiate incomplete ConfigGroup class TestGroup")
|
||||
);
|
||||
});
|
||||
|
||||
it("should not throw", function() {
|
||||
TestGroup.finalize();
|
||||
});
|
||||
|
||||
it("should throw when calling define after finalize", function() {
|
||||
assert.throws(
|
||||
() => TestGroup.define({}),
|
||||
new Error("Cannot define field for ConfigGroup class TestGroup after it has been finalized")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should create an instance", function() {
|
||||
let testInstance = new TestGroup();
|
||||
});
|
||||
});
|
||||
|
||||
describe(".init()", function() {
|
||||
it("should initialize all fields with defaults", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.init();
|
||||
|
||||
assert.equal(testInstance.get("enum"), "b");
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
assert.equal(testInstance.get("func"), 42);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".serialize()", function() {
|
||||
it("should serialize a basic group", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.init();
|
||||
|
||||
assert.deepEqual(testInstance.serialize(), {
|
||||
name: "test_group",
|
||||
fields: {
|
||||
enum: "b",
|
||||
test: null,
|
||||
func: 42,
|
||||
bool: false,
|
||||
json: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(".load()", function() {
|
||||
it("should load a basic group", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.load({ name: "test_group", fields: {
|
||||
enum: "c",
|
||||
test: "blah",
|
||||
func: 22,
|
||||
bool: null,
|
||||
json: { valid: true },
|
||||
}})
|
||||
|
||||
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.deepEqual(testInstance.get("json"), { valid: true });
|
||||
});
|
||||
it("should load defaults for missing serialized fields", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
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 preserve unknown fields when serialized back", async function() {
|
||||
let testFields = {
|
||||
enum: "a",
|
||||
test: "blah",
|
||||
func: 50,
|
||||
bool: false,
|
||||
json: {},
|
||||
alpha: null,
|
||||
beta: "decay",
|
||||
gamma: 99,
|
||||
}
|
||||
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.load({ name: "test_group", fields: testFields });
|
||||
|
||||
assert.deepEqual(testInstance.serialize().fields, testFields);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".update()", function() {
|
||||
it("should skip updating invalid values", async function() {
|
||||
let testInstance = new TestGroup();
|
||||
await testInstance.init();
|
||||
testInstance.update({ name: "test_group", fields: { enum: "wrong", test: 3, func: null }});
|
||||
|
||||
assert.equal(testInstance.get("enum"), "b");
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
assert.equal(testInstance.get("func"), 42);
|
||||
});
|
||||
|
||||
it("should throw on invalid input", function() {
|
||||
let testInstance = new TestGroup();
|
||||
assert.throws(
|
||||
() => testInstance.update(),
|
||||
new Error("Expected object, not undefined for ConfigGroup")
|
||||
);
|
||||
assert.throws(
|
||||
() => testInstance.update({}),
|
||||
new Error("Expected group name test_group, not undefined")
|
||||
);
|
||||
assert.throws(
|
||||
() => testInstance.update({ name: "test_group" }),
|
||||
new Error("Expected fields to be an object, not undefined")
|
||||
);
|
||||
assert.throws(
|
||||
() => testInstance.update({ name: "test_group", fields: []}),
|
||||
new Error("Expected fields to be an object, not array")
|
||||
);
|
||||
assert.throws(
|
||||
() => testInstance.update({ name: "test_group", fields: null}),
|
||||
new Error("Expected fields to be an object, not null")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".set()", function() {
|
||||
let testInstance;
|
||||
before(function() {
|
||||
testInstance = new TestGroup();
|
||||
testInstance.update({ name: "test_group", fields: {
|
||||
enum: "a", test: "blah", func: 27,
|
||||
}});
|
||||
});
|
||||
|
||||
it("should throw if field does not exist", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("bar", 1),
|
||||
new Error("No field named 'bar'")
|
||||
);
|
||||
})
|
||||
|
||||
it("should throw if field is not in enum", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("enum", "bar"),
|
||||
new Error("Expected one of [a, b, c], not bar")
|
||||
);
|
||||
})
|
||||
|
||||
it("should work if field is in enum", function() {
|
||||
testInstance.set("enum", "c"),
|
||||
assert.equal(testInstance.get("enum"), "c");
|
||||
})
|
||||
|
||||
it("should throw if field is not optional", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("func", null),
|
||||
new Error("Field func cannot be null")
|
||||
);
|
||||
})
|
||||
|
||||
it("should work if field is optional", function() {
|
||||
testInstance.set("test", "spam"),
|
||||
assert.equal(testInstance.get("test"), "spam");
|
||||
})
|
||||
|
||||
it("should throw if field is of wrong type", function() {
|
||||
assert.throws(
|
||||
() => testInstance.set("test", 1),
|
||||
new Error("Expected type of test to be string, not number")
|
||||
);
|
||||
})
|
||||
|
||||
it("should treat empty string as null", function() {
|
||||
testInstance.set("test", ""),
|
||||
assert.equal(testInstance.get("test"), null);
|
||||
})
|
||||
|
||||
it("should auto convert string to boolean if possible", function() {
|
||||
testInstance.set("bool", "true");
|
||||
assert.equal(testInstance.get("bool"), true);
|
||||
testInstance.set("bool", "false");
|
||||
assert.equal(testInstance.get("bool"), false);
|
||||
|
||||
assert.throws(
|
||||
() => testInstance.set("bool", "blah"),
|
||||
new classes.InvalidValue("Expected type of bool to be boolean, not string")
|
||||
);
|
||||
});
|
||||
|
||||
it("should auto convert string to number if possible", function() {
|
||||
for (let s of [
|
||||
"1",
|
||||
"+23",
|
||||
".23e5",
|
||||
"-.1e-2",
|
||||
"100.0001",
|
||||
"Infinity",
|
||||
]) {
|
||||
testInstance.set("func", s);
|
||||
assert.equal(testInstance.get("func"), Number.parseFloat(s));
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => testInstance.set("func", "blah"),
|
||||
new classes.InvalidValue("Expected type of func to be number, not string")
|
||||
);
|
||||
});
|
||||
|
||||
it("should auto convert string to object", function() {
|
||||
testInstance.set("json", '{"json": true}');
|
||||
assert.deepEqual(testInstance.get("json"), { json: true });
|
||||
|
||||
let errMsg;
|
||||
try { JSON.parse("blah"); }
|
||||
catch (err) { errMsg = err.message; }
|
||||
assert.throws(
|
||||
() => testInstance.set("json", "blah"),
|
||||
new classes.InvalidValue(`Error parsing value for json: ${errMsg}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Config", function() {
|
||||
class AlphaGroup extends classes.ConfigGroup { }
|
||||
AlphaGroup.groupName = "alpha";
|
||||
class BetaGroup extends classes.ConfigGroup { }
|
||||
BetaGroup.groupName = "beta";
|
||||
class TestConfig extends classes.Config { }
|
||||
|
||||
before(function() {
|
||||
AlphaGroup.define({ name: "foo", type: "string", optional: true });
|
||||
AlphaGroup.finalize();
|
||||
TestConfig.registerGroup(AlphaGroup);
|
||||
BetaGroup.define({ name: "bar", type: "number", initial_value: 2 });
|
||||
BetaGroup.finalize();
|
||||
TestConfig.registerGroup(BetaGroup);
|
||||
});
|
||||
|
||||
describe(".registerGroup()", function() {
|
||||
it("should throw if the class is finalized", function() {
|
||||
class TestConfig extends classes.Config { }
|
||||
TestConfig.finalize();
|
||||
class TestGroup extends classes.ConfigGroup { }
|
||||
TestGroup.groupName = "test";
|
||||
TestGroup.finalize();
|
||||
|
||||
assert.throws(
|
||||
() => TestConfig.registerGroup(TestGroup),
|
||||
new Error("Cannot register group on finalized config")
|
||||
);
|
||||
});
|
||||
it("should throw if the group is not finalized", function() {
|
||||
class UnfinishedGroup extends classes.ConfigGroup { }
|
||||
|
||||
assert.throws(
|
||||
() => TestConfig.registerGroup(UnfinishedGroup),
|
||||
new Error("Group must be finalized before it can be registered")
|
||||
);
|
||||
});
|
||||
it("should throw if the group is already registered", function() {
|
||||
assert.throws(
|
||||
() => TestConfig.registerGroup(AlphaGroup),
|
||||
new Error("alpha has already been registered")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructor", function() {
|
||||
it("should throw if config is not finalized", function() {
|
||||
assert.throws(
|
||||
() => new TestConfig(),
|
||||
new Error("Cannot instantiate incomplete Config class TestConfig")
|
||||
);
|
||||
});
|
||||
it("should construct a finalized class", function() {
|
||||
TestConfig.finalize();
|
||||
new TestConfig();
|
||||
});
|
||||
});
|
||||
|
||||
describe(".init()", function() {
|
||||
it("should initialize all fields", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.init();
|
||||
assert.equal(testInstance.get("alpha.foo"), null);
|
||||
assert.equal(testInstance.get("beta.bar"), 2);
|
||||
})
|
||||
});
|
||||
|
||||
describe(".serialize()", function() {
|
||||
it("should serialize a basic config", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.init();
|
||||
assert.deepEqual(testInstance.serialize(), {
|
||||
groups: [
|
||||
{
|
||||
name: "alpha",
|
||||
fields: { foo: null },
|
||||
},
|
||||
{
|
||||
name: "beta",
|
||||
fields: { bar: 2 },
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe(".load()", function() {
|
||||
it("should reject on incorrect input passed", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await assert.rejects(
|
||||
testInstance.load(null),
|
||||
new Error("Expected object, not null for config")
|
||||
);
|
||||
await assert.rejects(
|
||||
testInstance.load(undefined),
|
||||
new Error("Expected object, not undefined for config")
|
||||
);
|
||||
await assert.rejects(
|
||||
testInstance.load({ groups: null }),
|
||||
new Error("Expected groups to be an array, not null")
|
||||
);
|
||||
});
|
||||
|
||||
it("should load defaults for missing serialized groups", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.load({ groups: [{ name: "alpha", fields: { foo: "a" }}] });
|
||||
|
||||
assert.equal(testInstance.get("alpha.foo"), "a");
|
||||
assert.equal(testInstance.get("beta.bar"), 2);
|
||||
});
|
||||
|
||||
it("should preserve unknown groups when serialized back", async function() {
|
||||
let testGroups = [
|
||||
{
|
||||
name: "extra",
|
||||
fields: { blah: true, spam: "foobar" },
|
||||
},
|
||||
{
|
||||
name: "alpha",
|
||||
fields: { foo: "true" },
|
||||
},
|
||||
{
|
||||
name: "beta",
|
||||
fields: { bar: 20 },
|
||||
}
|
||||
]
|
||||
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.load({ groups: testGroups });
|
||||
|
||||
assert.deepEqual(testInstance.serialize().groups, testGroups);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".update()", function() {
|
||||
it("should update a basic config", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.init();
|
||||
testInstance.update({ groups: [
|
||||
{
|
||||
name: "extra",
|
||||
fields: { blah: true, spam: "baz" },
|
||||
},
|
||||
{
|
||||
name: "beta",
|
||||
fields: { bar: 30 },
|
||||
}
|
||||
]});
|
||||
|
||||
assert.deepEqual(testInstance.serialize(), {
|
||||
groups: [
|
||||
{
|
||||
name: "extra",
|
||||
fields: { blah: true, spam: "baz" },
|
||||
},
|
||||
{
|
||||
name: "alpha",
|
||||
fields: { foo: null },
|
||||
},
|
||||
{
|
||||
name: "beta",
|
||||
fields: { bar: 30 },
|
||||
}
|
||||
],
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe(".group()", function() {
|
||||
it("should throw if instance is not initialized", function() {
|
||||
let testInstance = new TestConfig();
|
||||
assert.throws(
|
||||
() => testInstance.group("test"),
|
||||
new Error("TestConfig instance is uninitialized")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(".get()", function() {
|
||||
it("should throw if group does not exist", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
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();
|
||||
await testInstance.init();
|
||||
|
||||
assert.throws(() => testInstance.get("alpha"), new Error("No field named ''"))
|
||||
});
|
||||
});
|
||||
|
||||
describe(".set()", function() {
|
||||
it("should set the value", async function() {
|
||||
let testInstance = new TestConfig();
|
||||
await testInstance.init();
|
||||
|
||||
testInstance.set("alpha.foo", "new value");
|
||||
assert.equal(testInstance.get("alpha.foo"), "new value");
|
||||
});
|
||||
});
|
||||
|
||||
describe("get groups", function() {
|
||||
it("should give the registered group classes", function() {
|
||||
assert.equal(TestConfig.groups, TestConfig._groups);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
+7
-22
@@ -34,12 +34,11 @@ describe("lib/plugin", function() {
|
||||
})
|
||||
});
|
||||
|
||||
describe("getPluginInfos()", function() {
|
||||
describe("loadPluginInfos()", function() {
|
||||
let baseDir = path.join('test', 'temp', 'plugin');
|
||||
let emptyDir = path.join(baseDir, 'emptyDir');
|
||||
let emptyPlugin = path.join(baseDir, 'emptyPlugin');
|
||||
let testPlugin = path.join(baseDir, 'testPlugin');
|
||||
let disabledPlugin = path.join(baseDir, 'disabledPlugin');
|
||||
let brokenPlugin = path.join(baseDir, 'brokenPlugin');
|
||||
let invalidPlugin = path.join(baseDir, 'invalidPlugin');
|
||||
before(async function() {
|
||||
@@ -54,46 +53,32 @@ describe("lib/plugin", function() {
|
||||
}
|
||||
|
||||
await writePlugin(testPlugin, 'test');
|
||||
await writePlugin(disabledPlugin, 'disabled');
|
||||
await fs.outputFile(path.join(disabledPlugin, 'disabled', 'DISABLED'), "");
|
||||
await writePlugin(brokenPlugin, 'broken');
|
||||
await fs.outputFile(path.join(brokenPlugin, 'broken', 'info.js'), "Syntax Error");
|
||||
await writePlugin(invalidPlugin, 'invalid', 'wrong');
|
||||
});
|
||||
|
||||
it("should return an empty array for an empty directory", async function() {
|
||||
assert.deepEqual(await plugin.getPluginInfos(emptyDir), []);
|
||||
assert.deepEqual(await plugin.loadPluginInfos(emptyDir), []);
|
||||
});
|
||||
it("should ignore plugin dirs without info module", async function() {
|
||||
assert.deepEqual(await plugin.getPluginInfos(emptyPlugin), []);
|
||||
assert.deepEqual(await plugin.loadPluginInfos(emptyPlugin), []);
|
||||
});
|
||||
it("should discover test plugin", async function() {
|
||||
assert.deepEqual(
|
||||
await plugin.getPluginInfos(testPlugin),
|
||||
[{ name: 'test', enabled: true }]
|
||||
);
|
||||
});
|
||||
it("should discover disabled plugin", async function() {
|
||||
assert.deepEqual(
|
||||
await plugin.getPluginInfos(disabledPlugin),
|
||||
[{ name: 'disabled', enabled: false }]
|
||||
);
|
||||
});
|
||||
it("should discover disabled plugin", async function() {
|
||||
assert.deepEqual(
|
||||
await plugin.getPluginInfos(disabledPlugin),
|
||||
[{ name: 'disabled', enabled: false }]
|
||||
await plugin.loadPluginInfos(testPlugin),
|
||||
[{ name: 'test' }]
|
||||
);
|
||||
});
|
||||
it("should reject on broken plugin", async function() {
|
||||
await assert.rejects(
|
||||
plugin.getPluginInfos(brokenPlugin),
|
||||
plugin.loadPluginInfos(brokenPlugin),
|
||||
{ message: "PluginError: Unexpected identifier" }
|
||||
);
|
||||
});
|
||||
it("should reject on invalid plugin", async function() {
|
||||
await assert.rejects(
|
||||
plugin.getPluginInfos(invalidPlugin),
|
||||
plugin.loadPluginInfos(invalidPlugin),
|
||||
{ message: `Plugin dir ${invalidPlugin}/invalid does not match the name of the plugin (wrong)` }
|
||||
);
|
||||
});
|
||||
|
||||
+20
-2
@@ -3,14 +3,32 @@ const request = require('supertest');
|
||||
const validateHTML = require('html5-validator');
|
||||
const parallel = require('mocha.parallel');
|
||||
|
||||
const config = require("lib/config");
|
||||
const plugin = require("lib/plugin");
|
||||
const mock = require('./mock');
|
||||
const master = require('../master');
|
||||
|
||||
|
||||
master._db.slaves = new Map();
|
||||
|
||||
// Fields required in the config
|
||||
master._config.factorioDirectory = "factorio";
|
||||
before(async function() {
|
||||
let pluginInfos = await plugin.loadPluginInfos("plugins");
|
||||
config.registerPluginConfigGroups(pluginInfos);
|
||||
config.finalizeConfigs();
|
||||
let masterConfig = new config.MasterConfig();
|
||||
await masterConfig.load({
|
||||
groups: [{
|
||||
name: "master",
|
||||
fields: {
|
||||
auth_secret: "TestSecretDoNotUse",
|
||||
http_port: 8880,
|
||||
https_port: 4443,
|
||||
},
|
||||
}]
|
||||
});
|
||||
|
||||
master._setConfig(masterConfig);
|
||||
})
|
||||
|
||||
async function get(path) {
|
||||
return await request(master.app).get(path).expect(200);
|
||||
|
||||
Reference in new issue
Block a user