Skip to content

Commit

Permalink
fix(startup): Better detection of invalid config file structure and c…
Browse files Browse the repository at this point in the history
…ontent

Fixes #925
  • Loading branch information
Göran Sander committed Oct 21, 2024
1 parent 6b6c965 commit befc5ee
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 92 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,4 @@ jspm_packages

# Optional REPL history
.node_repl_history
build.cjs
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"request": "launch",
"name": "Launch Program",
"program": "${workspaceFolder}/src/butler-sos.js",
"runtimeVersion": "20",
"runtimeVersion": "18",
"cwd": "${workspaceFolder}/src",
"env": {
"NODE_CONFIG_DIR": "${workspaceFolder}/src/config",
Expand Down
20 changes: 1 addition & 19 deletions src/butler-sos.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { udpInitUserActivityServer } from './lib/udp_handlers_user_activity.js';
import { udpInitLogEventServer } from './lib/udp_handlers_log_events.js';
import { setupAnonUsageReportTimer } from './lib/telemetry.js';
import { setupPromClient } from './lib/prom-client.js';
import { verifyConfigFile } from './lib/config-file-verify.js';
import { setupConfigVisServer } from './lib/config-visualise.js';
import { setupUdpEventsStorage } from './lib/udp-event.js';

Expand Down Expand Up @@ -60,25 +59,8 @@ async function mainScript() {
const globals = await settingsObj.init();
globals.logger.verbose(`START: Globals init done: ${globals.initialised}`);

// Verify that the config file has the correct format
// Only do this if the command line option no-config-file-verify is NOT set
let configFileVerify = false;
if (globals.options.skipConfigVerification) {
globals.logger.warn('MAIN: Skipping config file verification');
} else {
configFileVerify = await verifyConfigFile();
}

// If config file verification failed, the previous function would have returned false.
// In that case, we should exit the script.
if (!configFileVerify) {
globals.logger.error('MAIN: Config file verification failed. Exiting.');
process.exit(1);
}

// Ensure that initialisation of globals is complete
// Sleep 5 seconds otherwise to llow globals to be initialised

// Sleep 5 seconds otherwise to allow globals to be initialised
function sleepLocal(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
Expand Down
4 changes: 2 additions & 2 deletions src/config/production_template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Butler-SOS:
# All configuration items are mandatory, unless otherwise noted.

# Logging configuration
logLevel: info # Log level. Possible log levels are silly, debug, verbose, info, warn, error
logLevel: info # Log level. Possible log levels are silly, debug, verbose, info, warn, error. Case sensitive.
fileLogging: true # true/false to enable/disable logging to disk file
logDirectory: log # Subdirectory where log files are stored
anonTelemetry: true # Can Butler SOS send anonymous data about what computer it is running on?
Expand Down Expand Up @@ -81,7 +81,7 @@ Butler-SOS:
qlikSenseEvents: # Shared settings for user and log events (see below)
influxdb:
enable: false # Should summary (counter) of user/log events, and rejected events be stored in InfluxDB?
writeFrequency: 20000 # How often (milliseconds) should rejected event count be written to InfluxDB?
writeFrequency: 20000 # How often (milliseconds) should event counts be written to InfluxDB?
eventCount: # Track how many events are received from Sense.
# Some events are valid, some are not. Of the valid events, some are rejected by Butler SOS
# based on the configuration in this file.
Expand Down
33 changes: 33 additions & 0 deletions src/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { fileURLToPath } from 'url';

import { getServerTags } from './lib/servertags.js';
import { UdpEvents } from './lib/udp-event.js';
import { verifyConfigFileSchema, verifyAppConfig } from './lib/config-file-verify.js';

let instance = null;

Expand Down Expand Up @@ -134,6 +135,7 @@ class Settings {
process.exit(1);
}
} else {
// No config file specified on command line.
// Get value of env variable NODE_ENV
const env = process.env.NODE_ENV;

Expand All @@ -143,8 +145,39 @@ class Settings {
this.configFile = upath.resolve(dirname, `./config/${env}.yaml`);
}

// Full path to config file in this.configFile
// Verify schema of config file
// Only do this if the command line option no-config-file-verify is NOT set
if (this.options.skipConfigVerification) {
console.warn('MAIN: Skipping config file verification');
} else {
let configFileVerify = await verifyConfigFileSchema(this.configFile);

// If config file verification failed, the previous function would have returned false.
// In that case, we should exit the script.
if (!configFileVerify) {
console.error('MAIN: Config file verification failed. Exiting.');
process.exit(1);
}
}

// Load config file
this.config = (await import('config')).default;

// Verify application specific settings and relationships between settings
if (this.options.skipConfigVerification) {
console.warn('MAIN: Skipping application specific config verification');
} else {
let appConfigVerify = await verifyAppConfig(this.config);

// If application specific config verification failed, the previous function would have returned false.
// In that case, we should exit the script.
if (!appConfigVerify) {
console.error('MAIN: Application specific config verification failed. Exiting.');
process.exit(1);
}
}

this.execPath = this.isPkg ? upath.dirname(process.execPath) : process.cwd();

// Are we running as standalone app or not?
Expand Down
2 changes: 1 addition & 1 deletion src/lib/config-file-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const confifgFileSchema = {
logLevel: {
type: 'string',
enum: ['error', 'warn', 'info', 'verbose', 'debug', 'silly'],
transform: ['trim', 'toLowerCase'],
transform: ['trim'],
},
fileLogging: { type: 'boolean' },
logDirectory: { type: 'string' },
Expand Down
133 changes: 64 additions & 69 deletions src/lib/config-file-verify.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ import { load } from 'js-yaml';
import fs from 'fs/promises';
import { default as Ajv } from 'ajv';

import globals from '../globals.js';
import { confifgFileSchema } from './config-file-schema.js';

// Function to verify that the config file has the correct format
// Use yaml-validator to validate the config file
export async function verifyConfigFile() {
export async function verifyConfigFileSchema(configFile) {
try {
const ajv = new Ajv({
strict: true,
Expand All @@ -27,8 +26,8 @@ export async function verifyConfigFile() {
// Add formats to ajv instance
ajvFormats.default(ajv);

// Load the YAML schema file, identified by globals.configFile, from file
const fileContent = await fs.readFile(globals.configFile, 'utf8');
// Load the YAML schema file, identified by configFile, from file
const fileContent = await fs.readFile(configFile, 'utf8');

// Parse the YAML file
let parsedFileContent;
Expand All @@ -52,87 +51,83 @@ export async function verifyConfigFile() {
// - message: The error message

for (const error of validate.errors) {
globals.logger.error(
`VERIFY CONFIG FILE: ${error.instancePath} : ${error.message}`
);
console.error(`VERIFY CONFIG FILE ERROR: ${error.instancePath} : ${error.message}`);
}

process.exit(1);
}

// ------------------------------
// Verify values of specific config entries

// If InfluxDB is enabled, check if the version is valid
// Valid values: 1 and 2
if (globals.config.get('Butler-SOS.influxdbConfig.enable') === true) {
const influxdbVersion = globals.config.get('Butler-SOS.influxdbConfig.version');
if (influxdbVersion !== 1 && influxdbVersion !== 2) {
globals.logger.error(
`VERIFY CONFIG FILE: Butler-SOS.influxdbConfig.enable (=InfluxDB version) ${influxdbVersion} is invalid. Exiting.`
);
process.exit(1);
}
}
console.info(
`VERIFY CONFIG FILE: Your config file at ${configFile} is correctly formatted, good work!`
);

// Verify that server tags are correctly defined
// In the config file section `Butler-SOS.serversToMonitor.serverTagsDefinition` it's possible to define zero or more tags that can be set for each server that is to be monitored.
// When Butler SOS is started, do the following checks:
// 1. All tags present in `Butler-SOS.serversToMonitor.serverTagsDefinition` must be set for each server in `SOS.serversToMonitor.servers[]`
// 2. The tags specified for each server in `SOS.serversToMonitor.servers[].serverTags` must be present in `Butler-SOS.serversToMonitor.serverTagsDefinition`
// If either of the conditions above is false, an error should be logged and Butler SOS should not start.
try {
// Loop over all defined server tags
const serverTagsDefinition = globals.config.get(
'Butler-SOS.serversToMonitor.serverTagsDefinition'
return true;
} catch (err) {
console.error(`VERIFY CONFIG FILE: ${err}`);

return false;
}
}

// Function to do verification of app specific settings and relationships between settings
export async function verifyAppConfig(cfg) {
// Verify values of specific config entries

// If InfluxDB is enabled, check if the version is valid
// Valid values: 1 and 2
if (cfg.get('Butler-SOS.influxdbConfig.enable') === true) {
const influxdbVersion = cfg.get('Butler-SOS.influxdbConfig.version');
if (influxdbVersion !== 1 && influxdbVersion !== 2) {
console.error(
`VERIFY CONFIG FILE ERROR: Butler-SOS.influxdbConfig.enable (=InfluxDB version) ${influxdbVersion} is invalid. Exiting.`
);
for (const tag of serverTagsDefinition) {
// Check that all servers have this tag
const servers = globals.config.get('Butler-SOS.serversToMonitor.servers');
for (const server of servers) {
// Check if server.serverTags.tag is defined
if (server?.serverTags === null || !server?.serverTags[tag]) {
globals.logger.error(
`VERIFY CONFIG FILE: Server tag "${tag}" is not defined for server "${server.serverName}". Exiting.`
);
process.exit(1);
} else {
globals.logger.verbose(
`VERIFY CONFIG FILE: Server tag "${tag}" is defined for server "${server.serverName}".`
);
}
}
}
return false;
}
}

// Now ensure that the tags defined for each server are valid and that there are no extra tags there
const servers = globals.config.get('Butler-SOS.serversToMonitor.servers');
// Verify that server tags are correctly defined
// In the config file section `Butler-SOS.serversToMonitor.serverTagsDefinition` it's possible to define zero or more tags that can be set for each server that is to be monitored.
// When Butler SOS is started, do the following checks:
// 1. All tags present in `Butler-SOS.serversToMonitor.serverTagsDefinition` must be set for each server in `SOS.serversToMonitor.servers[]`
// 2. The tags specified for each server in `SOS.serversToMonitor.servers[].serverTags` must be present in `Butler-SOS.serversToMonitor.serverTagsDefinition`
// If either of the conditions above is false, an error should be logged and Butler SOS should not start.
try {
// Loop over all defined server tags
const serverTagsDefinition = cfg.get('Butler-SOS.serversToMonitor.serverTagsDefinition');
for (const tag of serverTagsDefinition) {
// Check that all servers have this tag
const servers = cfg.get('Butler-SOS.serversToMonitor.servers');
for (const server of servers) {
for (const tag in server.serverTags) {
if (!serverTagsDefinition.includes(tag)) {
globals.logger.error(
`VERIFY CONFIG FILE: Server tag "${tag}" for server "${server.serverName}" is not defined in Butler-SOS.serversToMonitor.serverTagsDefinition. Exiting.`
);
process.exit(1);
} else {
globals.logger.verbose(
`VERIFY CONFIG FILE: Server tag "${tag}" is defined in Butler-SOS.serversToMonitor.serverTagsDefinition.`
);
}
// Check if server.serverTags.tag is defined
if (server?.serverTags === null || !server?.serverTags[tag]) {
console.error(
`VERIFY CONFIG FILE: Server tag "${tag}" is not defined for server "${server.serverName}". Exiting.`
);
return false;
} else {
// The tag is defined for this server
}
}
} catch (err) {
globals.logger.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`);
process.exit(1);
}

globals.logger.info(
`VERIFY CONFIG FILE: Your config file at ${globals.configFile} is valid, good work!`
);
// Now ensure that the tags defined for each server are valid and that there are no extra tags there
const servers = cfg.get('Butler-SOS.serversToMonitor.servers');
for (const server of servers) {
for (const tag in server.serverTags) {
if (!serverTagsDefinition.includes(tag)) {
console.error(
`VERIFY CONFIG FILE: Server tag "${tag}" for server "${server.serverName}" is not defined in Butler-SOS.serversToMonitor.serverTagsDefinition. Exiting.`
);
return false;
} else {
// The tag is defined in Butler-SOS.serversToMonitor.serverTagsDefinition
}
}
}

return true;
} catch (err) {
globals.logger.error(`VERIFY CONFIG FILE: ${err}`);

console.error(`VERIFY CONFIG FILE: Server tags verification failed. ${err}`);
return false;
}
}

0 comments on commit befc5ee

Please sign in to comment.